@muxoai/platform 1.3.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/ARCHITECTURE.md +16 -6
  4. package/README.md +7 -5
  5. package/dist/cli.js +393 -13
  6. package/dist/composio-catalog.d.ts +58 -0
  7. package/dist/composio-catalog.js +474 -0
  8. package/dist/config.d.ts +10 -0
  9. package/dist/config.js +16 -0
  10. package/dist/durable-workflow.d.ts +45 -0
  11. package/dist/durable-workflow.js +90 -0
  12. package/dist/engine.d.ts +276 -5
  13. package/dist/engine.js +1261 -71
  14. package/dist/mcp-schema.d.ts +125 -0
  15. package/dist/mcp-schema.js +1076 -0
  16. package/dist/mcp.d.ts +2 -30
  17. package/dist/mcp.js +505 -539
  18. package/dist/memory-graph.d.ts +21 -0
  19. package/dist/memory-graph.js +173 -0
  20. package/dist/nl.js +155 -0
  21. package/dist/oauth.d.ts +32 -0
  22. package/dist/oauth.js +134 -0
  23. package/dist/policy.d.ts +75 -1
  24. package/dist/policy.js +1119 -0
  25. package/dist/providers/composio-instant.js +225 -5
  26. package/dist/providers/composio-sdk.d.ts +68 -5
  27. package/dist/providers/composio-sdk.js +683 -78
  28. package/dist/providers/microsoft-governance.d.ts +28 -1
  29. package/dist/providers/microsoft-governance.js +224 -11
  30. package/dist/providers/stripe-billing.d.ts +42 -0
  31. package/dist/providers/stripe-billing.js +230 -1
  32. package/dist/providers/stripe-projects.d.ts +451 -3
  33. package/dist/providers/stripe-projects.js +2106 -87
  34. package/dist/providers/supermemory.d.ts +74 -1
  35. package/dist/providers/supermemory.js +426 -0
  36. package/dist/provisioning-managed.d.ts +50 -0
  37. package/dist/provisioning-managed.js +155 -0
  38. package/dist/sdk.d.ts +538 -8
  39. package/dist/sdk.js +377 -10
  40. package/dist/server.js +1841 -232
  41. package/dist/store.d.ts +113 -6
  42. package/dist/store.js +675 -52
  43. package/dist/types.d.ts +212 -5
  44. package/dist/ui/console-html.d.ts +10 -1
  45. package/dist/ui/console-html.js +1188 -462
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +2 -2
  49. package/plugins/muxo/.claude-plugin/plugin.json +1 -1
  50. package/plugins/muxo/.codex-plugin/plugin.json +1 -1
  51. package/plugins/muxo/plugin.json +2 -2
  52. package/plugins/muxo/skills/muxo/SKILL.md +42 -1
  53. package/plugins/muxo/skills/muxo/references/mcp.md +3 -2
  54. package/plugins/muxo/skills/setup/SKILL.md +8 -0
package/dist/server.js CHANGED
@@ -5,10 +5,15 @@ 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, MuxoToolError, clampWaitSeconds, errorPayloadFrom, isPublicMcpRequest, workspaceAgentDid, } from "./mcp-schema.js";
8
9
  import { MuxoStore } from "./store.js";
9
- import { sanitizePolicyPatch } from "./policy.js";
10
+ import { managedProviderConnectionList, managedProviderConnectionRequest, managedProviderConnectionUnlink, managedResource, managedResourceAction, } from "./provisioning-managed.js";
11
+ import { DEFAULT_WORKSPACE_POLICY, describePolicyInPlainEnglish, sanitizePolicyPatch, } from "./policy.js";
12
+ import { parseNaturalLanguageMemoryInstruction } from "./providers/supermemory.js";
10
13
  import { MUXO_VERSION } from "./version.js";
11
- import { isConsolePath, renderConsoleHtml, renderLandingHtml } from "./ui/console-html.js";
14
+ import { isConsolePath, renderApprovalPageHtml, renderConsoleHtml, renderLandingHtml, renderOAuthConsentHtml, } from "./ui/console-html.js";
15
+ import { fetchClerkProfile, looksLikeJwt, readClerkConfig, verifyClerkSessionToken, } from "./clerk.js";
16
+ import { DEFAULT_OAUTH_SCOPE, OAUTH_ACCESS_TOKEN_TTL_SECONDS, OAUTH_CODE_TTL_MS, oauthConnectionKeyName, oauthRedirectOrigin, oauthSecretToken, parseAuthorizeRequest, parseOAuthFormBody, verifyPkce, } from "./oauth.js";
12
17
  function renderSkillMarkdown(baseUrl) {
13
18
  return `---
14
19
  name: muxo
@@ -89,9 +94,32 @@ ${MUXO_MCP_TOOLS.map((t, i) => `${i + 1}. \`${t.name}\` — ${t.description}`).j
89
94
  1. **Discover**: Call \`search_tools\` with a keyword or lane filter.
90
95
  2. **Inspect**: Call \`inspect_tool\` to read the tool's \`inputSchema\`, \`riskLevel\`, and \`creditCost\`.
91
96
  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\`. Show that URL to the user, then call \`wait_for_connection({ provider })\` until \`status === "active"\`.
97
+ - 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"\`.
98
+ - 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.
99
+ - Always follow \`llmGuidance.nextToolCalls\` for the exact next tool call sequence.
93
100
  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
101
  5. **Verify Receipt**: Every run issues an HMAC-SHA256 chained cryptographic receipt (\`rcpt_*\`) verifiable via \`get_receipt\`.
102
+
103
+ ## Plain-English & Conditional Guardrails (LLM & MCP Harness Native)
104
+
105
+ Users and LLM harnesses can set safety rules in natural language or structured parameter rules; Muxo compiles and enforces them across all tool calls:
106
+
107
+ - "Only allow sending emails to @muxo.ai" → conditional recipient domain guardrail (\`must_end_with: @muxo.ai\`)
108
+ - "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"\`)
109
+ - "Only post Slack messages to #ops-alerts or #deployments" → channel allowlist guardrail
110
+ - "Never spend more than 25 credits per run" → workspace per-run credit cap
111
+
112
+ Set or inspect them via MCP \`get_run_context({ action: "set_guardrail", prompt: "...", parameter_rules: [...] })\`, \`POST /api/v1/do\` (\`{ "prompt": "..." }\`), \`POST /api/v1/guardrails\` (\`{ "prompt": "...", "parameterRules": [...] }\`), or list them with \`GET /api/v1/guardrails\`. Responses always include \`plainEnglishGuardrails\` so humans and agents can read the active rules.
113
+
114
+ ## Active Supermemory (teach Muxo once, auto-fill forever)
115
+
116
+ Teach preferences in plain English and Muxo auto-fills matching tool inputs on every future plan:
117
+
118
+ - "Hey Muxo, remember our default GitHub repo is acme/api and default region is us-east-1"
119
+ - "Hey Muxo, forget our staging redis"
120
+ - "What do you remember?"
121
+
122
+ \`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
123
  `;
96
124
  }
97
125
  async function readBody(req) {
@@ -135,17 +163,68 @@ export function createMuxoServer(customConfig) {
135
163
  client_id_issued_at: Math.floor(Date.now() / 1000),
136
164
  });
137
165
  const oauthCodes = new Map();
138
- const resolveContext = (req) => {
139
- const authHeader = String(req.headers.authorization || "").trim();
140
- if (authHeader.toLowerCase().startsWith("bearer ")) {
141
- const token = authHeader.slice(7).trim();
142
- const resolved = store.resolveAuthToken(token);
143
- if (resolved)
144
- return resolved;
166
+ const clerkEnv = config.clerkEnv ?? {};
167
+ const clerkConfig = () => readClerkConfig(clerkEnv, config.baseUrl);
168
+ const ownerEmails = () => String(clerkEnv.MUXO_OWNER_EMAILS || "")
169
+ .split(",")
170
+ .map((s) => s.trim().toLowerCase())
171
+ .filter(Boolean);
172
+ const bearerTokenOf = (req) => {
173
+ const header = String(req.headers.authorization || "").trim();
174
+ return /^bearer\s+/i.test(header) ? header.replace(/^bearer\s+/i, "").trim() : null;
175
+ };
176
+ /**
177
+ * Resolves the caller from a workspace API key, a console session token, or a Clerk session
178
+ * JWT. A bearer token that resolves to nothing is "invalid" and must never fall back to a
179
+ * default workspace.
180
+ */
181
+ const authenticate = async (req) => {
182
+ const token = bearerTokenOf(req);
183
+ if (token === null)
184
+ return { kind: "anonymous" };
185
+ if (!token)
186
+ return { kind: "invalid" };
187
+ const wsOverride = String(req.headers["x-muxo-workspace-id"] || "").trim() || null;
188
+ const resolved = store.resolveAuthToken(token, wsOverride);
189
+ if (resolved)
190
+ return { kind: "ok", ctx: resolved, via: resolved.authType };
191
+ const clerk = looksLikeJwt(token) ? clerkConfig() : null;
192
+ if (!clerk)
193
+ return { kind: "invalid" };
194
+ const session = await verifyClerkSessionToken(token, clerk);
195
+ if (!session)
196
+ return { kind: "invalid" };
197
+ let user = store.getClerkLinkedUser(session.userId);
198
+ if (!user) {
199
+ const profile = clerk.secretKey ? await fetchClerkProfile(session.userId, clerk.secretKey) : null;
200
+ const identity = {
201
+ email: profile?.email || session.email || null,
202
+ emailVerified: profile ? profile.emailVerified : Boolean(session.emailVerified),
203
+ name: profile?.name || null,
204
+ };
205
+ const joinsPrimary = Boolean(identity.email && identity.emailVerified && ownerEmails().includes(identity.email.toLowerCase()));
206
+ user = store.provisionClerkUser(session.userId, identity, joinsPrimary ? bootstrap.workspace.id : null);
145
207
  }
146
- // Default local console session fallback to the primary workspace
147
- const ws = store.getWorkspace(bootstrap.workspace.id);
148
- const user = store.getUser(ws.ownerUserId);
208
+ const targetWsId = wsOverride && store.userCanAccessWorkspace(user.id, wsOverride) ? wsOverride : user.workspaceId;
209
+ const workspace = store.getWorkspace(targetWsId) || store.getWorkspace(user.workspaceId);
210
+ if (!workspace)
211
+ return { kind: "invalid" };
212
+ return {
213
+ kind: "ok",
214
+ ctx: { user: { ...user, workspaceId: workspace.id }, workspace, actorId: user.id, authType: "session" },
215
+ via: "clerk",
216
+ clerkUserId: session.userId,
217
+ clerkSessionEmail: session.email && session.emailVerified ? session.email : null,
218
+ };
219
+ };
220
+ /** Without Clerk, a local server serves anonymous callers from its primary workspace. */
221
+ const localFallbackContext = (req) => {
222
+ const wsOverride = String(req.headers["x-muxo-workspace-id"] || "").trim() || null;
223
+ const fallbackWsId = wsOverride && store.userCanAccessWorkspace(bootstrap.user.id, wsOverride)
224
+ ? wsOverride
225
+ : bootstrap.user.workspaceId || bootstrap.workspace.id;
226
+ const ws = store.getWorkspace(fallbackWsId) || store.getWorkspace(bootstrap.workspace.id);
227
+ const user = store.getUser(ws.ownerUserId) || bootstrap.user;
149
228
  return {
150
229
  user,
151
230
  workspace: ws,
@@ -153,11 +232,95 @@ export function createMuxoServer(customConfig) {
153
232
  authType: "session",
154
233
  };
155
234
  };
235
+ const anonymousContext = () => {
236
+ const epoch = new Date(0).toISOString();
237
+ return {
238
+ user: { id: "usr_anonymous", email: "", name: "Anonymous", workspaceId: "ws_public", role: "member", createdAt: epoch },
239
+ workspace: {
240
+ id: "ws_public",
241
+ name: "Public catalog",
242
+ slug: "public",
243
+ ownerUserId: "usr_anonymous",
244
+ tier: "free",
245
+ monthlyActionsLimit: 0,
246
+ monthlyActionsUsed: 0,
247
+ creditsBalance: 0,
248
+ stripeCustomerId: null,
249
+ stripeSubscriptionId: null,
250
+ projectsEnvName: "muxo-public",
251
+ policy: { ...DEFAULT_WORKSPACE_POLICY },
252
+ createdAt: epoch,
253
+ updatedAt: epoch,
254
+ },
255
+ actorId: "usr_anonymous",
256
+ authType: "session",
257
+ };
258
+ };
259
+ const requestOriginOf = (req) => {
260
+ const proto = String(req.headers["x-forwarded-proto"] || "http").split(",")[0].trim();
261
+ return req.headers.host ? `${proto}://${req.headers.host}` : config.baseUrl;
262
+ };
263
+ const sendAuthRequired = (req, res, pathname, message) => {
264
+ const origin = requestOriginOf(req);
265
+ const resourceMetadata = pathname === "/mcp"
266
+ ? `${origin}/.well-known/oauth-protected-resource/mcp`
267
+ : `${origin}/.well-known/oauth-protected-resource`;
268
+ sendJson(res, 401, {
269
+ ok: false,
270
+ code: "auth_required",
271
+ error: message ||
272
+ `Sign in required. Send a Muxo API key (Authorization: Bearer mk_live_...) or sign in at ${origin}/login.`,
273
+ }, { "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadata}"` });
274
+ };
275
+ /** Emails the signed-in person has verified; agent API keys never count as a person. */
276
+ const verifiedEmailsOf = async (auth) => {
277
+ if (auth.kind !== "ok")
278
+ return [];
279
+ if (auth.via === "session")
280
+ return auth.ctx.user.email ? [auth.ctx.user.email.toLowerCase()] : [];
281
+ if (auth.via !== "clerk")
282
+ return [];
283
+ const secretKey = clerkConfig()?.secretKey;
284
+ if (secretKey && auth.clerkUserId) {
285
+ const profile = await fetchClerkProfile(auth.clerkUserId, secretKey);
286
+ if (profile)
287
+ return profile.verifiedEmails;
288
+ }
289
+ return auth.clerkSessionEmail ? [auth.clerkSessionEmail.toLowerCase()] : [];
290
+ };
291
+ const signedInPerson = (auth) => auth.kind === "ok" && auth.via !== "api_key";
292
+ /**
293
+ * Resolves the caller of a protected route, or answers 401 and returns null. Public routes
294
+ * accept anonymous callers with an empty catalog context once Clerk sign-in is configured.
295
+ */
296
+ const requireContext = async (req, res, pathname, publicRoute = false) => {
297
+ const auth = await authenticate(req);
298
+ if (auth.kind === "ok")
299
+ return { ctx: auth.ctx, auth };
300
+ if (auth.kind === "invalid") {
301
+ sendAuthRequired(req, res, pathname);
302
+ return null;
303
+ }
304
+ if (clerkConfig()) {
305
+ if (publicRoute)
306
+ return { ctx: anonymousContext(), auth };
307
+ sendAuthRequired(req, res, pathname);
308
+ return null;
309
+ }
310
+ return { ctx: localFallbackContext(req), auth };
311
+ };
312
+ const consentPageHeaders = {
313
+ "Content-Type": "text/html; charset=utf-8",
314
+ "Cache-Control": "no-store",
315
+ "Content-Security-Policy": "frame-ancestors 'none'",
316
+ "X-Frame-Options": "DENY",
317
+ "Referrer-Policy": "no-referrer",
318
+ };
156
319
  const server = http.createServer(async (req, res) => {
157
320
  if (req.method === "OPTIONS") {
158
321
  res.writeHead(204, {
159
322
  "Access-Control-Allow-Origin": "*",
160
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
323
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Muxo-Workspace-Id",
161
324
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
162
325
  });
163
326
  res.end();
@@ -202,6 +365,20 @@ export function createMuxoServer(customConfig) {
202
365
  });
203
366
  return;
204
367
  }
368
+ if (pathname === "/.well-known/jwks.json" && req.method === "GET") {
369
+ sendJson(res, 200, {
370
+ keys: [
371
+ {
372
+ kty: "OKP",
373
+ crv: "Ed25519",
374
+ use: "sig",
375
+ alg: "EdDSA",
376
+ kid: "muxo-governance-ed25519-v1",
377
+ },
378
+ ],
379
+ });
380
+ return;
381
+ }
205
382
  if ((pathname === "/.well-known/oauth-protected-resource" ||
206
383
  pathname === "/.well-known/oauth-protected-resource/mcp") &&
207
384
  req.method === "GET") {
@@ -216,7 +393,7 @@ export function createMuxoServer(customConfig) {
216
393
  resource,
217
394
  authorization_servers: [requestOrigin],
218
395
  bearer_methods_supported: ["header"],
219
- scopes_supported: ["muxo:mcp", "muxo:read", "muxo:execute", "muxo:provision"],
396
+ scopes_supported: ["mcp:tools", "muxo:mcp", "muxo:read", "muxo:execute", "muxo:provision"],
220
397
  resource_documentation: `${config.docsUrl}/quickstart`,
221
398
  mcp_endpoint: `${requestOrigin}/mcp`,
222
399
  });
@@ -238,6 +415,7 @@ export function createMuxoServer(customConfig) {
238
415
  userinfo_endpoint: `${requestOrigin}/oauth/userinfo`,
239
416
  jwks_uri: `${requestOrigin}/.well-known/jwks.json`,
240
417
  scopes_supported: [
418
+ "mcp:tools",
241
419
  "muxo:mcp",
242
420
  "muxo:read",
243
421
  "muxo:execute",
@@ -247,11 +425,7 @@ export function createMuxoServer(customConfig) {
247
425
  "email",
248
426
  ],
249
427
  response_types_supported: ["code"],
250
- grant_types_supported: [
251
- "authorization_code",
252
- "refresh_token",
253
- "client_credentials",
254
- ],
428
+ grant_types_supported: ["authorization_code", "refresh_token"],
255
429
  token_endpoint_auth_methods_supported: [
256
430
  "none",
257
431
  "client_secret_post",
@@ -301,149 +475,185 @@ export function createMuxoServer(customConfig) {
301
475
  sendJson(res, 201, clientRecord);
302
476
  return;
303
477
  }
304
- if (pathname === "/oauth/authorize" &&
305
- (req.method === "GET" || req.method === "POST")) {
306
- const bodyParams = req.method === "POST"
307
- ? new URLSearchParams(await readBody(req))
308
- : url.searchParams;
309
- const clientId = bodyParams.get("client_id") ||
310
- url.searchParams.get("client_id") ||
311
- "muxo-openai-plugin";
312
- const redirectUri = bodyParams.get("redirect_uri") ||
313
- url.searchParams.get("redirect_uri") ||
314
- "http://127.0.0.1:3118/oauth/callback";
315
- const stateParam = bodyParams.get("state") || url.searchParams.get("state") || "";
316
- const scope = bodyParams.get("scope") ||
317
- url.searchParams.get("scope") ||
318
- "muxo:mcp muxo:read muxo:execute muxo:provision";
319
- const codeChallenge = bodyParams.get("code_challenge") ||
320
- url.searchParams.get("code_challenge") ||
321
- undefined;
322
- const codeChallengeMethod = bodyParams.get("code_challenge_method") ||
323
- url.searchParams.get("code_challenge_method") ||
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(),
478
+ if (pathname === "/oauth/authorize" && req.method === "GET") {
479
+ const parsed = parseAuthorizeRequest(url.searchParams);
480
+ const wantsJson = url.searchParams.get("format") === "json" ||
481
+ String(req.headers.accept || "").includes("application/json");
482
+ if ("error" in parsed) {
483
+ if (wantsJson) {
484
+ sendJson(res, 400, { error: "invalid_request", error_description: parsed.error }, { "Cache-Control": "no-store" });
485
+ }
486
+ else {
487
+ res.writeHead(400, consentPageHeaders);
488
+ res.end(renderOAuthConsentHtml({ error: parsed.error }));
489
+ }
490
+ return;
491
+ }
492
+ if (wantsJson) {
493
+ sendJson(res, 401, {
494
+ ok: false,
495
+ code: "interactive_sign_in_required",
496
+ error: "Open this authorization URL in a browser to sign in and approve access.",
497
+ authorizeUrl: url.toString(),
361
498
  });
362
499
  return;
363
500
  }
364
- res.writeHead(302, {
365
- Location: redirectTarget.toString(),
366
- "Access-Control-Allow-Origin": "*",
367
- });
368
- res.end();
501
+ const clerk = clerkConfig();
502
+ res.writeHead(200, consentPageHeaders);
503
+ res.end(renderOAuthConsentHtml({
504
+ clientName: parsed.clientName,
505
+ redirectOrigin: oauthRedirectOrigin(parsed.redirectUri),
506
+ clerk: clerk ? { publishableKey: clerk.publishableKey, frontendApi: clerk.frontendApi } : null,
507
+ request: {
508
+ client_id: parsed.clientId,
509
+ redirect_uri: parsed.redirectUri,
510
+ state: parsed.state,
511
+ code_challenge: parsed.codeChallenge || "",
512
+ code_challenge_method: parsed.codeChallengeMethod || "",
513
+ scope: parsed.scope,
514
+ },
515
+ }));
369
516
  return;
370
517
  }
371
- if (pathname === "/oauth/token" && req.method === "POST") {
372
- const rawBody = await readBody(req);
518
+ if (pathname === "/oauth/authorize" && req.method === "POST") {
519
+ const auth = await authenticate(req);
520
+ // A person must approve; agent API keys cannot grant themselves new access.
521
+ if (!signedInPerson(auth)) {
522
+ sendJson(res, 401, { ok: false, code: "auth_required", error: "Sign in to approve this connection." });
523
+ return;
524
+ }
525
+ const raw = await readBody(req);
373
526
  const contentType = String(req.headers["content-type"] || "");
374
- const params = {};
375
- if (contentType.includes("application/json")) {
376
- const parsed = JSON.parse(rawBody || "{}");
377
- for (const [k, v] of Object.entries(parsed)) {
378
- if (v !== undefined && v !== null)
379
- params[k] = String(v);
380
- }
527
+ const params = contentType.includes("application/json")
528
+ ? parseOAuthFormBody(raw, "application/json", "")
529
+ : new URLSearchParams(raw);
530
+ const parsed = parseAuthorizeRequest(params);
531
+ if ("error" in parsed) {
532
+ sendJson(res, 400, { ok: false, error: parsed.error });
533
+ return;
381
534
  }
382
- else {
383
- const sp = new URLSearchParams(rawBody);
384
- for (const [k, v] of sp.entries()) {
385
- params[k] = v;
386
- }
535
+ const redirectTarget = new URL(parsed.redirectUri);
536
+ if (parsed.state)
537
+ redirectTarget.searchParams.set("state", parsed.state);
538
+ redirectTarget.searchParams.set("iss", requestOriginOf(req));
539
+ if (params.get("decision") === "deny") {
540
+ redirectTarget.searchParams.set("error", "access_denied");
541
+ sendJson(res, 200, { ok: true, redirect: redirectTarget.toString() });
542
+ return;
387
543
  }
388
- const grantType = params.grant_type || "authorization_code";
389
- const code = params.code || "";
390
- const codeVerifier = params.code_verifier || "";
391
- let accessToken = bootstrap.defaultApiKey || "";
392
- let scope = params.scope || "muxo:mcp muxo:read muxo:execute muxo:provision";
393
- if (grantType === "authorization_code" && code) {
394
- const record = oauthCodes.get(code);
395
- if (!record || record.expiresAt < Date.now()) {
396
- sendJson(res, 400, {
397
- error: "invalid_grant",
398
- error_description: "Authorization code is invalid or expired.",
399
- });
400
- return;
544
+ const now = Date.now();
545
+ for (const [c, rec] of oauthCodes)
546
+ if (rec.expiresAt < now)
547
+ oauthCodes.delete(c);
548
+ const code = oauthSecretToken("muxo_code");
549
+ oauthCodes.set(code, {
550
+ userId: auth.ctx.user.id,
551
+ workspaceId: auth.ctx.workspace.id,
552
+ clientId: parsed.clientId,
553
+ clientName: parsed.clientName,
554
+ redirectUri: parsed.redirectUri,
555
+ scope: parsed.scope,
556
+ codeChallenge: parsed.codeChallenge,
557
+ codeChallengeMethod: parsed.codeChallengeMethod,
558
+ expiresAt: now + OAUTH_CODE_TTL_MS,
559
+ });
560
+ redirectTarget.searchParams.set("code", code);
561
+ sendJson(res, 200, { ok: true, redirect: redirectTarget.toString(), workspaceId: auth.ctx.workspace.id });
562
+ return;
563
+ }
564
+ if (pathname === "/oauth/token" && req.method === "POST") {
565
+ const form = parseOAuthFormBody(await readBody(req), String(req.headers["content-type"] || ""), String(req.headers.authorization || ""));
566
+ const grantType = form.get("grant_type") || "";
567
+ const tokenHeaders = { "Cache-Control": "no-store", Pragma: "no-cache" };
568
+ const oauthError = (error, description) => sendJson(res, 400, { error, error_description: description }, tokenHeaders);
569
+ if (grantType === "authorization_code") {
570
+ const code = form.get("code") || "";
571
+ const rec = oauthCodes.get(code);
572
+ if (!rec)
573
+ return oauthError("invalid_grant", "Authorization code is invalid or was already used.");
574
+ oauthCodes.delete(code);
575
+ if (rec.expiresAt < Date.now())
576
+ return oauthError("invalid_grant", "Authorization code expired.");
577
+ const clientId = form.get("client_id");
578
+ if (clientId && clientId !== rec.clientId)
579
+ return oauthError("invalid_grant", "client_id does not match.");
580
+ const redirectUri = form.get("redirect_uri");
581
+ if (redirectUri && redirectUri !== rec.redirectUri) {
582
+ return oauthError("invalid_grant", "redirect_uri does not match.");
401
583
  }
402
- if (record.codeChallenge) {
403
- if (!codeVerifier) {
404
- sendJson(res, 400, {
405
- error: "invalid_grant",
406
- error_description: "PKCE code_verifier is required.",
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
- }
584
+ if (rec.codeChallenge) {
585
+ const check = await verifyPkce(form.get("code_verifier") || "", rec.codeChallenge, rec.codeChallengeMethod);
586
+ if (check === "missing")
587
+ return oauthError("invalid_grant", "code_verifier is required.");
588
+ if (check === "mismatch")
589
+ return oauthError("invalid_grant", "code_verifier does not match.");
424
590
  }
425
- accessToken = record.apiKey;
426
- scope = record.scope;
427
- oauthCodes.delete(code);
428
- }
429
- else if (!accessToken) {
430
- accessToken = store.createApiKey({
431
- workspaceId: bootstrap.workspace.id,
432
- name: "OAuth Token Key",
591
+ const ws = store.getWorkspace(rec.workspaceId);
592
+ if (!ws || !store.userCanAccessWorkspace(rec.userId, ws.id)) {
593
+ return oauthError("invalid_grant", "Workspace is no longer available.");
594
+ }
595
+ const minted = store.createApiKey({
596
+ workspaceId: ws.id,
597
+ name: oauthConnectionKeyName(rec.clientName),
433
598
  environment: "live",
434
- }).secretKey;
599
+ });
600
+ const refreshToken = oauthSecretToken("muxo_rt");
601
+ store.saveOAuthRefreshToken({
602
+ refreshToken,
603
+ accessToken: minted.secretKey,
604
+ keyId: minted.record.id,
605
+ workspaceId: ws.id,
606
+ clientId: rec.clientId,
607
+ scope: rec.scope,
608
+ });
609
+ sendJson(res, 200, {
610
+ access_token: minted.secretKey,
611
+ token_type: "Bearer",
612
+ expires_in: OAUTH_ACCESS_TOKEN_TTL_SECONDS,
613
+ refresh_token: refreshToken,
614
+ scope: rec.scope,
615
+ }, tokenHeaders);
616
+ return;
435
617
  }
436
- sendJson(res, 200, {
437
- access_token: accessToken,
438
- token_type: "Bearer",
439
- expires_in: 3600,
440
- refresh_token: `muxo_rt_${crypto.randomBytes(16).toString("hex")}`,
441
- scope,
442
- });
443
- return;
618
+ if (grantType === "refresh_token") {
619
+ const refreshToken = form.get("refresh_token") || "";
620
+ const rec = refreshToken ? store.getOAuthRefreshToken(refreshToken) : null;
621
+ const active = rec ? store.findActiveApiKey(rec.accessToken) : null;
622
+ if (!rec || !active || active.id !== rec.keyId) {
623
+ if (rec)
624
+ store.deleteOAuthRefreshToken(refreshToken);
625
+ return oauthError("invalid_grant", "Refresh token is invalid or its access was revoked.");
626
+ }
627
+ const clientId = form.get("client_id");
628
+ if (clientId && clientId !== rec.clientId)
629
+ return oauthError("invalid_grant", "client_id does not match.");
630
+ // Refresh tokens rotate on every use; the replaced token stops working immediately.
631
+ store.deleteOAuthRefreshToken(refreshToken);
632
+ const rotated = oauthSecretToken("muxo_rt");
633
+ store.saveOAuthRefreshToken({
634
+ refreshToken: rotated,
635
+ accessToken: rec.accessToken,
636
+ keyId: rec.keyId,
637
+ workspaceId: rec.workspaceId,
638
+ clientId: rec.clientId,
639
+ scope: rec.scope,
640
+ });
641
+ sendJson(res, 200, {
642
+ access_token: rec.accessToken,
643
+ token_type: "Bearer",
644
+ expires_in: OAUTH_ACCESS_TOKEN_TTL_SECONDS,
645
+ refresh_token: rotated,
646
+ scope: rec.scope || DEFAULT_OAUTH_SCOPE,
647
+ }, tokenHeaders);
648
+ return;
649
+ }
650
+ return oauthError("unsupported_grant_type", "Use grant_type=authorization_code (with PKCE) or refresh_token.");
444
651
  }
445
652
  if (pathname === "/oauth/userinfo" && req.method === "GET") {
446
- const ctx = resolveContext(req);
653
+ const resolved = await requireContext(req, res, pathname);
654
+ if (!resolved)
655
+ return;
656
+ const { ctx } = resolved;
447
657
  const nickname = (ctx.user.email || "").split("@")[0] ||
448
658
  ctx.user.name.toLowerCase().replace(/\s+/g, ".") ||
449
659
  ctx.user.id;
@@ -452,7 +662,7 @@ export function createMuxoServer(customConfig) {
452
662
  id: ctx.user.id,
453
663
  name: ctx.user.name,
454
664
  email: ctx.user.email,
455
- email_verified: true,
665
+ email_verified: Boolean(ctx.user.email),
456
666
  nickname,
457
667
  workspace_id: ctx.workspace.id,
458
668
  workspace_name: ctx.workspace.name,
@@ -467,7 +677,7 @@ export function createMuxoServer(customConfig) {
467
677
  info: {
468
678
  title: "Muxo Unified Agent Execution, Infrastructure, Memory & Governance API",
469
679
  version: MUXO_VERSION,
470
- description: "One governed boundary over Connected Apps (Composio SDK), Instant Tools (Composio Instant), Cloud Infrastructure (Stripe Projects), Supermemory v5 Brain, and Microsoft Agent Governance Toolkit.",
680
+ description: "One governed boundary over Connected Apps, Instant Tools, Cloud Infrastructure, Workspace Memory, and Safety & Approvals.",
471
681
  },
472
682
  servers: [{ url: config.baseUrl }],
473
683
  paths: {
@@ -476,6 +686,12 @@ export function createMuxoServer(customConfig) {
476
686
  "/skill.md": { get: { summary: "Canonical agent skill & onboarding markdown" } },
477
687
  "/api/v1/agents/signup": { post: { summary: "Autonomous 2-call agent workspace & mk_live_* key signup" } },
478
688
  "/api/v1/agents/claim": { post: { summary: "Generate human owner claim link for an agent workspace" } },
689
+ "/api/v1/do": { post: { summary: "Natural-language task routing, planning, and optional execution" } },
690
+ "/api/v1/byok": {
691
+ get: { summary: "List configured BYOK provider keys in the AES-256-GCM vault" },
692
+ post: { summary: "Store or rotate a BYOK provider key (0 credit cost when active)" },
693
+ },
694
+ "/api/v1/byok/{provider}": { delete: { summary: "Delete a BYOK provider key from the vault" } },
479
695
  "/api/v1/tools": { get: { summary: "Search unified catalogue across instant.*, app.*, and resource.*" } },
480
696
  "/api/v1/tools/{slug}": { get: { summary: "Inspect tool schema, risk level, credit cost, and LLM context URL" } },
481
697
  "/api/v1/connections": {
@@ -485,19 +701,62 @@ export function createMuxoServer(customConfig) {
485
701
  "/api/v1/connections/wait": { post: { summary: "Wait/poll for browser OAuth handoff completion" } },
486
702
  "/api/v1/plans": {
487
703
  get: { summary: "List governed action plans" },
488
- post: { summary: "Create governed action plan with Supermemory recall & Ed25519 signature" },
704
+ post: { summary: "Create governed action plan with Workspace Memory recall & Ed25519 signature" },
489
705
  },
490
706
  "/api/v1/plans/{planId}/approve": { post: { summary: "Approve a pending high-risk action plan" } },
491
707
  "/api/v1/plans/{planId}/execute": { post: { summary: "Execute an approved plan and issue HMAC-SHA256 receipt" } },
492
708
  "/api/v1/resources": { get: { summary: "List provisioned cloud infrastructure resources" } },
493
709
  "/api/v1/resources/env": { get: { summary: "Pull decrypted AES-256-GCM workspace environment secrets" } },
710
+ "/api/v1/provisioning/connected_accounts": {
711
+ get: { summary: "Inspect workspace Stripe v2 Connected Account & requirements" },
712
+ post: { summary: "Create a Stripe v2 Connected Account with developer.projects capability" },
713
+ },
714
+ "/api/v1/provisioning/connected_account_links": {
715
+ post: { summary: "Create a Stripe v2 Hosted Account Link (account_onboarding / account_update)" },
716
+ },
717
+ "/api/v1/provisioning/connected_account_sessions": {
718
+ post: { summary: "Create a Stripe Connect Account Session for Accounts v2 Embedded Onboarding" },
719
+ },
720
+ "/api/v1/provisioning/eligibility": { get: { summary: "Check Stripe v2 Provisioning eligibility" } },
721
+ "/api/v1/provisioning/providers": { get: { summary: "List Stripe v2 Provisioning cloud providers" } },
722
+ "/api/v1/provisioning/services": { get: { summary: "List Stripe v2 Provisioning service catalog" } },
723
+ "/api/v1/provisioning/payment_profile": { get: { summary: "Inspect Stripe v2 Provisioning payment profile" } },
724
+ "/api/v1/provisioning/projects": {
725
+ get: { summary: "List Stripe v2 Provisioning projects" },
726
+ post: { summary: "Create a Stripe v2 Provisioning project" },
727
+ },
728
+ "/api/v1/provisioning/resources": {
729
+ get: { summary: "List Stripe v2 Provisioning resources" },
730
+ post: { summary: "Create a Stripe v2 Provisioning resource" },
731
+ },
732
+ "/api/v1/provisioning/resources/{id}/reveal_access_configuration": {
733
+ post: { summary: "Reveal decrypted credentials for a provisioned resource" },
734
+ },
494
735
  "/api/v1/memory": {
495
- get: { summary: "Recall isolated Supermemory v5 workspace brain facts" },
736
+ get: { summary: "Recall isolated Workspace Memory facts and hybrid context" },
496
737
  post: { summary: "Store a workspace memory fact or decision" },
497
738
  },
498
- "/api/v1/governance": { get: { summary: "Inspect Microsoft Agent Governance DID, policy, and Merkle audit chain" } },
499
- "/api/v1/billing/topup": { post: { summary: "Purchase Muxo credits (User pays Muxo -> Muxo pays suppliers)" } },
500
- "/api/v1/billing/settlements": { get: { summary: "Inspect Merchant-of-Record supplier settlement ledger" } },
739
+ "/api/v1/memory/facts": { get: { summary: "Query temporal Workspace Memory facts with lineage" } },
740
+ "/api/v1/memory/forget": { post: { summary: "Delete matching memories and retire matching facts" } },
741
+ "/api/v1/policy": {
742
+ get: { summary: "Inspect workspace safety & approval policy" },
743
+ post: { summary: "Update workspace safety & approval policy" },
744
+ },
745
+ "/api/v1/governance": { get: { summary: "Inspect workspace Safety & Approvals identity, policy, and audit chain" } },
746
+ "/api/v1/governance/kill-switch": { post: { summary: "Pause or resume workspace execution via kill switch" } },
747
+ "/api/v1/governance/scan-context": { post: { summary: "Scan untrusted text for prompt injection before storage or use" } },
748
+ "/api/v1/guardrails": {
749
+ get: { summary: "List active plain-English guardrails, policy, and learned memory defaults" },
750
+ post: { summary: "Set or reset guardrails in plain English (e.g. 'never send emails')" },
751
+ },
752
+ "/approve/{planId}": { get: { summary: "One-click human approval handoff page for a pending plan" } },
753
+ "/api/v1/context": {
754
+ get: { summary: "Inspect full workspace run context with a plain-English explanation" },
755
+ post: { summary: "Inspect workspace run context with an optional query" },
756
+ },
757
+ "/api/v1/receipts": { get: { summary: "List recent tamper-proof execution receipts with plain-English explanations" } },
758
+ "/api/v1/billing/topup": { post: { summary: "Purchase Muxo credits" } },
759
+ "/api/v1/billing/settlements": { get: { summary: "Inspect workspace credit activity ledger" } },
501
760
  },
502
761
  });
503
762
  return;
@@ -656,55 +915,98 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
656
915
  res.end(renderLandingHtml());
657
916
  return;
658
917
  }
659
- // Autonomous Agent Claim Redemption Route
918
+ // Workspace claim link: read-only; accepting requires the invited person's verified sign-in.
660
919
  if (pathname.startsWith("/claim/") && req.method === "GET") {
661
920
  const claimSlug = decodeURIComponent(pathname.slice("/claim/".length));
662
- const redeemed = store.redeemAgentClaim(claimSlug);
663
- if (!redeemed) {
664
- sendJson(res, 404, { ok: false, error: "Invalid or expired agent claim link" });
665
- return;
666
- }
667
- const claimConsoleKey = store.createApiKey({
668
- workspaceId: redeemed.workspace.id,
669
- name: `Owner Claim Session (${redeemed.targetEmail})`,
670
- environment: "live",
671
- }).secretKey;
921
+ const claim = store.getOpenAgentClaim(claimSlug);
672
922
  const wantsHtml = url.searchParams.get("format") !== "json" &&
673
923
  String(req.headers.accept || "").includes("text/html");
674
- if (wantsHtml) {
675
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
676
- res.end(`<!DOCTYPE html>
677
- <html lang="en">
678
- <head>
679
- <meta charset="utf-8" />
680
- <title>Muxo Agent Workspace Claimed</title>
681
- <style>
682
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #090a0f; color: #f4f4f6; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
683
- .card { background: #12141f; border: 1px solid #26293d; border-radius: 14px; padding: 32px; max-width: 480px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.45); }
684
- .badge { display: inline-block; background: rgba(45,212,191,0.15); color: #2dd4bf; border: 1px solid rgba(45,212,191,0.35); border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
685
- h1 { font-size: 22px; margin: 0 0 10px; }
686
- p { color: #a1a7bb; font-size: 14px; line-height: 1.5; margin: 0 0 20px; }
687
- code { background: #1c1f30; padding: 3px 7px; border-radius: 6px; color: #e2e8f0; font-size: 12px; }
688
- .btn { display: inline-block; background: #ff5a1f; color: #fff; text-decoration: none; padding: 10px 18px; border-radius: 8px; font-size: 14px; font-weight: 600; }
689
- </style>
690
- </head>
691
- <body>
692
- <div class="card">
693
- <div class="badge">Ownership Bound</div>
694
- <h1>Workspace Claimed</h1>
695
- <p>Workspace <code>${redeemed.workspace.name}</code> (${redeemed.workspace.id}) claimed by <strong>${redeemed.targetEmail}</strong>.</p>
696
- <p>Console key: <code>${claimConsoleKey}</code><br />Choose “Use an API key instead” in the console and paste it.</p>
697
- <a class="btn" href="/?claimed=${encodeURIComponent(redeemed.workspace.slug)}">Open Muxo Console →</a>
698
- </div>
699
- </body>
700
- </html>`);
924
+ if (!claim) {
925
+ const error = "This claim link is invalid, expired, or was already used.";
926
+ if (wantsHtml) {
927
+ res.writeHead(410, consentPageHeaders);
928
+ res.end(renderOAuthConsentHtml({
929
+ error,
930
+ claim: { workspaceName: "", targetEmail: "", action: "", cancelUrl: "/" },
931
+ }));
932
+ }
933
+ else {
934
+ sendJson(res, 410, { ok: false, code: "claim_invalid", error });
935
+ }
936
+ return;
937
+ }
938
+ if (!wantsHtml) {
939
+ sendJson(res, 200, {
940
+ ok: true,
941
+ code: "sign_in_required",
942
+ message: `Open this link in a browser and sign in as ${claim.targetEmail} to claim the workspace.`,
943
+ claim: {
944
+ workspaceName: claim.workspace.name,
945
+ targetEmail: claim.targetEmail,
946
+ status: claim.status,
947
+ expiresAt: claim.expiresAt,
948
+ },
949
+ acceptEndpoint: `${requestOriginOf(req)}/api/v1/claims/${encodeURIComponent(claimSlug)}/accept`,
950
+ });
951
+ return;
952
+ }
953
+ const clerk = clerkConfig();
954
+ res.writeHead(200, consentPageHeaders);
955
+ res.end(renderOAuthConsentHtml({
956
+ clerk: clerk ? { publishableKey: clerk.publishableKey, frontendApi: clerk.frontendApi } : null,
957
+ claim: {
958
+ workspaceName: claim.workspace.name,
959
+ targetEmail: claim.targetEmail,
960
+ action: `/api/v1/claims/${encodeURIComponent(claimSlug)}/accept`,
961
+ cancelUrl: "/",
962
+ },
963
+ }));
964
+ return;
965
+ }
966
+ const claimAcceptMatch = pathname.match(/^\/api\/v1\/claims\/([^/]+)\/accept$/);
967
+ if (claimAcceptMatch && req.method === "POST") {
968
+ const auth = await authenticate(req);
969
+ // Agents mint claim links with their own key, so a key must never be able to accept one.
970
+ if (!signedInPerson(auth)) {
971
+ sendJson(res, 401, {
972
+ ok: false,
973
+ code: "auth_required",
974
+ error: "Sign in as the invited person to claim this workspace.",
975
+ });
976
+ return;
977
+ }
978
+ const claimSlug = decodeURIComponent(claimAcceptMatch[1]);
979
+ const claim = store.getOpenAgentClaim(claimSlug);
980
+ if (!claim) {
981
+ sendJson(res, 410, {
982
+ ok: false,
983
+ code: "claim_invalid",
984
+ error: "This claim link is invalid, expired, or was already used.",
985
+ });
986
+ return;
987
+ }
988
+ const emails = await verifiedEmailsOf(auth);
989
+ if (!emails.includes(claim.targetEmail.toLowerCase())) {
990
+ sendJson(res, 403, {
991
+ ok: false,
992
+ code: "claim_email_mismatch",
993
+ error: `This workspace was offered to ${claim.targetEmail}. Sign in with that verified email to claim it.`,
994
+ });
995
+ return;
996
+ }
997
+ const workspace = store.acceptAgentClaim(claimSlug, auth.ctx.user.id);
998
+ if (!workspace) {
999
+ sendJson(res, 410, {
1000
+ ok: false,
1001
+ code: "claim_invalid",
1002
+ error: "This claim link is invalid, expired, or was already used.",
1003
+ });
701
1004
  return;
702
1005
  }
703
1006
  sendJson(res, 200, {
704
1007
  ok: true,
705
- message: `Workspace '${redeemed.workspace.name}' claimed by ${redeemed.targetEmail}`,
706
- apiKey: claimConsoleKey,
707
- claim: redeemed,
1008
+ redirect: `${requestOriginOf(req)}/`,
1009
+ workspace: { id: workspace.id, name: workspace.name },
708
1010
  });
709
1011
  return;
710
1012
  }
@@ -720,7 +1022,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
720
1022
  const connectedAccountId = url.searchParams.get("connected_account_id") ||
721
1023
  url.searchParams.get("connectedAccountId") ||
722
1024
  undefined;
723
- const callbackStatus = url.searchParams.get("status") || "success";
1025
+ const oauthError = url.searchParams.get("error") || url.searchParams.get("error_description");
1026
+ const callbackStatus = oauthError ? "failed" : url.searchParams.get("status") || "success";
724
1027
  const handoff = await engine.handleOAuthCallback({
725
1028
  workspaceId,
726
1029
  provider,
@@ -764,19 +1067,57 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
764
1067
  </html>`);
765
1068
  return;
766
1069
  }
1070
+ // One-click Plan Approval Handoff Page (GET /approve/:planId)
1071
+ const approvePageMatch = pathname.match(/^\/approve\/([^/]+)$/);
1072
+ if (approvePageMatch && req.method === "GET") {
1073
+ const planId = decodeURIComponent(approvePageMatch[1]);
1074
+ const clerk = clerkConfig();
1075
+ res.writeHead(200, {
1076
+ "Content-Type": "text/html; charset=utf-8",
1077
+ "Cache-Control": "no-store",
1078
+ "Content-Security-Policy": "frame-ancestors 'none'",
1079
+ "X-Frame-Options": "DENY",
1080
+ "Referrer-Policy": "no-referrer",
1081
+ });
1082
+ res.end(renderApprovalPageHtml({
1083
+ planId,
1084
+ clerk: clerk
1085
+ ? {
1086
+ publishableKey: clerk.publishableKey,
1087
+ frontendApi: clerk.frontendApi,
1088
+ }
1089
+ : null,
1090
+ }));
1091
+ return;
1092
+ }
767
1093
  // Autonomous Agent 2-Call Signup — Public
768
1094
  if ((pathname === "/api/v1/agents/signup" ||
769
1095
  pathname === "/api/v1/auth/signup" ||
770
1096
  pathname === "/api/signup") &&
771
1097
  req.method === "POST") {
772
1098
  const body = JSON.parse((await readBody(req)) || "{}");
1099
+ const ownerEmail = String(body.ownerEmail || body.email || "").trim().toLowerCase();
1100
+ if (ownerEmail && !/^[^\s@]+@[^\s@]+$/.test(ownerEmail)) {
1101
+ sendJson(res, 400, { ok: false, error: "ownerEmail must be a valid email address" });
1102
+ return;
1103
+ }
773
1104
  const created = store.createAutonomousAgentSignup({
774
1105
  agentName: body.agentName ? String(body.agentName) : undefined,
775
1106
  harness: body.harness ? String(body.harness) : undefined,
776
1107
  });
1108
+ const claim = store.createAgentClaim({
1109
+ workspaceId: created.workspace.id,
1110
+ email: ownerEmail || null,
1111
+ baseUrl: config.baseUrl,
1112
+ });
777
1113
  sendJson(res, 201, {
778
1114
  ok: true,
779
1115
  agentId: created.user.id,
1116
+ agentDid: workspaceAgentDid(created.workspace.id),
1117
+ claim_slug: claim.claimSlug,
1118
+ claimSlug: claim.claimSlug,
1119
+ claimUrl: claim.claimUrl,
1120
+ claim: { ...claim, targetEmail: claim.targetEmail || null },
780
1121
  workspace: {
781
1122
  id: created.workspace.id,
782
1123
  name: created.workspace.name,
@@ -794,6 +1135,12 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
794
1135
  return;
795
1136
  }
796
1137
  // Streamable HTTP MCP Endpoint (POST /mcp, GET /mcp, HEAD /mcp, DELETE /mcp)
1138
+ if (pathname === "/mcp" && (req.method === "GET" || req.method === "HEAD" || req.method === "DELETE")) {
1139
+ if ((await authenticate(req)).kind === "invalid") {
1140
+ sendAuthRequired(req, res, pathname);
1141
+ return;
1142
+ }
1143
+ }
797
1144
  if (pathname === "/mcp" && (req.method === "GET" || req.method === "HEAD")) {
798
1145
  const accept = String(req.headers.accept || "");
799
1146
  const sessionId = String(req.headers["mcp-session-id"] || "") ||
@@ -835,11 +1182,14 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
835
1182
  return;
836
1183
  }
837
1184
  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
1185
  const raw = await readBody(req);
842
1186
  const parsed = JSON.parse(raw || "{}");
1187
+ const resolved = await requireContext(req, res, pathname, !Array.isArray(parsed) && isPublicMcpRequest(parsed));
1188
+ if (!resolved)
1189
+ return;
1190
+ const { ctx } = resolved;
1191
+ const sessionId = String(req.headers["mcp-session-id"] || "") ||
1192
+ `mcp_sess_${ctx.workspace.id}`;
843
1193
  if (Array.isArray(parsed)) {
844
1194
  const responses = (await Promise.all(parsed.map((item) => handleJsonRpcMcpRequest(engine, ctx, (item || {}))))).filter((r) => r !== null);
845
1195
  sendJson(res, 200, responses, { "Mcp-Session-Id": sessionId });
@@ -860,7 +1210,23 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
860
1210
  }
861
1211
  // Auth Endpoints
862
1212
  if (pathname === "/api/v1/auth/config" && req.method === "GET") {
863
- sendJson(res, 200, { ok: true, provider: "password", clerk: null, signInUrl: "/login" });
1213
+ const clerk = clerkConfig();
1214
+ sendJson(res, 200, {
1215
+ ok: true,
1216
+ provider: clerk ? "clerk" : "password",
1217
+ clerk: clerk ? { publishableKey: clerk.publishableKey, frontendApi: clerk.frontendApi } : null,
1218
+ signInUrl: `${requestOriginOf(req)}/login`,
1219
+ });
1220
+ return;
1221
+ }
1222
+ if ((pathname === "/api/v1/auth/register" || pathname === "/api/v1/auth/login") &&
1223
+ req.method === "POST" &&
1224
+ clerkConfig()) {
1225
+ sendJson(res, 410, {
1226
+ ok: false,
1227
+ code: "use_clerk",
1228
+ error: `Muxo accounts use secure sign-in. Open ${requestOriginOf(req)}/login to sign in or create an account.`,
1229
+ });
864
1230
  return;
865
1231
  }
866
1232
  if (pathname === "/api/v1/auth/register" && req.method === "POST") {
@@ -879,6 +1245,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
879
1245
  sendJson(res, 201, {
880
1246
  ok: true,
881
1247
  ...created,
1248
+ token: created.sessionToken,
882
1249
  apiKey: initialKey.secretKey,
883
1250
  });
884
1251
  return;
@@ -890,21 +1257,34 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
890
1257
  sendJson(res, 401, { ok: false, error: "Invalid email or password" });
891
1258
  return;
892
1259
  }
893
- sendJson(res, 200, { ok: true, ...loggedIn });
1260
+ sendJson(res, 200, { ok: true, ...loggedIn, token: loggedIn.sessionToken });
894
1261
  return;
895
1262
  }
896
- const ctx = resolveContext(req);
1263
+ const isPublicRoute = ((pathname === "/api/v1/tools" || pathname === "/v1/tools/search") &&
1264
+ (req.method === "GET" || req.method === "POST")) ||
1265
+ (pathname.startsWith("/api/v1/tools/") && req.method === "GET") ||
1266
+ (pathname === "/api/v1/billing/webhook" && req.method === "POST");
1267
+ const resolvedCtx = await requireContext(req, res, pathname, isPublicRoute);
1268
+ if (!resolvedCtx)
1269
+ return;
1270
+ const { ctx, auth } = resolvedCtx;
897
1271
  // Console Bootstrap
898
1272
  if (pathname === "/api/v1/bootstrap" && req.method === "GET") {
899
1273
  const org = engine.getOrganization(ctx).organization;
1274
+ const guardrailsSummary = engine.getGuardrailsSummary(ctx);
900
1275
  sendJson(res, 200, {
901
1276
  ok: true,
902
1277
  profile: engine.getProfile(ctx),
903
1278
  organization: org,
904
1279
  governance: engine.governance.getStatus(),
1280
+ plainEnglishGuardrails: guardrailsSummary.plainEnglish,
1281
+ activeMemoryDefaults: guardrailsSummary.activeDefaults,
1282
+ activeGuardrails: guardrailsSummary.activeGuardrails,
905
1283
  supermemory: {
906
1284
  enabled: engine.supermemory.isEnabled(),
907
1285
  namespace: engine.supermemory.getWorkspaceNamespace(ctx.workspace.id),
1286
+ activeDefaults: guardrailsSummary.activeDefaults,
1287
+ activeGuardrails: guardrailsSummary.activeGuardrails,
908
1288
  },
909
1289
  surfaces: {
910
1290
  publicWebUrl: config.publicWebUrl,
@@ -913,7 +1293,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
913
1293
  },
914
1294
  apiKeys: store.listApiKeys(ctx.workspace.id),
915
1295
  byokKeys: store.listByokKeys(ctx.workspace.id),
916
- defaultApiKey: bootstrap.defaultApiKey || null,
1296
+ defaultApiKey: ctx.workspace.id === bootstrap.workspace.id ? bootstrap.defaultApiKey || null : null,
1297
+ workspaces: store.listUserWorkspaces(ctx.user.id, ctx.workspace.id),
917
1298
  connections: store.listConnections(ctx.workspace.id),
918
1299
  resources: store.listResources(ctx.workspace.id),
919
1300
  plans: store.listPlans(ctx.workspace.id, 25),
@@ -921,11 +1302,122 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
921
1302
  });
922
1303
  return;
923
1304
  }
1305
+ // Multi-Workspace & Team API Key Onboarding Endpoints
1306
+ if (pathname === "/api/v1/workspaces" && req.method === "GET") {
1307
+ sendJson(res, 200, {
1308
+ ok: true,
1309
+ currentWorkspaceId: ctx.workspace.id,
1310
+ workspaces: store.listUserWorkspaces(ctx.user.id, ctx.workspace.id),
1311
+ });
1312
+ return;
1313
+ }
1314
+ if (pathname === "/api/v1/workspaces" && req.method === "POST") {
1315
+ const body = JSON.parse((await readBody(req)) || "{}");
1316
+ const name = String(body.name || body.workspaceName || "Team Workspace").trim();
1317
+ const teamName = body.teamName ? String(body.teamName).trim() : null;
1318
+ const switchActive = body.switchActive !== false;
1319
+ const created = store.createWorkspaceForUser({
1320
+ userId: ctx.user.id,
1321
+ name,
1322
+ teamName,
1323
+ switchActive,
1324
+ });
1325
+ sendJson(res, 201, {
1326
+ ok: true,
1327
+ ...created,
1328
+ workspaces: store.listUserWorkspaces(ctx.user.id, created.workspace.id),
1329
+ });
1330
+ return;
1331
+ }
1332
+ if (pathname === "/api/v1/workspaces/switch" && req.method === "POST") {
1333
+ const body = JSON.parse((await readBody(req)) || "{}");
1334
+ const workspaceId = String(body.workspaceId || body.workspace_id || "").trim();
1335
+ const switched = store.switchUserWorkspace(ctx.user.id, workspaceId);
1336
+ if (!switched) {
1337
+ sendJson(res, 404, {
1338
+ ok: false,
1339
+ error: "Workspace not found or not linked to your account",
1340
+ });
1341
+ return;
1342
+ }
1343
+ sendJson(res, 200, {
1344
+ ok: true,
1345
+ ...switched,
1346
+ workspaces: store.listUserWorkspaces(ctx.user.id, switched.workspace.id),
1347
+ });
1348
+ return;
1349
+ }
1350
+ if ((pathname === "/api/v1/workspaces/join" || pathname === "/api/v1/auth/key") &&
1351
+ req.method === "POST") {
1352
+ const body = JSON.parse((await readBody(req)) || "{}");
1353
+ const rawKey = String(body.apiKey || body.api_key || body.key || "").trim();
1354
+ const hasExplicitSession = Boolean(String(req.headers.authorization || "").trim()) &&
1355
+ ctx.authType === "session";
1356
+ const joined = store.joinWorkspaceWithApiKey({
1357
+ apiKey: rawKey,
1358
+ userId: hasExplicitSession ? ctx.user.id : null,
1359
+ switchActive: body.switchActive !== false,
1360
+ });
1361
+ if (!joined) {
1362
+ sendJson(res, 401, {
1363
+ ok: false,
1364
+ error: "Invalid or revoked workspace API key (expected mk_live_... or mk_test_...)",
1365
+ });
1366
+ return;
1367
+ }
1368
+ const sessionToken = joined.user && !hasExplicitSession
1369
+ ? store.createSession(joined.user.id, joined.workspace.id)
1370
+ : undefined;
1371
+ sendJson(res, 200, {
1372
+ ok: true,
1373
+ workspace: joined.workspace,
1374
+ keyRecord: joined.keyRecord,
1375
+ user: joined.user,
1376
+ membership: joined.membership,
1377
+ ...(sessionToken ? { sessionToken, token: sessionToken } : {}),
1378
+ workspaces: joined.user
1379
+ ? store.listUserWorkspaces(joined.user.id, joined.workspace.id)
1380
+ : [],
1381
+ });
1382
+ return;
1383
+ }
1384
+ if ((pathname === "/api/v1/workspaces/onboard-team" ||
1385
+ pathname === "/api/v1/teams/onboard") &&
1386
+ req.method === "POST") {
1387
+ const body = JSON.parse((await readBody(req)) || "{}");
1388
+ const teamName = String(body.teamName || body.team_name || body.name || "Engineering Team").trim();
1389
+ const role = (body.role ? String(body.role) : "operator");
1390
+ const mode = body.mode === "dedicated_workspace" ? "dedicated_workspace" : "shared_workspace";
1391
+ const inviteEmail = body.inviteEmail || body.email ? String(body.inviteEmail || body.email).trim() : null;
1392
+ const kit = store.onboardTeam({
1393
+ currentWorkspaceId: ctx.workspace.id,
1394
+ userId: ctx.user.id,
1395
+ userEmail: ctx.user.email,
1396
+ teamName,
1397
+ role,
1398
+ mode,
1399
+ scopes: Array.isArray(body.scopes) ? body.scopes : undefined,
1400
+ inviteEmail,
1401
+ baseUrl: config.baseUrl,
1402
+ });
1403
+ sendJson(res, 201, {
1404
+ ok: true,
1405
+ kit,
1406
+ onboardingKit: kit,
1407
+ ...kit,
1408
+ workspaces: store.listUserWorkspaces(ctx.user.id, ctx.workspace.id),
1409
+ });
1410
+ return;
1411
+ }
924
1412
  // Autonomous Agent Claim Link Creation (Authenticated)
925
1413
  if (pathname === "/api/v1/agents/claim" && req.method === "POST") {
1414
+ if (auth.kind !== "ok") {
1415
+ sendAuthRequired(req, res, pathname);
1416
+ return;
1417
+ }
926
1418
  const body = JSON.parse((await readBody(req)) || "{}");
927
- const email = String(body.email || "").trim();
928
- if (!email || !email.includes("@")) {
1419
+ const email = String(body.email || body.ownerEmail || "").trim().toLowerCase();
1420
+ if (!email || !/^[^\s@]+@[^\s@]+$/.test(email)) {
929
1421
  sendJson(res, 400, { ok: false, error: "Valid target email is required" });
930
1422
  return;
931
1423
  }
@@ -938,6 +1430,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
938
1430
  ok: true,
939
1431
  claim: { ...claim, ownerEmail: email },
940
1432
  ...claim,
1433
+ claim_slug: claim.claimSlug,
941
1434
  ownerEmail: email,
942
1435
  });
943
1436
  return;
@@ -946,6 +1439,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
946
1439
  if (pathname === "/api/v1/governance" && req.method === "GET") {
947
1440
  const ws = store.getWorkspace(ctx.workspace.id) || ctx.workspace;
948
1441
  const fullReport = engine.governance.getGovernanceStatus(ws);
1442
+ const guardrailsSummary = engine.getGuardrailsSummary(ctx);
949
1443
  sendJson(res, 200, {
950
1444
  ok: true,
951
1445
  governance: {
@@ -957,6 +1451,65 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
957
1451
  auditChain: fullReport.auditChain,
958
1452
  },
959
1453
  workspacePolicy: ws.policy,
1454
+ plainEnglishGuardrails: guardrailsSummary.plainEnglish,
1455
+ activeMemoryDefaults: guardrailsSummary.activeDefaults,
1456
+ activeGuardrails: guardrailsSummary.activeGuardrails,
1457
+ });
1458
+ return;
1459
+ }
1460
+ if (pathname === "/api/v1/guardrails" && req.method === "GET") {
1461
+ const summary = engine.getGuardrailsSummary(ctx);
1462
+ sendJson(res, 200, {
1463
+ ok: true,
1464
+ ...summary,
1465
+ });
1466
+ return;
1467
+ }
1468
+ if (pathname === "/api/v1/guardrails" && req.method === "POST") {
1469
+ const body = JSON.parse((await readBody(req)) || "{}");
1470
+ if (body.reset === true) {
1471
+ const updated = store.updateWorkspacePolicy(ctx.workspace.id, DEFAULT_WORKSPACE_POLICY);
1472
+ const plainEnglish = describePolicyInPlainEnglish(updated.policy);
1473
+ sendJson(res, 200, {
1474
+ ok: true,
1475
+ workspace: updated,
1476
+ policy: updated.policy,
1477
+ plainEnglish,
1478
+ humanMessage: "Reset workspace guardrails to Safe Default.",
1479
+ });
1480
+ return;
1481
+ }
1482
+ const rawParamRules = Array.isArray(body.parameterRules)
1483
+ ? body.parameterRules
1484
+ : Array.isArray(body.parameter_rules)
1485
+ ? body.parameter_rules
1486
+ : undefined;
1487
+ const prompt = String(body.prompt ||
1488
+ body.rule ||
1489
+ body.instruction ||
1490
+ body.naturalLanguageRule ||
1491
+ (rawParamRules && rawParamRules.length > 0
1492
+ ? String(rawParamRules[0]?.description ||
1493
+ "Structured conditional parameter guardrail")
1494
+ : "")).trim();
1495
+ if (!prompt && (!rawParamRules || rawParamRules.length === 0)) {
1496
+ sendJson(res, 400, {
1497
+ ok: false,
1498
+ 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\").",
1499
+ });
1500
+ return;
1501
+ }
1502
+ const applied = await engine.applyNaturalLanguageGuardrail(ctx, prompt, {
1503
+ parameterRules: rawParamRules,
1504
+ policyPatch: body.policyPatch && typeof body.policyPatch === "object"
1505
+ ? body.policyPatch
1506
+ : undefined,
1507
+ source: rawParamRules ? "mcp_harness" : "deterministic",
1508
+ });
1509
+ sendJson(res, 200, {
1510
+ ...applied,
1511
+ policy: applied.workspace.policy,
1512
+ humanMessage: `Guardrail active: ${applied.guardrail.summary}`,
960
1513
  });
961
1514
  return;
962
1515
  }
@@ -989,15 +1542,41 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
989
1542
  sendJson(res, 200, { ok: true, scan });
990
1543
  return;
991
1544
  }
1545
+ // Workspace Context Inspector (natural-language friendly)
1546
+ if ((pathname === "/api/v1/context" || pathname === "/v1/context") &&
1547
+ (req.method === "GET" || req.method === "POST")) {
1548
+ const body = req.method === "POST"
1549
+ ? JSON.parse((await readBody(req)) || "{}")
1550
+ : {};
1551
+ const query = url.searchParams.get("query") ||
1552
+ url.searchParams.get("q") ||
1553
+ (body.query ? String(body.query) : undefined);
1554
+ const runContext = await engine.getRunContext(ctx, query);
1555
+ sendJson(res, 200, { ok: true, ...runContext });
1556
+ return;
1557
+ }
992
1558
  // Supermemory v5 Workspace Brain Endpoints
993
1559
  if (pathname === "/api/v1/memory" && req.method === "GET") {
994
1560
  const query = url.searchParams.get("query") ||
995
1561
  url.searchParams.get("q") ||
996
1562
  undefined;
997
1563
  const overview = await engine.getMemoryOverview(ctx, query);
998
- sendJson(res, 200, { ok: true, ...overview });
999
- return;
1000
- }
1564
+ sendJson(res, 200, {
1565
+ ok: true,
1566
+ ...overview,
1567
+ activeDefaults: overview.context.activeDefaults || {},
1568
+ activeGuardrails: overview.context.activeGuardrails || [],
1569
+ });
1570
+ return;
1571
+ }
1572
+ if (pathname === "/api/v1/memory/graph" && req.method === "GET") {
1573
+ const query = url.searchParams.get("query") ||
1574
+ url.searchParams.get("q") ||
1575
+ undefined;
1576
+ const graph = await engine.getMemoryGraph(ctx, query);
1577
+ sendJson(res, 200, { ok: true, ...graph });
1578
+ return;
1579
+ }
1001
1580
  if (pathname === "/api/v1/memory/facts" && req.method === "GET") {
1002
1581
  const query = url.searchParams.get("query") ||
1003
1582
  url.searchParams.get("q") ||
@@ -1031,6 +1610,48 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1031
1610
  }
1032
1611
  if (pathname === "/api/v1/memory" && req.method === "POST") {
1033
1612
  const body = JSON.parse((await readBody(req)) || "{}");
1613
+ const nlPrompt = String(body.prompt || body.remember || "").trim();
1614
+ if (nlPrompt && !body.content) {
1615
+ const parsed = parseNaturalLanguageMemoryInstruction(nlPrompt);
1616
+ if (parsed.isMemoryInstruction) {
1617
+ const primaryFact = parsed.temporalFacts[0];
1618
+ const record = await engine.addWorkspaceMemory(ctx, {
1619
+ title: parsed.title,
1620
+ content: parsed.content,
1621
+ kind: parsed.kind,
1622
+ subject: primaryFact?.subject,
1623
+ predicate: primaryFact?.predicate,
1624
+ object: primaryFact?.object,
1625
+ metadata: {
1626
+ extractedDefaults: parsed.extractedDefaults,
1627
+ source: "natural_language_api",
1628
+ },
1629
+ });
1630
+ for (const tf of parsed.temporalFacts) {
1631
+ store.upsertTemporalFact({
1632
+ workspaceId: ctx.workspace.id,
1633
+ subject: tf.subject,
1634
+ predicate: tf.predicate,
1635
+ object: tf.object,
1636
+ summary: tf.summary,
1637
+ confidence: 0.98,
1638
+ sourceKind: "preference",
1639
+ sourceReferenceId: record.id,
1640
+ metadata: { source: "natural_language_api" },
1641
+ });
1642
+ }
1643
+ const overview = await engine.getMemoryOverview(ctx);
1644
+ sendJson(res, 201, {
1645
+ ok: true,
1646
+ memory: record,
1647
+ activeDefaults: overview.activeDefaults,
1648
+ explanation: overview.explanation,
1649
+ humanMessage: parsed.humanSummary,
1650
+ });
1651
+ return;
1652
+ }
1653
+ body.content = nlPrompt;
1654
+ }
1034
1655
  const record = await engine.addWorkspaceMemory(ctx, {
1035
1656
  title: String(body.title || "Workspace Fact"),
1036
1657
  content: String(body.content || ""),
@@ -1050,18 +1671,37 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1050
1671
  return;
1051
1672
  }
1052
1673
  // API Keys
1053
- if (pathname === "/api/v1/api-keys" && req.method === "POST") {
1674
+ if ((pathname === "/api/v1/api-keys" || pathname === "/api/v1/keys") &&
1675
+ req.method === "GET") {
1676
+ sendJson(res, 200, {
1677
+ ok: true,
1678
+ apiKeys: store.listApiKeys(ctx.workspace.id),
1679
+ keys: store.listApiKeys(ctx.workspace.id),
1680
+ });
1681
+ return;
1682
+ }
1683
+ if ((pathname === "/api/v1/api-keys" || pathname === "/api/v1/keys") &&
1684
+ req.method === "POST") {
1054
1685
  const body = JSON.parse((await readBody(req)) || "{}");
1055
1686
  const created = store.createApiKey({
1056
1687
  workspaceId: ctx.workspace.id,
1057
- name: String(body.name || "API Key"),
1688
+ name: String(body.name || (body.teamName ? `${String(body.teamName)} Team Key` : "API Key")),
1058
1689
  environment: body.environment === "test" ? "test" : "live",
1690
+ scopes: Array.isArray(body.scopes) ? body.scopes : undefined,
1691
+ teamName: body.teamName ? String(body.teamName) : null,
1692
+ role: (body.role ? String(body.role) : "operator"),
1693
+ createdByEmail: ctx.user.email,
1059
1694
  });
1060
- sendJson(res, 201, { ok: true, ...created });
1695
+ sendJson(res, 201, { ok: true, apiKey: created.secretKey, ...created });
1061
1696
  return;
1062
1697
  }
1063
- if (pathname.startsWith("/api/v1/api-keys/") && req.method === "DELETE") {
1064
- const keyId = decodeURIComponent(pathname.slice("/api/v1/api-keys/".length));
1698
+ if ((pathname.startsWith("/api/v1/api-keys/") ||
1699
+ pathname.startsWith("/api/v1/keys/")) &&
1700
+ req.method === "DELETE") {
1701
+ const prefix = pathname.startsWith("/api/v1/api-keys/")
1702
+ ? "/api/v1/api-keys/"
1703
+ : "/api/v1/keys/";
1704
+ const keyId = decodeURIComponent(pathname.slice(prefix.length));
1065
1705
  if (!store.revokeApiKey(ctx.workspace.id, keyId)) {
1066
1706
  sendJson(res, 404, { ok: false, error: "API key not found" });
1067
1707
  return;
@@ -1128,13 +1768,18 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1128
1768
  }
1129
1769
  const result = await engine.waitForConnection(ctx, {
1130
1770
  provider,
1131
- timeoutSeconds: body.timeoutSeconds !== undefined
1132
- ? Number(body.timeoutSeconds)
1133
- : body.timeout_seconds !== undefined
1134
- ? Number(body.timeout_seconds)
1135
- : 15,
1771
+ timeoutSeconds: clampWaitSeconds(body.timeoutSeconds ?? body.timeout_seconds),
1772
+ });
1773
+ sendJson(res, 200, result);
1774
+ return;
1775
+ }
1776
+ const connectionWaitMatch = pathname.match(/^\/api\/v1\/connections\/([^/]+)\/wait$/);
1777
+ if (connectionWaitMatch && req.method === "GET") {
1778
+ const result = await engine.waitForConnection(ctx, {
1779
+ provider: decodeURIComponent(connectionWaitMatch[1]),
1780
+ timeoutSeconds: clampWaitSeconds(url.searchParams.get("timeout_seconds") ?? url.searchParams.get("timeoutSeconds")),
1136
1781
  });
1137
- sendJson(res, 200, { ok: true, ...result });
1782
+ sendJson(res, 200, result);
1138
1783
  return;
1139
1784
  }
1140
1785
  // Provisioned Resources & Decrypted Vault Env Pull
@@ -1165,11 +1810,687 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1165
1810
  });
1166
1811
  return;
1167
1812
  }
1813
+ const resourceSecretsMatch = pathname.match(/^\/api\/v1\/resources\/([^/]+)\/secrets$/);
1814
+ if (resourceSecretsMatch && req.method === "GET") {
1815
+ const resourceId = decodeURIComponent(resourceSecretsMatch[1]);
1816
+ const anyRes = store.findResourceAnyWorkspace(resourceId);
1817
+ if (anyRes && anyRes.workspaceId !== ctx.workspace.id) {
1818
+ sendJson(res, 403, {
1819
+ ok: false,
1820
+ code: "resource_workspace_mismatch",
1821
+ error: "Resource belongs to a different workspace.",
1822
+ });
1823
+ return;
1824
+ }
1825
+ const ownedRes = store.getResource(ctx.workspace.id, resourceId);
1826
+ if (!ownedRes) {
1827
+ sendJson(res, 404, {
1828
+ ok: false,
1829
+ code: "resource_not_found",
1830
+ error: `Resource '${resourceId}' not found in workspace.`,
1831
+ });
1832
+ return;
1833
+ }
1834
+ const secrets = store.getDecryptedResourceSecrets(ctx.workspace.id, resourceId);
1835
+ sendJson(res, 200, {
1836
+ ok: true,
1837
+ resourceId: ownedRes.id,
1838
+ resourceName: ownedRes.resourceName,
1839
+ workspaceId: ctx.workspace.id,
1840
+ envVarKeys: ownedRes.envVarKeys,
1841
+ secrets,
1842
+ });
1843
+ return;
1844
+ }
1845
+ // Stripe v2 Provisioning API (`Stripe-Version: 2026-09-30.preview`) Routes
1846
+ if (pathname === "/api/v1/provisioning/audit_log" && req.method === "GET") {
1847
+ const limit = Number(url.searchParams.get("limit") || 50);
1848
+ sendJson(res, 200, {
1849
+ ok: true,
1850
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
1851
+ entries: engine.stripeProjects.getProvisioningAuditLog(limit),
1852
+ });
1853
+ return;
1854
+ }
1855
+ if (pathname === "/api/v1/provisioning/connected_accounts" && req.method === "GET") {
1856
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
1857
+ const verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
1858
+ config.stripeConnectedAccountId ||
1859
+ "";
1860
+ if (!verifiedAccount) {
1861
+ sendJson(res, 200, {
1862
+ ok: true,
1863
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
1864
+ connected: false,
1865
+ account: null,
1866
+ publishable_key: config.stripePublishableKey || null,
1867
+ });
1868
+ return;
1869
+ }
1870
+ const retrieved = await engine.stripeProjects.retrieveConnectedAccount(verifiedAccount, stripeByok);
1871
+ sendJson(res, 200, {
1872
+ ok: true,
1873
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
1874
+ connected: true,
1875
+ account: retrieved,
1876
+ publishable_key: config.stripePublishableKey || null,
1877
+ });
1878
+ return;
1879
+ }
1880
+ if (pathname === "/api/v1/provisioning/connected_accounts" && req.method === "POST") {
1881
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
1882
+ const body = JSON.parse((await readBody(req)) || "{}");
1883
+ const created = await engine.stripeProjects.createConnectedAccount({
1884
+ contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
1885
+ display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
1886
+ country: body.country ? String(body.country) : "US",
1887
+ entity_type: body.entity_type === "individual" ? "individual" : "company",
1888
+ apiKeyOverride: stripeByok,
1889
+ });
1890
+ if (created.id) {
1891
+ store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
1892
+ }
1893
+ sendJson(res, 201, {
1894
+ ok: true,
1895
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
1896
+ ...created,
1897
+ });
1898
+ return;
1899
+ }
1900
+ if (pathname === "/api/v1/provisioning/connected_account_links" && req.method === "POST") {
1901
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
1902
+ let verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
1903
+ config.stripeConnectedAccountId ||
1904
+ "";
1905
+ const body = JSON.parse((await readBody(req)) || "{}");
1906
+ if (!verifiedAccount && !body.account && body.auto_create === true) {
1907
+ const created = await engine.stripeProjects.createConnectedAccount({
1908
+ contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
1909
+ display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
1910
+ country: body.country ? String(body.country) : "US",
1911
+ entity_type: body.entity_type === "individual" ? "individual" : "company",
1912
+ apiKeyOverride: stripeByok,
1913
+ });
1914
+ if (created.id) {
1915
+ store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
1916
+ verifiedAccount = created.id;
1917
+ }
1918
+ }
1919
+ const accountId = String(verifiedAccount || body.account || "").trim();
1920
+ if (!accountId) {
1921
+ sendJson(res, 400, {
1922
+ ok: false,
1923
+ error: "No verified connected_account_id is mapped to this tenant workspace.",
1924
+ });
1925
+ return;
1926
+ }
1927
+ const link = await engine.stripeProjects.createConnectedAccountLink({
1928
+ account: accountId,
1929
+ type: body.type === "account_update" ? "account_update" : "account_onboarding",
1930
+ fields: body.fields === "eventually_due" ? "eventually_due" : "currently_due",
1931
+ future_requirements: body.future_requirements === "omit"
1932
+ ? "omit"
1933
+ : body.future_requirements === "include"
1934
+ ? "include"
1935
+ : undefined,
1936
+ refresh_url: String(body.refresh_url || `${config.publicWebUrl}/?onboarding=refresh`),
1937
+ return_url: String(body.return_url || `${config.publicWebUrl}/?onboarding=return`),
1938
+ apiKeyOverride: stripeByok,
1939
+ });
1940
+ sendJson(res, 201, {
1941
+ ok: true,
1942
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
1943
+ ...link,
1944
+ });
1945
+ return;
1946
+ }
1947
+ if (pathname === "/api/v1/provisioning/connected_account_sessions" && req.method === "POST") {
1948
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
1949
+ let verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
1950
+ config.stripeConnectedAccountId ||
1951
+ "";
1952
+ const body = JSON.parse((await readBody(req)) || "{}");
1953
+ if (!verifiedAccount && !body.account && body.auto_create !== false) {
1954
+ const created = await engine.stripeProjects.createConnectedAccount({
1955
+ contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
1956
+ display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
1957
+ country: body.country ? String(body.country) : "US",
1958
+ entity_type: body.entity_type === "individual" ? "individual" : "company",
1959
+ apiKeyOverride: stripeByok,
1960
+ });
1961
+ if (created.id) {
1962
+ store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
1963
+ verifiedAccount = created.id;
1964
+ }
1965
+ }
1966
+ const accountId = String(verifiedAccount || body.account || "").trim();
1967
+ if (!accountId) {
1968
+ sendJson(res, 400, {
1969
+ ok: false,
1970
+ error: "No verified connected_account_id is mapped to this tenant workspace.",
1971
+ });
1972
+ return;
1973
+ }
1974
+ const session = await engine.stripeProjects.createConnectedAccountSession({
1975
+ account: accountId,
1976
+ enableAccountManagement: Boolean(body.enable_account_management ?? body.enableAccountManagement),
1977
+ enableNotificationBanner: body.enable_notification_banner !== undefined
1978
+ ? Boolean(body.enable_notification_banner)
1979
+ : body.enableNotificationBanner !== undefined
1980
+ ? Boolean(body.enableNotificationBanner)
1981
+ : undefined,
1982
+ disableStripeUserAuthentication: body.disable_stripe_user_authentication !== undefined
1983
+ ? Boolean(body.disable_stripe_user_authentication)
1984
+ : body.disableStripeUserAuthentication !== undefined
1985
+ ? Boolean(body.disableStripeUserAuthentication)
1986
+ : undefined,
1987
+ externalAccountCollection: body.external_account_collection !== undefined
1988
+ ? Boolean(body.external_account_collection)
1989
+ : body.externalAccountCollection !== undefined
1990
+ ? Boolean(body.externalAccountCollection)
1991
+ : undefined,
1992
+ apiKeyOverride: stripeByok,
1993
+ });
1994
+ sendJson(res, 201, {
1995
+ ok: true,
1996
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
1997
+ publishable_key: config.stripePublishableKey || null,
1998
+ embedded_component: "account-onboarding",
1999
+ collection_options: {
2000
+ fields: body.fields === "eventually_due" ? "eventually_due" : "currently_due",
2001
+ future_requirements: body.future_requirements === "omit" ? "omit" : "include",
2002
+ },
2003
+ ...session,
2004
+ });
2005
+ return;
2006
+ }
2007
+ const connAccountGetMatch = pathname.match(/^\/api\/v1\/provisioning\/connected_accounts\/([^/]+)$/);
2008
+ if (connAccountGetMatch && req.method === "GET") {
2009
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2010
+ const accountId = decodeURIComponent(connAccountGetMatch[1]);
2011
+ const retrieved = await engine.stripeProjects.retrieveConnectedAccount(accountId, stripeByok);
2012
+ sendJson(res, 200, {
2013
+ ok: true,
2014
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2015
+ ...retrieved,
2016
+ });
2017
+ return;
2018
+ }
2019
+ if (pathname === "/api/v1/provisioning/eligibility" && req.method === "GET") {
2020
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2021
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2022
+ const eligibility = await engine.stripeProjects.checkEligibility(stripeByok, verifiedContext);
2023
+ sendJson(res, 200, {
2024
+ ok: true,
2025
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2026
+ ...eligibility,
2027
+ });
2028
+ return;
2029
+ }
2030
+ if ((pathname === "/api/v1/provisioning/catalog/providers" ||
2031
+ pathname === "/api/v1/provisioning/providers") &&
2032
+ req.method === "GET") {
2033
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2034
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2035
+ const catalog = url.searchParams.get("catalog") || "prod";
2036
+ const limit = Number(url.searchParams.get("limit") || 100);
2037
+ const page = url.searchParams.get("page") || undefined;
2038
+ const page_url = url.searchParams.get("page_url") || undefined;
2039
+ const development = url.searchParams.get("development") !== null
2040
+ ? url.searchParams.get("development") === "true"
2041
+ : undefined;
2042
+ const providers = await engine.stripeProjects.listCatalogProviders({
2043
+ catalog,
2044
+ development,
2045
+ limit,
2046
+ page,
2047
+ page_url,
2048
+ apiKeyOverride: stripeByok,
2049
+ stripeContext: verifiedContext,
2050
+ });
2051
+ sendJson(res, 200, {
2052
+ ok: true,
2053
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2054
+ ...providers,
2055
+ });
2056
+ return;
2057
+ }
2058
+ if ((pathname === "/api/v1/provisioning/catalog/services" ||
2059
+ pathname === "/api/v1/provisioning/services") &&
2060
+ req.method === "GET") {
2061
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2062
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2063
+ const catalog = url.searchParams.get("catalog") || "prod";
2064
+ const limit = Number(url.searchParams.get("limit") || 100);
2065
+ const page = url.searchParams.get("page") || undefined;
2066
+ const page_url = url.searchParams.get("page_url") || undefined;
2067
+ const development = url.searchParams.get("development") !== null
2068
+ ? url.searchParams.get("development") === "true"
2069
+ : undefined;
2070
+ const provider_id = url.searchParams.get("provider_id") || undefined;
2071
+ const provider_name = url.searchParams.get("provider_name") || undefined;
2072
+ const services = await engine.stripeProjects.listCatalogServices({
2073
+ catalog,
2074
+ development,
2075
+ limit,
2076
+ page,
2077
+ page_url,
2078
+ provider_id,
2079
+ provider_name,
2080
+ apiKeyOverride: stripeByok,
2081
+ stripeContext: verifiedContext,
2082
+ });
2083
+ sendJson(res, 200, {
2084
+ ok: true,
2085
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2086
+ ...services,
2087
+ });
2088
+ return;
2089
+ }
2090
+ if (pathname === "/api/v1/provisioning/payment_profile" && req.method === "GET") {
2091
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2092
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2093
+ const provider = url.searchParams.get("provider") || undefined;
2094
+ const livemodeParam = url.searchParams.get("livemode");
2095
+ const livemode = livemodeParam !== null ? livemodeParam === "true" : undefined;
2096
+ const profile = await engine.stripeProjects.getPaymentProfile({
2097
+ provider,
2098
+ livemode,
2099
+ apiKeyOverride: stripeByok,
2100
+ stripeContext: verifiedContext,
2101
+ });
2102
+ sendJson(res, 200, {
2103
+ ok: true,
2104
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2105
+ ...profile,
2106
+ });
2107
+ return;
2108
+ }
2109
+ if (pathname === "/api/v1/provisioning/payment_profile/update_limit" &&
2110
+ req.method === "POST") {
2111
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2112
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2113
+ const body = JSON.parse((await readBody(req)) || "{}");
2114
+ const usageLimits = body.usage_limits || {
2115
+ max_amount: String(body.max_amount ?? "5000"),
2116
+ currency: String(body.currency || "usd"),
2117
+ recurring_interval: String(body.recurring_interval || "month"),
2118
+ };
2119
+ const updated = await engine.stripeProjects.updatePaymentProfileLimit({
2120
+ usage_limits: usageLimits,
2121
+ provider: body.provider ? String(body.provider) : undefined,
2122
+ livemode: body.livemode !== undefined ? Boolean(body.livemode) : undefined,
2123
+ apiKeyOverride: stripeByok,
2124
+ stripeContext: verifiedContext,
2125
+ });
2126
+ sendJson(res, 200, {
2127
+ ok: true,
2128
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2129
+ ...updated,
2130
+ });
2131
+ return;
2132
+ }
2133
+ if (pathname === "/api/v1/provisioning/payment_method_requests" &&
2134
+ req.method === "POST") {
2135
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2136
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2137
+ const body = JSON.parse((await readBody(req)) || "{}");
2138
+ const created = await engine.stripeProjects.createPaymentMethodRequest({
2139
+ payment_method_owner: body.payment_method_owner === "platform" ||
2140
+ body.payment_method_owner === "provider"
2141
+ ? body.payment_method_owner
2142
+ : undefined,
2143
+ provider: body.provider ? String(body.provider) : undefined,
2144
+ source_account: body.source_account ? String(body.source_account) : undefined,
2145
+ source_customer: body.source_customer ? String(body.source_customer) : undefined,
2146
+ source_payment_method: body.source_payment_method
2147
+ ? String(body.source_payment_method)
2148
+ : undefined,
2149
+ livemode: body.livemode !== undefined ? Boolean(body.livemode) : undefined,
2150
+ usage_limits: body.usage_limits,
2151
+ apiKeyOverride: stripeByok,
2152
+ stripeContext: verifiedContext,
2153
+ });
2154
+ sendJson(res, 201, {
2155
+ ok: true,
2156
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2157
+ ...created,
2158
+ });
2159
+ return;
2160
+ }
2161
+ if (pathname === "/api/v1/provisioning/projects" && req.method === "GET") {
2162
+ sendJson(res, 200, {
2163
+ ok: true,
2164
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2165
+ object: "v2.list",
2166
+ data: [
2167
+ {
2168
+ id: config.stripeProjectId || `proj_muxo_${ctx.workspace.id}`,
2169
+ object: "v2.provisioning.project",
2170
+ name: `${ctx.workspace.name} Cloud Project`,
2171
+ catalog: "prod",
2172
+ created: ctx.workspace.createdAt,
2173
+ livemode: false,
2174
+ },
2175
+ ],
2176
+ next_page_url: null,
2177
+ previous_page_url: null,
2178
+ });
2179
+ return;
2180
+ }
2181
+ if (pathname === "/api/v1/provisioning/projects" && req.method === "POST") {
2182
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2183
+ const body = JSON.parse((await readBody(req)) || "{}");
2184
+ const created = await engine.stripeProjects.createProject({
2185
+ name: String(body.name || "Muxo Cloud Project"),
2186
+ catalog: body.catalog || "prod",
2187
+ project_profile: body.project_profile
2188
+ ? String(body.project_profile)
2189
+ : undefined,
2190
+ apiKeyOverride: stripeByok,
2191
+ });
2192
+ sendJson(res, 201, {
2193
+ ok: true,
2194
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2195
+ ...created,
2196
+ });
2197
+ return;
2198
+ }
2199
+ if (pathname === "/api/v1/provisioning/provider_connections" &&
2200
+ req.method === "GET") {
2201
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2202
+ const limit = Number(url.searchParams.get("limit") || 25);
2203
+ const page = url.searchParams.get("page") || undefined;
2204
+ const provider = url.searchParams.get("provider") || undefined;
2205
+ const connections = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2206
+ ? await engine.stripeProjects.listProviderConnections({
2207
+ limit,
2208
+ page,
2209
+ provider,
2210
+ apiKeyOverride: stripeByok,
2211
+ })
2212
+ : managedProviderConnectionList();
2213
+ sendJson(res, 200, {
2214
+ ok: true,
2215
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2216
+ ...connections,
2217
+ });
2218
+ return;
2219
+ }
2220
+ const provConnUnlinkMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connections\/([^/]+)\/unlink$/);
2221
+ if (provConnUnlinkMatch && req.method === "POST") {
2222
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2223
+ const id = decodeURIComponent(provConnUnlinkMatch[1]);
2224
+ const unlinked = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2225
+ ? await engine.stripeProjects.unlinkProviderConnection(id, stripeByok)
2226
+ : managedProviderConnectionUnlink(id);
2227
+ sendJson(res, 200, {
2228
+ ok: true,
2229
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2230
+ ...unlinked,
2231
+ });
2232
+ return;
2233
+ }
2234
+ if (pathname === "/api/v1/provisioning/provider_connection_requests" &&
2235
+ req.method === "POST") {
2236
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2237
+ const body = JSON.parse((await readBody(req)) || "{}");
2238
+ const reqRes = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2239
+ ? await engine.stripeProjects.createProviderConnectionRequest({
2240
+ provider: String(body.provider || ""),
2241
+ configuration: body.configuration && typeof body.configuration === "object"
2242
+ ? body.configuration
2243
+ : {},
2244
+ project: body.project ? String(body.project) : undefined,
2245
+ apiKeyOverride: stripeByok,
2246
+ })
2247
+ : managedProviderConnectionRequest({ provider: String(body.provider || "") });
2248
+ sendJson(res, 201, {
2249
+ ok: true,
2250
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2251
+ ...reqRes,
2252
+ });
2253
+ return;
2254
+ }
2255
+ const provConnReqSubmitMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connection_requests\/([^/]+)\/submit_information$/);
2256
+ if (provConnReqSubmitMatch && req.method === "POST") {
2257
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2258
+ const id = decodeURIComponent(provConnReqSubmitMatch[1]);
2259
+ const body = JSON.parse((await readBody(req)) || "{}");
2260
+ const submitted = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2261
+ ? await engine.stripeProjects.submitProviderConnectionRequestInformation(id, body.submitted_information || body, stripeByok)
2262
+ : managedProviderConnectionRequest({ id, requestStatus: "complete" });
2263
+ sendJson(res, 200, {
2264
+ ok: true,
2265
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2266
+ ...submitted,
2267
+ });
2268
+ return;
2269
+ }
2270
+ const provConnReqGetMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connection_requests\/([^/]+)$/);
2271
+ if (provConnReqGetMatch && req.method === "GET") {
2272
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2273
+ const id = decodeURIComponent(provConnReqGetMatch[1]);
2274
+ const retrieved = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2275
+ ? await engine.stripeProjects.retrieveProviderConnectionRequest(id, stripeByok)
2276
+ : managedProviderConnectionRequest({ id });
2277
+ sendJson(res, 200, {
2278
+ ok: true,
2279
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2280
+ ...retrieved,
2281
+ });
2282
+ return;
2283
+ }
2284
+ const revealAccessMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)\/reveal_access_configuration$/);
2285
+ if (revealAccessMatch && req.method === "POST") {
2286
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2287
+ const resourceId = decodeURIComponent(revealAccessMatch[1]);
2288
+ const anyRes = store.findResourceAnyWorkspace(resourceId);
2289
+ if (anyRes && anyRes.workspaceId !== ctx.workspace.id) {
2290
+ sendJson(res, 403, {
2291
+ ok: false,
2292
+ code: "resource_workspace_mismatch",
2293
+ error: "Resource belongs to a different workspace.",
2294
+ });
2295
+ return;
2296
+ }
2297
+ if (engine.stripeProjects.hasStripeV2ApiKey(stripeByok)) {
2298
+ try {
2299
+ const revealed = await engine.stripeProjects.revealAccessConfiguration(resourceId, stripeByok);
2300
+ sendJson(res, 200, {
2301
+ ok: true,
2302
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2303
+ source: "stripe_v2_api",
2304
+ ...revealed,
2305
+ });
2306
+ return;
2307
+ }
2308
+ catch {
2309
+ // Fall through to workspace vault lookup if Stripe v2 returns error
2310
+ }
2311
+ }
2312
+ const resourceSpecific = store.getDecryptedResourceSecrets(ctx.workspace.id, resourceId);
2313
+ const vaultSecrets = Object.keys(resourceSpecific).length > 0
2314
+ ? resourceSpecific
2315
+ : store.getDecryptedWorkspaceEnv(ctx.workspace.id);
2316
+ sendJson(res, 200, {
2317
+ ok: true,
2318
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2319
+ object: "v2.provisioning.resource.access_configuration",
2320
+ resource: resourceId,
2321
+ configuration: vaultSecrets,
2322
+ created: new Date().toISOString(),
2323
+ expires_at: null,
2324
+ livemode: false,
2325
+ source: "muxo_managed",
2326
+ });
2327
+ return;
2328
+ }
2329
+ const resourceActionMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)\/(remove|unlink|rotate_credentials|submit_information)$/);
2330
+ if (resourceActionMatch && req.method === "POST") {
2331
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2332
+ const resourceId = decodeURIComponent(resourceActionMatch[1]);
2333
+ const action = resourceActionMatch[2];
2334
+ const body = JSON.parse((await readBody(req)) || "{}");
2335
+ let actionResult;
2336
+ if (!engine.stripeProjects.hasStripeV2ApiKey(stripeByok)) {
2337
+ actionResult = managedResourceAction(resourceId, action);
2338
+ }
2339
+ else if (action === "remove") {
2340
+ actionResult = await engine.stripeProjects.removeResource(resourceId, stripeByok);
2341
+ }
2342
+ else if (action === "unlink") {
2343
+ actionResult = await engine.stripeProjects.unlinkResource(resourceId, stripeByok);
2344
+ }
2345
+ else if (action === "rotate_credentials") {
2346
+ actionResult = await engine.stripeProjects.rotateResourceCredentials(resourceId, stripeByok);
2347
+ }
2348
+ else {
2349
+ actionResult = await engine.stripeProjects.submitResourceInformation(resourceId, body.submitted_information || body, stripeByok);
2350
+ }
2351
+ sendJson(res, 200, {
2352
+ ok: true,
2353
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2354
+ ...actionResult,
2355
+ });
2356
+ return;
2357
+ }
2358
+ if (pathname === "/api/v1/provisioning/resources/link" && req.method === "POST") {
2359
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2360
+ const body = JSON.parse((await readBody(req)) || "{}");
2361
+ const linked = !engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2362
+ ? managedResource({
2363
+ provider: String(body.provider || ""),
2364
+ serviceRef: String(body.service_ref || ""),
2365
+ environment: body.environment ? String(body.environment) : "prod",
2366
+ catalog: body.catalog ? String(body.catalog) : "prod",
2367
+ status: "active",
2368
+ })
2369
+ : await engine.stripeProjects.linkResource({
2370
+ provider: String(body.provider || ""),
2371
+ service_ref: String(body.service_ref || ""),
2372
+ provider_resource_id: body.provider_resource_id
2373
+ ? String(body.provider_resource_id)
2374
+ : undefined,
2375
+ environment: body.environment ? String(body.environment) : "prod",
2376
+ catalog: body.catalog || "prod",
2377
+ project: body.project ? String(body.project) : undefined,
2378
+ apiKeyOverride: stripeByok,
2379
+ });
2380
+ sendJson(res, 201, {
2381
+ ok: true,
2382
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2383
+ ...linked,
2384
+ });
2385
+ return;
2386
+ }
2387
+ if (pathname === "/api/v1/provisioning/resources" && req.method === "GET") {
2388
+ const resources = store.listResources(ctx.workspace.id);
2389
+ sendJson(res, 200, {
2390
+ ok: true,
2391
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2392
+ object: "v2.list",
2393
+ data: resources,
2394
+ next_page_url: null,
2395
+ previous_page_url: null,
2396
+ });
2397
+ return;
2398
+ }
2399
+ if (pathname === "/api/v1/provisioning/resources" && req.method === "POST") {
2400
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2401
+ const body = JSON.parse((await readBody(req)) || "{}");
2402
+ const providerSlug = String(body.provider || "turso").toLowerCase();
2403
+ const serviceRef = String(body.service_ref || "database").toLowerCase();
2404
+ const resourceName = body.name ? String(body.name) : `${providerSlug}-${serviceRef}`;
2405
+ const created = !engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2406
+ ? managedResource({
2407
+ provider: providerSlug,
2408
+ serviceRef,
2409
+ name: resourceName,
2410
+ environment: body.environment ? String(body.environment) : "prod",
2411
+ catalog: body.catalog ? String(body.catalog) : "prod",
2412
+ })
2413
+ : await engine.stripeProjects.createResource({
2414
+ provider: providerSlug,
2415
+ service_ref: serviceRef,
2416
+ configuration: body.configuration && typeof body.configuration === "object"
2417
+ ? body.configuration
2418
+ : {},
2419
+ name: resourceName,
2420
+ environment: body.environment ? String(body.environment) : "prod",
2421
+ catalog: body.catalog || "prod",
2422
+ project: body.project ? String(body.project) : undefined,
2423
+ apiKeyOverride: stripeByok,
2424
+ });
2425
+ const externalId = String(created.id || `prv_res_${Date.now().toString(36)}`);
2426
+ const envKey = `${providerSlug.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`;
2427
+ store.upsertResource({
2428
+ workspaceId: ctx.workspace.id,
2429
+ providerSlug,
2430
+ providerName: providerSlug.toUpperCase(),
2431
+ serviceSlug: serviceRef,
2432
+ resourceName,
2433
+ status: "active",
2434
+ externalResourceId: externalId,
2435
+ secrets: { [envKey]: `https://${resourceName}.${providerSlug}.muxo.internal` },
2436
+ metadata: { source: created.source || "muxo_managed" },
2437
+ });
2438
+ sendJson(res, 201, {
2439
+ ok: true,
2440
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2441
+ ...created,
2442
+ });
2443
+ return;
2444
+ }
2445
+ const resourceIdMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)$/);
2446
+ if (resourceIdMatch && (req.method === "GET" || req.method === "POST")) {
2447
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2448
+ const resourceId = decodeURIComponent(resourceIdMatch[1]);
2449
+ if (req.method === "GET") {
2450
+ const retrieved = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2451
+ ? await engine.stripeProjects.retrieveResource(resourceId, stripeByok)
2452
+ : managedResource({ id: resourceId });
2453
+ sendJson(res, 200, {
2454
+ ok: true,
2455
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2456
+ ...retrieved,
2457
+ });
2458
+ return;
2459
+ }
2460
+ const body = JSON.parse((await readBody(req)) || "{}");
2461
+ const updated = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2462
+ ? await engine.stripeProjects.updateResource(resourceId, body.configuration || body, stripeByok)
2463
+ : managedResource({ id: resourceId, status: "active" });
2464
+ sendJson(res, 200, {
2465
+ ok: true,
2466
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2467
+ ...updated,
2468
+ });
2469
+ return;
2470
+ }
1168
2471
  // Governance Policy
2472
+ if (pathname === "/api/v1/policy" && req.method === "GET") {
2473
+ const summary = engine.getGuardrailsSummary(ctx);
2474
+ sendJson(res, 200, {
2475
+ ok: true,
2476
+ policy: summary.policy,
2477
+ plainEnglish: summary.plainEnglish,
2478
+ explanation: summary.plainEnglish.explanation,
2479
+ activeDefaults: summary.activeDefaults,
2480
+ activeGuardrails: summary.activeGuardrails,
2481
+ });
2482
+ return;
2483
+ }
1169
2484
  if (pathname === "/api/v1/policy" && req.method === "POST") {
1170
2485
  const body = JSON.parse((await readBody(req)) || "{}");
1171
2486
  const updated = store.updateWorkspacePolicy(ctx.workspace.id, sanitizePolicyPatch(body));
1172
- sendJson(res, 200, { ok: true, workspace: updated });
2487
+ const plainEnglish = describePolicyInPlainEnglish(updated.policy);
2488
+ sendJson(res, 200, {
2489
+ ok: true,
2490
+ workspace: updated,
2491
+ plainEnglishGuardrails: plainEnglish,
2492
+ explanation: plainEnglish.explanation,
2493
+ });
1173
2494
  return;
1174
2495
  }
1175
2496
  // Plans & Approvals & Execution
@@ -1197,18 +2518,53 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1197
2518
  sendJson(res, 201, { ok: true, ...planned });
1198
2519
  return;
1199
2520
  }
2521
+ const resolvePlanCtx = (planId) => {
2522
+ const found = store.getPlanById(planId);
2523
+ if (found &&
2524
+ found.workspaceId !== ctx.workspace.id &&
2525
+ ctx.authType === "session" &&
2526
+ store.userCanAccessWorkspace(ctx.user.id, found.workspaceId)) {
2527
+ const targetWs = store.getWorkspace(found.workspaceId);
2528
+ if (targetWs) {
2529
+ return { ...ctx, workspace: targetWs };
2530
+ }
2531
+ }
2532
+ return ctx;
2533
+ };
1200
2534
  const planApproveMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/approve$/);
1201
2535
  if (planApproveMatch && req.method === "POST") {
1202
2536
  const planId = decodeURIComponent(planApproveMatch[1]);
1203
- const plan = engine.approvePlan(ctx, planId, "approved");
2537
+ const planCtx = resolvePlanCtx(planId);
2538
+ const body = JSON.parse((await readBody(req)) || "{}");
2539
+ const decision = body.decision === "rejected" || body.decision === "reject"
2540
+ ? "rejected"
2541
+ : "approved";
2542
+ const plan = engine.approvePlan(planCtx, planId, decision);
1204
2543
  sendJson(res, 200, { ok: true, plan });
1205
2544
  return;
1206
2545
  }
2546
+ const planWaitMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/wait$/);
2547
+ if (planWaitMatch && (req.method === "POST" || req.method === "GET")) {
2548
+ const planId = decodeURIComponent(planWaitMatch[1]);
2549
+ const planCtx = resolvePlanCtx(planId);
2550
+ const body = req.method === "POST" ? JSON.parse((await readBody(req)) || "{}") : {};
2551
+ const rawTimeout = body.timeoutSeconds ??
2552
+ body.timeout_seconds ??
2553
+ url.searchParams.get("timeout_seconds") ??
2554
+ url.searchParams.get("timeoutSeconds");
2555
+ const waited = await engine.waitForPlanApproval(planCtx, {
2556
+ planId,
2557
+ timeoutSeconds: clampWaitSeconds(rawTimeout, 15, 60),
2558
+ });
2559
+ sendJson(res, 200, waited);
2560
+ return;
2561
+ }
1207
2562
  const planExecuteMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/execute$/);
1208
2563
  if (planExecuteMatch && req.method === "POST") {
1209
2564
  const planId = decodeURIComponent(planExecuteMatch[1]);
1210
- const plan = engine.getPlan(ctx, planId);
1211
- const result = await engine.executePlanForRisk(ctx, planId, plan.riskLevel);
2565
+ const planCtx = resolvePlanCtx(planId);
2566
+ const plan = engine.getPlan(planCtx, planId);
2567
+ const result = await engine.executePlanForRisk(planCtx, planId, plan.riskLevel);
1212
2568
  sendJson(res, 200, {
1213
2569
  ok: true,
1214
2570
  ...result,
@@ -1219,8 +2575,24 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1219
2575
  const planGetMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)$/);
1220
2576
  if (planGetMatch && req.method === "GET") {
1221
2577
  const planId = decodeURIComponent(planGetMatch[1]);
1222
- const plan = engine.getPlan(ctx, planId);
1223
- sendJson(res, 200, { ok: true, plan });
2578
+ const planCtx = resolvePlanCtx(planId);
2579
+ const wait = url.searchParams.get("wait") === "true" ||
2580
+ url.searchParams.get("wait_for_approval") === "true";
2581
+ if (wait) {
2582
+ const waited = await engine.waitForPlanApproval(planCtx, {
2583
+ planId,
2584
+ timeoutSeconds: clampWaitSeconds(url.searchParams.get("timeout_seconds"), 15, 60),
2585
+ });
2586
+ sendJson(res, 200, waited);
2587
+ return;
2588
+ }
2589
+ const plan = engine.getPlan(planCtx, planId);
2590
+ sendJson(res, 200, {
2591
+ ok: true,
2592
+ approved: APPROVED_PLAN_STATUSES.has(plan.approvalStatus),
2593
+ timedOut: false,
2594
+ plan,
2595
+ });
1224
2596
  return;
1225
2597
  }
1226
2598
  // Runs & Receipts
@@ -1236,7 +2608,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1236
2608
  const runId = decodeURIComponent(runCancelMatch[1]);
1237
2609
  const body = JSON.parse((await readBody(req)) || "{}");
1238
2610
  const cancelled = engine.cancelRun(ctx, runId, body.reason ? String(body.reason) : undefined);
1239
- sendJson(res, 200, { ok: true, ...cancelled });
2611
+ sendJson(res, 200, cancelled);
1240
2612
  return;
1241
2613
  }
1242
2614
  const runGetMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
@@ -1246,6 +2618,20 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1246
2618
  sendJson(res, 200, { ok: true, run });
1247
2619
  return;
1248
2620
  }
2621
+ if (pathname === "/api/v1/receipts" && req.method === "GET") {
2622
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit") || 25)));
2623
+ const list = engine.listReceipts(ctx, limit);
2624
+ sendJson(res, 200, { ok: true, ...list });
2625
+ return;
2626
+ }
2627
+ if ((pathname === "/api/v1/receipts/verify-chain" ||
2628
+ pathname === "/api/v1/receipts/chain") &&
2629
+ req.method === "GET") {
2630
+ const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") || 100)));
2631
+ const chain = engine.verifyReceiptChain(ctx, limit);
2632
+ sendJson(res, 200, chain);
2633
+ return;
2634
+ }
1249
2635
  if (pathname.startsWith("/api/v1/receipts/") && req.method === "GET") {
1250
2636
  const receiptId = decodeURIComponent(pathname.slice("/api/v1/receipts/".length));
1251
2637
  const verified = engine.getReceipt(ctx, receiptId);
@@ -1466,13 +2852,236 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1466
2852
  sendJson(res, 200, { ok: true, received: true });
1467
2853
  return;
1468
2854
  }
2855
+ // Stripe v2 Billing Meter Event Stream (`POST /v2/billing/meter_event_session` + `POST /v2/billing/meter_event_stream`)
2856
+ if (pathname === "/api/v1/billing/meter_events" && req.method === "GET") {
2857
+ const limit = Number(url.searchParams.get("limit") || 50);
2858
+ const summary = engine.stripeBilling.listMeterEvents(ctx.workspace.id, limit);
2859
+ sendJson(res, 200, {
2860
+ ok: true,
2861
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2862
+ ...summary,
2863
+ });
2864
+ return;
2865
+ }
2866
+ if (pathname === "/api/v1/billing/meter_events" && req.method === "POST") {
2867
+ const body = JSON.parse((await readBody(req)) || "{}");
2868
+ const result = await engine.stripeBilling.recordMeterEvent({
2869
+ workspace: ctx.workspace,
2870
+ event_name: body.event_name ? String(body.event_name) : undefined,
2871
+ value: Number(body.value ?? 1),
2872
+ lane: body.lane ? String(body.lane) : undefined,
2873
+ tool_slug: body.tool_slug ? String(body.tool_slug) : undefined,
2874
+ identifier: body.identifier ? String(body.identifier) : undefined,
2875
+ timestamp: body.timestamp ? String(body.timestamp) : undefined,
2876
+ });
2877
+ sendJson(res, 201, {
2878
+ ok: true,
2879
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2880
+ ...result,
2881
+ });
2882
+ return;
2883
+ }
2884
+ // Stripe v2 Event Destinations & Provisioning Thin Event Webhook Handler
2885
+ if (pathname === "/api/v1/provisioning/event_destinations" &&
2886
+ req.method === "POST") {
2887
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2888
+ const body = JSON.parse((await readBody(req)) || "{}");
2889
+ const destination = await engine.stripeProjects.createV2EventDestination({
2890
+ name: body.name ? String(body.name) : undefined,
2891
+ url: String(body.url || `${config.baseUrl}/api/v1/provisioning/events`),
2892
+ events: Array.isArray(body.events) ? body.events.map(String) : undefined,
2893
+ apiKeyOverride: stripeByok,
2894
+ });
2895
+ sendJson(res, 201, {
2896
+ ok: true,
2897
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2898
+ ...destination,
2899
+ });
2900
+ return;
2901
+ }
2902
+ if (pathname === "/api/v1/provisioning/events" && req.method === "POST") {
2903
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2904
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2905
+ const body = JSON.parse((await readBody(req)) || "{}");
2906
+ const handled = await engine.stripeProjects.handleV2ProvisioningEvent({
2907
+ event: body,
2908
+ apiKeyOverride: stripeByok,
2909
+ stripeContext: verifiedContext,
2910
+ });
2911
+ if (handled.revealedConfiguration &&
2912
+ Object.keys(handled.revealedConfiguration).length > 0 &&
2913
+ handled.relatedObjectId) {
2914
+ store.upsertResource({
2915
+ workspaceId: ctx.workspace.id,
2916
+ resourceName: String(handled.resource?.name || handled.relatedObjectId),
2917
+ providerSlug: String(handled.resource?.provider || "neon"),
2918
+ providerName: String(handled.resource?.provider || "Neon"),
2919
+ serviceSlug: String(handled.resource?.service_ref || "postgres"),
2920
+ planSlug: "launch",
2921
+ status: "active",
2922
+ secrets: handled.revealedConfiguration,
2923
+ externalResourceId: handled.relatedObjectId,
2924
+ metadata: {
2925
+ region: "aws-us-east-1",
2926
+ },
2927
+ });
2928
+ }
2929
+ sendJson(res, 200, {
2930
+ ok: true,
2931
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2932
+ ...handled,
2933
+ });
2934
+ return;
2935
+ }
2936
+ // Composio Dynamic Enterprise Toolkits, Scoped Auth Configs, Triggers & Multi-Tool Batch Execution
2937
+ if (pathname === "/api/v1/toolkits" && req.method === "GET") {
2938
+ const composioByok = store.getByokKey(ctx.workspace.id, "composio");
2939
+ const category = url.searchParams.get("category") || undefined;
2940
+ const search = url.searchParams.get("search") || url.searchParams.get("query") || undefined;
2941
+ const catalog = await engine.composioSdk.listToolkits({
2942
+ category,
2943
+ search,
2944
+ composioApiKey: composioByok,
2945
+ });
2946
+ sendJson(res, 200, { ok: true, ...catalog });
2947
+ return;
2948
+ }
2949
+ if (pathname === "/api/v1/connections/auth-configs" && req.method === "POST") {
2950
+ const composioByok = store.getByokKey(ctx.workspace.id, "composio");
2951
+ const body = JSON.parse((await readBody(req)) || "{}");
2952
+ const created = await engine.composioSdk.createCustomAuthConfig({
2953
+ workspaceId: ctx.workspace.id,
2954
+ toolkitSlug: String(body.toolkitSlug || body.provider || "github"),
2955
+ name: body.name ? String(body.name) : undefined,
2956
+ authScheme: body.authScheme === "API_KEY" ? "API_KEY" : "OAUTH2",
2957
+ scopeMode: body.scopeMode === "full_access" ? "full_access" : "read_only",
2958
+ customScopes: Array.isArray(body.customScopes)
2959
+ ? body.customScopes.map(String)
2960
+ : undefined,
2961
+ credentials: body.credentials && typeof body.credentials === "object"
2962
+ ? body.credentials
2963
+ : undefined,
2964
+ composioApiKey: composioByok,
2965
+ });
2966
+ sendJson(res, 201, { ok: true, ...created });
2967
+ return;
2968
+ }
2969
+ if (pathname === "/api/v1/triggers" && req.method === "GET") {
2970
+ const provider = url.searchParams.get("provider") || undefined;
2971
+ const available = engine.composioSdk.listTriggerTypes(provider);
2972
+ const subscriptions = engine.composioSdk.listActiveTriggers(ctx.workspace.id);
2973
+ const events = engine.composioSdk.listTriggerEvents(ctx.workspace.id, 25);
2974
+ sendJson(res, 200, {
2975
+ ok: true,
2976
+ availableTriggers: available,
2977
+ activeSubscriptions: subscriptions,
2978
+ recentEvents: events,
2979
+ });
2980
+ return;
2981
+ }
2982
+ if (pathname === "/api/v1/triggers" && req.method === "POST") {
2983
+ const composioByok = store.getByokKey(ctx.workspace.id, "composio");
2984
+ const body = JSON.parse((await readBody(req)) || "{}");
2985
+ const subscription = await engine.composioSdk.enableTrigger({
2986
+ workspaceId: ctx.workspace.id,
2987
+ triggerSlug: String(body.triggerSlug || body.trigger_slug || "GITHUB_COMMIT_EVENT"),
2988
+ providerSlug: String(body.providerSlug || body.provider || "github"),
2989
+ connectedAccountId: body.connectedAccountId
2990
+ ? String(body.connectedAccountId)
2991
+ : undefined,
2992
+ webhookUrl: body.webhookUrl ? String(body.webhookUrl) : undefined,
2993
+ config: body.config && typeof body.config === "object" ? body.config : {},
2994
+ composioApiKey: composioByok,
2995
+ });
2996
+ sendJson(res, 201, { ok: true, subscription });
2997
+ return;
2998
+ }
2999
+ if (pathname === "/api/v1/triggers/webhook" && req.method === "POST") {
3000
+ const body = JSON.parse((await readBody(req)) || "{}");
3001
+ const triggerSlug = String(body.triggerSlug || body.type || "GITHUB_COMMIT_EVENT").toUpperCase();
3002
+ const providerSlug = String(body.providerSlug || triggerSlug.split("_")[0] || "github").toLowerCase();
3003
+ const payloadObj = body.payload && typeof body.payload === "object"
3004
+ ? body.payload
3005
+ : body;
3006
+ const rawPayloadText = JSON.stringify(payloadObj);
3007
+ const injectionScan = engine.governance.scanMemoryContent({
3008
+ workspaceId: ctx.workspace.id,
3009
+ content: rawPayloadText,
3010
+ });
3011
+ const disposition = injectionScan.safe ? "dispatched" : "quarantined";
3012
+ const eventRecord = {
3013
+ id: `trgevt_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
3014
+ workspaceId: ctx.workspace.id,
3015
+ triggerSlug,
3016
+ providerSlug,
3017
+ receivedAt: new Date().toISOString(),
3018
+ promptInjectionScan: {
3019
+ safe: injectionScan.safe,
3020
+ riskScore: injectionScan.findingsCount * 0.35,
3021
+ findings: injectionScan.findings.map((f) => `${f.patternId}: ${f.description}`),
3022
+ },
3023
+ disposition: disposition,
3024
+ payloadPreview: payloadObj,
3025
+ };
3026
+ engine.composioSdk.recordTriggerEvent(eventRecord);
3027
+ sendJson(res, injectionScan.safe ? 200 : 202, {
3028
+ ok: true,
3029
+ ...eventRecord,
3030
+ });
3031
+ return;
3032
+ }
3033
+ if (pathname === "/api/v1/plans/batch" && req.method === "POST") {
3034
+ const body = JSON.parse((await readBody(req)) || "{}");
3035
+ const batchResult = await engine.executeBatchPlans(ctx, {
3036
+ items: Array.isArray(body.items) ? body.items : [],
3037
+ // Only a signed-in person may pre-approve pending plans; agent keys never can.
3038
+ autoApprove: body.autoApprove === true && signedInPerson(auth),
3039
+ });
3040
+ sendJson(res, 200, { ok: true, ...batchResult });
3041
+ return;
3042
+ }
3043
+ // Cloudflare Vectorize Hybrid Semantic + Bi-Temporal Recall, Durable Workflows, and R2 WORM Audit Archive
3044
+ if (pathname === "/api/v1/memory/hybrid" && (req.method === "GET" || req.method === "POST")) {
3045
+ const body = req.method === "POST" ? JSON.parse((await readBody(req)) || "{}") : {};
3046
+ const query = String(body.query ?? body.q ?? url.searchParams.get("query") ?? url.searchParams.get("q") ?? "");
3047
+ const limit = Number(body.limit ?? url.searchParams.get("limit") ?? 10);
3048
+ const hybrid = engine.searchMemoryHybrid(ctx.workspace.id, query, limit);
3049
+ sendJson(res, 200, { ok: true, ...hybrid });
3050
+ return;
3051
+ }
3052
+ if (pathname === "/api/v1/workflows" && req.method === "GET") {
3053
+ const workflows = engine.listDurableWorkflows(ctx.workspace.id);
3054
+ sendJson(res, 200, { ok: true, workflows });
3055
+ return;
3056
+ }
3057
+ if (pathname === "/api/v1/workflows" && req.method === "POST") {
3058
+ const body = JSON.parse((await readBody(req)) || "{}");
3059
+ const workflow = await engine.startDurableWorkflow(ctx, {
3060
+ provider: body.provider ? String(body.provider) : undefined,
3061
+ serviceRef: body.serviceRef || body.service_ref ? String(body.serviceRef || body.service_ref) : undefined,
3062
+ name: body.name ? String(body.name) : undefined,
3063
+ region: body.region ? String(body.region) : undefined,
3064
+ });
3065
+ sendJson(res, 201, { ok: true, workflow });
3066
+ return;
3067
+ }
3068
+ const wfAdvanceMatch = pathname.match(/^\/api\/v1\/workflows\/([^/]+)\/advance$/);
3069
+ if (wfAdvanceMatch && req.method === "POST") {
3070
+ const wfId = decodeURIComponent(wfAdvanceMatch[1]);
3071
+ const body = JSON.parse((await readBody(req)) || "{}");
3072
+ const workflow = await engine.advanceDurableWorkflow(ctx, wfId, body);
3073
+ sendJson(res, 200, { ok: true, workflow });
3074
+ return;
3075
+ }
3076
+ if (pathname === "/api/v1/governance/archive" && req.method === "GET") {
3077
+ const archive = engine.exportTamperEvidentArchive(ctx.workspace.id);
3078
+ sendJson(res, 200, { ok: true, ...archive });
3079
+ return;
3080
+ }
1469
3081
  sendJson(res, 404, { ok: false, error: `Route not found: ${req.method} ${pathname}` });
1470
3082
  }
1471
3083
  catch (err) {
1472
- sendJson(res, 400, {
1473
- ok: false,
1474
- error: String(err?.message || err || "Internal error"),
1475
- });
3084
+ sendJson(res, err instanceof MuxoToolError ? err.statusCode : 400, errorPayloadFrom(err, "Internal error"));
1476
3085
  }
1477
3086
  });
1478
3087
  return { server, engine, store, defaultApiKey: bootstrap.defaultApiKey };