@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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +2 -2
- package/ARCHITECTURE.md +16 -6
- package/README.md +7 -5
- package/dist/cli.js +393 -13
- package/dist/composio-catalog.d.ts +58 -0
- package/dist/composio-catalog.js +474 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +16 -0
- package/dist/durable-workflow.d.ts +45 -0
- package/dist/durable-workflow.js +90 -0
- package/dist/engine.d.ts +276 -5
- package/dist/engine.js +1261 -71
- package/dist/mcp-schema.d.ts +125 -0
- package/dist/mcp-schema.js +1076 -0
- package/dist/mcp.d.ts +2 -30
- package/dist/mcp.js +505 -539
- package/dist/memory-graph.d.ts +21 -0
- package/dist/memory-graph.js +173 -0
- package/dist/nl.js +155 -0
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/policy.d.ts +75 -1
- package/dist/policy.js +1119 -0
- package/dist/providers/composio-instant.js +225 -5
- package/dist/providers/composio-sdk.d.ts +68 -5
- package/dist/providers/composio-sdk.js +683 -78
- package/dist/providers/microsoft-governance.d.ts +28 -1
- package/dist/providers/microsoft-governance.js +224 -11
- package/dist/providers/stripe-billing.d.ts +42 -0
- package/dist/providers/stripe-billing.js +230 -1
- package/dist/providers/stripe-projects.d.ts +451 -3
- package/dist/providers/stripe-projects.js +2106 -87
- package/dist/providers/supermemory.d.ts +74 -1
- package/dist/providers/supermemory.js +426 -0
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +538 -8
- package/dist/sdk.js +377 -10
- package/dist/server.js +1841 -232
- package/dist/store.d.ts +113 -6
- package/dist/store.js +675 -52
- package/dist/types.d.ts +212 -5
- package/dist/ui/console-html.d.ts +10 -1
- package/dist/ui/console-html.js +1188 -462
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/plugins/muxo/.claude-plugin/plugin.json +1 -1
- package/plugins/muxo/.codex-plugin/plugin.json +1 -1
- package/plugins/muxo/plugin.json +2 -2
- package/plugins/muxo/skills/muxo/SKILL.md +42 -1
- package/plugins/muxo/skills/muxo/references/mcp.md +3 -2
- 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 {
|
|
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
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
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
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
"S256";
|
|
325
|
-
const code = `muxo_auth_code_${crypto.randomBytes(16).toString("hex")}`;
|
|
326
|
-
const issuedKey = bootstrap.defaultApiKey ||
|
|
327
|
-
store.createApiKey({
|
|
328
|
-
workspaceId: bootstrap.workspace.id,
|
|
329
|
-
name: `OAuth Client (${clientId})`,
|
|
330
|
-
environment: "live",
|
|
331
|
-
}).secretKey;
|
|
332
|
-
oauthCodes.set(code, {
|
|
333
|
-
code,
|
|
334
|
-
clientId,
|
|
335
|
-
redirectUri,
|
|
336
|
-
scope,
|
|
337
|
-
codeChallenge,
|
|
338
|
-
codeChallengeMethod,
|
|
339
|
-
workspaceId: bootstrap.workspace.id,
|
|
340
|
-
apiKey: issuedKey,
|
|
341
|
-
expiresAt: Date.now() + 10 * 60 * 1000,
|
|
342
|
-
});
|
|
343
|
-
const proto = String(req.headers["x-forwarded-proto"] || "http").split(",")[0].trim();
|
|
344
|
-
const requestOrigin = req.headers.host
|
|
345
|
-
? `${proto}://${req.headers.host}`
|
|
346
|
-
: config.baseUrl;
|
|
347
|
-
const redirectTarget = new URL(redirectUri);
|
|
348
|
-
redirectTarget.searchParams.set("code", code);
|
|
349
|
-
if (stateParam)
|
|
350
|
-
redirectTarget.searchParams.set("state", stateParam);
|
|
351
|
-
redirectTarget.searchParams.set("iss", requestOrigin);
|
|
352
|
-
if (url.searchParams.get("format") === "json" ||
|
|
353
|
-
String(req.headers.accept || "").includes("application/json")) {
|
|
354
|
-
sendJson(res, 200, {
|
|
355
|
-
ok: true,
|
|
356
|
-
code,
|
|
357
|
-
state: stateParam || null,
|
|
358
|
-
iss: requestOrigin,
|
|
359
|
-
client_id: clientId,
|
|
360
|
-
redirect_uri: redirectTarget.toString(),
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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/
|
|
372
|
-
const
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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 (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
const method = (record.codeChallengeMethod || "S256").toUpperCase();
|
|
411
|
-
const computed = method === "S256"
|
|
412
|
-
? crypto
|
|
413
|
-
.createHash("sha256")
|
|
414
|
-
.update(codeVerifier)
|
|
415
|
-
.digest("base64url")
|
|
416
|
-
: codeVerifier;
|
|
417
|
-
if (computed !== record.codeChallenge) {
|
|
418
|
-
sendJson(res, 400, {
|
|
419
|
-
error: "invalid_grant",
|
|
420
|
-
error_description: "PKCE code_verifier mismatch.",
|
|
421
|
-
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
499
|
-
"/api/v1/
|
|
500
|
-
"/api/v1/
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
706
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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, {
|
|
999
|
-
|
|
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"
|
|
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/")
|
|
1064
|
-
|
|
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
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
1211
|
-
const
|
|
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
|
|
1223
|
-
|
|
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,
|
|
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 };
|