@muxoai/platform 1.4.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 +2 -2
- package/ARCHITECTURE.md +16 -6
- package/README.md +2 -2
- package/dist/cli.js +323 -13
- 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 +293 -5
- package/dist/engine.js +1502 -88
- package/dist/mcp-schema.d.ts +286 -0
- package/dist/mcp-schema.js +2559 -0
- package/dist/mcp.d.ts +2 -30
- package/dist/mcp.js +913 -558
- package/dist/memory-graph.d.ts +21 -0
- package/dist/memory-graph.js +173 -0
- package/dist/nl.js +155 -0
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/policy.d.ts +182 -1
- package/dist/policy.js +1524 -0
- package/dist/providers/composio-instant.js +262 -7
- package/dist/providers/composio-sdk.d.ts +125 -5
- package/dist/providers/composio-sdk.js +901 -82
- package/dist/providers/microsoft-governance.d.ts +28 -1
- package/dist/providers/microsoft-governance.js +224 -11
- package/dist/providers/stripe-billing.d.ts +42 -0
- package/dist/providers/stripe-billing.js +230 -1
- package/dist/providers/stripe-projects.d.ts +455 -3
- package/dist/providers/stripe-projects.js +2570 -129
- package/dist/providers/supermemory.d.ts +74 -1
- package/dist/providers/supermemory.js +426 -0
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +440 -3
- package/dist/sdk.js +305 -3
- package/dist/server.js +2343 -264
- package/dist/store.d.ts +113 -6
- package/dist/store.js +677 -52
- package/dist/types.d.ts +207 -3
- package/dist/ui/console-html.d.ts +40 -1
- package/dist/ui/console-html.js +1813 -629
- package/dist/ui/design-tokens.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
- package/plugins/muxo/.claude-plugin/plugin.json +2 -2
- package/plugins/muxo/.codex-plugin/plugin.json +15 -8
- package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
- package/plugins/muxo/hooks/session-start.sh +15 -3
- package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
- package/plugins/muxo/mcp.json +0 -10
- package/plugins/muxo/plugin.json +4 -4
- package/plugins/muxo/skills/muxo/SKILL.md +59 -9
- package/plugins/muxo/skills/muxo/references/cli.md +5 -1
- package/plugins/muxo/skills/muxo/references/mcp.md +13 -7
- package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
- package/plugins/muxo/skills/setup/SKILL.md +9 -1
- package/src/catalogues/stripe-projects-catalog.json +140 -0
package/dist/server.js
CHANGED
|
@@ -5,10 +5,16 @@ 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";
|
|
10
13
|
import { MUXO_VERSION } from "./version.js";
|
|
11
|
-
import {
|
|
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";
|
|
12
18
|
function renderSkillMarkdown(baseUrl) {
|
|
13
19
|
return `---
|
|
14
20
|
name: muxo
|
|
@@ -89,9 +95,45 @@ ${MUXO_MCP_TOOLS.map((t, i) => `${i + 1}. \`${t.name}\` — ${t.description}`).j
|
|
|
89
95
|
1. **Discover**: Call \`search_tools\` with a keyword or lane filter.
|
|
90
96
|
2. **Inspect**: Call \`inspect_tool\` to read the tool's \`inputSchema\`, \`riskLevel\`, and \`creditCost\`.
|
|
91
97
|
3. **Plan**: Call \`plan_action({ tool_slug, input, reason })\` to receive a \`plan_*\` ID.
|
|
92
|
-
- 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.
|
|
93
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 }\`.
|
|
94
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.
|
|
95
137
|
`;
|
|
96
138
|
}
|
|
97
139
|
async function readBody(req) {
|
|
@@ -106,8 +148,8 @@ function sendJson(res, status, payload, extraHeaders) {
|
|
|
106
148
|
res.writeHead(status, {
|
|
107
149
|
"Content-Type": "application/json; charset=utf-8",
|
|
108
150
|
"Access-Control-Allow-Origin": "*",
|
|
109
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id",
|
|
110
|
-
"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",
|
|
111
153
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
112
154
|
...(extraHeaders || {}),
|
|
113
155
|
});
|
|
@@ -135,17 +177,68 @@ export function createMuxoServer(customConfig) {
|
|
|
135
177
|
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
136
178
|
});
|
|
137
179
|
const oauthCodes = new Map();
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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);
|
|
145
221
|
}
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
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;
|
|
149
242
|
return {
|
|
150
243
|
user,
|
|
151
244
|
workspace: ws,
|
|
@@ -153,11 +246,96 @@ export function createMuxoServer(customConfig) {
|
|
|
153
246
|
authType: "session",
|
|
154
247
|
};
|
|
155
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
|
+
};
|
|
156
333
|
const server = http.createServer(async (req, res) => {
|
|
157
334
|
if (req.method === "OPTIONS") {
|
|
158
335
|
res.writeHead(204, {
|
|
159
336
|
"Access-Control-Allow-Origin": "*",
|
|
160
|
-
"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",
|
|
161
339
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
162
340
|
});
|
|
163
341
|
res.end();
|
|
@@ -202,6 +380,20 @@ export function createMuxoServer(customConfig) {
|
|
|
202
380
|
});
|
|
203
381
|
return;
|
|
204
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
|
+
}
|
|
205
397
|
if ((pathname === "/.well-known/oauth-protected-resource" ||
|
|
206
398
|
pathname === "/.well-known/oauth-protected-resource/mcp") &&
|
|
207
399
|
req.method === "GET") {
|
|
@@ -216,7 +408,7 @@ export function createMuxoServer(customConfig) {
|
|
|
216
408
|
resource,
|
|
217
409
|
authorization_servers: [requestOrigin],
|
|
218
410
|
bearer_methods_supported: ["header"],
|
|
219
|
-
scopes_supported: ["muxo:mcp", "muxo:read", "muxo:execute", "muxo:provision"],
|
|
411
|
+
scopes_supported: ["mcp:tools", "muxo:mcp", "muxo:read", "muxo:execute", "muxo:provision"],
|
|
220
412
|
resource_documentation: `${config.docsUrl}/quickstart`,
|
|
221
413
|
mcp_endpoint: `${requestOrigin}/mcp`,
|
|
222
414
|
});
|
|
@@ -238,6 +430,7 @@ export function createMuxoServer(customConfig) {
|
|
|
238
430
|
userinfo_endpoint: `${requestOrigin}/oauth/userinfo`,
|
|
239
431
|
jwks_uri: `${requestOrigin}/.well-known/jwks.json`,
|
|
240
432
|
scopes_supported: [
|
|
433
|
+
"mcp:tools",
|
|
241
434
|
"muxo:mcp",
|
|
242
435
|
"muxo:read",
|
|
243
436
|
"muxo:execute",
|
|
@@ -247,11 +440,7 @@ export function createMuxoServer(customConfig) {
|
|
|
247
440
|
"email",
|
|
248
441
|
],
|
|
249
442
|
response_types_supported: ["code"],
|
|
250
|
-
grant_types_supported: [
|
|
251
|
-
"authorization_code",
|
|
252
|
-
"refresh_token",
|
|
253
|
-
"client_credentials",
|
|
254
|
-
],
|
|
443
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
255
444
|
token_endpoint_auth_methods_supported: [
|
|
256
445
|
"none",
|
|
257
446
|
"client_secret_post",
|
|
@@ -301,149 +490,185 @@ export function createMuxoServer(customConfig) {
|
|
|
301
490
|
sendJson(res, 201, clientRecord);
|
|
302
491
|
return;
|
|
303
492
|
}
|
|
304
|
-
if (pathname === "/oauth/authorize" &&
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
"S256";
|
|
325
|
-
const code = `muxo_auth_code_${crypto.randomBytes(16).toString("hex")}`;
|
|
326
|
-
const issuedKey = bootstrap.defaultApiKey ||
|
|
327
|
-
store.createApiKey({
|
|
328
|
-
workspaceId: bootstrap.workspace.id,
|
|
329
|
-
name: `OAuth Client (${clientId})`,
|
|
330
|
-
environment: "live",
|
|
331
|
-
}).secretKey;
|
|
332
|
-
oauthCodes.set(code, {
|
|
333
|
-
code,
|
|
334
|
-
clientId,
|
|
335
|
-
redirectUri,
|
|
336
|
-
scope,
|
|
337
|
-
codeChallenge,
|
|
338
|
-
codeChallengeMethod,
|
|
339
|
-
workspaceId: bootstrap.workspace.id,
|
|
340
|
-
apiKey: issuedKey,
|
|
341
|
-
expiresAt: Date.now() + 10 * 60 * 1000,
|
|
342
|
-
});
|
|
343
|
-
const proto = String(req.headers["x-forwarded-proto"] || "http").split(",")[0].trim();
|
|
344
|
-
const requestOrigin = req.headers.host
|
|
345
|
-
? `${proto}://${req.headers.host}`
|
|
346
|
-
: config.baseUrl;
|
|
347
|
-
const redirectTarget = new URL(redirectUri);
|
|
348
|
-
redirectTarget.searchParams.set("code", code);
|
|
349
|
-
if (stateParam)
|
|
350
|
-
redirectTarget.searchParams.set("state", stateParam);
|
|
351
|
-
redirectTarget.searchParams.set("iss", requestOrigin);
|
|
352
|
-
if (url.searchParams.get("format") === "json" ||
|
|
353
|
-
String(req.headers.accept || "").includes("application/json")) {
|
|
354
|
-
sendJson(res, 200, {
|
|
355
|
-
ok: true,
|
|
356
|
-
code,
|
|
357
|
-
state: stateParam || null,
|
|
358
|
-
iss: requestOrigin,
|
|
359
|
-
client_id: clientId,
|
|
360
|
-
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(),
|
|
361
513
|
});
|
|
362
514
|
return;
|
|
363
515
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
+
}));
|
|
369
531
|
return;
|
|
370
532
|
}
|
|
371
|
-
if (pathname === "/oauth/
|
|
372
|
-
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);
|
|
373
541
|
const contentType = String(req.headers["content-type"] || "");
|
|
374
|
-
const params =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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;
|
|
381
549
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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;
|
|
387
558
|
}
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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.");
|
|
401
598
|
}
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
const method = (record.codeChallengeMethod || "S256").toUpperCase();
|
|
411
|
-
const computed = method === "S256"
|
|
412
|
-
? crypto
|
|
413
|
-
.createHash("sha256")
|
|
414
|
-
.update(codeVerifier)
|
|
415
|
-
.digest("base64url")
|
|
416
|
-
: codeVerifier;
|
|
417
|
-
if (computed !== record.codeChallenge) {
|
|
418
|
-
sendJson(res, 400, {
|
|
419
|
-
error: "invalid_grant",
|
|
420
|
-
error_description: "PKCE code_verifier mismatch.",
|
|
421
|
-
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
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.");
|
|
424
605
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
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),
|
|
433
613
|
environment: "live",
|
|
434
|
-
})
|
|
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;
|
|
435
632
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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.");
|
|
444
666
|
}
|
|
445
667
|
if (pathname === "/oauth/userinfo" && req.method === "GET") {
|
|
446
|
-
const
|
|
668
|
+
const resolved = await requireContext(req, res, pathname);
|
|
669
|
+
if (!resolved)
|
|
670
|
+
return;
|
|
671
|
+
const { ctx } = resolved;
|
|
447
672
|
const nickname = (ctx.user.email || "").split("@")[0] ||
|
|
448
673
|
ctx.user.name.toLowerCase().replace(/\s+/g, ".") ||
|
|
449
674
|
ctx.user.id;
|
|
@@ -452,7 +677,7 @@ export function createMuxoServer(customConfig) {
|
|
|
452
677
|
id: ctx.user.id,
|
|
453
678
|
name: ctx.user.name,
|
|
454
679
|
email: ctx.user.email,
|
|
455
|
-
email_verified:
|
|
680
|
+
email_verified: Boolean(ctx.user.email),
|
|
456
681
|
nickname,
|
|
457
682
|
workspace_id: ctx.workspace.id,
|
|
458
683
|
workspace_name: ctx.workspace.name,
|
|
@@ -467,7 +692,7 @@ export function createMuxoServer(customConfig) {
|
|
|
467
692
|
info: {
|
|
468
693
|
title: "Muxo Unified Agent Execution, Infrastructure, Memory & Governance API",
|
|
469
694
|
version: MUXO_VERSION,
|
|
470
|
-
description: "One governed boundary over Connected Apps
|
|
695
|
+
description: "One governed boundary over Connected Apps, Instant Tools, Cloud Infrastructure, Workspace Memory, and Safety & Approvals.",
|
|
471
696
|
},
|
|
472
697
|
servers: [{ url: config.baseUrl }],
|
|
473
698
|
paths: {
|
|
@@ -476,6 +701,12 @@ export function createMuxoServer(customConfig) {
|
|
|
476
701
|
"/skill.md": { get: { summary: "Canonical agent skill & onboarding markdown" } },
|
|
477
702
|
"/api/v1/agents/signup": { post: { summary: "Autonomous 2-call agent workspace & mk_live_* key signup" } },
|
|
478
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" } },
|
|
479
710
|
"/api/v1/tools": { get: { summary: "Search unified catalogue across instant.*, app.*, and resource.*" } },
|
|
480
711
|
"/api/v1/tools/{slug}": { get: { summary: "Inspect tool schema, risk level, credit cost, and LLM context URL" } },
|
|
481
712
|
"/api/v1/connections": {
|
|
@@ -485,19 +716,68 @@ export function createMuxoServer(customConfig) {
|
|
|
485
716
|
"/api/v1/connections/wait": { post: { summary: "Wait/poll for browser OAuth handoff completion" } },
|
|
486
717
|
"/api/v1/plans": {
|
|
487
718
|
get: { summary: "List governed action plans" },
|
|
488
|
-
post: { summary: "Create governed action plan with
|
|
719
|
+
post: { summary: "Create governed action plan with Workspace Memory recall & Ed25519 signature" },
|
|
489
720
|
},
|
|
490
721
|
"/api/v1/plans/{planId}/approve": { post: { summary: "Approve a pending high-risk action plan" } },
|
|
491
722
|
"/api/v1/plans/{planId}/execute": { post: { summary: "Execute an approved plan and issue HMAC-SHA256 receipt" } },
|
|
492
723
|
"/api/v1/resources": { get: { summary: "List provisioned cloud infrastructure resources" } },
|
|
493
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
|
+
},
|
|
494
750
|
"/api/v1/memory": {
|
|
495
|
-
get: { summary: "Recall isolated
|
|
751
|
+
get: { summary: "Recall isolated Workspace Memory facts and hybrid context" },
|
|
496
752
|
post: { summary: "Store a workspace memory fact or decision" },
|
|
497
753
|
},
|
|
498
|
-
"/api/v1/
|
|
499
|
-
"/api/v1/
|
|
500
|
-
"/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" } },
|
|
501
781
|
},
|
|
502
782
|
});
|
|
503
783
|
return;
|
|
@@ -547,10 +827,10 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
547
827
|
}
|
|
548
828
|
if (pathname === "/llms.txt" && req.method === "GET") {
|
|
549
829
|
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
550
|
-
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`);
|
|
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`);
|
|
551
831
|
return;
|
|
552
832
|
}
|
|
553
|
-
// 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)
|
|
554
834
|
const pluginJsonRoutes = {
|
|
555
835
|
"/.agents/plugins/marketplace.json": path.join(ROOT_DIR, ".agents", "plugins", "marketplace.json"),
|
|
556
836
|
"/.claude-plugin/marketplace.json": path.join(ROOT_DIR, ".claude-plugin", "marketplace.json"),
|
|
@@ -558,6 +838,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
558
838
|
"/plugins/muxo/mcp.json": path.join(ROOT_DIR, "plugins", "muxo", "mcp.json"),
|
|
559
839
|
"/plugins/muxo/.codex-plugin/plugin.json": path.join(ROOT_DIR, "plugins", "muxo", ".codex-plugin", "plugin.json"),
|
|
560
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"),
|
|
561
842
|
"/plugins/muxo/.app.json": path.join(ROOT_DIR, "plugins", "muxo", ".app.json"),
|
|
562
843
|
"/plugins/muxo/.mcp.json": path.join(ROOT_DIR, "plugins", "muxo", ".mcp.json"),
|
|
563
844
|
};
|
|
@@ -585,6 +866,10 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
585
866
|
file: path.join(ROOT_DIR, "plugins", "muxo", "skills", "setup", "SKILL.md"),
|
|
586
867
|
contentType: "text/markdown; charset=utf-8",
|
|
587
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
|
+
},
|
|
588
873
|
"/.codex/config.toml": {
|
|
589
874
|
file: path.join(ROOT_DIR, ".codex", "config.toml"),
|
|
590
875
|
contentType: "text/plain; charset=utf-8",
|
|
@@ -605,10 +890,15 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
605
890
|
const portableManifest = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, "plugins", "muxo", "plugin.json"), "utf8"));
|
|
606
891
|
const mcpConfig = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, "plugins", "muxo", "mcp.json"), "utf8"));
|
|
607
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;
|
|
608
897
|
sendJson(res, 200, {
|
|
609
898
|
ok: true,
|
|
610
899
|
plugin: portableManifest,
|
|
611
900
|
codexPlugin: codexManifest,
|
|
901
|
+
cursorPlugin: cursorManifest,
|
|
612
902
|
mcp: mcpConfig,
|
|
613
903
|
skills: getMuxoMcpSkills().map((s) => ({
|
|
614
904
|
name: s.name,
|
|
@@ -625,11 +915,14 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
625
915
|
portablePluginJson: `${config.baseUrl}/plugins/muxo/plugin.json`,
|
|
626
916
|
portableMcpJson: `${config.baseUrl}/plugins/muxo/mcp.json`,
|
|
627
917
|
onboardingSkill: `${config.baseUrl}/plugins/muxo/skills/setup/SKILL.md`,
|
|
918
|
+
activitySummarySkill: `${config.baseUrl}/plugins/muxo/skills/muxo-activity-summary/SKILL.md`,
|
|
628
919
|
codexPluginJson: `${config.baseUrl}/plugins/muxo/.codex-plugin/plugin.json`,
|
|
920
|
+
cursorPluginJson: `${config.baseUrl}/plugins/muxo/.cursor-plugin/plugin.json`,
|
|
629
921
|
codexConfigToml: `${config.baseUrl}/.codex/config.toml`,
|
|
630
922
|
},
|
|
631
923
|
installCommands: {
|
|
632
924
|
codex: "codex plugin add muxo@muxo",
|
|
925
|
+
cursor: "/add-plugin muxo",
|
|
633
926
|
cliSignup: "muxo login --signup --name my-agent",
|
|
634
927
|
oauthHandoff: "muxo link github --wait",
|
|
635
928
|
},
|
|
@@ -656,55 +949,127 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
656
949
|
res.end(renderLandingHtml());
|
|
657
950
|
return;
|
|
658
951
|
}
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
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
|
+
});
|
|
665
964
|
return;
|
|
666
965
|
}
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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);
|
|
672
985
|
const wantsHtml = url.searchParams.get("format") !== "json" &&
|
|
673
986
|
String(req.headers.accept || "").includes("text/html");
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
});
|
|
701
1067
|
return;
|
|
702
1068
|
}
|
|
703
1069
|
sendJson(res, 200, {
|
|
704
1070
|
ok: true,
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
claim: redeemed,
|
|
1071
|
+
redirect: `${requestOriginOf(req)}/`,
|
|
1072
|
+
workspace: { id: workspace.id, name: workspace.name },
|
|
708
1073
|
});
|
|
709
1074
|
return;
|
|
710
1075
|
}
|
|
@@ -720,7 +1085,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
720
1085
|
const connectedAccountId = url.searchParams.get("connected_account_id") ||
|
|
721
1086
|
url.searchParams.get("connectedAccountId") ||
|
|
722
1087
|
undefined;
|
|
723
|
-
const
|
|
1088
|
+
const oauthError = url.searchParams.get("error") || url.searchParams.get("error_description");
|
|
1089
|
+
const callbackStatus = oauthError ? "failed" : url.searchParams.get("status") || "success";
|
|
724
1090
|
const handoff = await engine.handleOAuthCallback({
|
|
725
1091
|
workspaceId,
|
|
726
1092
|
provider,
|
|
@@ -738,30 +1104,312 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
738
1104
|
return;
|
|
739
1105
|
}
|
|
740
1106
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
741
|
-
res.end(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
+
}));
|
|
765
1413
|
return;
|
|
766
1414
|
}
|
|
767
1415
|
// Autonomous Agent 2-Call Signup — Public
|
|
@@ -770,13 +1418,28 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
770
1418
|
pathname === "/api/signup") &&
|
|
771
1419
|
req.method === "POST") {
|
|
772
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
|
+
}
|
|
773
1426
|
const created = store.createAutonomousAgentSignup({
|
|
774
1427
|
agentName: body.agentName ? String(body.agentName) : undefined,
|
|
775
1428
|
harness: body.harness ? String(body.harness) : undefined,
|
|
776
1429
|
});
|
|
1430
|
+
const claim = store.createAgentClaim({
|
|
1431
|
+
workspaceId: created.workspace.id,
|
|
1432
|
+
email: ownerEmail || null,
|
|
1433
|
+
baseUrl: config.baseUrl,
|
|
1434
|
+
});
|
|
777
1435
|
sendJson(res, 201, {
|
|
778
1436
|
ok: true,
|
|
779
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 },
|
|
780
1443
|
workspace: {
|
|
781
1444
|
id: created.workspace.id,
|
|
782
1445
|
name: created.workspace.name,
|
|
@@ -794,6 +1457,12 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
794
1457
|
return;
|
|
795
1458
|
}
|
|
796
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
|
+
}
|
|
797
1466
|
if (pathname === "/mcp" && (req.method === "GET" || req.method === "HEAD")) {
|
|
798
1467
|
const accept = String(req.headers.accept || "");
|
|
799
1468
|
const sessionId = String(req.headers["mcp-session-id"] || "") ||
|
|
@@ -804,8 +1473,9 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
804
1473
|
"Cache-Control": "no-cache",
|
|
805
1474
|
Connection: "keep-alive",
|
|
806
1475
|
"Mcp-Session-Id": sessionId,
|
|
1476
|
+
"MCP-Protocol-Version": LATEST_MCP_PROTOCOL_VERSION,
|
|
807
1477
|
"Access-Control-Allow-Origin": "*",
|
|
808
|
-
"Access-Control-Expose-Headers": "Mcp-Session-Id",
|
|
1478
|
+
"Access-Control-Expose-Headers": "Mcp-Session-Id, MCP-Protocol-Version",
|
|
809
1479
|
});
|
|
810
1480
|
if (req.method !== "HEAD") {
|
|
811
1481
|
res.write(`event: endpoint\ndata: ${JSON.stringify({ uri: `${config.baseUrl}/mcp`, sessionId })}\n\n`);
|
|
@@ -816,15 +1486,20 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
816
1486
|
sendJson(res, 200, {
|
|
817
1487
|
ok: true,
|
|
818
1488
|
transport: "streamable-http",
|
|
819
|
-
protocolVersion:
|
|
1489
|
+
protocolVersion: LATEST_MCP_PROTOCOL_VERSION,
|
|
1490
|
+
supportedVersions: [...SUPPORTED_MCP_PROTOCOL_VERSIONS],
|
|
820
1491
|
serverInfo: {
|
|
821
1492
|
name: "muxo-mcp-server",
|
|
1493
|
+
title: "Muxo Unified Agent Runtime & Governance MCP Server",
|
|
822
1494
|
version: MUXO_VERSION,
|
|
823
1495
|
},
|
|
824
1496
|
instructions: MUXO_MCP_SERVER_INSTRUCTIONS,
|
|
825
1497
|
toolsCount: MUXO_MCP_TOOLS.length,
|
|
826
1498
|
skillsCount: getMuxoMcpSkills().length,
|
|
827
|
-
}, {
|
|
1499
|
+
}, {
|
|
1500
|
+
"Mcp-Session-Id": sessionId,
|
|
1501
|
+
"MCP-Protocol-Version": LATEST_MCP_PROTOCOL_VERSION,
|
|
1502
|
+
});
|
|
828
1503
|
return;
|
|
829
1504
|
}
|
|
830
1505
|
if (pathname === "/mcp" && req.method === "DELETE") {
|
|
@@ -835,14 +1510,41 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
835
1510
|
return;
|
|
836
1511
|
}
|
|
837
1512
|
if (pathname === "/mcp" && req.method === "POST") {
|
|
838
|
-
const ctx = resolveContext(req);
|
|
839
|
-
const sessionId = String(req.headers["mcp-session-id"] || "") ||
|
|
840
|
-
`mcp_sess_${ctx.workspace.id}`;
|
|
841
1513
|
const raw = await readBody(req);
|
|
842
1514
|
const parsed = JSON.parse(raw || "{}");
|
|
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
|
+
};
|
|
843
1545
|
if (Array.isArray(parsed)) {
|
|
844
1546
|
const responses = (await Promise.all(parsed.map((item) => handleJsonRpcMcpRequest(engine, ctx, (item || {}))))).filter((r) => r !== null);
|
|
845
|
-
sendJson(res, 200, responses,
|
|
1547
|
+
sendJson(res, 200, responses, responseHeaders);
|
|
846
1548
|
return;
|
|
847
1549
|
}
|
|
848
1550
|
const rpc = parsed;
|
|
@@ -850,17 +1552,33 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
850
1552
|
if (!response) {
|
|
851
1553
|
res.writeHead(202, {
|
|
852
1554
|
"Access-Control-Allow-Origin": "*",
|
|
853
|
-
|
|
1555
|
+
...responseHeaders,
|
|
854
1556
|
});
|
|
855
1557
|
res.end();
|
|
856
1558
|
return;
|
|
857
1559
|
}
|
|
858
|
-
sendJson(res, 200, response,
|
|
1560
|
+
sendJson(res, 200, response, responseHeaders);
|
|
859
1561
|
return;
|
|
860
1562
|
}
|
|
861
1563
|
// Auth Endpoints
|
|
862
1564
|
if (pathname === "/api/v1/auth/config" && req.method === "GET") {
|
|
863
|
-
|
|
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
|
+
});
|
|
864
1582
|
return;
|
|
865
1583
|
}
|
|
866
1584
|
if (pathname === "/api/v1/auth/register" && req.method === "POST") {
|
|
@@ -879,6 +1597,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
879
1597
|
sendJson(res, 201, {
|
|
880
1598
|
ok: true,
|
|
881
1599
|
...created,
|
|
1600
|
+
token: created.sessionToken,
|
|
882
1601
|
apiKey: initialKey.secretKey,
|
|
883
1602
|
});
|
|
884
1603
|
return;
|
|
@@ -890,21 +1609,39 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
890
1609
|
sendJson(res, 401, { ok: false, error: "Invalid email or password" });
|
|
891
1610
|
return;
|
|
892
1611
|
}
|
|
893
|
-
sendJson(res, 200, { ok: true, ...loggedIn });
|
|
1612
|
+
sendJson(res, 200, { ok: true, ...loggedIn, token: loggedIn.sessionToken });
|
|
894
1613
|
return;
|
|
895
1614
|
}
|
|
896
|
-
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;
|
|
897
1627
|
// Console Bootstrap
|
|
898
1628
|
if (pathname === "/api/v1/bootstrap" && req.method === "GET") {
|
|
899
1629
|
const org = engine.getOrganization(ctx).organization;
|
|
1630
|
+
const guardrailsSummary = engine.getGuardrailsSummary(ctx);
|
|
900
1631
|
sendJson(res, 200, {
|
|
901
1632
|
ok: true,
|
|
902
1633
|
profile: engine.getProfile(ctx),
|
|
903
1634
|
organization: org,
|
|
904
1635
|
governance: engine.governance.getStatus(),
|
|
1636
|
+
llmPolicyBrief: guardrailsSummary.llmPolicyBrief,
|
|
1637
|
+
plainEnglishGuardrails: guardrailsSummary.plainEnglish,
|
|
1638
|
+
activeMemoryDefaults: guardrailsSummary.activeDefaults,
|
|
1639
|
+
activeGuardrails: guardrailsSummary.activeGuardrails,
|
|
905
1640
|
supermemory: {
|
|
906
1641
|
enabled: engine.supermemory.isEnabled(),
|
|
907
1642
|
namespace: engine.supermemory.getWorkspaceNamespace(ctx.workspace.id),
|
|
1643
|
+
activeDefaults: guardrailsSummary.activeDefaults,
|
|
1644
|
+
activeGuardrails: guardrailsSummary.activeGuardrails,
|
|
908
1645
|
},
|
|
909
1646
|
surfaces: {
|
|
910
1647
|
publicWebUrl: config.publicWebUrl,
|
|
@@ -913,7 +1650,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
913
1650
|
},
|
|
914
1651
|
apiKeys: store.listApiKeys(ctx.workspace.id),
|
|
915
1652
|
byokKeys: store.listByokKeys(ctx.workspace.id),
|
|
916
|
-
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),
|
|
917
1655
|
connections: store.listConnections(ctx.workspace.id),
|
|
918
1656
|
resources: store.listResources(ctx.workspace.id),
|
|
919
1657
|
plans: store.listPlans(ctx.workspace.id, 25),
|
|
@@ -921,11 +1659,122 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
921
1659
|
});
|
|
922
1660
|
return;
|
|
923
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
|
+
}
|
|
924
1769
|
// Autonomous Agent Claim Link Creation (Authenticated)
|
|
925
1770
|
if (pathname === "/api/v1/agents/claim" && req.method === "POST") {
|
|
1771
|
+
if (auth.kind !== "ok") {
|
|
1772
|
+
sendAuthRequired(req, res, pathname);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
926
1775
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
927
|
-
const email = String(body.email || "").trim();
|
|
928
|
-
if (!email ||
|
|
1776
|
+
const email = String(body.email || body.ownerEmail || "").trim().toLowerCase();
|
|
1777
|
+
if (!email || !/^[^\s@]+@[^\s@]+$/.test(email)) {
|
|
929
1778
|
sendJson(res, 400, { ok: false, error: "Valid target email is required" });
|
|
930
1779
|
return;
|
|
931
1780
|
}
|
|
@@ -938,6 +1787,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
938
1787
|
ok: true,
|
|
939
1788
|
claim: { ...claim, ownerEmail: email },
|
|
940
1789
|
...claim,
|
|
1790
|
+
claim_slug: claim.claimSlug,
|
|
941
1791
|
ownerEmail: email,
|
|
942
1792
|
});
|
|
943
1793
|
return;
|
|
@@ -946,6 +1796,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
946
1796
|
if (pathname === "/api/v1/governance" && req.method === "GET") {
|
|
947
1797
|
const ws = store.getWorkspace(ctx.workspace.id) || ctx.workspace;
|
|
948
1798
|
const fullReport = engine.governance.getGovernanceStatus(ws);
|
|
1799
|
+
const guardrailsSummary = engine.getGuardrailsSummary(ctx);
|
|
949
1800
|
sendJson(res, 200, {
|
|
950
1801
|
ok: true,
|
|
951
1802
|
governance: {
|
|
@@ -957,6 +1808,87 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
957
1808
|
auditChain: fullReport.auditChain,
|
|
958
1809
|
},
|
|
959
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}`,
|
|
960
1892
|
});
|
|
961
1893
|
return;
|
|
962
1894
|
}
|
|
@@ -989,13 +1921,39 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
989
1921
|
sendJson(res, 200, { ok: true, scan });
|
|
990
1922
|
return;
|
|
991
1923
|
}
|
|
1924
|
+
// Workspace Context Inspector (natural-language friendly)
|
|
1925
|
+
if ((pathname === "/api/v1/context" || pathname === "/v1/context") &&
|
|
1926
|
+
(req.method === "GET" || req.method === "POST")) {
|
|
1927
|
+
const body = req.method === "POST"
|
|
1928
|
+
? JSON.parse((await readBody(req)) || "{}")
|
|
1929
|
+
: {};
|
|
1930
|
+
const query = url.searchParams.get("query") ||
|
|
1931
|
+
url.searchParams.get("q") ||
|
|
1932
|
+
(body.query ? String(body.query) : undefined);
|
|
1933
|
+
const runContext = await engine.getRunContext(ctx, query);
|
|
1934
|
+
sendJson(res, 200, { ok: true, ...runContext });
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
992
1937
|
// Supermemory v5 Workspace Brain Endpoints
|
|
993
1938
|
if (pathname === "/api/v1/memory" && req.method === "GET") {
|
|
994
1939
|
const query = url.searchParams.get("query") ||
|
|
995
1940
|
url.searchParams.get("q") ||
|
|
996
1941
|
undefined;
|
|
997
1942
|
const overview = await engine.getMemoryOverview(ctx, query);
|
|
998
|
-
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 });
|
|
999
1957
|
return;
|
|
1000
1958
|
}
|
|
1001
1959
|
if (pathname === "/api/v1/memory/facts" && req.method === "GET") {
|
|
@@ -1031,6 +1989,48 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1031
1989
|
}
|
|
1032
1990
|
if (pathname === "/api/v1/memory" && req.method === "POST") {
|
|
1033
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
|
+
}
|
|
1034
2034
|
const record = await engine.addWorkspaceMemory(ctx, {
|
|
1035
2035
|
title: String(body.title || "Workspace Fact"),
|
|
1036
2036
|
content: String(body.content || ""),
|
|
@@ -1050,18 +2050,37 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1050
2050
|
return;
|
|
1051
2051
|
}
|
|
1052
2052
|
// API Keys
|
|
1053
|
-
if (pathname === "/api/v1/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") {
|
|
1054
2064
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
1055
2065
|
const created = store.createApiKey({
|
|
1056
2066
|
workspaceId: ctx.workspace.id,
|
|
1057
|
-
name: String(body.name || "API Key"),
|
|
2067
|
+
name: String(body.name || (body.teamName ? `${String(body.teamName)} Team Key` : "API Key")),
|
|
1058
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,
|
|
1059
2073
|
});
|
|
1060
|
-
sendJson(res, 201, { ok: true, ...created });
|
|
2074
|
+
sendJson(res, 201, { ok: true, apiKey: created.secretKey, ...created });
|
|
1061
2075
|
return;
|
|
1062
2076
|
}
|
|
1063
|
-
if (pathname.startsWith("/api/v1/api-keys/")
|
|
1064
|
-
|
|
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));
|
|
1065
2084
|
if (!store.revokeApiKey(ctx.workspace.id, keyId)) {
|
|
1066
2085
|
sendJson(res, 404, { ok: false, error: "API key not found" });
|
|
1067
2086
|
return;
|
|
@@ -1128,13 +2147,18 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1128
2147
|
}
|
|
1129
2148
|
const result = await engine.waitForConnection(ctx, {
|
|
1130
2149
|
provider,
|
|
1131
|
-
timeoutSeconds: body.timeoutSeconds
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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")),
|
|
1136
2160
|
});
|
|
1137
|
-
sendJson(res, 200,
|
|
2161
|
+
sendJson(res, 200, result);
|
|
1138
2162
|
return;
|
|
1139
2163
|
}
|
|
1140
2164
|
// Provisioned Resources & Decrypted Vault Env Pull
|
|
@@ -1165,11 +2189,687 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1165
2189
|
});
|
|
1166
2190
|
return;
|
|
1167
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,
|
|
2610
|
+
});
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
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
|
+
});
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
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
|
+
});
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
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
|
+
});
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
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
|
+
});
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
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
|
+
});
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (pathname === "/api/v1/provisioning/resources/link" && req.method === "POST") {
|
|
2738
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2739
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
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,
|
|
2763
|
+
});
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
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");
|
|
2780
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
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,
|
|
2803
|
+
});
|
|
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,
|
|
2821
|
+
});
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
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" });
|
|
2843
|
+
sendJson(res, 200, {
|
|
2844
|
+
ok: true,
|
|
2845
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2846
|
+
...updated,
|
|
2847
|
+
});
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
1168
2850
|
// Governance Policy
|
|
2851
|
+
if (pathname === "/api/v1/policy" && req.method === "GET") {
|
|
2852
|
+
const summary = engine.getGuardrailsSummary(ctx);
|
|
2853
|
+
sendJson(res, 200, {
|
|
2854
|
+
ok: true,
|
|
2855
|
+
policy: summary.policy,
|
|
2856
|
+
plainEnglish: summary.plainEnglish,
|
|
2857
|
+
explanation: summary.plainEnglish.explanation,
|
|
2858
|
+
activeDefaults: summary.activeDefaults,
|
|
2859
|
+
activeGuardrails: summary.activeGuardrails,
|
|
2860
|
+
});
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
1169
2863
|
if (pathname === "/api/v1/policy" && req.method === "POST") {
|
|
1170
2864
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
1171
2865
|
const updated = store.updateWorkspacePolicy(ctx.workspace.id, sanitizePolicyPatch(body));
|
|
1172
|
-
|
|
2866
|
+
const plainEnglish = describePolicyInPlainEnglish(updated.policy);
|
|
2867
|
+
sendJson(res, 200, {
|
|
2868
|
+
ok: true,
|
|
2869
|
+
workspace: updated,
|
|
2870
|
+
plainEnglishGuardrails: plainEnglish,
|
|
2871
|
+
explanation: plainEnglish.explanation,
|
|
2872
|
+
});
|
|
1173
2873
|
return;
|
|
1174
2874
|
}
|
|
1175
2875
|
// Plans & Approvals & Execution
|
|
@@ -1197,18 +2897,53 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1197
2897
|
sendJson(res, 201, { ok: true, ...planned });
|
|
1198
2898
|
return;
|
|
1199
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
|
+
};
|
|
1200
2913
|
const planApproveMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/approve$/);
|
|
1201
2914
|
if (planApproveMatch && req.method === "POST") {
|
|
1202
2915
|
const planId = decodeURIComponent(planApproveMatch[1]);
|
|
1203
|
-
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);
|
|
1204
2922
|
sendJson(res, 200, { ok: true, plan });
|
|
1205
2923
|
return;
|
|
1206
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
|
+
}
|
|
1207
2941
|
const planExecuteMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/execute$/);
|
|
1208
2942
|
if (planExecuteMatch && req.method === "POST") {
|
|
1209
2943
|
const planId = decodeURIComponent(planExecuteMatch[1]);
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
2944
|
+
const planCtx = resolvePlanCtx(planId);
|
|
2945
|
+
const plan = engine.getPlan(planCtx, planId);
|
|
2946
|
+
const result = await engine.executePlanForRisk(planCtx, planId, plan.riskLevel);
|
|
1212
2947
|
sendJson(res, 200, {
|
|
1213
2948
|
ok: true,
|
|
1214
2949
|
...result,
|
|
@@ -1219,8 +2954,24 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1219
2954
|
const planGetMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)$/);
|
|
1220
2955
|
if (planGetMatch && req.method === "GET") {
|
|
1221
2956
|
const planId = decodeURIComponent(planGetMatch[1]);
|
|
1222
|
-
const
|
|
1223
|
-
|
|
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
|
+
});
|
|
1224
2975
|
return;
|
|
1225
2976
|
}
|
|
1226
2977
|
// Runs & Receipts
|
|
@@ -1236,7 +2987,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1236
2987
|
const runId = decodeURIComponent(runCancelMatch[1]);
|
|
1237
2988
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
1238
2989
|
const cancelled = engine.cancelRun(ctx, runId, body.reason ? String(body.reason) : undefined);
|
|
1239
|
-
sendJson(res, 200,
|
|
2990
|
+
sendJson(res, 200, cancelled);
|
|
1240
2991
|
return;
|
|
1241
2992
|
}
|
|
1242
2993
|
const runGetMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
|
|
@@ -1246,6 +2997,20 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1246
2997
|
sendJson(res, 200, { ok: true, run });
|
|
1247
2998
|
return;
|
|
1248
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
|
+
}
|
|
1249
3014
|
if (pathname.startsWith("/api/v1/receipts/") && req.method === "GET") {
|
|
1250
3015
|
const receiptId = decodeURIComponent(pathname.slice("/api/v1/receipts/".length));
|
|
1251
3016
|
const verified = engine.getReceipt(ctx, receiptId);
|
|
@@ -1466,13 +3231,327 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1466
3231
|
sendJson(res, 200, { ok: true, received: true });
|
|
1467
3232
|
return;
|
|
1468
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
|
+
}
|
|
1469
3551
|
sendJson(res, 404, { ok: false, error: `Route not found: ${req.method} ${pathname}` });
|
|
1470
3552
|
}
|
|
1471
3553
|
catch (err) {
|
|
1472
|
-
sendJson(res, 400,
|
|
1473
|
-
ok: false,
|
|
1474
|
-
error: String(err?.message || err || "Internal error"),
|
|
1475
|
-
});
|
|
3554
|
+
sendJson(res, err instanceof MuxoToolError ? err.statusCode : 400, errorPayloadFrom(err, "Internal error"));
|
|
1476
3555
|
}
|
|
1477
3556
|
});
|
|
1478
3557
|
return { server, engine, store, defaultApiKey: bootstrap.defaultApiKey };
|