@muxoai/platform 1.2.3 → 1.3.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/dist/store.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
- import type { ActionPlan, ApiKeyRecord, ConnectionRecord, ExecutionReceipt, ExecutionRun, GovernanceDecisionRecord, GovernancePolicy, LedgerEntry, MemoryContextSummary, ProviderLane, ProvisionedResource, RiskLevel, SupplierSettlement, SupplierSettlementSummary, User, Workspace, WorkspaceMemoryRecord } from "./types.js";
2
+ import type { ActionPlan, ApiKeyRecord, ByokKeyRecord, ConnectionRecord, ExecutionReceipt, ExecutionRun, GovernanceDecisionRecord, GovernancePolicy, LedgerEntry, MemoryContextSummary, ProviderLane, ProvisionedResource, RiskLevel, SupplierSettlement, SupplierSettlementSummary, User, Workspace, WorkspaceMemoryRecord } from "./types.js";
3
+ export declare const BYOK_PROVIDER_CATALOG: Record<string, {
4
+ category: "llm" | "supplier";
5
+ envVarName: string;
6
+ label: string;
7
+ }>;
3
8
  export declare class MuxoStore {
4
9
  readonly db: DatabaseSync;
5
10
  private readonly vaultKey;
@@ -77,6 +82,16 @@ export declare class MuxoStore {
77
82
  getResource(workspaceId: string, resourceNameOrId: string): ProvisionedResource | null;
78
83
  listResources(workspaceId: string): ProvisionedResource[];
79
84
  getDecryptedWorkspaceEnv(workspaceId: string): Record<string, string>;
85
+ normalizeByokProvider(providerInput: string): {
86
+ provider: string;
87
+ category: "llm" | "supplier";
88
+ envVarName: string;
89
+ };
90
+ setByokKey(workspaceId: string, providerInput: string, secretKey: string): ByokKeyRecord;
91
+ getByokKey(workspaceId: string, providerInput: string): string | null;
92
+ deleteByokKey(workspaceId: string, providerInput: string): boolean;
93
+ listByokKeys(workspaceId: string): ByokKeyRecord[];
94
+ hasByokForLane(workspaceId: string, lane: ProviderLane, toolSlug?: string): boolean;
80
95
  createPlan(params: {
81
96
  workspaceId: string;
82
97
  actorId: string;
@@ -126,6 +141,7 @@ export declare class MuxoStore {
126
141
  stripeCustomerId?: string | null;
127
142
  stripeSubscriptionId?: string | null;
128
143
  referenceId: string;
144
+ bonusCredits?: number;
129
145
  }): Workspace;
130
146
  addWorkspaceCredits(params: {
131
147
  workspaceId: string;
@@ -142,7 +158,7 @@ export declare class MuxoStore {
142
158
  referenceId: string | null;
143
159
  description: string;
144
160
  }): LedgerEntry;
145
- computeSupplierSettlement(lane: ProviderLane, toolSlug: string, creditCost: number, settlementStatus?: "settled_by_muxo_master_wallet" | "planned_muxo_master_wallet"): SupplierSettlement;
161
+ computeSupplierSettlement(lane: ProviderLane, toolSlug: string, creditCost: number, settlementStatus?: "settled_by_muxo_master_wallet" | "planned_muxo_master_wallet" | "settled_by_customer_byok" | "planned_customer_byok", workspaceId?: string): SupplierSettlement;
146
162
  getSupplierSettlementSummary(workspaceId: string): SupplierSettlementSummary;
147
163
  listLedgerEntries(workspaceId: string, limit?: number): LedgerEntry[];
148
164
  saveMemoryRecord(params: {
package/dist/store.js CHANGED
@@ -1,5 +1,16 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import crypto from "node:crypto";
3
+ export const BYOK_PROVIDER_CATALOG = {
4
+ openai: { category: "llm", envVarName: "OPENAI_API_KEY", label: "OpenAI (GPT-4o / o3-mini)" },
5
+ anthropic: { category: "llm", envVarName: "ANTHROPIC_API_KEY", label: "Anthropic (Claude 3.7 Sonnet)" },
6
+ gemini: { category: "llm", envVarName: "GEMINI_API_KEY", label: "Google Gemini" },
7
+ openrouter: { category: "llm", envVarName: "OPENROUTER_API_KEY", label: "OpenRouter (Universal LLM)" },
8
+ composio: { category: "supplier", envVarName: "COMPOSIO_API_KEY", label: "Composio SDK & Instant" },
9
+ stripe: { category: "supplier", envVarName: "STRIPE_SECRET_KEY", label: "Stripe Projects & Billing" },
10
+ supermemory: { category: "supplier", envVarName: "SUPERMEMORY_API_KEY", label: "Supermemory v5 Knowledge Graph" },
11
+ firecrawl: { category: "supplier", envVarName: "FIRECRAWL_API_KEY", label: "Firecrawl Web Scrape & Crawl" },
12
+ exa: { category: "supplier", envVarName: "EXA_API_KEY", label: "Exa Semantic Web Search" },
13
+ };
3
14
  const DEFAULT_POLICY = {
4
15
  mode: "enforce",
5
16
  requireApprovalFor: ["admin", "financial", "provisioning", "destructive"],
@@ -201,6 +212,17 @@ export class MuxoStore {
201
212
  event_id TEXT PRIMARY KEY,
202
213
  processed_at TEXT NOT NULL
203
214
  );
215
+
216
+ CREATE TABLE IF NOT EXISTS byok_keys (
217
+ workspace_id TEXT NOT NULL,
218
+ provider TEXT NOT NULL,
219
+ category TEXT NOT NULL,
220
+ env_var_name TEXT NOT NULL,
221
+ key_preview TEXT NOT NULL,
222
+ encrypted_secret_hex TEXT NOT NULL,
223
+ updated_at TEXT NOT NULL,
224
+ PRIMARY KEY (workspace_id, provider)
225
+ );
204
226
  `);
205
227
  // Backwards-compatible column migrations for existing databases
206
228
  for (const stmt of [
@@ -560,8 +582,146 @@ export class MuxoStore {
560
582
  for (const row of rows) {
561
583
  Object.assign(merged, this.decryptSecrets(workspaceId, row.encrypted_secrets_hex));
562
584
  }
585
+ const byokRows = this.db
586
+ .prepare("SELECT encrypted_secret_hex FROM byok_keys WHERE workspace_id = ?")
587
+ .all(workspaceId);
588
+ for (const row of byokRows) {
589
+ Object.assign(merged, this.decryptSecrets(workspaceId, row.encrypted_secret_hex));
590
+ }
563
591
  return merged;
564
592
  }
593
+ // --- BYOK (Bring Your Own Key) Encrypted Vault ---
594
+ normalizeByokProvider(providerInput) {
595
+ const raw = providerInput.trim().toLowerCase();
596
+ for (const [slug, meta] of Object.entries(BYOK_PROVIDER_CATALOG)) {
597
+ if (raw === slug || raw === meta.envVarName.toLowerCase()) {
598
+ return { provider: slug, category: meta.category, envVarName: meta.envVarName };
599
+ }
600
+ }
601
+ const clean = raw.replace(/[^a-z0-9_]/g, "");
602
+ const envVarName = raw.endsWith("_key") || raw.endsWith("_token")
603
+ ? raw.toUpperCase()
604
+ : `${clean.toUpperCase()}_API_KEY`;
605
+ return {
606
+ provider: clean || "custom",
607
+ category: "supplier",
608
+ envVarName,
609
+ };
610
+ }
611
+ setByokKey(workspaceId, providerInput, secretKey) {
612
+ const trimmed = secretKey.trim();
613
+ if (!trimmed) {
614
+ throw new Error("BYOK secretKey must not be empty");
615
+ }
616
+ const { provider, category, envVarName } = this.normalizeByokProvider(providerInput);
617
+ const now = new Date().toISOString();
618
+ const keyPreview = trimmed.length <= 8
619
+ ? `${trimmed.slice(0, 2)}***`
620
+ : `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
621
+ const encryptedHex = this.encryptSecrets(workspaceId, { [envVarName]: trimmed });
622
+ this.db
623
+ .prepare(`INSERT INTO byok_keys (
624
+ workspace_id, provider, category, env_var_name, key_preview, encrypted_secret_hex, updated_at
625
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
626
+ ON CONFLICT(workspace_id, provider) DO UPDATE SET
627
+ category = excluded.category,
628
+ env_var_name = excluded.env_var_name,
629
+ key_preview = excluded.key_preview,
630
+ encrypted_secret_hex = excluded.encrypted_secret_hex,
631
+ updated_at = excluded.updated_at`)
632
+ .run(workspaceId, provider, category, envVarName, keyPreview, encryptedHex, now);
633
+ return {
634
+ provider,
635
+ providerSlug: provider,
636
+ category,
637
+ envVarName,
638
+ keyPreview,
639
+ configured: true,
640
+ updatedAt: now,
641
+ };
642
+ }
643
+ getByokKey(workspaceId, providerInput) {
644
+ const { provider, envVarName } = this.normalizeByokProvider(providerInput);
645
+ const row = this.db
646
+ .prepare("SELECT encrypted_secret_hex FROM byok_keys WHERE workspace_id = ? AND provider = ?")
647
+ .get(workspaceId, provider);
648
+ if (!row)
649
+ return null;
650
+ const dec = this.decryptSecrets(workspaceId, row.encrypted_secret_hex);
651
+ return dec[envVarName] || Object.values(dec)[0] || null;
652
+ }
653
+ deleteByokKey(workspaceId, providerInput) {
654
+ const { provider } = this.normalizeByokProvider(providerInput);
655
+ const res = this.db
656
+ .prepare("DELETE FROM byok_keys WHERE workspace_id = ? AND provider = ?")
657
+ .run(workspaceId, provider);
658
+ return Number(res.changes || 0) > 0;
659
+ }
660
+ listByokKeys(workspaceId) {
661
+ const rows = this.db
662
+ .prepare("SELECT provider, category, env_var_name, key_preview, updated_at FROM byok_keys WHERE workspace_id = ? ORDER BY updated_at DESC")
663
+ .all(workspaceId);
664
+ const configuredMap = new Map();
665
+ for (const r of rows) {
666
+ const p = String(r.provider);
667
+ configuredMap.set(p, {
668
+ provider: p,
669
+ providerSlug: p,
670
+ category: String(r.category),
671
+ envVarName: String(r.env_var_name),
672
+ keyPreview: String(r.key_preview),
673
+ configured: true,
674
+ updatedAt: String(r.updated_at),
675
+ });
676
+ }
677
+ const result = [];
678
+ for (const [slug, meta] of Object.entries(BYOK_PROVIDER_CATALOG)) {
679
+ const existing = configuredMap.get(slug);
680
+ if (existing) {
681
+ result.push(existing);
682
+ configuredMap.delete(slug);
683
+ }
684
+ else {
685
+ result.push({
686
+ provider: slug,
687
+ providerSlug: slug,
688
+ category: meta.category,
689
+ envVarName: meta.envVarName,
690
+ keyPreview: "",
691
+ configured: false,
692
+ updatedAt: "",
693
+ });
694
+ }
695
+ }
696
+ for (const extra of configuredMap.values()) {
697
+ result.push(extra);
698
+ }
699
+ return result;
700
+ }
701
+ hasByokForLane(workspaceId, lane, toolSlug) {
702
+ const slug = (toolSlug || "").toLowerCase();
703
+ if (slug.startsWith("billing."))
704
+ return false;
705
+ if (lane === "composio_sdk") {
706
+ return Boolean(this.getByokKey(workspaceId, "composio"));
707
+ }
708
+ if (lane === "composio_instant") {
709
+ if (this.getByokKey(workspaceId, "composio"))
710
+ return true;
711
+ if ((slug.includes("scrape") || slug.includes("crawl") || slug.includes("seo")) &&
712
+ this.getByokKey(workspaceId, "firecrawl")) {
713
+ return true;
714
+ }
715
+ if (slug.includes("search") && this.getByokKey(workspaceId, "exa")) {
716
+ return true;
717
+ }
718
+ return false;
719
+ }
720
+ if (lane === "stripe_projects") {
721
+ return Boolean(this.getByokKey(workspaceId, "stripe"));
722
+ }
723
+ return false;
724
+ }
565
725
  // --- Plans ---
566
726
  createPlan(params) {
567
727
  const id = `plan_${crypto.randomBytes(8).toString("hex")}`;
@@ -774,7 +934,8 @@ export class MuxoStore {
774
934
  if (!ws)
775
935
  throw new Error(`Workspace ${params.workspaceId} not found`);
776
936
  const now = new Date().toISOString();
777
- const newCredits = ws.creditsBalance + 1000;
937
+ const bonus = params.bonusCredits !== undefined ? Number(params.bonusCredits) : 1000;
938
+ const newCredits = ws.creditsBalance + bonus;
778
939
  this.db
779
940
  .prepare(`UPDATE workspaces
780
941
  SET tier = 'starter',
@@ -789,10 +950,10 @@ export class MuxoStore {
789
950
  workspaceId: params.workspaceId,
790
951
  kind: "subscription_upgrade",
791
952
  actionsDelta: 4000,
792
- creditsDelta: 1000,
953
+ creditsDelta: bonus,
793
954
  balanceAfter: newCredits,
794
955
  referenceId: params.referenceId,
795
- description: "Upgraded to Starter Plan ($29/mo — 5,000 actions/mo + 1,000 credits)",
956
+ description: `Upgraded to Starter Plan ($29/mo — 5,000 actions/mo + ${bonus} credits)`,
796
957
  });
797
958
  return this.getWorkspace(params.workspaceId);
798
959
  }
@@ -840,12 +1001,33 @@ export class MuxoStore {
840
1001
  createdAt: now,
841
1002
  };
842
1003
  }
843
- computeSupplierSettlement(lane, toolSlug, creditCost, settlementStatus = "settled_by_muxo_master_wallet") {
1004
+ computeSupplierSettlement(lane, toolSlug, creditCost, settlementStatus = "settled_by_muxo_master_wallet", workspaceId) {
1005
+ const useByok = settlementStatus === "settled_by_customer_byok" ||
1006
+ settlementStatus === "planned_customer_byok" ||
1007
+ (workspaceId ? this.hasByokForLane(workspaceId, lane, toolSlug) : false);
1008
+ const upstreamProvider = toolSlug.split(".")[1] || lane;
1009
+ if (useByok) {
1010
+ const isPlanned = settlementStatus === "planned_muxo_master_wallet" ||
1011
+ settlementStatus === "planned_customer_byok";
1012
+ return {
1013
+ fundingMode: "byok",
1014
+ supplier: lane,
1015
+ supplierName: "Customer BYOK Upstream Key (0% Muxo Markup)",
1016
+ upstreamProvider,
1017
+ userChargeCredits: 0,
1018
+ userRetailChargeCents: 0,
1019
+ supplierWholesalePayableCents: 0,
1020
+ muxoNetMarginCents: 0,
1021
+ settlementStatus: isPlanned
1022
+ ? "planned_customer_byok"
1023
+ : "settled_by_customer_byok",
1024
+ masterAccountRef: "customer_byok_vault",
1025
+ };
1026
+ }
844
1027
  const userChargeCredits = Math.max(0, Number(creditCost || 0));
845
1028
  const userRetailChargeCents = Math.round(userChargeCredits * 100) / 100;
846
1029
  const supplierWholesalePayableCents = Math.round(userRetailChargeCents * 0.6 * 100) / 100;
847
1030
  const muxoNetMarginCents = Math.round((userRetailChargeCents - supplierWholesalePayableCents) * 100) / 100;
848
- const upstreamProvider = toolSlug.split(".")[1] || lane;
849
1031
  const supplierMeta = {
850
1032
  composio_instant: {
851
1033
  supplierName: "Composio Instant Wholesale Wallet",
@@ -878,6 +1060,9 @@ export class MuxoStore {
878
1060
  const ws = this.getWorkspace(workspaceId);
879
1061
  const ledger = this.listLedgerEntries(workspaceId, 500);
880
1062
  const runs = this.listRuns(workspaceId, 500).filter((r) => r.status === "succeeded");
1063
+ const byokProviders = this.listByokKeys(workspaceId)
1064
+ .filter((k) => k.configured)
1065
+ .map((k) => k.provider);
881
1066
  const totalCreditsPurchasedOrGranted = ledger
882
1067
  .filter((e) => e.creditsDelta > 0)
883
1068
  .reduce((acc, e) => acc + e.creditsDelta, 0);
@@ -909,7 +1094,9 @@ export class MuxoStore {
909
1094
  let totalSupplierWholesalePaidCents = 0;
910
1095
  let totalMuxoNetMarginCents = 0;
911
1096
  for (const run of runs) {
912
- const s = this.computeSupplierSettlement(run.lane, run.toolSlug, run.creditsMetered, "settled_by_muxo_master_wallet");
1097
+ const s = this.computeSupplierSettlement(run.lane, run.toolSlug, run.creditsMetered, run.creditsMetered === 0 && this.hasByokForLane(workspaceId, run.lane, run.toolSlug)
1098
+ ? "settled_by_customer_byok"
1099
+ : "settled_by_muxo_master_wallet", workspaceId);
913
1100
  const bucket = bySupplier[run.lane] || bySupplier.composio_instant;
914
1101
  bucket.runsCount += 1;
915
1102
  bucket.creditsConsumed += s.userChargeCredits;
@@ -930,6 +1117,8 @@ export class MuxoStore {
930
1117
  return {
931
1118
  workspaceId,
932
1119
  fundingModel: "user_pays_muxo_muxo_pays_suppliers",
1120
+ activeFundingMode: byokProviders.length > 0 ? "byok_hybrid" : "muxo_funded",
1121
+ byokProviders,
933
1122
  creditPriceCents: 1,
934
1123
  totalCreditsPurchasedOrGranted,
935
1124
  totalCreditsConsumed,
@@ -1203,7 +1392,7 @@ export class MuxoStore {
1203
1392
  : null,
1204
1393
  supplierSettlement: this.computeSupplierSettlement(lane, toolSlug, creditCost, approvalStatus === "executed"
1205
1394
  ? "settled_by_muxo_master_wallet"
1206
- : "planned_muxo_master_wallet"),
1395
+ : "planned_muxo_master_wallet", String(row.workspace_id)),
1207
1396
  createdAt: String(row.created_at),
1208
1397
  expiresAt: String(row.expires_at),
1209
1398
  };
@@ -1253,7 +1442,7 @@ export class MuxoStore {
1253
1442
  decisionRecord: row.decision_record_json
1254
1443
  ? JSON.parse(String(row.decision_record_json))
1255
1444
  : null,
1256
- supplierSettlement: this.computeSupplierSettlement(lane, toolSlug, creditsMetered, "settled_by_muxo_master_wallet"),
1445
+ supplierSettlement: this.computeSupplierSettlement(lane, toolSlug, creditsMetered, "settled_by_muxo_master_wallet", String(row.workspace_id)),
1257
1446
  issuedAt: String(row.issued_at),
1258
1447
  };
1259
1448
  }
package/dist/types.d.ts CHANGED
@@ -62,6 +62,7 @@ export interface MuxoTool {
62
62
  providerUrl?: string;
63
63
  metadata?: Record<string, unknown>;
64
64
  }
65
+ export type ToolDescriptor = MuxoTool;
65
66
  export interface ConnectionRecord {
66
67
  id: string;
67
68
  workspaceId: string;
@@ -168,8 +169,17 @@ export interface WorkspaceMemoryRecord {
168
169
  metadata: Record<string, unknown>;
169
170
  createdAt: string;
170
171
  }
172
+ export interface ByokKeyRecord {
173
+ provider: string;
174
+ providerSlug?: string;
175
+ category: "llm" | "supplier";
176
+ envVarName: string;
177
+ keyPreview: string;
178
+ configured: boolean;
179
+ updatedAt: string;
180
+ }
171
181
  export interface SupplierSettlement {
172
- fundingMode: "muxo_funded";
182
+ fundingMode: "muxo_funded" | "byok";
173
183
  supplier: ProviderLane;
174
184
  supplierName: string;
175
185
  upstreamProvider: string;
@@ -177,12 +187,14 @@ export interface SupplierSettlement {
177
187
  userRetailChargeCents: number;
178
188
  supplierWholesalePayableCents: number;
179
189
  muxoNetMarginCents: number;
180
- settlementStatus: "settled_by_muxo_master_wallet" | "planned_muxo_master_wallet";
190
+ settlementStatus: "settled_by_muxo_master_wallet" | "planned_muxo_master_wallet" | "settled_by_customer_byok" | "planned_customer_byok";
181
191
  masterAccountRef: string;
182
192
  }
183
193
  export interface SupplierSettlementSummary {
184
194
  workspaceId: string;
185
195
  fundingModel: "user_pays_muxo_muxo_pays_suppliers";
196
+ activeFundingMode?: "muxo_funded" | "byok_hybrid";
197
+ byokProviders?: string[];
186
198
  creditPriceCents: number;
187
199
  totalCreditsPurchasedOrGranted: number;
188
200
  totalCreditsConsumed: number;