@muxoai/platform 1.3.2 → 1.4.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Muxo (`@muxoai/platform` / `@muxoai/cli` / `@muxoai/sdk`)
2
2
 
3
- Muxo (`v1.3.2`) is the unified execution, cloud infrastructure provisioning, contextual memory, and cryptographic governance platform for AI agents and applications. Customers and their agents interact exclusively with **Muxo** — either via structured tool slugs or free-form **Natural Language** (`muxo do`, `POST /api/v1/do`, `client.do()`, and MCP `plan_action`).
3
+ Muxo (`v1.4.0`) is the unified execution, cloud infrastructure provisioning, contextual memory, and cryptographic governance platform for AI agents and applications. Customers and their agents interact exclusively with **Muxo** — either via structured tool slugs or free-form **Natural Language** (`muxo do`, `POST /api/v1/do`, `client.do()`, and MCP `plan_action`).
4
4
 
5
5
  See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the complete internal engineering blueprint, open-source ecosystem Keep/Adopt/Drop matrix, and 4-pillar architecture.
6
6
 
@@ -37,10 +37,12 @@ muxo byok set openai # Interactive hidden/line prompt if key omitted
37
37
  muxo byok set composio # Executes app.* and instant.* tools at 0 Muxo credit cost
38
38
  muxo byok list
39
39
 
40
- # 6. Manage Billing, Credits & Starter Plan ($29/mo)
40
+ # 6. Manage Billing, Credits & Plans (Starter $29/mo, Pro $99/mo, yearly 2 months free)
41
41
  muxo billing status
42
+ muxo billing plans
42
43
  muxo billing topup 1000
43
- muxo billing upgrade
44
+ muxo billing upgrade --plan pro --yearly
45
+ muxo billing portal
44
46
 
45
47
  # 7. Connect an OAuth SaaS App (browser consent handoff) & Provision Live Cloud Infrastructure
46
48
  muxo link gmail --wait
@@ -115,7 +117,7 @@ Muxo ships a portable OpenAI Plugin (`plugins/muxo/plugin.json` & `plugins/muxo/
115
117
  | `src/providers/composio-sdk.ts` | Live Composio v3 Connected Apps adapter (catalog search, OAuth sessions/links, tool execution) |
116
118
  | `src/providers/composio-instant.ts` | Live Composio Instant adapter (`search`, `scrape`, `crawl`, `seo.audit`, `hackernews`, `github.repo_intel`, `dns.lookup`, `workbench`) |
117
119
  | `src/providers/stripe-projects.ts` | Live Stripe Projects CLI bridge (`status`, `catalog`, `add`, `rotate`, `remove`, `env --pull`) |
118
- | `src/providers/stripe-billing.ts` | Live Stripe Checkout billing (`$29/mo Starter Plan` & `$10 / +1,000 Credits Pack`) |
120
+ | `src/providers/stripe-billing.ts` | Live Stripe Checkout billing (Starter `$29/mo`, Pro `$99/mo`, yearly billing & credit packs with volume discounts) |
119
121
  | `src/providers/microsoft-governance.ts` | Microsoft Agent Governance Toolkit v5.0.0 (`McpSecurityScanner`, `PolicyEngine`, `AgentIdentity` Ed25519 DID, `AuditLogger`) |
120
122
  | `src/providers/supermemory.ts` | Supermemory v5 Contextual Brain (`profile`, hybrid `search`, secret-redacted `add`, `delete`) |
121
123
  | `src/engine.ts` | Unified Governance, Memory & Execution Engine implementing all 19 canonical MCP operations |
package/dist/clerk.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface ClerkProfile {
15
15
  email: string | null;
16
16
  emailVerified: boolean;
17
17
  name: string | null;
18
+ /** Every verified address on the account, lowercased, primary included. */
19
+ verifiedEmails: string[];
18
20
  }
19
21
  export interface ClerkEnv {
20
22
  CLERK_PUBLISHABLE_KEY?: string;
package/dist/clerk.js CHANGED
@@ -187,6 +187,9 @@ export async function fetchClerkProfile(userId, secretKey) {
187
187
  email: primary?.email_address ? primary.email_address.toLowerCase() : null,
188
188
  emailVerified: primary?.verification?.status === "verified",
189
189
  name,
190
+ verifiedEmails: addresses
191
+ .filter((e) => e.email_address && e.verification?.status === "verified")
192
+ .map((e) => e.email_address.toLowerCase()),
190
193
  };
191
194
  }
192
195
  catch {
package/dist/cli.js CHANGED
@@ -110,6 +110,12 @@ async function runCli() {
110
110
  body: opts?.body !== undefined ? JSON.stringify(opts.body) : undefined,
111
111
  });
112
112
  const data = (await res.json().catch(() => ({})));
113
+ if (opts?.acceptPaymentRequired && res.status === 402 && data.checkoutUrl) {
114
+ return data;
115
+ }
116
+ if (opts?.acceptHumanRequired && res.status === 403 && data.code === "human_required") {
117
+ return data;
118
+ }
113
119
  if (!res.ok && data.error) {
114
120
  throw new Error(String(data.error));
115
121
  }
@@ -171,6 +177,44 @@ async function runCli() {
171
177
  };
172
178
  return { store, engine: getEngine(), ctx, tokenAuth };
173
179
  };
180
+ const maskKey = (key) => `${key.slice(0, 14)}…`;
181
+ // Hosted Muxo answers purchases with 402 + a Stripe Checkout URL; a self-hosted dev server may grant credits directly.
182
+ const printBillingResult = (data) => {
183
+ if (data.code === "human_required" || data.status === "human_required") {
184
+ console.log(JSON.stringify({
185
+ ok: false,
186
+ humanRequired: true,
187
+ url: data.url,
188
+ error: data.error,
189
+ next: "Open the url in a browser and sign in to the Muxo console to finish this step. Agent API keys cannot manage billing or approvals.",
190
+ }, null, 2));
191
+ return;
192
+ }
193
+ if (data.portalUrl) {
194
+ console.log(JSON.stringify({
195
+ ok: true,
196
+ actionRequired: true,
197
+ tier: data.tier,
198
+ interval: data.interval,
199
+ portalUrl: data.portalUrl,
200
+ next: data.nextStep || "Open portalUrl in a browser; a signed-in person confirms the plan change in the Stripe billing portal.",
201
+ }, null, 2));
202
+ return;
203
+ }
204
+ if (!data.checkoutUrl) {
205
+ console.log(JSON.stringify(data, null, 2));
206
+ return;
207
+ }
208
+ console.log(JSON.stringify({
209
+ ok: true,
210
+ paymentRequired: true,
211
+ kind: data.kind,
212
+ credits: data.credits,
213
+ checkoutUrl: data.checkoutUrl,
214
+ sessionId: data.sessionId,
215
+ next: `Open checkoutUrl to pay with Stripe. Muxo adds the credits once Stripe confirms the payment. Check with: muxo billing verify ${data.sessionId}`,
216
+ }, null, 2));
217
+ };
174
218
  // CLI Login / Autonomous External Signup (`muxo login`)
175
219
  if (cmd === "login" || cmd === "signup") {
176
220
  const explicitKey = typeof flags["api-key"] === "string"
@@ -203,6 +247,7 @@ async function runCli() {
203
247
  configPath: cfgPath,
204
248
  workspace: ws,
205
249
  ...authPayload,
250
+ apiKey: maskKey(explicitKey),
206
251
  }, null, 2));
207
252
  return;
208
253
  }
@@ -227,6 +272,7 @@ async function runCli() {
227
272
  mode: "api_key",
228
273
  configPath: cfgPath,
229
274
  ...authPayload,
275
+ apiKey: maskKey(explicitKey),
230
276
  }, null, 2));
231
277
  return;
232
278
  }
@@ -402,7 +448,7 @@ async function runCli() {
402
448
  else {
403
449
  console.log(`\n✓ Muxo is ready!`);
404
450
  console.log(` Workspace : ${ensuredConfig.workspaceName} (${ensuredConfig.workspaceId})`);
405
- console.log(` API Key : ${ensuredConfig.apiKey}`);
451
+ console.log(` API Key : ${maskKey(ensuredConfig.apiKey)} (saved in ${getCliConfigPath()})`);
406
452
  console.log(` MCP URL : ${mcpServerUrl}`);
407
453
  console.log(` MCP Config: ${writtenFiles.join(", ")}`);
408
454
  console.log(`\nTry it now:`);
@@ -976,7 +1022,18 @@ async function runCli() {
976
1022
  if (!planId)
977
1023
  throw new Error("Usage: muxo approve <plan_id>");
978
1024
  if (remoteUrl) {
979
- const approved = await remoteFetch(`/api/v1/plans/${encodeURIComponent(planId)}/approve`, { method: "POST", body: {} });
1025
+ const approved = await remoteFetch(`/api/v1/plans/${encodeURIComponent(planId)}/approve`, { method: "POST", body: {}, acceptHumanRequired: true });
1026
+ if (approved.code === "human_required") {
1027
+ console.log(JSON.stringify({
1028
+ ok: false,
1029
+ humanRequired: true,
1030
+ planId,
1031
+ approvalUrl: approved.url,
1032
+ error: approved.error,
1033
+ hint: "Open approvalUrl in a browser; a signed-in person approves the plan in the Muxo console. Agent API keys cannot approve plans.",
1034
+ }, null, 2));
1035
+ return;
1036
+ }
980
1037
  console.log(JSON.stringify(approved.plan || approved, null, 2));
981
1038
  return;
982
1039
  }
@@ -1028,7 +1085,18 @@ async function runCli() {
1028
1085
  const plan = planned.plan || planned;
1029
1086
  if (plan.approvalStatus === "pending_approval") {
1030
1087
  if (flags.approve || cmd === "provision") {
1031
- await remoteFetch(`/api/v1/plans/${encodeURIComponent(plan.id)}/approve`, { method: "POST", body: {} });
1088
+ const approved = await remoteFetch(`/api/v1/plans/${encodeURIComponent(plan.id)}/approve`, { method: "POST", body: {}, acceptHumanRequired: true });
1089
+ if (approved.code === "human_required") {
1090
+ console.log(JSON.stringify({
1091
+ ok: false,
1092
+ status: "pending_approval",
1093
+ humanRequired: true,
1094
+ plan,
1095
+ approvalUrl: approved.url,
1096
+ hint: "This plan needs approval from a signed-in person. Open approvalUrl in a browser to approve it in the Muxo console; agent API keys cannot approve plans.",
1097
+ }, null, 2));
1098
+ return;
1099
+ }
1032
1100
  }
1033
1101
  else {
1034
1102
  console.log(JSON.stringify({
@@ -1183,8 +1251,9 @@ async function runCli() {
1183
1251
  const data = await remoteFetch("/api/v1/billing/topup", {
1184
1252
  method: "POST",
1185
1253
  body: { credits },
1254
+ acceptPaymentRequired: true,
1186
1255
  });
1187
- console.log(JSON.stringify(data, null, 2));
1256
+ printBillingResult(data);
1188
1257
  return;
1189
1258
  }
1190
1259
  const { store, ctx } = getLocalContext();
@@ -1207,12 +1276,19 @@ async function runCli() {
1207
1276
  return;
1208
1277
  }
1209
1278
  if (sub === "upgrade") {
1279
+ const planArg = String(flags.plan || positional[2] || "starter").toLowerCase();
1280
+ const tier = planArg === "pro" ? "pro" : "starter";
1281
+ const interval = flags.yearly || flags.year || String(flags.interval || "").startsWith("y")
1282
+ ? "year"
1283
+ : "month";
1210
1284
  if (remoteUrl) {
1211
1285
  const data = await remoteFetch("/api/v1/billing/upgrade", {
1212
1286
  method: "POST",
1213
- body: { tier: "starter", bonusCredits: 1000 },
1287
+ body: { tier, interval },
1288
+ acceptPaymentRequired: true,
1289
+ acceptHumanRequired: true,
1214
1290
  });
1215
- console.log(JSON.stringify(data, null, 2));
1291
+ printBillingResult(data);
1216
1292
  return;
1217
1293
  }
1218
1294
  const { store, ctx } = getLocalContext();
@@ -1258,6 +1334,27 @@ async function runCli() {
1258
1334
  console.log(JSON.stringify({ ok: true, kind, ...session }, null, 2));
1259
1335
  return;
1260
1336
  }
1337
+ if (sub === "plans") {
1338
+ const plansUrl = remoteUrl || config.baseUrl;
1339
+ const res = await fetch(`${plansUrl}/api/v1/billing/plans`, {
1340
+ headers: { Accept: "application/json" },
1341
+ });
1342
+ const data = (await res.json().catch(() => ({})));
1343
+ console.log(JSON.stringify(data, null, 2));
1344
+ return;
1345
+ }
1346
+ if (sub === "portal") {
1347
+ if (remoteUrl) {
1348
+ const data = await remoteFetch("/api/v1/billing/portal", {
1349
+ method: "POST",
1350
+ body: {},
1351
+ acceptHumanRequired: true,
1352
+ });
1353
+ printBillingResult(data);
1354
+ return;
1355
+ }
1356
+ throw new Error("muxo billing portal is only available for the hosted Muxo service (set --remote or log in first).");
1357
+ }
1261
1358
  if (sub === "verify") {
1262
1359
  const sessionId = positional[2] || String(flags.session || "");
1263
1360
  if (!sessionId)
@@ -1483,21 +1580,18 @@ async function runCli() {
1483
1580
  }
1484
1581
  if (choice === "3") {
1485
1582
  const action = (await promptUserLine("Type 'upgrade' for Starter Plan or credit amount (default 1000): ")).toLowerCase();
1486
- if (action === "upgrade") {
1487
- const up = await remoteFetch("/api/v1/billing/upgrade", {
1583
+ const up = action === "upgrade"
1584
+ ? await remoteFetch("/api/v1/billing/upgrade", {
1488
1585
  method: "POST",
1489
1586
  body: { tier: "starter", bonusCredits: 1000 },
1490
- });
1491
- console.log(JSON.stringify(up, null, 2));
1492
- }
1493
- else {
1494
- const cr = Math.max(100, Number(action) || 1000);
1495
- const up = await remoteFetch("/api/v1/billing/topup", {
1587
+ acceptPaymentRequired: true,
1588
+ })
1589
+ : await remoteFetch("/api/v1/billing/topup", {
1496
1590
  method: "POST",
1497
- body: { credits: cr },
1591
+ body: { credits: Math.max(100, Number(action) || 1000) },
1592
+ acceptPaymentRequired: true,
1498
1593
  });
1499
- console.log(JSON.stringify(up, null, 2));
1500
- }
1594
+ printBillingResult(up);
1501
1595
  return;
1502
1596
  }
1503
1597
  if (choice === "4") {
package/dist/sdk.d.ts CHANGED
@@ -11,6 +11,77 @@ export interface MuxoClientOptions {
11
11
  fetch?: typeof fetch;
12
12
  timeoutMs?: number;
13
13
  }
14
+ /** Error thrown for any non-OK Muxo API response. Carries the machine-readable code and payload. */
15
+ export declare class MuxoApiError extends Error {
16
+ readonly status: number;
17
+ readonly code?: string;
18
+ readonly url?: string;
19
+ readonly checkoutUrl?: string;
20
+ readonly data: Record<string, unknown>;
21
+ constructor(status: number, payload: Record<string, unknown>);
22
+ }
23
+ export interface BillingPlanInfo {
24
+ tier: "free" | "starter" | "pro";
25
+ name: string;
26
+ tagline: string;
27
+ monthlyActions: number;
28
+ monthlyCredits: number;
29
+ prices: {
30
+ month: {
31
+ cents: number;
32
+ label: string;
33
+ };
34
+ year: {
35
+ cents: number;
36
+ label: string;
37
+ perMonthLabel: string;
38
+ };
39
+ };
40
+ features: string[];
41
+ }
42
+ export interface PlanCatalog {
43
+ plans: BillingPlanInfo[];
44
+ yearlyDiscount: string;
45
+ creditPriceCents: number;
46
+ creditPacks: Array<{
47
+ credits: number;
48
+ cents: number;
49
+ label: string;
50
+ }>;
51
+ minCheckoutCredits: number;
52
+ }
53
+ export type UpgradePlanResult = {
54
+ ok: true;
55
+ unchanged: true;
56
+ tier: string;
57
+ message: string;
58
+ workspace?: Workspace;
59
+ } | {
60
+ ok: true;
61
+ mode: "portal";
62
+ action: "billing_portal";
63
+ tier: string;
64
+ interval: string;
65
+ portalUrl: string;
66
+ url: string;
67
+ nextStep: string;
68
+ } | {
69
+ ok: false;
70
+ code: "payment_required";
71
+ paymentRequired: true;
72
+ checkoutUrl: string;
73
+ sessionId: string;
74
+ kind: string;
75
+ tier: string;
76
+ interval: string;
77
+ credits: number;
78
+ error: string;
79
+ } | {
80
+ ok: false;
81
+ code: "human_required";
82
+ url: string;
83
+ error: string;
84
+ };
14
85
  export declare class MuxoClient {
15
86
  readonly baseUrl: string;
16
87
  private readonly apiKeyResolver?;
@@ -398,17 +469,41 @@ export declare class MuxoClient {
398
469
  provider: string;
399
470
  keys: ByokKeyRecord[];
400
471
  }>;
401
- upgradePlan(): Promise<{
472
+ /** Lists plans, prices, and credit packs. No auth required. */
473
+ getPlans(): Promise<{
474
+ ok: true;
475
+ } & PlanCatalog>;
476
+ /**
477
+ * Moves the workspace to a plan. A first subscription returns a Stripe checkout URL
478
+ * (`payment_required`); changing an existing subscription returns a billing-portal URL.
479
+ * Plan changes are confirmed by a signed-in person; API keys get `human_required`.
480
+ */
481
+ upgradePlan(options?: {
482
+ tier?: "starter" | "pro";
483
+ interval?: "month" | "year";
484
+ }): Promise<UpgradePlanResult>;
485
+ /**
486
+ * Opens the Stripe billing portal to manage payment details, switch plans, or cancel.
487
+ * Signed-in people only; API keys get a `human_required` MuxoApiError.
488
+ */
489
+ openBillingPortal(): Promise<{
402
490
  ok: true;
403
- tier: string;
404
- workspace: Workspace;
405
- settlementSummary: SupplierSettlementSummary;
491
+ url: string;
492
+ portalUrl: string;
406
493
  }>;
407
- createCheckoutSession(kind?: "starter_subscription" | "credit_topup"): Promise<{
494
+ createCheckoutSession(kind?: "subscription" | "starter_subscription" | "credit_topup", optionsOrCredits?: number | {
495
+ credits?: number;
496
+ tier?: "starter" | "pro";
497
+ interval?: "month" | "year";
498
+ }): Promise<{
408
499
  ok: true;
409
500
  sessionId: string;
410
501
  url: string;
411
502
  mode?: string;
503
+ kind?: string;
504
+ credits?: number;
505
+ tier?: string;
506
+ interval?: string;
412
507
  }>;
413
508
  verifyCheckoutSession(sessionId: string): Promise<{
414
509
  ok: true;
package/dist/sdk.js CHANGED
@@ -1,3 +1,21 @@
1
+ /** Error thrown for any non-OK Muxo API response. Carries the machine-readable code and payload. */
2
+ export class MuxoApiError extends Error {
3
+ status;
4
+ code;
5
+ url;
6
+ checkoutUrl;
7
+ data;
8
+ constructor(status, payload) {
9
+ super(String(payload.error || `Muxo request failed with HTTP ${status}`));
10
+ this.name = "MuxoApiError";
11
+ this.status = status;
12
+ this.code = typeof payload.code === "string" ? payload.code : undefined;
13
+ this.url = typeof payload.url === "string" ? payload.url : undefined;
14
+ this.checkoutUrl =
15
+ typeof payload.checkoutUrl === "string" ? payload.checkoutUrl : undefined;
16
+ this.data = payload;
17
+ }
18
+ }
1
19
  export class MuxoClient {
2
20
  baseUrl;
3
21
  apiKeyResolver;
@@ -41,7 +59,7 @@ export class MuxoClient {
41
59
  });
42
60
  const payload = (await res.json());
43
61
  if (!res.ok || payload.ok === false) {
44
- throw new Error(String(payload.error || `Muxo request failed with HTTP ${res.status}`));
62
+ throw new MuxoApiError(res.status, payload);
45
63
  }
46
64
  return payload;
47
65
  }
@@ -166,8 +184,21 @@ export class MuxoClient {
166
184
  });
167
185
  let plan = planned.plan;
168
186
  if (plan.approvalStatus === "pending_approval" && options.approve) {
169
- const approved = await this.approvePlan(plan.id);
170
- plan = approved.plan;
187
+ try {
188
+ const approved = await this.approvePlan(plan.id);
189
+ plan = approved.plan;
190
+ }
191
+ catch (err) {
192
+ if (err instanceof MuxoApiError && err.code === "human_required") {
193
+ throw new MuxoApiError(err.status, {
194
+ ...err.data,
195
+ planId: plan.id,
196
+ approvalUrl: err.url,
197
+ error: `Plan ${plan.id} needs approval from a signed-in person at ${err.url || "the Muxo console"}; API keys cannot approve plans.`,
198
+ });
199
+ }
200
+ throw err;
201
+ }
171
202
  }
172
203
  const executed = await this.executePlan(plan.id);
173
204
  return {
@@ -360,16 +391,50 @@ export class MuxoClient {
360
391
  method: "DELETE",
361
392
  });
362
393
  }
363
- async upgradePlan() {
364
- return this.request("/api/v1/billing/upgrade", {
394
+ /** Lists plans, prices, and credit packs. No auth required. */
395
+ async getPlans() {
396
+ return this.request("/api/v1/billing/plans");
397
+ }
398
+ /**
399
+ * Moves the workspace to a plan. A first subscription returns a Stripe checkout URL
400
+ * (`payment_required`); changing an existing subscription returns a billing-portal URL.
401
+ * Plan changes are confirmed by a signed-in person; API keys get `human_required`.
402
+ */
403
+ async upgradePlan(options = {}) {
404
+ try {
405
+ return await this.request("/api/v1/billing/upgrade", {
406
+ method: "POST",
407
+ body: {
408
+ tier: options.tier || "starter",
409
+ interval: options.interval || "month",
410
+ },
411
+ });
412
+ }
413
+ catch (err) {
414
+ if (err instanceof MuxoApiError &&
415
+ (err.code === "payment_required" || err.code === "human_required")) {
416
+ return err.data;
417
+ }
418
+ throw err;
419
+ }
420
+ }
421
+ /**
422
+ * Opens the Stripe billing portal to manage payment details, switch plans, or cancel.
423
+ * Signed-in people only; API keys get a `human_required` MuxoApiError.
424
+ */
425
+ async openBillingPortal() {
426
+ return this.request("/api/v1/billing/portal", {
365
427
  method: "POST",
366
428
  body: {},
367
429
  });
368
430
  }
369
- async createCheckoutSession(kind = "credit_topup") {
431
+ async createCheckoutSession(kind = "credit_topup", optionsOrCredits) {
432
+ const opts = typeof optionsOrCredits === "number"
433
+ ? { credits: optionsOrCredits }
434
+ : optionsOrCredits || {};
370
435
  return this.request("/api/v1/billing/checkout", {
371
436
  method: "POST",
372
- body: { kind },
437
+ body: { kind, ...opts },
373
438
  });
374
439
  }
375
440
  async verifyCheckoutSession(sessionId) {
package/dist/server.js CHANGED
@@ -693,7 +693,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
693
693
  <div class="badge">Ownership Bound</div>
694
694
  <h1>Workspace Claimed</h1>
695
695
  <p>Workspace <code>${redeemed.workspace.name}</code> (${redeemed.workspace.id}) claimed by <strong>${redeemed.targetEmail}</strong>.</p>
696
- <a class="btn" href="/?claimed=${encodeURIComponent(redeemed.workspace.slug)}&api_key=${encodeURIComponent(claimConsoleKey)}">Open Muxo Console →</a>
696
+ <p>Console key: <code>${claimConsoleKey}</code><br />Choose “Use an API key instead” in the console and paste it.</p>
697
+ <a class="btn" href="/?claimed=${encodeURIComponent(redeemed.workspace.slug)}">Open Muxo Console →</a>
697
698
  </div>
698
699
  </body>
699
700
  </html>`);
package/dist/types.d.ts CHANGED
@@ -1,6 +1,23 @@
1
1
  export type RiskLevel = "read" | "write" | "admin" | "financial" | "provisioning" | "destructive";
2
2
  export type ProviderLane = "composio_sdk" | "composio_instant" | "stripe_projects";
3
- export type SubscriptionTier = "free" | "starter";
3
+ export type SubscriptionTier = "free" | "starter" | "pro";
4
+ export type SubscriptionStatus = "none" | "active" | "trialing" | "past_due" | "unpaid" | "canceled" | "incomplete" | "comped";
5
+ export interface WorkspaceBilling {
6
+ interval: "month" | "year" | null;
7
+ status: SubscriptionStatus;
8
+ cancelAtPeriodEnd: boolean;
9
+ /** When Stripe next renews or ends the subscription. */
10
+ currentPeriodEnd: string | null;
11
+ /** Monthly usage periods are counted from this moment. */
12
+ usagePeriodAnchor: string;
13
+ usagePeriodStart: string;
14
+ usagePeriodEnd: string;
15
+ /** usagePeriodStart of the last period whose plan credits were granted. */
16
+ creditsGrantedFor: string | null;
17
+ paymentFailedAt?: string | null;
18
+ /** Subscription whose start (fresh usage period and first credits) was already applied. */
19
+ startedSubscriptionId?: string | null;
20
+ }
4
21
  export interface GovernancePolicy {
5
22
  mode: "monitor" | "enforce";
6
23
  requireApprovalFor: RiskLevel[];
@@ -32,6 +49,7 @@ export interface Workspace {
32
49
  creditsBalance: number;
33
50
  stripeCustomerId: string | null;
34
51
  stripeSubscriptionId: string | null;
52
+ billing?: WorkspaceBilling;
35
53
  projectsEnvName: string;
36
54
  policy: GovernancePolicy;
37
55
  createdAt: string;
@@ -360,7 +378,7 @@ export interface ExecutionReceipt {
360
378
  export interface LedgerEntry {
361
379
  id: string;
362
380
  workspaceId: string;
363
- kind: "plan_grant" | "credit_topup" | "run_deduction" | "subscription_upgrade" | "manual_adjustment";
381
+ kind: "plan_grant" | "credit_topup" | "run_deduction" | "subscription_upgrade" | "plan_change" | "refund" | "manual_adjustment";
364
382
  actionsDelta: number;
365
383
  creditsDelta: number;
366
384
  balanceAfter: number;
@@ -13,6 +13,13 @@ export interface OAuthConsentOptions {
13
13
  frontendApi: string;
14
14
  } | null;
15
15
  request?: Record<string, string>;
16
+ /** Renders the workspace-claim variant, which posts to `action` instead of /oauth/authorize. */
17
+ claim?: {
18
+ workspaceName: string;
19
+ targetEmail: string;
20
+ action: string;
21
+ cancelUrl: string;
22
+ };
16
23
  }
17
24
  /** Consent screen for MCP clients (ChatGPT, Claude, CLIs) asking to use a Muxo workspace. */
18
25
  export declare function renderOAuthConsentHtml(opts: OAuthConsentOptions): string;
@@ -238,6 +238,10 @@ body:has(dialog[open]){overflow:hidden}
238
238
  .auth-alt{margin-top:12px}
239
239
  .row-actions{display:inline-flex;align-items:center;gap:8px;flex:none}
240
240
  .clerk-auth{display:flex;justify-content:center;min-height:120px;margin-top:20px;text-align:left}
241
+ .clerk-loading{display:flex;align-items:center;gap:10px;align-self:center;margin:0;color:var(--color-muted);font-size:15px}
242
+ .spinner{flex:none;width:18px;height:18px;border:2px solid var(--color-border);border-top-color:var(--color-text);border-radius:50%;animation:muxo-spin .8s linear infinite}
243
+ @keyframes muxo-spin{to{transform:rotate(360deg)}}
244
+ @media (prefers-reduced-motion:reduce){.spinner{animation:none}}
241
245
  .auth-switch{margin-top:4px}
242
246
  .auth-switch:has([hidden]){display:none}
243
247
  .auth-foot{margin-top:16px;color:var(--color-muted);font-size:12px}
@@ -263,7 +267,7 @@ var LANE_LABELS = { composio_instant: 'Instant tool', composio_sdk: 'Connected a
263
267
  var TOKEN_KEY = 'muxo_external_api_key';
264
268
  var ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
265
269
 
266
- var state = { view: 'overview', boot: null, harness: 'plugin', lane: 'all', factsMode: 'current', facts: null, authMode: 'login' };
270
+ var state = { view: 'overview', boot: null, harness: 'plugin', lane: 'all', factsMode: 'current', facts: null, authMode: 'login', billingInterval: 'month', planCatalog: null };
267
271
  var currentTool = null;
268
272
  var activeKeyHint = 'mk_live_...';
269
273
  var externalApiKey = readToken();
@@ -468,6 +472,7 @@ async function api(path, opts) {
468
472
  if (!res.ok || data.ok === false) {
469
473
  var err = new Error(data.error || data.message || ('HTTP ' + res.status));
470
474
  err.status = res.status;
475
+ err.data = data;
471
476
  if (res.status === 401 && !opts.anonymous) handleUnauthorized();
472
477
  throw err;
473
478
  }
@@ -535,14 +540,20 @@ async function loadAuth() {
535
540
  });
536
541
  } catch (err) {
537
542
  clerk = null;
543
+ removeClerkLoading();
538
544
  setText('auth-error', err.message);
539
545
  }
540
546
  }
547
+ function removeClerkLoading() {
548
+ var loading = byId('clerk-loading');
549
+ if (loading) loading.remove();
550
+ }
541
551
  function mountClerkAuth(view) {
542
552
  if (!clerk || !clerk.loaded || clerkSignedIn()) return;
543
553
  var key = view === 'signup' ? 'signup' : 'login';
544
554
  if (clerkMounted === key) return;
545
555
  unmountClerkAuth();
556
+ removeClerkLoading();
546
557
  var target = location.origin + '/';
547
558
  var opts = { routing: 'hash', forceRedirectUrl: target, signInForceRedirectUrl: target, signUpForceRedirectUrl: target, appearance: clerkAppearance() };
548
559
  if (key === 'signup') clerk.mountSignUp(byId('clerk-auth'), opts); else clerk.mountSignIn(byId('clerk-auth'), opts);
@@ -1238,10 +1249,19 @@ async function loadPlans() {
1238
1249
  : (p.approvalStatus === 'approved' || p.approvalStatus === 'auto_approved')
1239
1250
  ? '<button class="btn btn-sm" data-action="execute-plan" data-id="' + id + '">Run</button>'
1240
1251
  : '<span class="mono small muted">' + esc(p.runId ? shortId(p.runId) : 'Done') + '</span>';
1241
- return '<tr><td class="mono small">' + esc(shortId(p.id)) + '</td><td class="mono small">' + esc(p.toolSlug) + '</td>' +
1252
+ return '<tr id="plan-row-' + id + '"><td class="mono small">' + esc(shortId(p.id)) + '</td><td class="mono small">' + esc(p.toolSlug) + '</td>' +
1242
1253
  '<td>' + riskBadge(p.riskLevel) + ' ' + pill(verdict, verdict === 'deny' ? 'danger' : '') + '</td>' +
1243
1254
  '<td>' + statusBadge(p.approvalStatus) + '</td><td class="cell-actions">' + action + '</td></tr>';
1244
1255
  }).join('') : emptyRow(5, 'Nothing waiting. Plans appear here when an agent asks to run a tool.');
1256
+ var focusPlan = new URLSearchParams(window.location.search).get('plan');
1257
+ if (focusPlan && /^[A-Za-z0-9_-]+$/.test(focusPlan)) {
1258
+ var row = byId('plan-row-' + focusPlan);
1259
+ if (row) {
1260
+ row.style.background = 'rgba(99, 102, 241, 0.14)';
1261
+ row.scrollIntoView({ behavior: 'smooth', block: 'center' });
1262
+ showToast('This plan is waiting for your approval.');
1263
+ }
1264
+ }
1245
1265
  }
1246
1266
  async function approveAndRun(planId) {
1247
1267
  await post('/api/v1/plans/' + planId + '/approve');
@@ -1339,6 +1359,7 @@ async function loadLedger() {
1339
1359
  '<td class="mono" style="color:' + (delta >= 0 ? 'var(--color-ok)' : 'var(--color-text)') + '">' + (delta >= 0 ? '+' : '') + esc(fmt(delta)) + '</td>' +
1340
1360
  '<td class="mono">' + esc(fmt(e.balanceAfter)) + '</td></tr>';
1341
1361
  }).join('') : emptyRow(4, 'No billing activity yet.');
1362
+ await renderPlanCards();
1342
1363
  await loadByokKeys();
1343
1364
  }
1344
1365
  async function loadByokKeys() {
@@ -1409,19 +1430,6 @@ async function runNaturalLanguagePrompt() {
1409
1430
  throw err;
1410
1431
  }
1411
1432
  }
1412
- async function instantUpgradeStarter() {
1413
- var res = await post('/api/v1/billing/upgrade', { tier: 'starter' });
1414
- showToast(res.message || 'Upgraded to the Starter plan');
1415
- await refreshOverview();
1416
- await loadLedger();
1417
- }
1418
- async function instantTopupCustom() {
1419
- var credits = Math.max(1, Number(byId('topup-credits-input').value || 1000));
1420
- var res = await post('/api/v1/billing/topup', { credits: credits });
1421
- showToast('Added ' + fmt(res.creditsAdded) + ' credits' + (res.usdPaid != null ? ' ($' + Number(res.usdPaid).toFixed(2) + ')' : ''));
1422
- await refreshOverview();
1423
- await loadLedger();
1424
- }
1425
1433
  async function startCheckout(kind) {
1426
1434
  showToast('Opening Stripe Checkout…');
1427
1435
  var credits = Math.max(100, Number(byId('topup-credits-input').value || 1000));
@@ -1429,19 +1437,105 @@ async function startCheckout(kind) {
1429
1437
  if (res.url) window.location.href = res.url;
1430
1438
  }
1431
1439
 
1440
+ var FALLBACK_PLANS = [
1441
+ { tier: 'free', name: 'Free', tagline: 'Try Muxo with your first agent.', monthlyActions: 500, monthlyCredits: 0, prices: { month: { cents: 0, label: '$0' }, year: { cents: 0, label: '$0', perMonthLabel: '$0' } }, features: ['500 governed actions a month', '250 trial credits', 'Approvals, audit log, and workspace memory', 'Bring your own provider keys'] },
1442
+ { tier: 'starter', name: 'Starter', tagline: 'For builders running agents every day.', monthlyActions: 5000, monthlyCredits: 1000, prices: { month: { cents: 2900, label: '$29' }, year: { cents: 29000, label: '$290', perMonthLabel: '$24' } }, features: ['5,000 governed actions a month', '1,000 credits every month', 'Everything in Free', 'Email support'] },
1443
+ { tier: 'pro', name: 'Pro', tagline: 'For teams with agents in production.', monthlyActions: 25000, monthlyCredits: 5000, prices: { month: { cents: 9900, label: '$99' }, year: { cents: 99000, label: '$990', perMonthLabel: '$82.50' } }, features: ['25,000 governed actions a month', '5,000 credits every month', 'Everything in Starter', 'Priority support'] }
1444
+ ];
1445
+ var TIER_ORDER = { free: 0, starter: 1, pro: 2 };
1446
+
1447
+ async function planCatalog() {
1448
+ if (!state.planCatalog) {
1449
+ try { state.planCatalog = await api('/api/v1/billing/plans', { anonymous: true }); } catch (e) { state.planCatalog = { plans: FALLBACK_PLANS }; }
1450
+ }
1451
+ return state.planCatalog;
1452
+ }
1453
+
1454
+ async function renderPlanCards() {
1455
+ var box = byId('plan-cards');
1456
+ if (!box) return;
1457
+ var catalog = await planCatalog();
1458
+ var org = (state.boot && state.boot.organization) || {};
1459
+ var billing = (state.boot && state.boot.billing) || {};
1460
+ var tier = String(org.tier || 'free');
1461
+ var interval = state.billingInterval;
1462
+ box.innerHTML = (catalog.plans || FALLBACK_PLANS).map(function (p) {
1463
+ var current = tier === p.tier;
1464
+ var price = p.prices[interval] || p.prices.month;
1465
+ var priceLine = p.tier === 'free'
1466
+ ? '<p class="price">$0 <span>forever</span></p>'
1467
+ : '<p class="price">' + esc(price.label) + ' <span>' + (interval === 'year' ? 'per year · ' + esc(price.perMonthLabel) + '/mo' : 'per month') + '</span></p>';
1468
+ var action;
1469
+ if (current) action = pill('Current plan', 'ok');
1470
+ else if (TIER_ORDER[p.tier] > TIER_ORDER[tier]) action = '<button class="btn btn-primary" type="button" data-action="change-plan" data-tier="' + esc(p.tier) + '">Upgrade to ' + esc(p.name) + '</button>';
1471
+ else action = '<button class="btn" type="button" data-action="change-plan" data-tier="' + esc(p.tier) + '">Switch to ' + esc(p.name) + '</button>';
1472
+ return '<div class="panel"><div class="panel-header"><div><h2>' + esc(p.name) + '</h2><p>' + esc(p.tagline || '') + '</p></div></div>' +
1473
+ priceLine +
1474
+ '<ul class="plan-list">' + (p.features || []).map(function (f) { return '<li>' + esc(f) + '</li>'; }).join('') + '</ul>' +
1475
+ '<div class="page-actions">' + action + '</div></div>';
1476
+ }).join('');
1477
+ var manage = byId('manage-billing-btn');
1478
+ if (manage) manage.hidden = !billing.canManageBilling;
1479
+ var note = [];
1480
+ if (billing.status === 'comped') note.push('Complimentary plan');
1481
+ else if (billing.status && billing.status !== 'none') note.push('Subscription ' + String(billing.status).replace(/_/g, ' '));
1482
+ if (billing.cancelAtPeriodEnd && billing.renewsAt) note.push('ends ' + String(billing.renewsAt).slice(0, 10));
1483
+ else if (billing.renewsAt && billing.status !== 'comped') note.push('renews ' + String(billing.renewsAt).slice(0, 10));
1484
+ if (billing.paymentFailed) note.push('Payment failed — update your card in Manage billing');
1485
+ if (billing.usage && billing.usage.resetsAt) note.push('Usage resets ' + String(billing.usage.resetsAt).slice(0, 10));
1486
+ setText('billing-status-note', note.join(' · '));
1487
+ }
1488
+
1489
+ function setBillingInterval(interval) {
1490
+ state.billingInterval = interval === 'year' ? 'year' : 'month';
1491
+ document.querySelectorAll('[data-action="billing-interval"]').forEach(function (b) {
1492
+ b.setAttribute('aria-pressed', String(b.dataset.interval === state.billingInterval));
1493
+ });
1494
+ renderPlanCards().catch(showError);
1495
+ }
1496
+
1497
+ async function changePlan(tier) {
1498
+ showToast('Talking to Stripe…');
1499
+ try {
1500
+ var res = await post('/api/v1/billing/upgrade', { tier: tier, interval: state.billingInterval });
1501
+ if (res.portalUrl || (res.action === 'billing_portal' && res.url)) { window.location.href = res.portalUrl || res.url; return; }
1502
+ if (res.url) { window.location.href = res.url; return; }
1503
+ showToast(res.message || 'Plan is unchanged.');
1504
+ await refreshOverview();
1505
+ await renderPlanCards();
1506
+ } catch (err) {
1507
+ var data = err && err.data;
1508
+ if (data && (data.checkoutUrl || data.url)) { window.location.href = data.checkoutUrl || data.url; return; }
1509
+ showError(err);
1510
+ }
1511
+ }
1512
+
1513
+ async function manageBilling() {
1514
+ showToast('Opening the Stripe billing portal…');
1515
+ var res = await post('/api/v1/billing/portal', {});
1516
+ if (res.url) window.location.href = res.url;
1517
+ }
1518
+
1432
1519
  async function checkCheckoutReturn() {
1433
1520
  var params = new URLSearchParams(window.location.search);
1434
1521
  var clean = false;
1435
1522
  var sessionId = params.get('session_id');
1436
1523
  if (params.get('checkout') === 'success' && sessionId) {
1437
1524
  clean = true;
1438
- try {
1439
- var res = await post('/api/v1/billing/verify-session', { sessionId: sessionId });
1440
- if (res.fulfilled) showToast('Payment confirmed. Your workspace has been updated.');
1441
- } catch (err) { console.error(err); }
1525
+ if (isSignedIn()) {
1526
+ try {
1527
+ var res = await post('/api/v1/billing/verify-session', { sessionId: sessionId });
1528
+ showToast(res.fulfilled ? 'Payment confirmed. Your workspace has been updated.' : 'Payment is processing. Credits appear as soon as Stripe confirms it.');
1529
+ } catch (err) { showToast(err.message, 'danger'); }
1530
+ } else {
1531
+ showToast('Payment received. Sign in to see your updated balance.');
1532
+ }
1442
1533
  }
1534
+ if (params.get('checkout') === 'cancelled') { clean = true; showToast('Checkout cancelled. You were not charged.'); }
1535
+ if (params.get('plan') === 'updated') { clean = true; showToast('Your plan change is confirmed.'); }
1443
1536
  if (params.get('connected')) { clean = true; showToast(params.get('connected') + ' is connected'); }
1444
- if (params.get('api_key')) { clean = true; externalApiKey = params.get('api_key'); writeToken(externalApiKey); }
1537
+ // Keys are never accepted from the URL: a crafted link could sign a person into someone else's workspace.
1538
+ if (params.get('api_key')) clean = true;
1445
1539
  if (params.get('claimed')) { clean = true; showToast('You now own this workspace (' + params.get('claimed') + ')'); }
1446
1540
  if (clean) window.history.replaceState({}, '', window.location.pathname);
1447
1541
  }
@@ -1473,14 +1567,15 @@ var ACTIONS = {
1473
1567
  'delete-memory': function (el) { return deleteMemoryItem(el.dataset.id); },
1474
1568
  'facts-mode': function (el) { setFactsMode(el.dataset.mode); },
1475
1569
  'approve-plan': function (el) { return approveAndRun(el.dataset.id); },
1570
+ 'billing-interval': function (el) { setBillingInterval(el.dataset.interval); },
1571
+ 'change-plan': function (el) { return changePlan(el.dataset.tier); },
1572
+ 'manage-billing': function () { return manageBilling(); },
1476
1573
  'execute-plan': function (el) { return executeExistingPlan(el.dataset.id); },
1477
1574
  'kill-resume': resumeKillSwitch,
1478
1575
  'refresh-governance': loadPlans,
1479
1576
  'refresh-runs': loadRuns,
1480
1577
  'inspect-receipt': function (el) { return inspectReceipt(el.dataset.id); },
1481
1578
  'checkout': function (el) { return startCheckout(el.dataset.kind); },
1482
- 'instant-upgrade': instantUpgradeStarter,
1483
- 'instant-topup': instantTopupCustom,
1484
1579
  'delete-byok': function (el) { return deleteByokKeyFromUi(el.dataset.provider); },
1485
1580
  'plan-only': submitPlanOnly,
1486
1581
  'plan-run': submitPlanAndExecute,
@@ -2057,17 +2152,18 @@ export function renderConsoleHtml() {
2057
2152
  <div class="metric"><p class="metric-label">Paid to providers</p><p class="metric-value" id="billing-wholesale">$0.00</p><p class="metric-sub">Settled by Muxo for you</p></div>
2058
2153
  </div>
2059
2154
  <div class="split even">
2060
- <div class="panel">
2061
- <div class="panel-header"><div><h2>Starter plan</h2><p>For teams running agents every day.</p></div></div>
2062
- <p class="price">$29 <span>per month</span></p>
2063
- <ul class="plan-list">
2064
- <li>10,000 governed actions a month</li>
2065
- <li>1,000 credits included</li>
2066
- <li>Approvals, audit log, and workspace memory</li>
2067
- </ul>
2068
- <div class="page-actions">
2069
- <button class="btn btn-primary" type="button" data-action="checkout" data-kind="starter_subscription">Upgrade with Stripe</button>
2070
- <button class="btn" type="button" data-action="instant-upgrade">Upgrade now (test mode)</button>
2155
+ <div class="panel" style="grid-column:1/-1">
2156
+ <div class="panel-header">
2157
+ <div><h2>Plans</h2><p>Every plan includes approvals, the audit log, and workspace memory. Yearly billing gives you 2 months free.</p></div>
2158
+ <div class="row-actions" role="group" aria-label="Billing period">
2159
+ <button class="chip" type="button" data-action="billing-interval" data-interval="month" aria-pressed="true">Monthly</button>
2160
+ <button class="chip" type="button" data-action="billing-interval" data-interval="year" aria-pressed="false">Yearly · 2 months free</button>
2161
+ </div>
2162
+ </div>
2163
+ <div id="plan-cards" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px"><p class="empty-note">Loading plans…</p></div>
2164
+ <div class="page-actions" style="margin-top:16px;align-items:center">
2165
+ <button class="btn" type="button" data-action="manage-billing" id="manage-billing-btn" hidden>Manage billing in Stripe</button>
2166
+ <small class="muted" id="billing-status-note"></small>
2071
2167
  </div>
2072
2168
  </div>
2073
2169
  <div class="panel">
@@ -2075,7 +2171,6 @@ export function renderConsoleHtml() {
2075
2171
  <label class="field"><span>Credits</span><input id="topup-credits-input" type="number" value="1000" min="100" step="100" inputmode="numeric" /><small class="field-help">1,000 credits cost $10.</small></label>
2076
2172
  <div class="page-actions" style="margin-top:16px">
2077
2173
  <button class="btn btn-primary" type="button" data-action="checkout" data-kind="credit_topup">Pay with Stripe</button>
2078
- <button class="btn" type="button" data-action="instant-topup">Add now (test mode)</button>
2079
2174
  </div>
2080
2175
  </div>
2081
2176
  </div>
@@ -2139,7 +2234,7 @@ export function renderConsoleHtml() {
2139
2234
  <p id="auth-error" class="field-error" role="alert"></p>
2140
2235
  <button id="auth-submit" class="btn btn-primary btn-lg btn-block" type="submit">Sign in</button>
2141
2236
  </form>
2142
- <div id="clerk-auth" class="clerk-auth" hidden></div>
2237
+ <div id="clerk-auth" class="clerk-auth" hidden><p id="clerk-loading" class="clerk-loading" role="status"><span class="spinner" aria-hidden="true"></span>Loading sign-in…</p></div>
2143
2238
  <p class="auth-switch"><button id="auth-switch-link" class="btn btn-quiet btn-sm" type="button" data-action="auth-mode" data-mode="signup" hidden>New to Muxo? Create an account</button></p>
2144
2239
  <div class="auth-alt"><button class="btn btn-quiet" type="button" data-action="switch-workspace">${uiIcon("key")}Use an API key instead</button></div>
2145
2240
  <p class="auth-foot">Setting up an agent? Run <code>npx -y @muxoai/cli@latest login --signup</code></p>
@@ -2450,20 +2545,21 @@ const consentScript = String.raw `
2450
2545
  }
2451
2546
  async function decide(decision) {
2452
2547
  if (busy) return;
2548
+ if (decision === 'deny' && C.cancelUrl) { location.assign(C.cancelUrl); return; }
2453
2549
  busy = true;
2454
2550
  fail('');
2455
2551
  byId('consent-allow').disabled = true;
2456
2552
  byId('consent-deny').disabled = true;
2457
2553
  try {
2458
2554
  var token = await clerk.session.getToken();
2459
- var res = await fetch('/oauth/authorize', {
2555
+ var res = await fetch(C.action || '/oauth/authorize', {
2460
2556
  method: 'POST',
2461
2557
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
2462
2558
  body: JSON.stringify(Object.assign({}, C.request, { decision: decision }))
2463
2559
  });
2464
2560
  var data = await res.json().catch(function () { return {}; });
2465
2561
  if (!res.ok || !data.redirect) throw new Error(data.error || 'Muxo could not finish this request. Try again.');
2466
- byId(decision === 'allow' ? 'consent-allow' : 'consent-deny').textContent = decision === 'allow' ? 'Returning…' : 'Cancelling…';
2562
+ byId(decision === 'allow' ? 'consent-allow' : 'consent-deny').textContent = decision === 'allow' ? (C.action ? 'Opening…' : 'Returning…') : 'Cancelling…';
2467
2563
  location.assign(data.redirect);
2468
2564
  } catch (err) {
2469
2565
  fail(err.message);
@@ -2501,7 +2597,7 @@ export function renderOAuthConsentHtml(opts) {
2501
2597
  <head>
2502
2598
  <meta charset="utf-8" />
2503
2599
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
2504
- <title>${opts.error ? "Connection problem" : `Connect ${clientNameMidSentence}`} · Muxo</title>
2600
+ <title>${opts.error ? (opts.claim ? "Claim problem" : "Connection problem") : opts.claim ? `Claim ${escapeHtml(opts.claim.workspaceName)}` : `Connect ${clientNameMidSentence}`} · Muxo</title>
2505
2601
  <meta name="theme-color" content="${muxoThemes.light.bg}" />
2506
2602
  <meta name="color-scheme" content="light dark" />
2507
2603
  <meta name="robots" content="noindex" />
@@ -2515,30 +2611,43 @@ export function renderOAuthConsentHtml(opts) {
2515
2611
  <main class="auth-view">
2516
2612
  <div class="auth-card">
2517
2613
  <div class="auth-mark">${muxoMark}</div>
2518
- <h1>This connection link isn't valid</h1>
2614
+ <h1>${opts.claim ? "This claim link isn't valid" : "This connection link isn't valid"}</h1>
2519
2615
  <p class="lede">${escapeHtml(opts.error)}</p>
2520
- <p class="auth-foot">Start the connection again from your app, or <a href="${DOCS_URL}">read the setup guide</a>.</p>
2616
+ <p class="auth-foot">${opts.claim ? "Ask the agent for a new claim link" : "Start the connection again from your app"}, or <a href="${DOCS_URL}">read the setup guide</a>.</p>
2521
2617
  </div>
2522
2618
  </main>
2523
2619
  </body>
2524
2620
  </html>`;
2525
2621
  }
2622
+ const claim = opts.claim;
2526
2623
  const config = {
2527
2624
  clerk: opts.clerk || null,
2528
2625
  request: opts.request || {},
2626
+ action: claim?.action,
2627
+ cancelUrl: claim?.cancelUrl,
2529
2628
  };
2530
- return `${head}
2531
- <main class="auth-view">
2532
- <div class="auth-card">
2533
- <div class="auth-mark">${muxoMark}</div>
2534
- <h1>Connect ${clientNameMidSentence} to Muxo</h1>
2629
+ const intro = claim
2630
+ ? `<h1>Claim ${escapeHtml(claim.workspaceName)}</h1>
2631
+ <p class="lede">An agent created this Muxo workspace and invited you to own it.</p>
2632
+ <ul class="consent-list">
2633
+ <li>You become the owner of this workspace and its credits</li>
2634
+ <li>You manage its keys, safety rules, memory, and billing</li>
2635
+ <li>The agent keeps working with its key until you revoke it</li>
2636
+ </ul>
2637
+ <p class="consent-redirect">Sign in as <strong>${escapeHtml(claim.targetEmail)}</strong> to accept.</p>`
2638
+ : `<h1>Connect ${clientNameMidSentence} to Muxo</h1>
2535
2639
  <p class="lede">${clientName} wants to use your Muxo workspace.</p>
2536
2640
  <ul class="consent-list">
2537
2641
  <li>Find tools and plan actions in your workspace</li>
2538
2642
  <li>Run tools under your workspace's safety rules and approvals</li>
2539
2643
  <li>Read the status of connections, plans, and runs</li>
2540
2644
  </ul>
2541
- <p class="consent-redirect">After you decide, you'll return to <strong>${escapeHtml(opts.redirectOrigin || "")}</strong></p>
2645
+ <p class="consent-redirect">After you decide, you'll return to <strong>${escapeHtml(opts.redirectOrigin || "")}</strong></p>`;
2646
+ return `${head}
2647
+ <main class="auth-view">
2648
+ <div class="auth-card">
2649
+ <div class="auth-mark">${muxoMark}</div>
2650
+ ${intro}
2542
2651
  <div class="consent-state">
2543
2652
  <p id="consent-loading" class="muted">Checking your sign-in…</p>
2544
2653
  <div id="consent-signin" hidden>
@@ -2548,14 +2657,14 @@ export function renderOAuthConsentHtml(opts) {
2548
2657
  <div id="consent-approve" hidden>
2549
2658
  <p class="consent-account">Signed in as <strong id="consent-email"></strong></p>
2550
2659
  <div class="consent-actions">
2551
- <button id="consent-deny" class="btn btn-lg" type="button">Cancel</button>
2552
- <button id="consent-allow" class="btn btn-primary btn-lg" type="button">Allow</button>
2660
+ <button id="consent-deny" class="btn btn-lg" type="button">${claim ? "Not now" : "Cancel"}</button>
2661
+ <button id="consent-allow" class="btn btn-primary btn-lg" type="button">${claim ? "Claim workspace" : "Allow"}</button>
2553
2662
  </div>
2554
2663
  <p class="consent-switch"><button id="consent-switch" class="btn btn-quiet btn-sm" type="button">Use a different account</button></p>
2555
2664
  </div>
2556
2665
  <p id="consent-error" class="field-error" role="alert"></p>
2557
2666
  </div>
2558
- <p class="auth-foot">You can remove this connection anytime by deleting its key in the Muxo console.</p>
2667
+ <p class="auth-foot">${claim ? "Only the invited email can claim this workspace, and the link works once." : "You can remove this connection anytime by deleting its key in the Muxo console."}</p>
2559
2668
  </div>
2560
2669
  </main>
2561
2670
  <script>window.MUXO_CONSENT = ${inlineJson(config)};${consentScript}</script>
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MUXO_VERSION = "1.3.2";
1
+ export declare const MUXO_VERSION = "1.4.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const MUXO_VERSION = "1.3.2";
1
+ export const MUXO_VERSION = "1.4.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muxoai/platform",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Muxo — Unified Execution, Provisioning & Governance Platform over Composio, Stripe Projects, Microsoft Agent Governance & Supermemory",
5
5
  "private": false,
6
6
  "license": "MIT",