@muxoai/sdk 1.2.3 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +22 -0
- package/.claude-plugin/marketplace.json +32 -0
- package/.codex/config.toml +12 -0
- package/ARCHITECTURE.md +121 -0
- package/README.md +85 -31
- package/dist/clerk.d.ts +35 -0
- package/dist/clerk.js +201 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2380 -0
- package/dist/composio-catalog.d.ts +234 -0
- package/dist/composio-catalog.js +1894 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +115 -0
- package/dist/durable-workflow.d.ts +45 -0
- package/dist/durable-workflow.js +90 -0
- package/dist/engine.d.ts +465 -0
- package/dist/engine.js +2560 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp-schema.d.ts +286 -0
- package/dist/mcp-schema.js +2559 -0
- package/dist/mcp.d.ts +22 -0
- package/dist/mcp.js +1356 -0
- package/dist/memory-graph.d.ts +21 -0
- package/dist/memory-graph.js +173 -0
- package/dist/nl.d.ts +34 -0
- package/dist/nl.js +756 -0
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/policy.d.ts +190 -0
- package/dist/policy.js +1597 -0
- package/dist/providers/composio-instant.d.ts +18 -0
- package/dist/providers/composio-instant.js +1169 -0
- package/dist/providers/composio-sdk.d.ts +162 -0
- package/dist/providers/composio-sdk.js +1395 -0
- package/dist/providers/microsoft-governance.d.ts +152 -0
- package/dist/providers/microsoft-governance.js +1062 -0
- package/dist/providers/stripe-billing.d.ts +83 -0
- package/dist/providers/stripe-billing.js +611 -0
- package/dist/providers/stripe-projects.d.ts +508 -0
- package/dist/providers/stripe-projects.js +3188 -0
- package/dist/providers/supermemory.d.ts +160 -0
- package/dist/providers/supermemory.js +778 -0
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +951 -0
- package/dist/sdk.js +748 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +3575 -0
- package/dist/store.d.ts +348 -0
- package/dist/store.js +2323 -0
- package/dist/types.d.ts +593 -0
- package/dist/types.js +1 -0
- package/dist/ui/console-html.d.ts +64 -0
- package/dist/ui/console-html.js +3857 -0
- package/dist/ui/design-tokens.d.ts +177 -0
- package/dist/ui/design-tokens.js +210 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +68 -7
- package/plugins/muxo/.app.json +8 -0
- package/plugins/muxo/.claude-plugin/plugin.json +16 -0
- package/plugins/muxo/.codex-plugin/plugin.json +64 -0
- package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
- package/plugins/muxo/.mcp.json +8 -0
- package/plugins/muxo/assets/logo.png +0 -0
- package/plugins/muxo/assets/logo.svg +6 -0
- package/plugins/muxo/hooks/hooks.json +27 -0
- package/plugins/muxo/hooks/session-start.sh +97 -0
- package/plugins/muxo/hooks/user-prompt-submit.sh +58 -0
- package/plugins/muxo/mcp.json +28 -0
- package/plugins/muxo/plugin.json +116 -0
- package/plugins/muxo/skills/muxo/SKILL.md +153 -0
- package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
- package/plugins/muxo/skills/muxo/references/cli.md +63 -0
- package/plugins/muxo/skills/muxo/references/mcp.md +37 -0
- package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
- package/plugins/muxo/skills/setup/SKILL.md +42 -0
- package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
- package/src/catalogues/stripe-projects-catalog.json +9972 -0
- package/vendor/microsoft-governance/README.md +363 -0
- package/vendor/microsoft-governance/dist/audit.d.ts +26 -0
- package/vendor/microsoft-governance/dist/audit.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/audit.js +107 -0
- package/vendor/microsoft-governance/dist/audit.js.map +1 -0
- package/vendor/microsoft-governance/dist/cascade-containment.d.ts +84 -0
- package/vendor/microsoft-governance/dist/cascade-containment.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/cascade-containment.js +387 -0
- package/vendor/microsoft-governance/dist/cascade-containment.js.map +1 -0
- package/vendor/microsoft-governance/dist/client.d.ts +34 -0
- package/vendor/microsoft-governance/dist/client.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/client.js +112 -0
- package/vendor/microsoft-governance/dist/client.js.map +1 -0
- package/vendor/microsoft-governance/dist/context-poisoning.d.ts +58 -0
- package/vendor/microsoft-governance/dist/context-poisoning.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/context-poisoning.js +410 -0
- package/vendor/microsoft-governance/dist/context-poisoning.js.map +1 -0
- package/vendor/microsoft-governance/dist/credential-vault.d.ts +213 -0
- package/vendor/microsoft-governance/dist/credential-vault.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/credential-vault.js +567 -0
- package/vendor/microsoft-governance/dist/credential-vault.js.map +1 -0
- package/vendor/microsoft-governance/dist/discovery.d.ts +80 -0
- package/vendor/microsoft-governance/dist/discovery.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/discovery.js +404 -0
- package/vendor/microsoft-governance/dist/discovery.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/channel.d.ts +29 -0
- package/vendor/microsoft-governance/dist/encryption/channel.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/channel.js +78 -0
- package/vendor/microsoft-governance/dist/encryption/channel.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/index.d.ts +9 -0
- package/vendor/microsoft-governance/dist/encryption/index.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/index.js +23 -0
- package/vendor/microsoft-governance/dist/encryption/index.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.d.ts +267 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.js +1001 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.d.ts +38 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.js +223 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.d.ts +136 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.js +369 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.d.ts +80 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.js +238 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.js.map +1 -0
- package/vendor/microsoft-governance/dist/framework-adapter.d.ts +63 -0
- package/vendor/microsoft-governance/dist/framework-adapter.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/framework-adapter.js +253 -0
- package/vendor/microsoft-governance/dist/framework-adapter.js.map +1 -0
- package/vendor/microsoft-governance/dist/identity.d.ts +124 -0
- package/vendor/microsoft-governance/dist/identity.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/identity.js +446 -0
- package/vendor/microsoft-governance/dist/identity.js.map +1 -0
- package/vendor/microsoft-governance/dist/index.d.ts +39 -0
- package/vendor/microsoft-governance/dist/index.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/index.js +95 -0
- package/vendor/microsoft-governance/dist/index.js.map +1 -0
- package/vendor/microsoft-governance/dist/kill-switch.d.ts +22 -0
- package/vendor/microsoft-governance/dist/kill-switch.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/kill-switch.js +60 -0
- package/vendor/microsoft-governance/dist/kill-switch.js.map +1 -0
- package/vendor/microsoft-governance/dist/lifecycle.d.ts +50 -0
- package/vendor/microsoft-governance/dist/lifecycle.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/lifecycle.js +113 -0
- package/vendor/microsoft-governance/dist/lifecycle.js.map +1 -0
- package/vendor/microsoft-governance/dist/mcp.d.ts +41 -0
- package/vendor/microsoft-governance/dist/mcp.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/mcp.js +233 -0
- package/vendor/microsoft-governance/dist/mcp.js.map +1 -0
- package/vendor/microsoft-governance/dist/metrics.d.ts +123 -0
- package/vendor/microsoft-governance/dist/metrics.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/metrics.js +289 -0
- package/vendor/microsoft-governance/dist/metrics.js.map +1 -0
- package/vendor/microsoft-governance/dist/oci-manifest.d.ts +49 -0
- package/vendor/microsoft-governance/dist/oci-manifest.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/oci-manifest.js +182 -0
- package/vendor/microsoft-governance/dist/oci-manifest.js.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.d.ts +15 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.js +88 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.js.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.d.ts +17 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.js +74 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.js.map +1 -0
- package/vendor/microsoft-governance/dist/policy.d.ts +77 -0
- package/vendor/microsoft-governance/dist/policy.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/policy.js +760 -0
- package/vendor/microsoft-governance/dist/policy.js.map +1 -0
- package/vendor/microsoft-governance/dist/prompt-defense.d.ts +37 -0
- package/vendor/microsoft-governance/dist/prompt-defense.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/prompt-defense.js +229 -0
- package/vendor/microsoft-governance/dist/prompt-defense.js.map +1 -0
- package/vendor/microsoft-governance/dist/protocol-facets.d.ts +87 -0
- package/vendor/microsoft-governance/dist/protocol-facets.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/protocol-facets.js +728 -0
- package/vendor/microsoft-governance/dist/protocol-facets.js.map +1 -0
- package/vendor/microsoft-governance/dist/rings.d.ts +17 -0
- package/vendor/microsoft-governance/dist/rings.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/rings.js +61 -0
- package/vendor/microsoft-governance/dist/rings.js.map +1 -0
- package/vendor/microsoft-governance/dist/sandbox.d.ts +61 -0
- package/vendor/microsoft-governance/dist/sandbox.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/sandbox.js +199 -0
- package/vendor/microsoft-governance/dist/sandbox.js.map +1 -0
- package/vendor/microsoft-governance/dist/surface-parity.d.ts +45 -0
- package/vendor/microsoft-governance/dist/surface-parity.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/surface-parity.js +216 -0
- package/vendor/microsoft-governance/dist/surface-parity.js.map +1 -0
- package/vendor/microsoft-governance/dist/trust.d.ts +28 -0
- package/vendor/microsoft-governance/dist/trust.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/trust.js +209 -0
- package/vendor/microsoft-governance/dist/trust.js.map +1 -0
- package/vendor/microsoft-governance/dist/types.d.ts +422 -0
- package/vendor/microsoft-governance/dist/types.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/types.js +29 -0
- package/vendor/microsoft-governance/dist/types.js.map +1 -0
- package/vendor/microsoft-governance/dist/verify.d.ts +135 -0
- package/vendor/microsoft-governance/dist/verify.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/verify.js +356 -0
- package/vendor/microsoft-governance/dist/verify.js.map +1 -0
- package/vendor/microsoft-governance/package.json +64 -0
- package/index.d.ts +0 -177
- package/index.js +0 -1
package/dist/store.js
ADDED
|
@@ -0,0 +1,2323 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
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
|
+
};
|
|
14
|
+
const DEFAULT_POLICY = {
|
|
15
|
+
mode: "enforce",
|
|
16
|
+
requireApprovalFor: ["admin", "financial", "provisioning", "destructive"],
|
|
17
|
+
allowInstantWallet: true,
|
|
18
|
+
allowProvisioning: true,
|
|
19
|
+
maxCreditsPerRun: 250,
|
|
20
|
+
};
|
|
21
|
+
const RECEIPTS_COLUMN_NAMES = [
|
|
22
|
+
"id",
|
|
23
|
+
"run_id",
|
|
24
|
+
"plan_id",
|
|
25
|
+
"workspace_id",
|
|
26
|
+
"actor_id",
|
|
27
|
+
"tool_slug",
|
|
28
|
+
"lane",
|
|
29
|
+
"risk_level",
|
|
30
|
+
"status",
|
|
31
|
+
"actions_metered",
|
|
32
|
+
"credits_metered",
|
|
33
|
+
"input_hash",
|
|
34
|
+
"output_hash",
|
|
35
|
+
"prev_receipt_hash",
|
|
36
|
+
"receipt_hash",
|
|
37
|
+
"signature",
|
|
38
|
+
"decision_record_json",
|
|
39
|
+
"issued_at",
|
|
40
|
+
];
|
|
41
|
+
const RECEIPTS_TABLE_COLUMNS = `
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
run_id TEXT NOT NULL,
|
|
44
|
+
plan_id TEXT NOT NULL,
|
|
45
|
+
workspace_id TEXT NOT NULL,
|
|
46
|
+
actor_id TEXT NOT NULL,
|
|
47
|
+
tool_slug TEXT NOT NULL,
|
|
48
|
+
lane TEXT NOT NULL,
|
|
49
|
+
risk_level TEXT NOT NULL,
|
|
50
|
+
status TEXT NOT NULL,
|
|
51
|
+
actions_metered INTEGER NOT NULL,
|
|
52
|
+
credits_metered INTEGER NOT NULL,
|
|
53
|
+
input_hash TEXT NOT NULL,
|
|
54
|
+
output_hash TEXT NOT NULL,
|
|
55
|
+
prev_receipt_hash TEXT NOT NULL,
|
|
56
|
+
receipt_hash TEXT NOT NULL,
|
|
57
|
+
signature TEXT NOT NULL,
|
|
58
|
+
decision_record_json TEXT,
|
|
59
|
+
issued_at TEXT NOT NULL
|
|
60
|
+
`;
|
|
61
|
+
export class MuxoStore {
|
|
62
|
+
db;
|
|
63
|
+
vaultKey;
|
|
64
|
+
constructor(dbPath, vaultKey) {
|
|
65
|
+
this.db = new DatabaseSync(dbPath);
|
|
66
|
+
this.db.exec("PRAGMA busy_timeout = 10000;");
|
|
67
|
+
this.vaultKey = vaultKey;
|
|
68
|
+
this.initSchema();
|
|
69
|
+
}
|
|
70
|
+
initSchema() {
|
|
71
|
+
this.db.exec(`
|
|
72
|
+
PRAGMA busy_timeout = 10000;
|
|
73
|
+
PRAGMA journal_mode = WAL;
|
|
74
|
+
PRAGMA foreign_keys = ON;
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
77
|
+
id TEXT PRIMARY KEY,
|
|
78
|
+
email TEXT NOT NULL UNIQUE,
|
|
79
|
+
name TEXT NOT NULL,
|
|
80
|
+
password_hash TEXT NOT NULL,
|
|
81
|
+
workspace_id TEXT NOT NULL,
|
|
82
|
+
role TEXT NOT NULL DEFAULT 'owner',
|
|
83
|
+
created_at TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
87
|
+
token_hash TEXT PRIMARY KEY,
|
|
88
|
+
user_id TEXT NOT NULL,
|
|
89
|
+
workspace_id TEXT NOT NULL,
|
|
90
|
+
expires_at TEXT NOT NULL,
|
|
91
|
+
created_at TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
name TEXT NOT NULL,
|
|
97
|
+
slug TEXT NOT NULL UNIQUE,
|
|
98
|
+
owner_user_id TEXT NOT NULL,
|
|
99
|
+
tier TEXT NOT NULL DEFAULT 'free',
|
|
100
|
+
monthly_actions_limit INTEGER NOT NULL DEFAULT 1000,
|
|
101
|
+
monthly_actions_used INTEGER NOT NULL DEFAULT 0,
|
|
102
|
+
credits_balance INTEGER NOT NULL DEFAULT 100,
|
|
103
|
+
stripe_customer_id TEXT,
|
|
104
|
+
stripe_subscription_id TEXT,
|
|
105
|
+
projects_env_name TEXT NOT NULL,
|
|
106
|
+
policy_json TEXT NOT NULL,
|
|
107
|
+
created_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
workspace_id TEXT NOT NULL,
|
|
114
|
+
name TEXT NOT NULL,
|
|
115
|
+
key_prefix TEXT NOT NULL,
|
|
116
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
117
|
+
environment TEXT NOT NULL DEFAULT 'live',
|
|
118
|
+
scopes_json TEXT NOT NULL,
|
|
119
|
+
last_used_at TEXT,
|
|
120
|
+
revoked_at TEXT,
|
|
121
|
+
created_at TEXT NOT NULL
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
workspace_id TEXT NOT NULL,
|
|
127
|
+
lane TEXT NOT NULL,
|
|
128
|
+
provider_slug TEXT NOT NULL,
|
|
129
|
+
provider_name TEXT NOT NULL,
|
|
130
|
+
status TEXT NOT NULL,
|
|
131
|
+
external_account_id TEXT,
|
|
132
|
+
redirect_url TEXT,
|
|
133
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
134
|
+
updated_at TEXT NOT NULL,
|
|
135
|
+
created_at TEXT NOT NULL,
|
|
136
|
+
UNIQUE(workspace_id, lane, provider_slug)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
CREATE TABLE IF NOT EXISTS resources (
|
|
140
|
+
id TEXT PRIMARY KEY,
|
|
141
|
+
workspace_id TEXT NOT NULL,
|
|
142
|
+
resource_name TEXT NOT NULL,
|
|
143
|
+
provider_slug TEXT NOT NULL,
|
|
144
|
+
provider_name TEXT NOT NULL,
|
|
145
|
+
service_slug TEXT NOT NULL,
|
|
146
|
+
plan_slug TEXT,
|
|
147
|
+
status TEXT NOT NULL,
|
|
148
|
+
env_var_keys_json TEXT NOT NULL DEFAULT '[]',
|
|
149
|
+
encrypted_secrets_hex TEXT,
|
|
150
|
+
external_resource_id TEXT,
|
|
151
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
152
|
+
created_at TEXT NOT NULL,
|
|
153
|
+
updated_at TEXT NOT NULL,
|
|
154
|
+
UNIQUE(workspace_id, resource_name)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
158
|
+
id TEXT PRIMARY KEY,
|
|
159
|
+
workspace_id TEXT NOT NULL,
|
|
160
|
+
actor_id TEXT NOT NULL,
|
|
161
|
+
tool_slug TEXT NOT NULL,
|
|
162
|
+
upstream_slug TEXT NOT NULL,
|
|
163
|
+
lane TEXT NOT NULL,
|
|
164
|
+
risk_level TEXT NOT NULL,
|
|
165
|
+
input_json TEXT NOT NULL,
|
|
166
|
+
reason TEXT NOT NULL,
|
|
167
|
+
credit_cost INTEGER NOT NULL DEFAULT 0,
|
|
168
|
+
approval_status TEXT NOT NULL,
|
|
169
|
+
approved_by TEXT,
|
|
170
|
+
approved_at TEXT,
|
|
171
|
+
run_id TEXT,
|
|
172
|
+
decision_record_json TEXT,
|
|
173
|
+
memory_context_json TEXT,
|
|
174
|
+
created_at TEXT NOT NULL,
|
|
175
|
+
expires_at TEXT NOT NULL
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
179
|
+
id TEXT PRIMARY KEY,
|
|
180
|
+
plan_id TEXT NOT NULL,
|
|
181
|
+
workspace_id TEXT NOT NULL,
|
|
182
|
+
actor_id TEXT NOT NULL,
|
|
183
|
+
tool_slug TEXT NOT NULL,
|
|
184
|
+
lane TEXT NOT NULL,
|
|
185
|
+
risk_level TEXT NOT NULL,
|
|
186
|
+
status TEXT NOT NULL,
|
|
187
|
+
input_json TEXT NOT NULL,
|
|
188
|
+
output_json TEXT,
|
|
189
|
+
error TEXT,
|
|
190
|
+
actions_metered INTEGER NOT NULL DEFAULT 0,
|
|
191
|
+
credits_metered INTEGER NOT NULL DEFAULT 0,
|
|
192
|
+
duration_ms INTEGER,
|
|
193
|
+
receipt_id TEXT,
|
|
194
|
+
started_at TEXT NOT NULL,
|
|
195
|
+
completed_at TEXT
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE TABLE IF NOT EXISTS receipts (${RECEIPTS_TABLE_COLUMNS});
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
201
|
+
id TEXT PRIMARY KEY,
|
|
202
|
+
workspace_id TEXT NOT NULL,
|
|
203
|
+
namespace TEXT NOT NULL,
|
|
204
|
+
document_id TEXT NOT NULL,
|
|
205
|
+
title TEXT NOT NULL,
|
|
206
|
+
content TEXT NOT NULL,
|
|
207
|
+
kind TEXT NOT NULL,
|
|
208
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
209
|
+
created_at TEXT NOT NULL
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
CREATE TABLE IF NOT EXISTS temporal_facts (
|
|
213
|
+
id TEXT PRIMARY KEY,
|
|
214
|
+
workspace_id TEXT NOT NULL,
|
|
215
|
+
subject TEXT NOT NULL,
|
|
216
|
+
predicate TEXT NOT NULL,
|
|
217
|
+
object TEXT NOT NULL,
|
|
218
|
+
summary TEXT NOT NULL,
|
|
219
|
+
confidence REAL NOT NULL DEFAULT 0.95,
|
|
220
|
+
valid_from TEXT NOT NULL,
|
|
221
|
+
valid_to TEXT,
|
|
222
|
+
superseded_by_id TEXT,
|
|
223
|
+
source_kind TEXT NOT NULL DEFAULT 'fact',
|
|
224
|
+
source_reference_id TEXT,
|
|
225
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
226
|
+
created_at TEXT NOT NULL
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
CREATE TABLE IF NOT EXISTS agent_claims (
|
|
230
|
+
claim_slug TEXT PRIMARY KEY,
|
|
231
|
+
workspace_id TEXT NOT NULL,
|
|
232
|
+
target_email TEXT NOT NULL,
|
|
233
|
+
status TEXT NOT NULL DEFAULT 'invited',
|
|
234
|
+
claimed_at TEXT,
|
|
235
|
+
expires_at TEXT NOT NULL,
|
|
236
|
+
created_at TEXT NOT NULL
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
CREATE TABLE IF NOT EXISTS ledger_entries (
|
|
240
|
+
id TEXT PRIMARY KEY,
|
|
241
|
+
workspace_id TEXT NOT NULL,
|
|
242
|
+
kind TEXT NOT NULL,
|
|
243
|
+
actions_delta INTEGER NOT NULL DEFAULT 0,
|
|
244
|
+
credits_delta INTEGER NOT NULL DEFAULT 0,
|
|
245
|
+
balance_after INTEGER NOT NULL DEFAULT 0,
|
|
246
|
+
reference_id TEXT,
|
|
247
|
+
description TEXT NOT NULL,
|
|
248
|
+
created_at TEXT NOT NULL
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
CREATE TABLE IF NOT EXISTS processed_stripe_events (
|
|
252
|
+
event_id TEXT PRIMARY KEY,
|
|
253
|
+
processed_at TEXT NOT NULL
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
CREATE TABLE IF NOT EXISTS byok_keys (
|
|
257
|
+
workspace_id TEXT NOT NULL,
|
|
258
|
+
provider TEXT NOT NULL,
|
|
259
|
+
category TEXT NOT NULL,
|
|
260
|
+
env_var_name TEXT NOT NULL,
|
|
261
|
+
key_preview TEXT NOT NULL,
|
|
262
|
+
encrypted_secret_hex TEXT NOT NULL,
|
|
263
|
+
updated_at TEXT NOT NULL,
|
|
264
|
+
PRIMARY KEY (workspace_id, provider)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
CREATE TABLE IF NOT EXISTS workspace_members (
|
|
268
|
+
id TEXT PRIMARY KEY,
|
|
269
|
+
workspace_id TEXT NOT NULL,
|
|
270
|
+
user_id TEXT NOT NULL,
|
|
271
|
+
role TEXT NOT NULL DEFAULT 'operator',
|
|
272
|
+
team_name TEXT,
|
|
273
|
+
joined_via_key_id TEXT,
|
|
274
|
+
created_at TEXT NOT NULL,
|
|
275
|
+
UNIQUE(workspace_id, user_id)
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
CREATE TABLE IF NOT EXISTS clerk_users (
|
|
279
|
+
clerk_user_id TEXT PRIMARY KEY,
|
|
280
|
+
user_id TEXT NOT NULL,
|
|
281
|
+
created_at TEXT NOT NULL
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
|
|
285
|
+
token_hash TEXT PRIMARY KEY,
|
|
286
|
+
key_id TEXT NOT NULL,
|
|
287
|
+
workspace_id TEXT NOT NULL,
|
|
288
|
+
client_id TEXT NOT NULL,
|
|
289
|
+
scope TEXT NOT NULL,
|
|
290
|
+
encrypted_access_token TEXT NOT NULL,
|
|
291
|
+
created_at TEXT NOT NULL
|
|
292
|
+
);
|
|
293
|
+
`);
|
|
294
|
+
// Backwards-compatible column migrations for existing databases
|
|
295
|
+
for (const stmt of [
|
|
296
|
+
"ALTER TABLE plans ADD COLUMN decision_record_json TEXT",
|
|
297
|
+
"ALTER TABLE plans ADD COLUMN memory_context_json TEXT",
|
|
298
|
+
"ALTER TABLE receipts ADD COLUMN decision_record_json TEXT",
|
|
299
|
+
"ALTER TABLE api_keys ADD COLUMN team_name TEXT",
|
|
300
|
+
"ALTER TABLE api_keys ADD COLUMN role TEXT DEFAULT 'operator'",
|
|
301
|
+
"ALTER TABLE api_keys ADD COLUMN created_by_email TEXT",
|
|
302
|
+
]) {
|
|
303
|
+
try {
|
|
304
|
+
this.db.exec(stmt);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Column already exists
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
this.migrateReceiptsAllowMultiplePerRun();
|
|
311
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_receipts_workspace_run ON receipts(workspace_id, run_id)");
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Older databases declared receipts.run_id UNIQUE, which blocks cancellation receipts from
|
|
315
|
+
* superseding a run's original receipt. SQLite cannot drop a constraint, so rebuild the table.
|
|
316
|
+
*/
|
|
317
|
+
migrateReceiptsAllowMultiplePerRun() {
|
|
318
|
+
const indexes = this.db.prepare("PRAGMA index_list(receipts)").all();
|
|
319
|
+
const hasUniqueRunId = indexes.some((idx) => {
|
|
320
|
+
if (!idx.unique || idx.origin !== "u")
|
|
321
|
+
return false;
|
|
322
|
+
const cols = this.db.prepare(`PRAGMA index_info("${idx.name}")`).all();
|
|
323
|
+
return cols.length === 1 && cols[0].name === "run_id";
|
|
324
|
+
});
|
|
325
|
+
if (!hasUniqueRunId)
|
|
326
|
+
return;
|
|
327
|
+
const columns = RECEIPTS_COLUMN_NAMES.join(", ");
|
|
328
|
+
this.db.exec("BEGIN");
|
|
329
|
+
try {
|
|
330
|
+
this.db.exec(`
|
|
331
|
+
CREATE TABLE receipts_migrated (${RECEIPTS_TABLE_COLUMNS});
|
|
332
|
+
INSERT INTO receipts_migrated (${columns}) SELECT ${columns} FROM receipts ORDER BY rowid;
|
|
333
|
+
DROP TABLE receipts;
|
|
334
|
+
ALTER TABLE receipts_migrated RENAME TO receipts;
|
|
335
|
+
`);
|
|
336
|
+
this.db.exec("COMMIT");
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
this.db.exec("ROLLBACK");
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// --- Encryption Vault (AES-256-GCM) ---
|
|
344
|
+
encryptSecrets(workspaceId, secrets) {
|
|
345
|
+
const iv = crypto.randomBytes(12);
|
|
346
|
+
const aad = Buffer.from(`muxo-vault:${workspaceId}`, "utf8");
|
|
347
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.vaultKey, iv);
|
|
348
|
+
cipher.setAAD(aad);
|
|
349
|
+
const plaintext = Buffer.from(JSON.stringify(secrets), "utf8");
|
|
350
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
351
|
+
const tag = cipher.getAuthTag();
|
|
352
|
+
return `${iv.toString("hex")}:${tag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
353
|
+
}
|
|
354
|
+
decryptSecrets(workspaceId, payloadHex) {
|
|
355
|
+
if (!payloadHex)
|
|
356
|
+
return {};
|
|
357
|
+
const parts = payloadHex.split(":");
|
|
358
|
+
if (parts.length !== 3)
|
|
359
|
+
return {};
|
|
360
|
+
const [ivHex, tagHex, dataHex] = parts;
|
|
361
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
362
|
+
const tag = Buffer.from(tagHex, "hex");
|
|
363
|
+
const encrypted = Buffer.from(dataHex, "hex");
|
|
364
|
+
const aad = Buffer.from(`muxo-vault:${workspaceId}`, "utf8");
|
|
365
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.vaultKey, iv);
|
|
366
|
+
decipher.setAAD(aad);
|
|
367
|
+
decipher.setAuthTag(tag);
|
|
368
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
369
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
370
|
+
}
|
|
371
|
+
// --- Bootstrap / Auth ---
|
|
372
|
+
ensureDefaultWorkspace() {
|
|
373
|
+
const existing = this.db
|
|
374
|
+
.prepare("SELECT id FROM workspaces ORDER BY created_at ASC LIMIT 1")
|
|
375
|
+
.get();
|
|
376
|
+
if (existing) {
|
|
377
|
+
const workspace = this.getWorkspace(existing.id);
|
|
378
|
+
const user = this.getUser(workspace.ownerUserId);
|
|
379
|
+
return { user, workspace };
|
|
380
|
+
}
|
|
381
|
+
const created = this.registerUser({
|
|
382
|
+
email: "founder@muxo.ai",
|
|
383
|
+
name: "Muxo Founder",
|
|
384
|
+
password: "muxo-demo-password",
|
|
385
|
+
workspaceName: "Muxo Default Workspace",
|
|
386
|
+
});
|
|
387
|
+
const apiKeyRes = this.createApiKey({
|
|
388
|
+
workspaceId: created.workspace.id,
|
|
389
|
+
name: "Default CLI & MCP Key",
|
|
390
|
+
environment: "live",
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
user: created.user,
|
|
394
|
+
workspace: created.workspace,
|
|
395
|
+
defaultApiKey: apiKeyRes.secretKey,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
hashPassword(password) {
|
|
399
|
+
const salt = "muxo-pw-salt-v1";
|
|
400
|
+
return crypto.scryptSync(password, salt, 32).toString("hex");
|
|
401
|
+
}
|
|
402
|
+
hashToken(token) {
|
|
403
|
+
return crypto.createHash("sha256").update(token).digest("hex");
|
|
404
|
+
}
|
|
405
|
+
registerUser(params) {
|
|
406
|
+
const now = new Date().toISOString();
|
|
407
|
+
const userId = `usr_${crypto.randomBytes(8).toString("hex")}`;
|
|
408
|
+
const workspaceId = `ws_${crypto.randomBytes(8).toString("hex")}`;
|
|
409
|
+
const slugBase = (params.workspaceName || params.name || "workspace")
|
|
410
|
+
.toLowerCase()
|
|
411
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
412
|
+
.replace(/^-|-$/g, "");
|
|
413
|
+
const slug = `${slugBase || "ws"}-${crypto.randomBytes(3).toString("hex")}`;
|
|
414
|
+
const projectsEnvName = `muxo_${workspaceId.slice(3, 11)}`;
|
|
415
|
+
const starterCredits = 250; // Initial trial credits for instant tools & exploration
|
|
416
|
+
this.db
|
|
417
|
+
.prepare(`INSERT INTO users (id, email, name, password_hash, workspace_id, role, created_at)
|
|
418
|
+
VALUES (?, ?, ?, ?, ?, 'owner', ?)`)
|
|
419
|
+
.run(userId, params.email.toLowerCase().trim(), params.name.trim(), this.hashPassword(params.password), workspaceId, now);
|
|
420
|
+
this.db
|
|
421
|
+
.prepare(`INSERT INTO workspaces (
|
|
422
|
+
id, name, slug, owner_user_id, tier,
|
|
423
|
+
monthly_actions_limit, monthly_actions_used, credits_balance,
|
|
424
|
+
stripe_customer_id, stripe_subscription_id, projects_env_name,
|
|
425
|
+
policy_json, created_at, updated_at
|
|
426
|
+
) VALUES (?, ?, ?, ?, 'free', 1000, 0, ?, NULL, NULL, ?, ?, ?, ?)`)
|
|
427
|
+
.run(workspaceId, params.workspaceName?.trim() || `${params.name.trim()}'s Workspace`, slug, userId, starterCredits, projectsEnvName, JSON.stringify(DEFAULT_POLICY), now, now);
|
|
428
|
+
this.addLedgerEntry({
|
|
429
|
+
workspaceId,
|
|
430
|
+
kind: "plan_grant",
|
|
431
|
+
actionsDelta: 1000,
|
|
432
|
+
creditsDelta: starterCredits,
|
|
433
|
+
balanceAfter: starterCredits,
|
|
434
|
+
referenceId: workspaceId,
|
|
435
|
+
description: "Free Plan initial monthly allowance (1,000 actions + 250 trial credits)",
|
|
436
|
+
});
|
|
437
|
+
this.ensureWorkspaceMembership({
|
|
438
|
+
workspaceId,
|
|
439
|
+
userId,
|
|
440
|
+
role: "owner",
|
|
441
|
+
});
|
|
442
|
+
const sessionToken = this.createSession(userId, workspaceId);
|
|
443
|
+
const defaultKey = this.createApiKey({
|
|
444
|
+
workspaceId,
|
|
445
|
+
name: "Default Live Key",
|
|
446
|
+
environment: "live",
|
|
447
|
+
role: "owner",
|
|
448
|
+
createdByEmail: params.email.toLowerCase().trim(),
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
user: this.getUser(userId),
|
|
452
|
+
workspace: this.getWorkspace(workspaceId),
|
|
453
|
+
sessionToken,
|
|
454
|
+
apiKey: defaultKey.secretKey,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
loginUser(email, password) {
|
|
458
|
+
const row = this.db
|
|
459
|
+
.prepare("SELECT * FROM users WHERE email = ?")
|
|
460
|
+
.get(email.toLowerCase().trim());
|
|
461
|
+
if (!row)
|
|
462
|
+
return null;
|
|
463
|
+
if (row.password_hash !== this.hashPassword(password))
|
|
464
|
+
return null;
|
|
465
|
+
const user = this.mapUser(row);
|
|
466
|
+
const workspace = this.getWorkspace(user.workspaceId);
|
|
467
|
+
this.ensureWorkspaceMembership({
|
|
468
|
+
workspaceId: workspace.id,
|
|
469
|
+
userId: user.id,
|
|
470
|
+
role: user.role,
|
|
471
|
+
});
|
|
472
|
+
const sessionToken = this.createSession(user.id, workspace.id);
|
|
473
|
+
return { user, workspace, sessionToken };
|
|
474
|
+
}
|
|
475
|
+
createSession(userId, workspaceId) {
|
|
476
|
+
const rawToken = `mxs_${crypto.randomBytes(24).toString("hex")}`;
|
|
477
|
+
const now = new Date();
|
|
478
|
+
const expires = new Date(now.getTime() + 30 * 24 * 3600 * 1000);
|
|
479
|
+
this.db
|
|
480
|
+
.prepare(`INSERT INTO sessions (token_hash, user_id, workspace_id, expires_at, created_at)
|
|
481
|
+
VALUES (?, ?, ?, ?, ?)`)
|
|
482
|
+
.run(this.hashToken(rawToken), userId, workspaceId, expires.toISOString(), now.toISOString());
|
|
483
|
+
return rawToken;
|
|
484
|
+
}
|
|
485
|
+
resolveAuthToken(token, workspaceOverrideId) {
|
|
486
|
+
const trimmed = token.trim();
|
|
487
|
+
if (!trimmed)
|
|
488
|
+
return null;
|
|
489
|
+
if (trimmed.startsWith("mk_live_") || trimmed.startsWith("mk_test_")) {
|
|
490
|
+
const hash = this.hashToken(trimmed);
|
|
491
|
+
const row = this.db
|
|
492
|
+
.prepare("SELECT * FROM api_keys WHERE key_hash = ? AND revoked_at IS NULL")
|
|
493
|
+
.get(hash);
|
|
494
|
+
if (!row)
|
|
495
|
+
return null;
|
|
496
|
+
const now = new Date().toISOString();
|
|
497
|
+
this.db
|
|
498
|
+
.prepare("UPDATE api_keys SET last_used_at = ? WHERE id = ?")
|
|
499
|
+
.run(now, String(row.id));
|
|
500
|
+
const workspace = this.getWorkspace(String(row.workspace_id));
|
|
501
|
+
if (!workspace)
|
|
502
|
+
return null;
|
|
503
|
+
const user = this.getUser(workspace.ownerUserId);
|
|
504
|
+
if (!user)
|
|
505
|
+
return null;
|
|
506
|
+
return {
|
|
507
|
+
user,
|
|
508
|
+
workspace,
|
|
509
|
+
authType: "api_key",
|
|
510
|
+
actorId: `key:${String(row.id)}`,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const hash = this.hashToken(trimmed);
|
|
514
|
+
const sessionRow = this.db
|
|
515
|
+
.prepare("SELECT * FROM sessions WHERE token_hash = ?")
|
|
516
|
+
.get(hash);
|
|
517
|
+
if (!sessionRow)
|
|
518
|
+
return null;
|
|
519
|
+
if (new Date(String(sessionRow.expires_at)).getTime() < Date.now()) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const user = this.getUser(String(sessionRow.user_id));
|
|
523
|
+
if (!user)
|
|
524
|
+
return null;
|
|
525
|
+
let targetWorkspaceId = String(sessionRow.workspace_id || user.workspaceId);
|
|
526
|
+
if (workspaceOverrideId &&
|
|
527
|
+
workspaceOverrideId.trim() &&
|
|
528
|
+
this.userCanAccessWorkspace(user.id, workspaceOverrideId.trim())) {
|
|
529
|
+
targetWorkspaceId = workspaceOverrideId.trim();
|
|
530
|
+
}
|
|
531
|
+
else if (user.workspaceId && this.userCanAccessWorkspace(user.id, user.workspaceId)) {
|
|
532
|
+
targetWorkspaceId = user.workspaceId;
|
|
533
|
+
}
|
|
534
|
+
const workspace = this.getWorkspace(targetWorkspaceId);
|
|
535
|
+
if (!workspace)
|
|
536
|
+
return null;
|
|
537
|
+
return {
|
|
538
|
+
user: { ...user, workspaceId: workspace.id },
|
|
539
|
+
workspace,
|
|
540
|
+
authType: "session",
|
|
541
|
+
actorId: user.id,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
getUser(userId) {
|
|
545
|
+
const row = this.db
|
|
546
|
+
.prepare("SELECT * FROM users WHERE id = ?")
|
|
547
|
+
.get(userId);
|
|
548
|
+
return row ? this.mapUser(row) : null;
|
|
549
|
+
}
|
|
550
|
+
getWorkspace(workspaceId) {
|
|
551
|
+
const row = this.db
|
|
552
|
+
.prepare("SELECT * FROM workspaces WHERE id = ?")
|
|
553
|
+
.get(workspaceId);
|
|
554
|
+
return row ? this.mapWorkspace(row) : null;
|
|
555
|
+
}
|
|
556
|
+
ensureWorkspaceMembership(params) {
|
|
557
|
+
const now = new Date().toISOString();
|
|
558
|
+
const role = params.role || "operator";
|
|
559
|
+
const existing = this.db
|
|
560
|
+
.prepare("SELECT * FROM workspace_members WHERE workspace_id = ? AND user_id = ?")
|
|
561
|
+
.get(params.workspaceId, params.userId);
|
|
562
|
+
if (existing) {
|
|
563
|
+
if (params.teamName || params.role) {
|
|
564
|
+
this.db
|
|
565
|
+
.prepare("UPDATE workspace_members SET role = COALESCE(?, role), team_name = COALESCE(?, team_name) WHERE id = ?")
|
|
566
|
+
.run(params.role || null, params.teamName || null, String(existing.id));
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
id: String(existing.id),
|
|
570
|
+
workspaceId: String(existing.workspace_id),
|
|
571
|
+
userId: String(existing.user_id),
|
|
572
|
+
role: (params.role || String(existing.role) || "operator"),
|
|
573
|
+
teamName: params.teamName ?? (existing.team_name ? String(existing.team_name) : null),
|
|
574
|
+
joinedViaKeyId: existing.joined_via_key_id ? String(existing.joined_via_key_id) : null,
|
|
575
|
+
createdAt: String(existing.created_at),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const id = `wsm_${crypto.randomBytes(8).toString("hex")}`;
|
|
579
|
+
this.db
|
|
580
|
+
.prepare(`INSERT INTO workspace_members (
|
|
581
|
+
id, workspace_id, user_id, role, team_name, joined_via_key_id, created_at
|
|
582
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
583
|
+
.run(id, params.workspaceId, params.userId, role, params.teamName || null, params.joinedViaKeyId || null, now);
|
|
584
|
+
return {
|
|
585
|
+
id,
|
|
586
|
+
workspaceId: params.workspaceId,
|
|
587
|
+
userId: params.userId,
|
|
588
|
+
role,
|
|
589
|
+
teamName: params.teamName || null,
|
|
590
|
+
joinedViaKeyId: params.joinedViaKeyId || null,
|
|
591
|
+
createdAt: now,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
userCanAccessWorkspace(userId, workspaceId) {
|
|
595
|
+
const ws = this.getWorkspace(workspaceId);
|
|
596
|
+
if (!ws)
|
|
597
|
+
return false;
|
|
598
|
+
if (ws.ownerUserId === userId)
|
|
599
|
+
return true;
|
|
600
|
+
const user = this.getUser(userId);
|
|
601
|
+
if (user?.workspaceId === workspaceId)
|
|
602
|
+
return true;
|
|
603
|
+
const mem = this.db
|
|
604
|
+
.prepare("SELECT id FROM workspace_members WHERE workspace_id = ? AND user_id = ?")
|
|
605
|
+
.get(workspaceId, userId);
|
|
606
|
+
return Boolean(mem);
|
|
607
|
+
}
|
|
608
|
+
listUserWorkspaces(userId, activeWorkspaceId) {
|
|
609
|
+
const user = this.getUser(userId);
|
|
610
|
+
const currentId = activeWorkspaceId || user?.workspaceId || "";
|
|
611
|
+
const ownedRows = this.db
|
|
612
|
+
.prepare("SELECT * FROM workspaces WHERE owner_user_id = ? ORDER BY created_at ASC")
|
|
613
|
+
.all(userId);
|
|
614
|
+
const memberRows = this.db
|
|
615
|
+
.prepare(`SELECT w.*, m.role AS member_role, m.team_name AS member_team_name
|
|
616
|
+
FROM workspace_members m
|
|
617
|
+
JOIN workspaces w ON w.id = m.workspace_id
|
|
618
|
+
WHERE m.user_id = ?
|
|
619
|
+
ORDER BY m.created_at ASC`)
|
|
620
|
+
.all(userId);
|
|
621
|
+
const byId = new Map();
|
|
622
|
+
for (const r of ownedRows) {
|
|
623
|
+
const ws = this.mapWorkspace(r);
|
|
624
|
+
byId.set(ws.id, {
|
|
625
|
+
id: ws.id,
|
|
626
|
+
name: ws.name,
|
|
627
|
+
slug: ws.slug,
|
|
628
|
+
tier: ws.tier,
|
|
629
|
+
creditsBalance: ws.creditsBalance,
|
|
630
|
+
monthlyActionsUsed: ws.monthlyActionsUsed,
|
|
631
|
+
monthlyActionsLimit: ws.monthlyActionsLimit,
|
|
632
|
+
role: "owner",
|
|
633
|
+
teamName: null,
|
|
634
|
+
isCurrent: ws.id === currentId,
|
|
635
|
+
createdAt: ws.createdAt,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
for (const r of memberRows) {
|
|
639
|
+
const ws = this.mapWorkspace(r);
|
|
640
|
+
const existing = byId.get(ws.id);
|
|
641
|
+
byId.set(ws.id, {
|
|
642
|
+
id: ws.id,
|
|
643
|
+
name: ws.name,
|
|
644
|
+
slug: ws.slug,
|
|
645
|
+
tier: ws.tier,
|
|
646
|
+
creditsBalance: ws.creditsBalance,
|
|
647
|
+
monthlyActionsUsed: ws.monthlyActionsUsed,
|
|
648
|
+
monthlyActionsLimit: ws.monthlyActionsLimit,
|
|
649
|
+
role: existing?.role === "owner" ? "owner" : String(r.member_role || "operator"),
|
|
650
|
+
teamName: r.member_team_name ? String(r.member_team_name) : existing?.teamName || null,
|
|
651
|
+
isCurrent: ws.id === currentId,
|
|
652
|
+
createdAt: ws.createdAt,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (currentId && !byId.has(currentId)) {
|
|
656
|
+
const ws = this.getWorkspace(currentId);
|
|
657
|
+
if (ws) {
|
|
658
|
+
byId.set(ws.id, {
|
|
659
|
+
id: ws.id,
|
|
660
|
+
name: ws.name,
|
|
661
|
+
slug: ws.slug,
|
|
662
|
+
tier: ws.tier,
|
|
663
|
+
creditsBalance: ws.creditsBalance,
|
|
664
|
+
monthlyActionsUsed: ws.monthlyActionsUsed,
|
|
665
|
+
monthlyActionsLimit: ws.monthlyActionsLimit,
|
|
666
|
+
role: ws.ownerUserId === userId ? "owner" : "operator",
|
|
667
|
+
teamName: null,
|
|
668
|
+
isCurrent: true,
|
|
669
|
+
createdAt: ws.createdAt,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return Array.from(byId.values());
|
|
674
|
+
}
|
|
675
|
+
switchUserWorkspace(userId, workspaceId) {
|
|
676
|
+
if (!this.userCanAccessWorkspace(userId, workspaceId))
|
|
677
|
+
return null;
|
|
678
|
+
const workspace = this.getWorkspace(workspaceId);
|
|
679
|
+
if (!workspace)
|
|
680
|
+
return null;
|
|
681
|
+
this.db
|
|
682
|
+
.prepare("UPDATE users SET workspace_id = ? WHERE id = ?")
|
|
683
|
+
.run(workspaceId, userId);
|
|
684
|
+
this.db
|
|
685
|
+
.prepare("UPDATE sessions SET workspace_id = ? WHERE user_id = ?")
|
|
686
|
+
.run(workspaceId, userId);
|
|
687
|
+
const user = this.getUser(userId);
|
|
688
|
+
if (!user)
|
|
689
|
+
return null;
|
|
690
|
+
return { user, workspace };
|
|
691
|
+
}
|
|
692
|
+
createWorkspaceForUser(params) {
|
|
693
|
+
const now = new Date().toISOString();
|
|
694
|
+
const workspaceId = `ws_${crypto.randomBytes(8).toString("hex")}`;
|
|
695
|
+
const cleanName = params.name.trim() || "Team Workspace";
|
|
696
|
+
const slugBase = cleanName
|
|
697
|
+
.toLowerCase()
|
|
698
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
699
|
+
.replace(/^-|-$/g, "");
|
|
700
|
+
const slug = `${slugBase || "ws"}-${crypto.randomBytes(3).toString("hex")}`;
|
|
701
|
+
const projectsEnvName = `muxo_${workspaceId.slice(3, 11)}`;
|
|
702
|
+
const starterCredits = 250;
|
|
703
|
+
this.db
|
|
704
|
+
.prepare(`INSERT INTO workspaces (
|
|
705
|
+
id, name, slug, owner_user_id, tier,
|
|
706
|
+
monthly_actions_limit, monthly_actions_used, credits_balance,
|
|
707
|
+
stripe_customer_id, stripe_subscription_id, projects_env_name,
|
|
708
|
+
policy_json, created_at, updated_at
|
|
709
|
+
) VALUES (?, ?, ?, ?, 'free', 1000, 0, ?, NULL, NULL, ?, ?, ?, ?)`)
|
|
710
|
+
.run(workspaceId, cleanName, slug, params.userId, starterCredits, projectsEnvName, JSON.stringify(DEFAULT_POLICY), now, now);
|
|
711
|
+
this.addLedgerEntry({
|
|
712
|
+
workspaceId,
|
|
713
|
+
kind: "plan_grant",
|
|
714
|
+
actionsDelta: 1000,
|
|
715
|
+
creditsDelta: starterCredits,
|
|
716
|
+
balanceAfter: starterCredits,
|
|
717
|
+
referenceId: workspaceId,
|
|
718
|
+
description: `Workspace '${cleanName}' initial monthly allowance (1,000 actions + 250 trial credits)`,
|
|
719
|
+
});
|
|
720
|
+
const user = this.getUser(params.userId);
|
|
721
|
+
const keyRes = this.createApiKey({
|
|
722
|
+
workspaceId,
|
|
723
|
+
name: params.teamName ? `${params.teamName} — Team Key` : `${cleanName} — Default Key`,
|
|
724
|
+
environment: "live",
|
|
725
|
+
teamName: params.teamName || cleanName,
|
|
726
|
+
role: params.role || "owner",
|
|
727
|
+
createdByEmail: user?.email || null,
|
|
728
|
+
});
|
|
729
|
+
const membership = this.ensureWorkspaceMembership({
|
|
730
|
+
workspaceId,
|
|
731
|
+
userId: params.userId,
|
|
732
|
+
role: "owner",
|
|
733
|
+
teamName: params.teamName || null,
|
|
734
|
+
joinedViaKeyId: keyRes.record.id,
|
|
735
|
+
});
|
|
736
|
+
if (params.switchActive !== false) {
|
|
737
|
+
this.switchUserWorkspace(params.userId, workspaceId);
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
workspace: this.getWorkspace(workspaceId),
|
|
741
|
+
apiKey: keyRes.secretKey,
|
|
742
|
+
keyRecord: keyRes.record,
|
|
743
|
+
membership,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
joinWorkspaceWithApiKey(params) {
|
|
747
|
+
const trimmed = String(params.apiKey || "").trim();
|
|
748
|
+
if (!trimmed.startsWith("mk_live_") && !trimmed.startsWith("mk_test_")) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
const hash = this.hashToken(trimmed);
|
|
752
|
+
const row = this.db
|
|
753
|
+
.prepare("SELECT * FROM api_keys WHERE key_hash = ? AND revoked_at IS NULL")
|
|
754
|
+
.get(hash);
|
|
755
|
+
if (!row)
|
|
756
|
+
return null;
|
|
757
|
+
const workspace = this.getWorkspace(String(row.workspace_id));
|
|
758
|
+
if (!workspace)
|
|
759
|
+
return null;
|
|
760
|
+
const keyRecord = {
|
|
761
|
+
id: String(row.id),
|
|
762
|
+
workspaceId: String(row.workspace_id),
|
|
763
|
+
name: String(row.name),
|
|
764
|
+
keyPrefix: String(row.key_prefix),
|
|
765
|
+
environment: String(row.environment),
|
|
766
|
+
scopes: JSON.parse(String(row.scopes_json || "[]")),
|
|
767
|
+
teamName: row.team_name ? String(row.team_name) : null,
|
|
768
|
+
role: (row.role ? String(row.role) : "operator"),
|
|
769
|
+
createdByEmail: row.created_by_email ? String(row.created_by_email) : null,
|
|
770
|
+
lastUsedAt: row.last_used_at ? String(row.last_used_at) : null,
|
|
771
|
+
revokedAt: row.revoked_at ? String(row.revoked_at) : null,
|
|
772
|
+
createdAt: String(row.created_at),
|
|
773
|
+
};
|
|
774
|
+
let membership = null;
|
|
775
|
+
let user = null;
|
|
776
|
+
if (params.userId) {
|
|
777
|
+
membership = this.ensureWorkspaceMembership({
|
|
778
|
+
workspaceId: workspace.id,
|
|
779
|
+
userId: params.userId,
|
|
780
|
+
role: workspace.ownerUserId === params.userId ? "owner" : (keyRecord.role || "operator"),
|
|
781
|
+
teamName: keyRecord.teamName || keyRecord.name,
|
|
782
|
+
joinedViaKeyId: keyRecord.id,
|
|
783
|
+
});
|
|
784
|
+
if (params.switchActive !== false) {
|
|
785
|
+
this.switchUserWorkspace(params.userId, workspace.id);
|
|
786
|
+
}
|
|
787
|
+
user = this.getUser(params.userId);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
user = this.getUser(workspace.ownerUserId);
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
workspace,
|
|
794
|
+
keyRecord,
|
|
795
|
+
user,
|
|
796
|
+
membership,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
onboardTeam(params) {
|
|
800
|
+
const cleanTeam = params.teamName.trim() || "Engineering Team";
|
|
801
|
+
const role = params.role || "operator";
|
|
802
|
+
const mode = params.mode === "dedicated_workspace" ? "dedicated_workspace" : "shared_workspace";
|
|
803
|
+
const cleanBase = params.baseUrl.replace(/\/$/, "");
|
|
804
|
+
const env = params.environment === "test" ? "test" : "live";
|
|
805
|
+
let targetWorkspace;
|
|
806
|
+
let apiKey;
|
|
807
|
+
let keyRecord;
|
|
808
|
+
if (mode === "dedicated_workspace") {
|
|
809
|
+
const currentWs = this.getWorkspace(params.currentWorkspaceId);
|
|
810
|
+
const wsName = currentWs ? `${currentWs.name} — ${cleanTeam}` : `${cleanTeam} Workspace`;
|
|
811
|
+
const created = this.createWorkspaceForUser({
|
|
812
|
+
userId: params.userId,
|
|
813
|
+
name: wsName,
|
|
814
|
+
teamName: cleanTeam,
|
|
815
|
+
role,
|
|
816
|
+
switchActive: false,
|
|
817
|
+
});
|
|
818
|
+
targetWorkspace = created.workspace;
|
|
819
|
+
apiKey = created.apiKey;
|
|
820
|
+
keyRecord = created.keyRecord;
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
const currentWs = this.getWorkspace(params.currentWorkspaceId);
|
|
824
|
+
if (!currentWs)
|
|
825
|
+
throw new Error(`Workspace ${params.currentWorkspaceId} not found`);
|
|
826
|
+
targetWorkspace = currentWs;
|
|
827
|
+
const createdKey = this.createApiKey({
|
|
828
|
+
workspaceId: targetWorkspace.id,
|
|
829
|
+
name: `${cleanTeam} (${role})`,
|
|
830
|
+
environment: env,
|
|
831
|
+
scopes: params.scopes,
|
|
832
|
+
teamName: cleanTeam,
|
|
833
|
+
role,
|
|
834
|
+
createdByEmail: params.userEmail || null,
|
|
835
|
+
});
|
|
836
|
+
apiKey = createdKey.secretKey;
|
|
837
|
+
keyRecord = createdKey.record;
|
|
838
|
+
}
|
|
839
|
+
let claimUrl = null;
|
|
840
|
+
if (params.inviteEmail && params.inviteEmail.includes("@")) {
|
|
841
|
+
const claim = this.createAgentClaim({
|
|
842
|
+
workspaceId: targetWorkspace.id,
|
|
843
|
+
email: params.inviteEmail,
|
|
844
|
+
baseUrl: cleanBase,
|
|
845
|
+
});
|
|
846
|
+
claimUrl = claim.claimUrl;
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
mode,
|
|
850
|
+
workspace: targetWorkspace,
|
|
851
|
+
teamName: cleanTeam,
|
|
852
|
+
role,
|
|
853
|
+
apiKey,
|
|
854
|
+
keyRecord,
|
|
855
|
+
claimUrl,
|
|
856
|
+
consoleUrl: `${cleanBase}/login`,
|
|
857
|
+
cliSetupCommand: `npx muxo setup --api-key ${apiKey} --url ${cleanBase}`,
|
|
858
|
+
mcpConfig: {
|
|
859
|
+
mcpServers: {
|
|
860
|
+
muxo: {
|
|
861
|
+
url: `${cleanBase}/mcp`,
|
|
862
|
+
headers: {
|
|
863
|
+
Authorization: `Bearer ${apiKey}`,
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
instructions: [
|
|
869
|
+
`Share this Team API Key with ${cleanTeam} (${role} access to '${targetWorkspace.name}').`,
|
|
870
|
+
`Console: Open ${cleanBase}/login, click "Join with a Team API Key", and paste the key (if signed in with Clerk, the workspace is permanently linked to their switcher).`,
|
|
871
|
+
`CLI & MCP: Run \`npx -y @muxoai/platform setup --api-key ${apiKey}\` to configure Claude Code, Cursor, Windsurf, or Codex in one command.`,
|
|
872
|
+
`Approvals: Team members can also paste this key directly on any ${cleanBase}/approve/<planId> link to review and approve actions.`,
|
|
873
|
+
],
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
updateWorkspacePolicy(workspaceId, policy) {
|
|
877
|
+
const ws = this.getWorkspace(workspaceId);
|
|
878
|
+
if (!ws)
|
|
879
|
+
throw new Error(`Workspace ${workspaceId} not found`);
|
|
880
|
+
const merged = {
|
|
881
|
+
...ws.policy,
|
|
882
|
+
...policy,
|
|
883
|
+
};
|
|
884
|
+
const now = new Date().toISOString();
|
|
885
|
+
this.db
|
|
886
|
+
.prepare("UPDATE workspaces SET policy_json = ?, updated_at = ? WHERE id = ?")
|
|
887
|
+
.run(JSON.stringify(merged), now, workspaceId);
|
|
888
|
+
return this.getWorkspace(workspaceId);
|
|
889
|
+
}
|
|
890
|
+
// --- API Keys ---
|
|
891
|
+
createApiKey(params) {
|
|
892
|
+
const env = params.environment || "live";
|
|
893
|
+
const id = `key_${crypto.randomBytes(6).toString("hex")}`;
|
|
894
|
+
const rawSecret = `mk_${env}_${id.slice(4)}_${crypto.randomBytes(16).toString("hex")}`;
|
|
895
|
+
const keyPrefix = `${rawSecret.slice(0, 16)}...${rawSecret.slice(-4)}`;
|
|
896
|
+
const scopes = params.scopes || ["tools:read", "tools:execute", "resources:provision"];
|
|
897
|
+
const teamName = params.teamName ? params.teamName.trim() : null;
|
|
898
|
+
const role = params.role || "operator";
|
|
899
|
+
const createdByEmail = params.createdByEmail ? params.createdByEmail.trim() : null;
|
|
900
|
+
const now = new Date().toISOString();
|
|
901
|
+
this.db
|
|
902
|
+
.prepare(`INSERT INTO api_keys (
|
|
903
|
+
id, workspace_id, name, key_prefix, key_hash, environment, scopes_json,
|
|
904
|
+
team_name, role, created_by_email, created_at
|
|
905
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
906
|
+
.run(id, params.workspaceId, params.name, keyPrefix, this.hashToken(rawSecret), env, JSON.stringify(scopes), teamName, role, createdByEmail, now);
|
|
907
|
+
return {
|
|
908
|
+
record: {
|
|
909
|
+
id,
|
|
910
|
+
workspaceId: params.workspaceId,
|
|
911
|
+
name: params.name,
|
|
912
|
+
keyPrefix,
|
|
913
|
+
environment: env,
|
|
914
|
+
scopes,
|
|
915
|
+
teamName,
|
|
916
|
+
role,
|
|
917
|
+
createdByEmail,
|
|
918
|
+
lastUsedAt: null,
|
|
919
|
+
revokedAt: null,
|
|
920
|
+
createdAt: now,
|
|
921
|
+
},
|
|
922
|
+
secretKey: rawSecret,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
listApiKeys(workspaceId) {
|
|
926
|
+
const rows = this.db
|
|
927
|
+
.prepare("SELECT * FROM api_keys WHERE workspace_id = ? ORDER BY created_at DESC")
|
|
928
|
+
.all(workspaceId);
|
|
929
|
+
return rows.map((r) => ({
|
|
930
|
+
id: String(r.id),
|
|
931
|
+
workspaceId: String(r.workspace_id),
|
|
932
|
+
name: String(r.name),
|
|
933
|
+
keyPrefix: String(r.key_prefix),
|
|
934
|
+
environment: String(r.environment),
|
|
935
|
+
scopes: JSON.parse(String(r.scopes_json || "[]")),
|
|
936
|
+
teamName: r.team_name ? String(r.team_name) : null,
|
|
937
|
+
role: (r.role ? String(r.role) : "operator"),
|
|
938
|
+
createdByEmail: r.created_by_email ? String(r.created_by_email) : null,
|
|
939
|
+
lastUsedAt: r.last_used_at ? String(r.last_used_at) : null,
|
|
940
|
+
revokedAt: r.revoked_at ? String(r.revoked_at) : null,
|
|
941
|
+
createdAt: String(r.created_at),
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
944
|
+
revokeApiKey(workspaceId, keyId) {
|
|
945
|
+
const now = new Date().toISOString();
|
|
946
|
+
const res = this.db
|
|
947
|
+
.prepare("UPDATE api_keys SET revoked_at = ? WHERE id = ? AND workspace_id = ?")
|
|
948
|
+
.run(now, keyId, workspaceId);
|
|
949
|
+
if (res.changes > 0) {
|
|
950
|
+
this.db.prepare("DELETE FROM oauth_refresh_tokens WHERE key_id = ?").run(keyId);
|
|
951
|
+
}
|
|
952
|
+
return res.changes > 0;
|
|
953
|
+
}
|
|
954
|
+
/** Looks up an active workspace API key by its raw secret. */
|
|
955
|
+
findActiveApiKey(secret) {
|
|
956
|
+
const row = this.db
|
|
957
|
+
.prepare("SELECT id, workspace_id FROM api_keys WHERE key_hash = ? AND revoked_at IS NULL")
|
|
958
|
+
.get(this.hashToken(secret.trim()));
|
|
959
|
+
return row ? { id: String(row.id), workspaceId: String(row.workspace_id) } : null;
|
|
960
|
+
}
|
|
961
|
+
// --- OAuth refresh tokens (hashed; the access token is kept encrypted so refresh can return it) ---
|
|
962
|
+
saveOAuthRefreshToken(params) {
|
|
963
|
+
this.db
|
|
964
|
+
.prepare(`INSERT INTO oauth_refresh_tokens (
|
|
965
|
+
token_hash, key_id, workspace_id, client_id, scope, encrypted_access_token, created_at
|
|
966
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
967
|
+
.run(this.hashToken(params.refreshToken), params.keyId, params.workspaceId, params.clientId, params.scope, this.encryptSecrets(params.workspaceId, { accessToken: params.accessToken }), new Date().toISOString());
|
|
968
|
+
}
|
|
969
|
+
getOAuthRefreshToken(refreshToken) {
|
|
970
|
+
const row = this.db
|
|
971
|
+
.prepare("SELECT * FROM oauth_refresh_tokens WHERE token_hash = ?")
|
|
972
|
+
.get(this.hashToken(refreshToken));
|
|
973
|
+
if (!row)
|
|
974
|
+
return null;
|
|
975
|
+
const workspaceId = String(row.workspace_id);
|
|
976
|
+
let accessToken = "";
|
|
977
|
+
try {
|
|
978
|
+
accessToken = this.decryptSecrets(workspaceId, String(row.encrypted_access_token)).accessToken || "";
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
keyId: String(row.key_id),
|
|
985
|
+
workspaceId,
|
|
986
|
+
clientId: String(row.client_id),
|
|
987
|
+
scope: String(row.scope),
|
|
988
|
+
accessToken,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
deleteOAuthRefreshToken(refreshToken) {
|
|
992
|
+
this.db.prepare("DELETE FROM oauth_refresh_tokens WHERE token_hash = ?").run(this.hashToken(refreshToken));
|
|
993
|
+
}
|
|
994
|
+
// --- Clerk sign-in ---
|
|
995
|
+
/**
|
|
996
|
+
* Maps a verified Clerk session to a local user. Only a verified email can link to an
|
|
997
|
+
* existing account; otherwise a fresh user and workspace are created so an unverified
|
|
998
|
+
* address can never take over someone else's account.
|
|
999
|
+
*/
|
|
1000
|
+
provisionClerkUser(clerkUserId, identity, ownerWorkspaceId) {
|
|
1001
|
+
const mappedUser = this.getClerkLinkedUser(clerkUserId);
|
|
1002
|
+
if (mappedUser)
|
|
1003
|
+
return mappedUser;
|
|
1004
|
+
const email = identity.email ? identity.email.trim().toLowerCase() : null;
|
|
1005
|
+
const verifiedEmail = email && identity.emailVerified ? email : null;
|
|
1006
|
+
let user = verifiedEmail ? this.getUserByEmail(verifiedEmail) : null;
|
|
1007
|
+
if (!user) {
|
|
1008
|
+
const displayName = identity.name || (verifiedEmail ? verifiedEmail.split("@")[0] : "Muxo user");
|
|
1009
|
+
const placeholder = `${clerkUserId.toLowerCase()}@clerk.users.muxo.invalid`;
|
|
1010
|
+
user = this.registerUser({
|
|
1011
|
+
email: verifiedEmail || placeholder,
|
|
1012
|
+
name: displayName,
|
|
1013
|
+
password: crypto.randomBytes(24).toString("hex"),
|
|
1014
|
+
}).user;
|
|
1015
|
+
}
|
|
1016
|
+
if (ownerWorkspaceId && verifiedEmail && this.getWorkspace(ownerWorkspaceId)) {
|
|
1017
|
+
this.ensureWorkspaceMembership({ workspaceId: ownerWorkspaceId, userId: user.id, role: "owner" });
|
|
1018
|
+
this.db.prepare("UPDATE users SET workspace_id = ? WHERE id = ?").run(ownerWorkspaceId, user.id);
|
|
1019
|
+
user = this.getUser(user.id);
|
|
1020
|
+
}
|
|
1021
|
+
this.db
|
|
1022
|
+
.prepare("INSERT OR REPLACE INTO clerk_users (clerk_user_id, user_id, created_at) VALUES (?, ?, ?)")
|
|
1023
|
+
.run(clerkUserId, user.id, new Date().toISOString());
|
|
1024
|
+
return user;
|
|
1025
|
+
}
|
|
1026
|
+
getClerkLinkedUser(clerkUserId) {
|
|
1027
|
+
const mapped = this.db
|
|
1028
|
+
.prepare("SELECT user_id FROM clerk_users WHERE clerk_user_id = ?")
|
|
1029
|
+
.get(clerkUserId);
|
|
1030
|
+
return mapped ? this.getUser(mapped.user_id) : null;
|
|
1031
|
+
}
|
|
1032
|
+
getUserByEmail(email) {
|
|
1033
|
+
const row = this.db
|
|
1034
|
+
.prepare("SELECT * FROM users WHERE email = ?")
|
|
1035
|
+
.get(email.trim().toLowerCase());
|
|
1036
|
+
return row ? this.mapUser(row) : null;
|
|
1037
|
+
}
|
|
1038
|
+
// --- Connections ---
|
|
1039
|
+
upsertConnection(params) {
|
|
1040
|
+
const now = new Date().toISOString();
|
|
1041
|
+
const existing = this.db
|
|
1042
|
+
.prepare("SELECT id FROM connections WHERE workspace_id = ? AND lane = ? AND provider_slug = ?")
|
|
1043
|
+
.get(params.workspaceId, params.lane, params.providerSlug);
|
|
1044
|
+
const id = existing?.id || `conn_${crypto.randomBytes(8).toString("hex")}`;
|
|
1045
|
+
this.db
|
|
1046
|
+
.prepare(`INSERT INTO connections (
|
|
1047
|
+
id, workspace_id, lane, provider_slug, provider_name, status,
|
|
1048
|
+
external_account_id, redirect_url, metadata_json, updated_at, created_at
|
|
1049
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1050
|
+
ON CONFLICT(workspace_id, lane, provider_slug) DO UPDATE SET
|
|
1051
|
+
provider_name = excluded.provider_name,
|
|
1052
|
+
status = excluded.status,
|
|
1053
|
+
external_account_id = COALESCE(excluded.external_account_id, connections.external_account_id),
|
|
1054
|
+
redirect_url = CASE
|
|
1055
|
+
WHEN excluded.status = 'active' THEN NULL
|
|
1056
|
+
ELSE COALESCE(excluded.redirect_url, connections.redirect_url)
|
|
1057
|
+
END,
|
|
1058
|
+
metadata_json = excluded.metadata_json,
|
|
1059
|
+
updated_at = excluded.updated_at`)
|
|
1060
|
+
.run(id, params.workspaceId, params.lane, params.providerSlug, params.providerName, params.status, params.externalAccountId ?? null, params.redirectUrl ?? null, JSON.stringify(params.metadata || {}), now, now);
|
|
1061
|
+
const connRecord = this.getConnection(id);
|
|
1062
|
+
this.upsertTemporalFact({
|
|
1063
|
+
workspaceId: params.workspaceId,
|
|
1064
|
+
subject: `app:${params.providerSlug}`,
|
|
1065
|
+
predicate: "CONNECTION_STATUS",
|
|
1066
|
+
object: `${params.lane}:${params.status}`,
|
|
1067
|
+
summary: `App ${params.providerName} (${params.providerSlug}) connection on ${params.lane} is ${params.status}`,
|
|
1068
|
+
confidence: 1.0,
|
|
1069
|
+
sourceKind: "resource_topology",
|
|
1070
|
+
sourceReferenceId: connRecord.id,
|
|
1071
|
+
metadata: {
|
|
1072
|
+
lane: params.lane,
|
|
1073
|
+
providerSlug: params.providerSlug,
|
|
1074
|
+
status: params.status,
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
return connRecord;
|
|
1078
|
+
}
|
|
1079
|
+
getConnection(id) {
|
|
1080
|
+
const row = this.db
|
|
1081
|
+
.prepare("SELECT * FROM connections WHERE id = ?")
|
|
1082
|
+
.get(id);
|
|
1083
|
+
return row ? this.mapConnection(row) : null;
|
|
1084
|
+
}
|
|
1085
|
+
listConnections(workspaceId) {
|
|
1086
|
+
const rows = this.db
|
|
1087
|
+
.prepare("SELECT * FROM connections WHERE workspace_id = ? ORDER BY updated_at DESC")
|
|
1088
|
+
.all(workspaceId);
|
|
1089
|
+
return rows.map((r) => this.mapConnection(r));
|
|
1090
|
+
}
|
|
1091
|
+
// --- Provisioned Resources (Stripe Projects Vault) ---
|
|
1092
|
+
upsertResource(params) {
|
|
1093
|
+
const now = new Date().toISOString();
|
|
1094
|
+
const existing = this.db
|
|
1095
|
+
.prepare("SELECT id, env_var_keys_json, encrypted_secrets_hex FROM resources WHERE workspace_id = ? AND resource_name = ?")
|
|
1096
|
+
.get(params.workspaceId, params.resourceName);
|
|
1097
|
+
const id = existing?.id || `res_${crypto.randomBytes(8).toString("hex")}`;
|
|
1098
|
+
let envVarKeys = existing
|
|
1099
|
+
? JSON.parse(existing.env_var_keys_json || "[]")
|
|
1100
|
+
: [];
|
|
1101
|
+
let encryptedHex = existing?.encrypted_secrets_hex || null;
|
|
1102
|
+
if (params.secrets && Object.keys(params.secrets).length > 0) {
|
|
1103
|
+
const mergedSecrets = {
|
|
1104
|
+
...this.decryptSecrets(params.workspaceId, encryptedHex),
|
|
1105
|
+
...params.secrets,
|
|
1106
|
+
};
|
|
1107
|
+
envVarKeys = Object.keys(mergedSecrets).sort();
|
|
1108
|
+
encryptedHex = this.encryptSecrets(params.workspaceId, mergedSecrets);
|
|
1109
|
+
}
|
|
1110
|
+
this.db
|
|
1111
|
+
.prepare(`INSERT INTO resources (
|
|
1112
|
+
id, workspace_id, resource_name, provider_slug, provider_name,
|
|
1113
|
+
service_slug, plan_slug, status, env_var_keys_json,
|
|
1114
|
+
encrypted_secrets_hex, external_resource_id, metadata_json,
|
|
1115
|
+
created_at, updated_at
|
|
1116
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1117
|
+
ON CONFLICT(workspace_id, resource_name) DO UPDATE SET
|
|
1118
|
+
status = excluded.status,
|
|
1119
|
+
service_slug = excluded.service_slug,
|
|
1120
|
+
plan_slug = COALESCE(excluded.plan_slug, resources.plan_slug),
|
|
1121
|
+
env_var_keys_json = excluded.env_var_keys_json,
|
|
1122
|
+
encrypted_secrets_hex = COALESCE(excluded.encrypted_secrets_hex, resources.encrypted_secrets_hex),
|
|
1123
|
+
external_resource_id = COALESCE(excluded.external_resource_id, resources.external_resource_id),
|
|
1124
|
+
metadata_json = excluded.metadata_json,
|
|
1125
|
+
updated_at = excluded.updated_at`)
|
|
1126
|
+
.run(id, params.workspaceId, params.resourceName, params.providerSlug, params.providerName, params.serviceSlug, params.planSlug ?? null, params.status, JSON.stringify(envVarKeys), encryptedHex, params.externalResourceId ?? null, JSON.stringify(params.metadata || {}), now, now);
|
|
1127
|
+
const resRecord = this.getResource(params.workspaceId, params.resourceName);
|
|
1128
|
+
this.upsertTemporalFact({
|
|
1129
|
+
workspaceId: params.workspaceId,
|
|
1130
|
+
subject: `resource:${params.resourceName}`,
|
|
1131
|
+
predicate: "PROVISIONED_ON",
|
|
1132
|
+
object: `${params.serviceSlug}:${params.status}`,
|
|
1133
|
+
summary: `Resource "${params.resourceName}" provisioned on ${params.serviceSlug} (${params.providerName}) with status ${params.status} [${resRecord.envVarKeys.join(", ") || "no env keys"}]`,
|
|
1134
|
+
confidence: 1.0,
|
|
1135
|
+
sourceKind: "resource_topology",
|
|
1136
|
+
sourceReferenceId: resRecord.id,
|
|
1137
|
+
metadata: {
|
|
1138
|
+
providerSlug: params.providerSlug,
|
|
1139
|
+
serviceSlug: params.serviceSlug,
|
|
1140
|
+
planSlug: params.planSlug ?? null,
|
|
1141
|
+
status: params.status,
|
|
1142
|
+
envVarKeys: resRecord.envVarKeys,
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
return resRecord;
|
|
1146
|
+
}
|
|
1147
|
+
getResource(workspaceId, resourceNameOrId) {
|
|
1148
|
+
const row = this.db
|
|
1149
|
+
.prepare("SELECT * FROM resources WHERE workspace_id = ? AND (resource_name = ? OR id = ? OR external_resource_id = ?)")
|
|
1150
|
+
.get(workspaceId, resourceNameOrId, resourceNameOrId, resourceNameOrId);
|
|
1151
|
+
return row ? this.mapResource(row) : null;
|
|
1152
|
+
}
|
|
1153
|
+
findResourceAnyWorkspace(resourceNameOrId) {
|
|
1154
|
+
const row = this.db
|
|
1155
|
+
.prepare("SELECT * FROM resources WHERE id = ? OR external_resource_id = ? OR resource_name = ? LIMIT 1")
|
|
1156
|
+
.get(resourceNameOrId, resourceNameOrId, resourceNameOrId);
|
|
1157
|
+
return row ? this.mapResource(row) : null;
|
|
1158
|
+
}
|
|
1159
|
+
getDecryptedResourceSecrets(workspaceId, resourceNameOrId) {
|
|
1160
|
+
const row = this.db
|
|
1161
|
+
.prepare("SELECT encrypted_secrets_hex FROM resources WHERE workspace_id = ? AND (id = ? OR external_resource_id = ? OR resource_name = ?) LIMIT 1")
|
|
1162
|
+
.get(workspaceId, resourceNameOrId, resourceNameOrId, resourceNameOrId);
|
|
1163
|
+
if (!row)
|
|
1164
|
+
return {};
|
|
1165
|
+
return this.decryptSecrets(workspaceId, row.encrypted_secrets_hex);
|
|
1166
|
+
}
|
|
1167
|
+
listResources(workspaceId) {
|
|
1168
|
+
const rows = this.db
|
|
1169
|
+
.prepare("SELECT * FROM resources WHERE workspace_id = ? ORDER BY updated_at DESC")
|
|
1170
|
+
.all(workspaceId);
|
|
1171
|
+
return rows.map((r) => this.mapResource(r));
|
|
1172
|
+
}
|
|
1173
|
+
getDecryptedWorkspaceEnv(workspaceId) {
|
|
1174
|
+
const rows = this.db
|
|
1175
|
+
.prepare("SELECT encrypted_secrets_hex FROM resources WHERE workspace_id = ? AND status = 'active'")
|
|
1176
|
+
.all(workspaceId);
|
|
1177
|
+
const merged = {};
|
|
1178
|
+
for (const row of rows) {
|
|
1179
|
+
Object.assign(merged, this.decryptSecrets(workspaceId, row.encrypted_secrets_hex));
|
|
1180
|
+
}
|
|
1181
|
+
const byokRows = this.db
|
|
1182
|
+
.prepare("SELECT encrypted_secret_hex FROM byok_keys WHERE workspace_id = ?")
|
|
1183
|
+
.all(workspaceId);
|
|
1184
|
+
for (const row of byokRows) {
|
|
1185
|
+
Object.assign(merged, this.decryptSecrets(workspaceId, row.encrypted_secret_hex));
|
|
1186
|
+
}
|
|
1187
|
+
return merged;
|
|
1188
|
+
}
|
|
1189
|
+
// --- BYOK (Bring Your Own Key) Encrypted Vault ---
|
|
1190
|
+
normalizeByokProvider(providerInput) {
|
|
1191
|
+
const raw = providerInput.trim().toLowerCase();
|
|
1192
|
+
for (const [slug, meta] of Object.entries(BYOK_PROVIDER_CATALOG)) {
|
|
1193
|
+
if (raw === slug || raw === meta.envVarName.toLowerCase()) {
|
|
1194
|
+
return { provider: slug, category: meta.category, envVarName: meta.envVarName };
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const clean = raw.replace(/[^a-z0-9_]/g, "");
|
|
1198
|
+
const envVarName = raw.endsWith("_key") || raw.endsWith("_token")
|
|
1199
|
+
? raw.toUpperCase()
|
|
1200
|
+
: `${clean.toUpperCase()}_API_KEY`;
|
|
1201
|
+
return {
|
|
1202
|
+
provider: clean || "custom",
|
|
1203
|
+
category: "supplier",
|
|
1204
|
+
envVarName,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
setByokKey(workspaceId, providerInput, secretKey) {
|
|
1208
|
+
const trimmed = secretKey.trim();
|
|
1209
|
+
if (!trimmed) {
|
|
1210
|
+
throw new Error("BYOK secretKey must not be empty");
|
|
1211
|
+
}
|
|
1212
|
+
const { provider, category, envVarName } = this.normalizeByokProvider(providerInput);
|
|
1213
|
+
const now = new Date().toISOString();
|
|
1214
|
+
const keyPreview = trimmed.length <= 8
|
|
1215
|
+
? `${trimmed.slice(0, 2)}***`
|
|
1216
|
+
: `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
|
|
1217
|
+
const encryptedHex = this.encryptSecrets(workspaceId, { [envVarName]: trimmed });
|
|
1218
|
+
this.db
|
|
1219
|
+
.prepare(`INSERT INTO byok_keys (
|
|
1220
|
+
workspace_id, provider, category, env_var_name, key_preview, encrypted_secret_hex, updated_at
|
|
1221
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1222
|
+
ON CONFLICT(workspace_id, provider) DO UPDATE SET
|
|
1223
|
+
category = excluded.category,
|
|
1224
|
+
env_var_name = excluded.env_var_name,
|
|
1225
|
+
key_preview = excluded.key_preview,
|
|
1226
|
+
encrypted_secret_hex = excluded.encrypted_secret_hex,
|
|
1227
|
+
updated_at = excluded.updated_at`)
|
|
1228
|
+
.run(workspaceId, provider, category, envVarName, keyPreview, encryptedHex, now);
|
|
1229
|
+
return {
|
|
1230
|
+
provider,
|
|
1231
|
+
providerSlug: provider,
|
|
1232
|
+
category,
|
|
1233
|
+
envVarName,
|
|
1234
|
+
keyPreview,
|
|
1235
|
+
configured: true,
|
|
1236
|
+
updatedAt: now,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
getByokKey(workspaceId, providerInput) {
|
|
1240
|
+
const { provider, envVarName } = this.normalizeByokProvider(providerInput);
|
|
1241
|
+
const row = this.db
|
|
1242
|
+
.prepare("SELECT encrypted_secret_hex FROM byok_keys WHERE workspace_id = ? AND provider = ?")
|
|
1243
|
+
.get(workspaceId, provider);
|
|
1244
|
+
if (!row)
|
|
1245
|
+
return null;
|
|
1246
|
+
const dec = this.decryptSecrets(workspaceId, row.encrypted_secret_hex);
|
|
1247
|
+
return dec[envVarName] || Object.values(dec)[0] || null;
|
|
1248
|
+
}
|
|
1249
|
+
deleteByokKey(workspaceId, providerInput) {
|
|
1250
|
+
const { provider } = this.normalizeByokProvider(providerInput);
|
|
1251
|
+
const res = this.db
|
|
1252
|
+
.prepare("DELETE FROM byok_keys WHERE workspace_id = ? AND provider = ?")
|
|
1253
|
+
.run(workspaceId, provider);
|
|
1254
|
+
return Number(res.changes || 0) > 0;
|
|
1255
|
+
}
|
|
1256
|
+
listByokKeys(workspaceId) {
|
|
1257
|
+
const rows = this.db
|
|
1258
|
+
.prepare("SELECT provider, category, env_var_name, key_preview, updated_at FROM byok_keys WHERE workspace_id = ? ORDER BY updated_at DESC")
|
|
1259
|
+
.all(workspaceId);
|
|
1260
|
+
const configuredMap = new Map();
|
|
1261
|
+
for (const r of rows) {
|
|
1262
|
+
const p = String(r.provider);
|
|
1263
|
+
configuredMap.set(p, {
|
|
1264
|
+
provider: p,
|
|
1265
|
+
providerSlug: p,
|
|
1266
|
+
category: String(r.category),
|
|
1267
|
+
envVarName: String(r.env_var_name),
|
|
1268
|
+
keyPreview: String(r.key_preview),
|
|
1269
|
+
configured: true,
|
|
1270
|
+
updatedAt: String(r.updated_at),
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
const result = [];
|
|
1274
|
+
for (const [slug, meta] of Object.entries(BYOK_PROVIDER_CATALOG)) {
|
|
1275
|
+
const existing = configuredMap.get(slug);
|
|
1276
|
+
if (existing) {
|
|
1277
|
+
result.push(existing);
|
|
1278
|
+
configuredMap.delete(slug);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
result.push({
|
|
1282
|
+
provider: slug,
|
|
1283
|
+
providerSlug: slug,
|
|
1284
|
+
category: meta.category,
|
|
1285
|
+
envVarName: meta.envVarName,
|
|
1286
|
+
keyPreview: "",
|
|
1287
|
+
configured: false,
|
|
1288
|
+
updatedAt: "",
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
for (const extra of configuredMap.values()) {
|
|
1293
|
+
result.push(extra);
|
|
1294
|
+
}
|
|
1295
|
+
return result;
|
|
1296
|
+
}
|
|
1297
|
+
hasByokForLane(workspaceId, lane, toolSlug) {
|
|
1298
|
+
const slug = (toolSlug || "").toLowerCase();
|
|
1299
|
+
if (slug.startsWith("billing."))
|
|
1300
|
+
return false;
|
|
1301
|
+
if (lane === "composio_sdk") {
|
|
1302
|
+
return Boolean(this.getByokKey(workspaceId, "composio"));
|
|
1303
|
+
}
|
|
1304
|
+
if (lane === "composio_instant") {
|
|
1305
|
+
if (this.getByokKey(workspaceId, "composio"))
|
|
1306
|
+
return true;
|
|
1307
|
+
if ((slug.includes("scrape") || slug.includes("crawl") || slug.includes("seo")) &&
|
|
1308
|
+
this.getByokKey(workspaceId, "firecrawl")) {
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
if (slug.includes("search") && this.getByokKey(workspaceId, "exa")) {
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
if (lane === "stripe_projects") {
|
|
1317
|
+
return Boolean(this.getByokKey(workspaceId, "stripe"));
|
|
1318
|
+
}
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
// --- Plans ---
|
|
1322
|
+
createPlan(params) {
|
|
1323
|
+
const id = `plan_${crypto.randomBytes(8).toString("hex")}`;
|
|
1324
|
+
const now = new Date();
|
|
1325
|
+
const expires = new Date(now.getTime() + 60 * 60 * 1000);
|
|
1326
|
+
this.db
|
|
1327
|
+
.prepare(`INSERT INTO plans (
|
|
1328
|
+
id, workspace_id, actor_id, tool_slug, upstream_slug, lane, risk_level,
|
|
1329
|
+
input_json, reason, credit_cost, approval_status, approved_by, approved_at,
|
|
1330
|
+
run_id, decision_record_json, memory_context_json, created_at, expires_at
|
|
1331
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?)`)
|
|
1332
|
+
.run(id, params.workspaceId, params.actorId, params.toolSlug, params.upstreamSlug, params.lane, params.riskLevel, JSON.stringify(params.input || {}), params.reason, params.creditCost, params.approvalStatus, params.approvalStatus === "auto_approved" ? "policy:auto" : null, params.approvalStatus === "auto_approved" ? now.toISOString() : null, params.decisionRecord ? JSON.stringify(params.decisionRecord) : null, params.memoryContext ? JSON.stringify(params.memoryContext) : null, now.toISOString(), expires.toISOString());
|
|
1333
|
+
return this.getPlan(params.workspaceId, id);
|
|
1334
|
+
}
|
|
1335
|
+
getPlan(workspaceId, planId) {
|
|
1336
|
+
const row = this.db
|
|
1337
|
+
.prepare("SELECT * FROM plans WHERE id = ? AND workspace_id = ?")
|
|
1338
|
+
.get(planId, workspaceId);
|
|
1339
|
+
return row ? this.mapPlan(row) : null;
|
|
1340
|
+
}
|
|
1341
|
+
getPlanById(planId) {
|
|
1342
|
+
const row = this.db
|
|
1343
|
+
.prepare("SELECT * FROM plans WHERE id = ?")
|
|
1344
|
+
.get(planId);
|
|
1345
|
+
return row ? this.mapPlan(row) : null;
|
|
1346
|
+
}
|
|
1347
|
+
listPlans(workspaceId, limit = 50) {
|
|
1348
|
+
const rows = this.db
|
|
1349
|
+
.prepare("SELECT * FROM plans WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
1350
|
+
.all(workspaceId, limit);
|
|
1351
|
+
return rows.map((r) => this.mapPlan(r));
|
|
1352
|
+
}
|
|
1353
|
+
updatePlanApproval(params) {
|
|
1354
|
+
const now = new Date().toISOString();
|
|
1355
|
+
this.db
|
|
1356
|
+
.prepare(`UPDATE plans
|
|
1357
|
+
SET approval_status = ?, approved_by = ?, approved_at = ?
|
|
1358
|
+
WHERE id = ? AND workspace_id = ?`)
|
|
1359
|
+
.run(params.approvalStatus, params.approvedBy, now, params.planId, params.workspaceId);
|
|
1360
|
+
return this.getPlan(params.workspaceId, params.planId);
|
|
1361
|
+
}
|
|
1362
|
+
// --- Runs & Cryptographic Receipts ---
|
|
1363
|
+
createRun(plan, actorId) {
|
|
1364
|
+
const id = `run_${crypto.randomBytes(8).toString("hex")}`;
|
|
1365
|
+
const now = new Date().toISOString();
|
|
1366
|
+
this.db
|
|
1367
|
+
.prepare(`INSERT INTO runs (
|
|
1368
|
+
id, plan_id, workspace_id, actor_id, tool_slug, lane, risk_level,
|
|
1369
|
+
status, input_json, output_json, error, actions_metered, credits_metered,
|
|
1370
|
+
duration_ms, receipt_id, started_at, completed_at
|
|
1371
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?, NULL, NULL, 0, 0, NULL, NULL, ?, NULL)`)
|
|
1372
|
+
.run(id, plan.id, plan.workspaceId, actorId, plan.toolSlug, plan.lane, plan.riskLevel, JSON.stringify(plan.input), now);
|
|
1373
|
+
this.db
|
|
1374
|
+
.prepare("UPDATE plans SET run_id = ?, approval_status = 'executed' WHERE id = ?")
|
|
1375
|
+
.run(id, plan.id);
|
|
1376
|
+
return this.getRun(plan.workspaceId, id);
|
|
1377
|
+
}
|
|
1378
|
+
finalizeRun(params) {
|
|
1379
|
+
const run = this.getRun(params.workspaceId, params.runId);
|
|
1380
|
+
if (!run)
|
|
1381
|
+
throw new Error(`Run ${params.runId} not found`);
|
|
1382
|
+
const now = new Date().toISOString();
|
|
1383
|
+
if (params.actionsMetered > 0 || params.creditsMetered > 0) {
|
|
1384
|
+
const ws = this.getWorkspace(params.workspaceId);
|
|
1385
|
+
const newActionsUsed = ws.monthlyActionsUsed + params.actionsMetered;
|
|
1386
|
+
const newCreditsBalance = Math.max(0, ws.creditsBalance - params.creditsMetered);
|
|
1387
|
+
this.db
|
|
1388
|
+
.prepare(`UPDATE workspaces
|
|
1389
|
+
SET monthly_actions_used = ?, credits_balance = ?, updated_at = ?
|
|
1390
|
+
WHERE id = ?`)
|
|
1391
|
+
.run(newActionsUsed, newCreditsBalance, now, params.workspaceId);
|
|
1392
|
+
const byokUsed = params.creditsMetered === 0 &&
|
|
1393
|
+
this.hasByokForLane(params.workspaceId, run.lane, run.toolSlug);
|
|
1394
|
+
const settlement = this.computeSupplierSettlement(run.lane, run.toolSlug, params.creditsMetered, byokUsed ? "settled_by_customer_byok" : "settled_by_muxo_master_wallet", params.workspaceId);
|
|
1395
|
+
this.addLedgerEntry({
|
|
1396
|
+
workspaceId: params.workspaceId,
|
|
1397
|
+
kind: "run_deduction",
|
|
1398
|
+
actionsDelta: -params.actionsMetered,
|
|
1399
|
+
creditsDelta: -params.creditsMetered,
|
|
1400
|
+
balanceAfter: newCreditsBalance,
|
|
1401
|
+
referenceId: run.id,
|
|
1402
|
+
description: byokUsed
|
|
1403
|
+
? `Executed ${run.toolSlug} (${run.lane}) — BYOK 0-credit execution (Customer's own ${settlement.supplierName} key)`
|
|
1404
|
+
: `Executed ${run.toolSlug} (${run.lane}) — User paid Muxo ${params.creditsMetered} credits ($${(settlement.userRetailChargeCents / 100).toFixed(2)}) -> Muxo paid ${settlement.supplierName} ($${(settlement.supplierWholesalePayableCents / 100).toFixed(2)})`,
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
const receipt = this.issueReceipt({
|
|
1408
|
+
run,
|
|
1409
|
+
status: params.status,
|
|
1410
|
+
output: params.output ?? null,
|
|
1411
|
+
actionsMetered: params.actionsMetered,
|
|
1412
|
+
creditsMetered: params.creditsMetered,
|
|
1413
|
+
decisionRecord: params.decisionRecord ?? null,
|
|
1414
|
+
issuedAt: now,
|
|
1415
|
+
});
|
|
1416
|
+
this.db
|
|
1417
|
+
.prepare(`UPDATE runs
|
|
1418
|
+
SET status = ?, output_json = ?, error = ?, actions_metered = ?,
|
|
1419
|
+
credits_metered = ?, duration_ms = ?, receipt_id = ?, completed_at = ?
|
|
1420
|
+
WHERE id = ? AND workspace_id = ?`)
|
|
1421
|
+
.run(params.status, params.output ? JSON.stringify(params.output) : null, params.error ?? null, params.actionsMetered, params.creditsMetered, params.durationMs, receipt.id, now, params.runId, params.workspaceId);
|
|
1422
|
+
return {
|
|
1423
|
+
run: this.getRun(params.workspaceId, params.runId),
|
|
1424
|
+
receipt,
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
cancelRun(params) {
|
|
1428
|
+
const run = this.getRun(params.workspaceId, params.runId);
|
|
1429
|
+
if (!run)
|
|
1430
|
+
throw new Error(`Run ${params.runId} not found`);
|
|
1431
|
+
const now = new Date().toISOString();
|
|
1432
|
+
const cancellation = {
|
|
1433
|
+
cancelled: true,
|
|
1434
|
+
cancelledBy: params.cancelledBy,
|
|
1435
|
+
cancelledAt: now,
|
|
1436
|
+
previousStatus: run.status,
|
|
1437
|
+
supersedesReceiptId: run.receiptId,
|
|
1438
|
+
...(params.reason ? { reason: params.reason } : {}),
|
|
1439
|
+
};
|
|
1440
|
+
const receipt = this.issueReceipt({
|
|
1441
|
+
run,
|
|
1442
|
+
status: "cancelled",
|
|
1443
|
+
output: cancellation,
|
|
1444
|
+
actionsMetered: 0,
|
|
1445
|
+
creditsMetered: 0,
|
|
1446
|
+
decisionRecord: null,
|
|
1447
|
+
issuedAt: now,
|
|
1448
|
+
});
|
|
1449
|
+
this.db
|
|
1450
|
+
.prepare(`UPDATE runs
|
|
1451
|
+
SET status = 'cancelled', output_json = COALESCE(output_json, ?), error = ?,
|
|
1452
|
+
receipt_id = ?, completed_at = COALESCE(completed_at, ?),
|
|
1453
|
+
duration_ms = COALESCE(duration_ms, ?)
|
|
1454
|
+
WHERE id = ? AND workspace_id = ?`)
|
|
1455
|
+
.run(JSON.stringify(cancellation), params.reason || "Cancelled by operator", receipt.id, now, Math.max(0, Date.now() - new Date(run.startedAt).getTime()), run.id, params.workspaceId);
|
|
1456
|
+
return { run: this.getRun(params.workspaceId, run.id), receipt };
|
|
1457
|
+
}
|
|
1458
|
+
issueReceipt(params) {
|
|
1459
|
+
const id = `rcpt_${crypto.randomBytes(8).toString("hex")}`;
|
|
1460
|
+
const lastReceipt = this.db
|
|
1461
|
+
.prepare("SELECT receipt_hash FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC, rowid DESC LIMIT 1")
|
|
1462
|
+
.get(params.run.workspaceId);
|
|
1463
|
+
const prevReceiptHash = lastReceipt?.receipt_hash || "genesis_0000000000000000";
|
|
1464
|
+
const inputHash = crypto
|
|
1465
|
+
.createHash("sha256")
|
|
1466
|
+
.update(JSON.stringify(params.run.input || {}))
|
|
1467
|
+
.digest("hex");
|
|
1468
|
+
const outputHash = crypto
|
|
1469
|
+
.createHash("sha256")
|
|
1470
|
+
.update(JSON.stringify(params.output || {}))
|
|
1471
|
+
.digest("hex");
|
|
1472
|
+
const canonicalPayload = [
|
|
1473
|
+
id,
|
|
1474
|
+
params.run.id,
|
|
1475
|
+
params.run.planId,
|
|
1476
|
+
params.run.workspaceId,
|
|
1477
|
+
params.run.actorId,
|
|
1478
|
+
params.run.toolSlug,
|
|
1479
|
+
params.run.lane,
|
|
1480
|
+
params.run.riskLevel,
|
|
1481
|
+
params.status,
|
|
1482
|
+
String(params.actionsMetered),
|
|
1483
|
+
String(params.creditsMetered),
|
|
1484
|
+
inputHash,
|
|
1485
|
+
outputHash,
|
|
1486
|
+
prevReceiptHash,
|
|
1487
|
+
params.issuedAt,
|
|
1488
|
+
].join("|");
|
|
1489
|
+
const receiptHash = crypto.createHash("sha256").update(canonicalPayload).digest("hex");
|
|
1490
|
+
const signature = crypto
|
|
1491
|
+
.createHmac("sha256", this.vaultKey)
|
|
1492
|
+
.update(receiptHash)
|
|
1493
|
+
.digest("hex");
|
|
1494
|
+
this.db
|
|
1495
|
+
.prepare(`INSERT INTO receipts (
|
|
1496
|
+
id, run_id, plan_id, workspace_id, actor_id, tool_slug, lane, risk_level,
|
|
1497
|
+
status, actions_metered, credits_metered, input_hash, output_hash,
|
|
1498
|
+
prev_receipt_hash, receipt_hash, signature, decision_record_json, issued_at
|
|
1499
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1500
|
+
.run(id, params.run.id, params.run.planId, params.run.workspaceId, params.run.actorId, params.run.toolSlug, params.run.lane, params.run.riskLevel, params.status, params.actionsMetered, params.creditsMetered, inputHash, outputHash, prevReceiptHash, receiptHash, signature, params.decisionRecord ? JSON.stringify(params.decisionRecord) : null, params.issuedAt);
|
|
1501
|
+
return this.getReceipt(params.run.workspaceId, id);
|
|
1502
|
+
}
|
|
1503
|
+
verifyReceipt(receipt) {
|
|
1504
|
+
const canonicalPayload = [
|
|
1505
|
+
receipt.id,
|
|
1506
|
+
receipt.runId,
|
|
1507
|
+
receipt.planId,
|
|
1508
|
+
receipt.workspaceId,
|
|
1509
|
+
receipt.actorId,
|
|
1510
|
+
receipt.toolSlug,
|
|
1511
|
+
receipt.lane,
|
|
1512
|
+
receipt.riskLevel,
|
|
1513
|
+
receipt.status,
|
|
1514
|
+
String(receipt.actionsMetered),
|
|
1515
|
+
String(receipt.creditsMetered),
|
|
1516
|
+
receipt.inputHash,
|
|
1517
|
+
receipt.outputHash,
|
|
1518
|
+
receipt.prevReceiptHash,
|
|
1519
|
+
receipt.issuedAt,
|
|
1520
|
+
].join("|");
|
|
1521
|
+
const expectedHash = crypto.createHash("sha256").update(canonicalPayload).digest("hex");
|
|
1522
|
+
if (expectedHash !== receipt.receiptHash)
|
|
1523
|
+
return false;
|
|
1524
|
+
const expectedSig = crypto
|
|
1525
|
+
.createHmac("sha256", this.vaultKey)
|
|
1526
|
+
.update(expectedHash)
|
|
1527
|
+
.digest("hex");
|
|
1528
|
+
return expectedSig === receipt.signature;
|
|
1529
|
+
}
|
|
1530
|
+
getRun(workspaceId, runId) {
|
|
1531
|
+
const row = this.db
|
|
1532
|
+
.prepare("SELECT * FROM runs WHERE id = ? AND workspace_id = ?")
|
|
1533
|
+
.get(runId, workspaceId);
|
|
1534
|
+
return row ? this.mapRun(row) : null;
|
|
1535
|
+
}
|
|
1536
|
+
listRuns(workspaceId, limit = 50) {
|
|
1537
|
+
const rows = this.db
|
|
1538
|
+
.prepare("SELECT * FROM runs WHERE workspace_id = ? ORDER BY started_at DESC LIMIT ?")
|
|
1539
|
+
.all(workspaceId, limit);
|
|
1540
|
+
return rows.map((r) => this.mapRun(r));
|
|
1541
|
+
}
|
|
1542
|
+
getReceipt(workspaceId, receiptIdOrRunId) {
|
|
1543
|
+
// A cancelled run has both its original and its cancellation receipt; prefer an exact
|
|
1544
|
+
// receipt id match, otherwise the newest receipt for the run.
|
|
1545
|
+
const row = this.db
|
|
1546
|
+
.prepare(`SELECT * FROM receipts WHERE workspace_id = ? AND (id = ? OR run_id = ?)
|
|
1547
|
+
ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END, issued_at DESC, rowid DESC LIMIT 1`)
|
|
1548
|
+
.get(workspaceId, receiptIdOrRunId, receiptIdOrRunId, receiptIdOrRunId);
|
|
1549
|
+
return row ? this.mapReceipt(row) : null;
|
|
1550
|
+
}
|
|
1551
|
+
listReceipts(workspaceId, limit = 50) {
|
|
1552
|
+
const rows = this.db
|
|
1553
|
+
.prepare("SELECT * FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC, rowid DESC LIMIT ?")
|
|
1554
|
+
.all(workspaceId, limit);
|
|
1555
|
+
return rows.map((r) => this.mapReceipt(r));
|
|
1556
|
+
}
|
|
1557
|
+
// --- Billing & Credit Ledger ---
|
|
1558
|
+
markStripeEventProcessed(eventId) {
|
|
1559
|
+
const existing = this.db
|
|
1560
|
+
.prepare("SELECT event_id FROM processed_stripe_events WHERE event_id = ?")
|
|
1561
|
+
.get(eventId);
|
|
1562
|
+
if (existing)
|
|
1563
|
+
return false;
|
|
1564
|
+
this.db
|
|
1565
|
+
.prepare("INSERT INTO processed_stripe_events (event_id, processed_at) VALUES (?, ?)")
|
|
1566
|
+
.run(eventId, new Date().toISOString());
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
upgradeWorkspaceToStarter(params) {
|
|
1570
|
+
if (!this.markStripeEventProcessed(`upgrade:${params.referenceId}`)) {
|
|
1571
|
+
return this.getWorkspace(params.workspaceId);
|
|
1572
|
+
}
|
|
1573
|
+
const ws = this.getWorkspace(params.workspaceId);
|
|
1574
|
+
if (!ws)
|
|
1575
|
+
throw new Error(`Workspace ${params.workspaceId} not found`);
|
|
1576
|
+
const now = new Date().toISOString();
|
|
1577
|
+
const bonus = params.bonusCredits !== undefined ? Number(params.bonusCredits) : 1000;
|
|
1578
|
+
const newCredits = ws.creditsBalance + bonus;
|
|
1579
|
+
this.db
|
|
1580
|
+
.prepare(`UPDATE workspaces
|
|
1581
|
+
SET tier = 'starter',
|
|
1582
|
+
monthly_actions_limit = 5000,
|
|
1583
|
+
credits_balance = ?,
|
|
1584
|
+
stripe_customer_id = COALESCE(?, stripe_customer_id),
|
|
1585
|
+
stripe_subscription_id = COALESCE(?, stripe_subscription_id),
|
|
1586
|
+
updated_at = ?
|
|
1587
|
+
WHERE id = ?`)
|
|
1588
|
+
.run(newCredits, params.stripeCustomerId ?? null, params.stripeSubscriptionId ?? null, now, params.workspaceId);
|
|
1589
|
+
this.addLedgerEntry({
|
|
1590
|
+
workspaceId: params.workspaceId,
|
|
1591
|
+
kind: "subscription_upgrade",
|
|
1592
|
+
actionsDelta: 4000,
|
|
1593
|
+
creditsDelta: bonus,
|
|
1594
|
+
balanceAfter: newCredits,
|
|
1595
|
+
referenceId: params.referenceId,
|
|
1596
|
+
description: `Upgraded to Starter Plan ($29/mo — 5,000 actions/mo + ${bonus} credits)`,
|
|
1597
|
+
});
|
|
1598
|
+
return this.getWorkspace(params.workspaceId);
|
|
1599
|
+
}
|
|
1600
|
+
addWorkspaceCredits(params) {
|
|
1601
|
+
if (!this.markStripeEventProcessed(`credits:${params.referenceId}`)) {
|
|
1602
|
+
return this.getWorkspace(params.workspaceId);
|
|
1603
|
+
}
|
|
1604
|
+
const ws = this.getWorkspace(params.workspaceId);
|
|
1605
|
+
if (!ws)
|
|
1606
|
+
throw new Error(`Workspace ${params.workspaceId} not found`);
|
|
1607
|
+
const now = new Date().toISOString();
|
|
1608
|
+
const newCredits = ws.creditsBalance + params.credits;
|
|
1609
|
+
this.db
|
|
1610
|
+
.prepare("UPDATE workspaces SET credits_balance = ?, updated_at = ? WHERE id = ?")
|
|
1611
|
+
.run(newCredits, now, params.workspaceId);
|
|
1612
|
+
this.addLedgerEntry({
|
|
1613
|
+
workspaceId: params.workspaceId,
|
|
1614
|
+
kind: "credit_topup",
|
|
1615
|
+
actionsDelta: 0,
|
|
1616
|
+
creditsDelta: params.credits,
|
|
1617
|
+
balanceAfter: newCredits,
|
|
1618
|
+
referenceId: params.referenceId,
|
|
1619
|
+
description: params.description,
|
|
1620
|
+
});
|
|
1621
|
+
return this.getWorkspace(params.workspaceId);
|
|
1622
|
+
}
|
|
1623
|
+
addLedgerEntry(params) {
|
|
1624
|
+
const id = `led_${crypto.randomBytes(8).toString("hex")}`;
|
|
1625
|
+
const now = new Date().toISOString();
|
|
1626
|
+
this.db
|
|
1627
|
+
.prepare(`INSERT INTO ledger_entries (
|
|
1628
|
+
id, workspace_id, kind, actions_delta, credits_delta,
|
|
1629
|
+
balance_after, reference_id, description, created_at
|
|
1630
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1631
|
+
.run(id, params.workspaceId, params.kind, params.actionsDelta, params.creditsDelta, params.balanceAfter, params.referenceId, params.description, now);
|
|
1632
|
+
return {
|
|
1633
|
+
id,
|
|
1634
|
+
workspaceId: params.workspaceId,
|
|
1635
|
+
kind: params.kind,
|
|
1636
|
+
actionsDelta: params.actionsDelta,
|
|
1637
|
+
creditsDelta: params.creditsDelta,
|
|
1638
|
+
balanceAfter: params.balanceAfter,
|
|
1639
|
+
referenceId: params.referenceId,
|
|
1640
|
+
description: params.description,
|
|
1641
|
+
createdAt: now,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
computeSupplierSettlement(lane, toolSlug, creditCost, settlementStatus = "settled_by_muxo_master_wallet", workspaceId) {
|
|
1645
|
+
const useByok = settlementStatus === "settled_by_customer_byok" ||
|
|
1646
|
+
settlementStatus === "planned_customer_byok" ||
|
|
1647
|
+
(Number(creditCost || 0) === 0 && workspaceId
|
|
1648
|
+
? this.hasByokForLane(workspaceId, lane, toolSlug)
|
|
1649
|
+
: false);
|
|
1650
|
+
const upstreamProvider = toolSlug.split(".")[1] || lane;
|
|
1651
|
+
if (useByok) {
|
|
1652
|
+
const isPlanned = settlementStatus === "planned_muxo_master_wallet" ||
|
|
1653
|
+
settlementStatus === "planned_customer_byok";
|
|
1654
|
+
return {
|
|
1655
|
+
fundingMode: "byok",
|
|
1656
|
+
supplier: lane,
|
|
1657
|
+
supplierName: "Customer BYOK Upstream Key (0% Muxo Markup)",
|
|
1658
|
+
upstreamProvider,
|
|
1659
|
+
userChargeCredits: 0,
|
|
1660
|
+
userRetailChargeCents: 0,
|
|
1661
|
+
supplierWholesalePayableCents: 0,
|
|
1662
|
+
muxoNetMarginCents: 0,
|
|
1663
|
+
settlementStatus: isPlanned
|
|
1664
|
+
? "planned_customer_byok"
|
|
1665
|
+
: "settled_by_customer_byok",
|
|
1666
|
+
masterAccountRef: "customer_byok_vault",
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const userChargeCredits = Math.max(0, Number(creditCost || 0));
|
|
1670
|
+
const userRetailChargeCents = Math.round(userChargeCredits * 100) / 100;
|
|
1671
|
+
const supplierWholesalePayableCents = Math.round(userRetailChargeCents * 0.6 * 100) / 100;
|
|
1672
|
+
const muxoNetMarginCents = Math.round((userRetailChargeCents - supplierWholesalePayableCents) * 100) / 100;
|
|
1673
|
+
const supplierMeta = {
|
|
1674
|
+
composio_instant: {
|
|
1675
|
+
supplierName: "Composio Instant Wholesale Wallet",
|
|
1676
|
+
masterAccountRef: "muxo_master_composio_wallet",
|
|
1677
|
+
},
|
|
1678
|
+
composio_sdk: {
|
|
1679
|
+
supplierName: "Composio Managed OAuth Gateway",
|
|
1680
|
+
masterAccountRef: "muxo_master_composio_sdk_account",
|
|
1681
|
+
},
|
|
1682
|
+
stripe_projects: {
|
|
1683
|
+
supplierName: "Stripe Projects Master Infrastructure Account",
|
|
1684
|
+
masterAccountRef: "muxo_master_stripe_projects_account",
|
|
1685
|
+
},
|
|
1686
|
+
};
|
|
1687
|
+
const meta = supplierMeta[lane] || supplierMeta.composio_instant;
|
|
1688
|
+
return {
|
|
1689
|
+
fundingMode: "muxo_funded",
|
|
1690
|
+
supplier: lane,
|
|
1691
|
+
supplierName: meta.supplierName,
|
|
1692
|
+
upstreamProvider,
|
|
1693
|
+
userChargeCredits,
|
|
1694
|
+
userRetailChargeCents,
|
|
1695
|
+
supplierWholesalePayableCents,
|
|
1696
|
+
muxoNetMarginCents,
|
|
1697
|
+
settlementStatus,
|
|
1698
|
+
masterAccountRef: meta.masterAccountRef,
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
getSupplierSettlementSummary(workspaceId) {
|
|
1702
|
+
const ws = this.getWorkspace(workspaceId);
|
|
1703
|
+
const ledger = this.listLedgerEntries(workspaceId, 500);
|
|
1704
|
+
const runs = this.listRuns(workspaceId, 500).filter((r) => r.status === "succeeded");
|
|
1705
|
+
const byokProviders = this.listByokKeys(workspaceId)
|
|
1706
|
+
.filter((k) => k.configured)
|
|
1707
|
+
.map((k) => k.provider);
|
|
1708
|
+
const totalCreditsPurchasedOrGranted = ledger
|
|
1709
|
+
.filter((e) => e.creditsDelta > 0)
|
|
1710
|
+
.reduce((acc, e) => acc + e.creditsDelta, 0);
|
|
1711
|
+
const bySupplier = {
|
|
1712
|
+
composio_instant: {
|
|
1713
|
+
runsCount: 0,
|
|
1714
|
+
creditsConsumed: 0,
|
|
1715
|
+
userRetailSpendCents: 0,
|
|
1716
|
+
supplierWholesalePaidCents: 0,
|
|
1717
|
+
muxoNetMarginCents: 0,
|
|
1718
|
+
},
|
|
1719
|
+
composio_sdk: {
|
|
1720
|
+
runsCount: 0,
|
|
1721
|
+
creditsConsumed: 0,
|
|
1722
|
+
userRetailSpendCents: 0,
|
|
1723
|
+
supplierWholesalePaidCents: 0,
|
|
1724
|
+
muxoNetMarginCents: 0,
|
|
1725
|
+
},
|
|
1726
|
+
stripe_projects: {
|
|
1727
|
+
runsCount: 0,
|
|
1728
|
+
creditsConsumed: 0,
|
|
1729
|
+
userRetailSpendCents: 0,
|
|
1730
|
+
supplierWholesalePaidCents: 0,
|
|
1731
|
+
muxoNetMarginCents: 0,
|
|
1732
|
+
},
|
|
1733
|
+
};
|
|
1734
|
+
let totalCreditsConsumed = 0;
|
|
1735
|
+
let totalUserRetailSpendCents = 0;
|
|
1736
|
+
let totalSupplierWholesalePaidCents = 0;
|
|
1737
|
+
let totalMuxoNetMarginCents = 0;
|
|
1738
|
+
for (const run of runs) {
|
|
1739
|
+
const s = this.computeSupplierSettlement(run.lane, run.toolSlug, run.creditsMetered, run.creditsMetered === 0 && this.hasByokForLane(workspaceId, run.lane, run.toolSlug)
|
|
1740
|
+
? "settled_by_customer_byok"
|
|
1741
|
+
: "settled_by_muxo_master_wallet", workspaceId);
|
|
1742
|
+
const bucket = bySupplier[run.lane] || bySupplier.composio_instant;
|
|
1743
|
+
bucket.runsCount += 1;
|
|
1744
|
+
bucket.creditsConsumed += s.userChargeCredits;
|
|
1745
|
+
bucket.userRetailSpendCents =
|
|
1746
|
+
Math.round((bucket.userRetailSpendCents + s.userRetailChargeCents) * 100) / 100;
|
|
1747
|
+
bucket.supplierWholesalePaidCents =
|
|
1748
|
+
Math.round((bucket.supplierWholesalePaidCents + s.supplierWholesalePayableCents) * 100) / 100;
|
|
1749
|
+
bucket.muxoNetMarginCents =
|
|
1750
|
+
Math.round((bucket.muxoNetMarginCents + s.muxoNetMarginCents) * 100) / 100;
|
|
1751
|
+
totalCreditsConsumed += s.userChargeCredits;
|
|
1752
|
+
totalUserRetailSpendCents =
|
|
1753
|
+
Math.round((totalUserRetailSpendCents + s.userRetailChargeCents) * 100) / 100;
|
|
1754
|
+
totalSupplierWholesalePaidCents =
|
|
1755
|
+
Math.round((totalSupplierWholesalePaidCents + s.supplierWholesalePayableCents) * 100) / 100;
|
|
1756
|
+
totalMuxoNetMarginCents =
|
|
1757
|
+
Math.round((totalMuxoNetMarginCents + s.muxoNetMarginCents) * 100) / 100;
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
workspaceId,
|
|
1761
|
+
fundingModel: "user_pays_muxo_muxo_pays_suppliers",
|
|
1762
|
+
activeFundingMode: byokProviders.length > 0 ? "byok_hybrid" : "muxo_funded",
|
|
1763
|
+
byokProviders,
|
|
1764
|
+
creditPriceCents: 1,
|
|
1765
|
+
totalCreditsPurchasedOrGranted,
|
|
1766
|
+
totalCreditsConsumed,
|
|
1767
|
+
currentCreditsBalance: ws?.creditsBalance ?? 0,
|
|
1768
|
+
totalUserRetailSpendCents,
|
|
1769
|
+
totalSupplierWholesalePaidCents,
|
|
1770
|
+
totalMuxoNetMarginCents,
|
|
1771
|
+
bySupplier,
|
|
1772
|
+
masterWalletProtection: {
|
|
1773
|
+
status: process.env.MUXO_MASTER_WALLET_DEPLETED === "true"
|
|
1774
|
+
? "failover_active"
|
|
1775
|
+
: "healthy",
|
|
1776
|
+
zeroChargeOnFailure: true,
|
|
1777
|
+
instantFailoverMode: "zero_cost_native_edge",
|
|
1778
|
+
paidProvisioningPolicy: "muxo_master_wallet_or_customer_stripe_byok",
|
|
1779
|
+
retailToWholesaleCoverageRatio: totalSupplierWholesalePaidCents > 0
|
|
1780
|
+
? Math.round((totalUserRetailSpendCents / totalSupplierWholesalePaidCents) * 100) / 100
|
|
1781
|
+
: 1.67,
|
|
1782
|
+
},
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
listLedgerEntries(workspaceId, limit = 50) {
|
|
1786
|
+
const rows = this.db
|
|
1787
|
+
.prepare("SELECT * FROM ledger_entries WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
1788
|
+
.all(workspaceId, limit);
|
|
1789
|
+
return rows.map((r) => {
|
|
1790
|
+
const kind = String(r.kind);
|
|
1791
|
+
const refId = r.reference_id ? String(r.reference_id) : null;
|
|
1792
|
+
const creditsDelta = Number(r.credits_delta);
|
|
1793
|
+
let supplierSettlement = null;
|
|
1794
|
+
if (kind === "run_deduction" && refId) {
|
|
1795
|
+
const run = this.getRun(workspaceId, refId);
|
|
1796
|
+
if (run) {
|
|
1797
|
+
const isByokEntry = creditsDelta === 0 &&
|
|
1798
|
+
(String(r.description || "").includes("BYOK") ||
|
|
1799
|
+
this.hasByokForLane(workspaceId, run.lane, run.toolSlug));
|
|
1800
|
+
supplierSettlement = this.computeSupplierSettlement(run.lane, run.toolSlug, Math.abs(creditsDelta), isByokEntry
|
|
1801
|
+
? "settled_by_customer_byok"
|
|
1802
|
+
: "settled_by_muxo_master_wallet", workspaceId);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
return {
|
|
1806
|
+
id: String(r.id),
|
|
1807
|
+
workspaceId: String(r.workspace_id),
|
|
1808
|
+
kind,
|
|
1809
|
+
actionsDelta: Number(r.actions_delta),
|
|
1810
|
+
creditsDelta,
|
|
1811
|
+
balanceAfter: Number(r.balance_after),
|
|
1812
|
+
referenceId: refId,
|
|
1813
|
+
description: String(r.description),
|
|
1814
|
+
supplierSettlement,
|
|
1815
|
+
createdAt: String(r.created_at),
|
|
1816
|
+
};
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
// --- Supermemory Local Index ---
|
|
1820
|
+
saveMemoryRecord(params) {
|
|
1821
|
+
const id = `mem_${crypto.randomBytes(8).toString("hex")}`;
|
|
1822
|
+
const now = new Date().toISOString();
|
|
1823
|
+
this.db
|
|
1824
|
+
.prepare(`INSERT INTO memories (
|
|
1825
|
+
id, workspace_id, namespace, document_id, title, content, kind, metadata_json, created_at
|
|
1826
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1827
|
+
.run(id, params.workspaceId, params.namespace, params.documentId, params.title, params.content, params.kind, JSON.stringify(params.metadata || {}), now);
|
|
1828
|
+
return {
|
|
1829
|
+
id,
|
|
1830
|
+
workspaceId: params.workspaceId,
|
|
1831
|
+
namespace: params.namespace,
|
|
1832
|
+
documentId: params.documentId,
|
|
1833
|
+
title: params.title,
|
|
1834
|
+
content: params.content,
|
|
1835
|
+
kind: params.kind,
|
|
1836
|
+
metadata: params.metadata || {},
|
|
1837
|
+
createdAt: now,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
listMemoryRecords(workspaceId, limit = 30) {
|
|
1841
|
+
const rows = this.db
|
|
1842
|
+
.prepare("SELECT * FROM memories WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
1843
|
+
.all(workspaceId, limit);
|
|
1844
|
+
return rows.map((r) => ({
|
|
1845
|
+
id: String(r.id),
|
|
1846
|
+
workspaceId: String(r.workspace_id),
|
|
1847
|
+
namespace: String(r.namespace),
|
|
1848
|
+
documentId: String(r.document_id),
|
|
1849
|
+
title: String(r.title),
|
|
1850
|
+
content: String(r.content),
|
|
1851
|
+
kind: String(r.kind),
|
|
1852
|
+
metadata: JSON.parse(String(r.metadata_json || "{}")),
|
|
1853
|
+
createdAt: String(r.created_at),
|
|
1854
|
+
}));
|
|
1855
|
+
}
|
|
1856
|
+
deleteMemoryRecord(workspaceId, idOrDocId) {
|
|
1857
|
+
const row = this.db
|
|
1858
|
+
.prepare("SELECT * FROM memories WHERE workspace_id = ? AND (id = ? OR document_id = ?)")
|
|
1859
|
+
.get(workspaceId, idOrDocId, idOrDocId);
|
|
1860
|
+
if (!row)
|
|
1861
|
+
return null;
|
|
1862
|
+
this.db
|
|
1863
|
+
.prepare("DELETE FROM memories WHERE id = ? AND workspace_id = ?")
|
|
1864
|
+
.run(String(row.id), workspaceId);
|
|
1865
|
+
// Also mark any temporal facts sourced from this memory record as invalidated (valid_to = now)
|
|
1866
|
+
const now = new Date().toISOString();
|
|
1867
|
+
this.db
|
|
1868
|
+
.prepare(`UPDATE temporal_facts
|
|
1869
|
+
SET valid_to = ?
|
|
1870
|
+
WHERE workspace_id = ?
|
|
1871
|
+
AND valid_to IS NULL
|
|
1872
|
+
AND (source_reference_id = ? OR source_reference_id = ? OR id = ?)`)
|
|
1873
|
+
.run(now, workspaceId, String(row.id), String(row.document_id), idOrDocId);
|
|
1874
|
+
return {
|
|
1875
|
+
id: String(row.id),
|
|
1876
|
+
workspaceId: String(row.workspace_id),
|
|
1877
|
+
namespace: String(row.namespace),
|
|
1878
|
+
documentId: String(row.document_id),
|
|
1879
|
+
title: String(row.title),
|
|
1880
|
+
content: String(row.content),
|
|
1881
|
+
kind: String(row.kind),
|
|
1882
|
+
metadata: JSON.parse(String(row.metadata_json || "{}")),
|
|
1883
|
+
createdAt: String(row.created_at),
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
// --- Bi-Temporal Resource & Fact Lineage (Zero-Dependency Graphiti Alternative) ---
|
|
1887
|
+
upsertTemporalFact(params) {
|
|
1888
|
+
const now = new Date().toISOString();
|
|
1889
|
+
const subject = params.subject.trim();
|
|
1890
|
+
const predicate = params.predicate.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
1891
|
+
const object = params.object.trim();
|
|
1892
|
+
const confidence = params.confidence !== undefined ? Number(params.confidence) : 0.95;
|
|
1893
|
+
const sourceKind = params.sourceKind || "fact";
|
|
1894
|
+
// Check if an active fact already exists for (workspace_id, subject, predicate)
|
|
1895
|
+
const activeRow = this.db
|
|
1896
|
+
.prepare(`SELECT * FROM temporal_facts
|
|
1897
|
+
WHERE workspace_id = ? AND subject = ? AND predicate = ? AND valid_to IS NULL
|
|
1898
|
+
ORDER BY created_at DESC LIMIT 1`)
|
|
1899
|
+
.get(params.workspaceId, subject, predicate);
|
|
1900
|
+
if (activeRow && String(activeRow.object) === object) {
|
|
1901
|
+
// Same object is still valid — update summary/confidence/metadata in place
|
|
1902
|
+
const existingId = String(activeRow.id);
|
|
1903
|
+
this.db
|
|
1904
|
+
.prepare(`UPDATE temporal_facts
|
|
1905
|
+
SET summary = ?, confidence = ?, metadata_json = ?, source_reference_id = COALESCE(?, source_reference_id)
|
|
1906
|
+
WHERE id = ?`)
|
|
1907
|
+
.run(params.summary, confidence, JSON.stringify(params.metadata || {}), params.sourceReferenceId ?? null, existingId);
|
|
1908
|
+
return this.mapTemporalFact({
|
|
1909
|
+
...activeRow,
|
|
1910
|
+
summary: params.summary,
|
|
1911
|
+
confidence,
|
|
1912
|
+
metadata_json: JSON.stringify(params.metadata || {}),
|
|
1913
|
+
source_reference_id: params.sourceReferenceId ?? activeRow.source_reference_id,
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
const newId = `tf_${crypto.randomBytes(8).toString("hex")}`;
|
|
1917
|
+
if (activeRow && params.supersedeExisting !== false) {
|
|
1918
|
+
this.db
|
|
1919
|
+
.prepare(`UPDATE temporal_facts
|
|
1920
|
+
SET valid_to = ?, superseded_by_id = ?
|
|
1921
|
+
WHERE id = ? AND workspace_id = ?`)
|
|
1922
|
+
.run(now, newId, String(activeRow.id), params.workspaceId);
|
|
1923
|
+
}
|
|
1924
|
+
this.db
|
|
1925
|
+
.prepare(`INSERT INTO temporal_facts (
|
|
1926
|
+
id, workspace_id, subject, predicate, object, summary, confidence,
|
|
1927
|
+
valid_from, valid_to, superseded_by_id, source_kind, source_reference_id,
|
|
1928
|
+
metadata_json, created_at
|
|
1929
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?)`)
|
|
1930
|
+
.run(newId, params.workspaceId, subject, predicate, object, params.summary, confidence, now, sourceKind, params.sourceReferenceId ?? null, JSON.stringify(params.metadata || {}), now);
|
|
1931
|
+
return {
|
|
1932
|
+
id: newId,
|
|
1933
|
+
workspaceId: params.workspaceId,
|
|
1934
|
+
subject,
|
|
1935
|
+
predicate,
|
|
1936
|
+
object,
|
|
1937
|
+
summary: params.summary,
|
|
1938
|
+
confidence,
|
|
1939
|
+
validFrom: now,
|
|
1940
|
+
validTo: null,
|
|
1941
|
+
supersededById: null,
|
|
1942
|
+
sourceKind,
|
|
1943
|
+
sourceReferenceId: params.sourceReferenceId ?? null,
|
|
1944
|
+
metadata: params.metadata || {},
|
|
1945
|
+
createdAt: now,
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
listTemporalFacts(workspaceId, opts) {
|
|
1949
|
+
const limit = Math.min(200, Math.max(1, Number(opts?.limit || 50)));
|
|
1950
|
+
const includeHistorical = Boolean(opts?.includeHistorical);
|
|
1951
|
+
const rows = this.db
|
|
1952
|
+
.prepare(includeHistorical
|
|
1953
|
+
? `SELECT * FROM temporal_facts WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?`
|
|
1954
|
+
: `SELECT * FROM temporal_facts WHERE workspace_id = ? AND valid_to IS NULL ORDER BY created_at DESC LIMIT ?`)
|
|
1955
|
+
.all(workspaceId, limit);
|
|
1956
|
+
let facts = rows.map((r) => this.mapTemporalFact(r));
|
|
1957
|
+
if (opts?.subject) {
|
|
1958
|
+
const subj = opts.subject.toLowerCase();
|
|
1959
|
+
facts = facts.filter((f) => f.subject.toLowerCase().includes(subj));
|
|
1960
|
+
}
|
|
1961
|
+
if (opts?.predicate) {
|
|
1962
|
+
const pred = opts.predicate.toUpperCase();
|
|
1963
|
+
facts = facts.filter((f) => f.predicate.toUpperCase().includes(pred));
|
|
1964
|
+
}
|
|
1965
|
+
if (opts?.query && opts.query.trim()) {
|
|
1966
|
+
const tokens = opts.query
|
|
1967
|
+
.toLowerCase()
|
|
1968
|
+
.split(/\s+/)
|
|
1969
|
+
.filter((t) => t.length >= 2);
|
|
1970
|
+
if (tokens.length > 0) {
|
|
1971
|
+
facts = facts.filter((f) => {
|
|
1972
|
+
const hay = `${f.subject} ${f.predicate} ${f.object} ${f.summary}`.toLowerCase();
|
|
1973
|
+
return tokens.some((tok) => hay.includes(tok));
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return facts;
|
|
1978
|
+
}
|
|
1979
|
+
forgetMatchingMemoryRecords(workspaceId, query) {
|
|
1980
|
+
const trimmed = query.trim().toLowerCase();
|
|
1981
|
+
if (!trimmed) {
|
|
1982
|
+
return { deletedMemories: [], invalidatedFacts: [] };
|
|
1983
|
+
}
|
|
1984
|
+
const tokens = trimmed.split(/\s+/).filter((t) => t.length >= 2);
|
|
1985
|
+
const allMemories = this.listMemoryRecords(workspaceId, 200);
|
|
1986
|
+
const matchingMemories = allMemories.filter((m) => {
|
|
1987
|
+
const hay = `${m.title} ${m.content} ${m.kind}`.toLowerCase();
|
|
1988
|
+
return (hay.includes(trimmed) ||
|
|
1989
|
+
(tokens.length > 0 && tokens.every((tok) => hay.includes(tok))));
|
|
1990
|
+
});
|
|
1991
|
+
// Snapshot before deleting: deleteMemoryRecord retires sourced facts itself,
|
|
1992
|
+
// and those must still be reported as invalidated.
|
|
1993
|
+
const activeFacts = this.listTemporalFacts(workspaceId, {
|
|
1994
|
+
includeHistorical: false,
|
|
1995
|
+
limit: 200,
|
|
1996
|
+
});
|
|
1997
|
+
const deletedMemories = [];
|
|
1998
|
+
for (const mem of matchingMemories) {
|
|
1999
|
+
const deleted = this.deleteMemoryRecord(workspaceId, mem.id);
|
|
2000
|
+
if (deleted)
|
|
2001
|
+
deletedMemories.push(deleted);
|
|
2002
|
+
}
|
|
2003
|
+
const deletedSourceIds = new Set(deletedMemories.flatMap((m) => [m.id, m.documentId]));
|
|
2004
|
+
const matchingFacts = activeFacts.filter((f) => {
|
|
2005
|
+
const hay = `${f.subject} ${f.predicate} ${f.object} ${f.summary}`.toLowerCase();
|
|
2006
|
+
return (hay.includes(trimmed) ||
|
|
2007
|
+
(tokens.length > 0 && tokens.every((tok) => hay.includes(tok))) ||
|
|
2008
|
+
Boolean(f.sourceReferenceId && deletedSourceIds.has(f.sourceReferenceId)));
|
|
2009
|
+
});
|
|
2010
|
+
const now = new Date().toISOString();
|
|
2011
|
+
const invalidatedFacts = [];
|
|
2012
|
+
for (const fact of matchingFacts) {
|
|
2013
|
+
this.db
|
|
2014
|
+
.prepare(`UPDATE temporal_facts SET valid_to = ? WHERE id = ? AND workspace_id = ?`)
|
|
2015
|
+
.run(now, fact.id, workspaceId);
|
|
2016
|
+
invalidatedFacts.push({
|
|
2017
|
+
...fact,
|
|
2018
|
+
validTo: now,
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
return {
|
|
2022
|
+
deletedMemories,
|
|
2023
|
+
invalidatedFacts,
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
mapTemporalFact(row) {
|
|
2027
|
+
return {
|
|
2028
|
+
id: String(row.id),
|
|
2029
|
+
workspaceId: String(row.workspace_id),
|
|
2030
|
+
subject: String(row.subject),
|
|
2031
|
+
predicate: String(row.predicate),
|
|
2032
|
+
object: String(row.object),
|
|
2033
|
+
summary: String(row.summary),
|
|
2034
|
+
confidence: Number(row.confidence ?? 0.95),
|
|
2035
|
+
validFrom: String(row.valid_from),
|
|
2036
|
+
validTo: row.valid_to ? String(row.valid_to) : null,
|
|
2037
|
+
supersededById: row.superseded_by_id ? String(row.superseded_by_id) : null,
|
|
2038
|
+
sourceKind: String(row.source_kind || "fact"),
|
|
2039
|
+
sourceReferenceId: row.source_reference_id
|
|
2040
|
+
? String(row.source_reference_id)
|
|
2041
|
+
: null,
|
|
2042
|
+
metadata: JSON.parse(String(row.metadata_json || "{}")),
|
|
2043
|
+
createdAt: String(row.created_at),
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
// --- Autonomous Agent Onboarding (agents.composio.dev pattern) ---
|
|
2047
|
+
createAutonomousAgentSignup(params) {
|
|
2048
|
+
const suffix = crypto.randomBytes(4).toString("hex");
|
|
2049
|
+
const agentName = (params?.agentName || `Autonomous Agent ${suffix}`).trim();
|
|
2050
|
+
const email = `agent+${suffix}@agents.muxo.ai`;
|
|
2051
|
+
const password = crypto.randomBytes(16).toString("hex");
|
|
2052
|
+
const created = this.registerUser({
|
|
2053
|
+
email,
|
|
2054
|
+
name: agentName,
|
|
2055
|
+
password,
|
|
2056
|
+
workspaceName: `${agentName} Workspace`,
|
|
2057
|
+
});
|
|
2058
|
+
const keyRes = this.createApiKey({
|
|
2059
|
+
workspaceId: created.workspace.id,
|
|
2060
|
+
name: params?.harness
|
|
2061
|
+
? `${params.harness} Autonomous Key`
|
|
2062
|
+
: "Autonomous Agent Key",
|
|
2063
|
+
environment: "live",
|
|
2064
|
+
});
|
|
2065
|
+
return {
|
|
2066
|
+
user: created.user,
|
|
2067
|
+
workspace: created.workspace,
|
|
2068
|
+
apiKey: keyRes.secretKey,
|
|
2069
|
+
apiKeyRecord: keyRes.record,
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
getLatestClaimForWorkspace(workspaceId) {
|
|
2073
|
+
const row = this.db
|
|
2074
|
+
.prepare(`SELECT * FROM agent_claims
|
|
2075
|
+
WHERE workspace_id = ?
|
|
2076
|
+
ORDER BY created_at DESC
|
|
2077
|
+
LIMIT 1`)
|
|
2078
|
+
.get(workspaceId);
|
|
2079
|
+
if (!row)
|
|
2080
|
+
return null;
|
|
2081
|
+
return {
|
|
2082
|
+
claimSlug: String(row.claim_slug),
|
|
2083
|
+
targetEmail: String(row.target_email),
|
|
2084
|
+
status: String(row.status),
|
|
2085
|
+
claimedAt: row.claimed_at ? String(row.claimed_at) : null,
|
|
2086
|
+
expiresAt: String(row.expires_at),
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Creates a claim link for a workspace. A claim minted without an email (at agent signup)
|
|
2091
|
+
* stays unusable until the agent names the owner; naming one reuses that same link.
|
|
2092
|
+
*/
|
|
2093
|
+
createAgentClaim(params) {
|
|
2094
|
+
const targetEmail = (params.email || "").trim().toLowerCase();
|
|
2095
|
+
const now = new Date();
|
|
2096
|
+
const expires = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
2097
|
+
const unbound = targetEmail
|
|
2098
|
+
? this.db
|
|
2099
|
+
.prepare(`SELECT claim_slug FROM agent_claims
|
|
2100
|
+
WHERE workspace_id = ? AND status = 'invited' AND target_email = ''
|
|
2101
|
+
ORDER BY created_at DESC LIMIT 1`)
|
|
2102
|
+
.get(params.workspaceId)
|
|
2103
|
+
: undefined;
|
|
2104
|
+
const claimSlug = unbound?.claim_slug || crypto.randomBytes(24).toString("base64url");
|
|
2105
|
+
if (unbound) {
|
|
2106
|
+
this.db
|
|
2107
|
+
.prepare("UPDATE agent_claims SET target_email = ?, expires_at = ? WHERE claim_slug = ?")
|
|
2108
|
+
.run(targetEmail, expires.toISOString(), claimSlug);
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
this.db
|
|
2112
|
+
.prepare(`INSERT INTO agent_claims (
|
|
2113
|
+
claim_slug, workspace_id, target_email, status, claimed_at, expires_at, created_at
|
|
2114
|
+
) VALUES (?, ?, ?, 'invited', NULL, ?, ?)`)
|
|
2115
|
+
.run(claimSlug, params.workspaceId, targetEmail, expires.toISOString(), now.toISOString());
|
|
2116
|
+
}
|
|
2117
|
+
return {
|
|
2118
|
+
claimSlug,
|
|
2119
|
+
claimUrl: `${params.baseUrl.replace(/\/$/, "")}/claim/${claimSlug}`,
|
|
2120
|
+
targetEmail,
|
|
2121
|
+
status: "invited",
|
|
2122
|
+
expiresAt: expires.toISOString(),
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
/** Returns a claim only while it can still be accepted (invited and not expired). */
|
|
2126
|
+
getOpenAgentClaim(claimSlug) {
|
|
2127
|
+
const row = this.db
|
|
2128
|
+
.prepare("SELECT * FROM agent_claims WHERE claim_slug = ?")
|
|
2129
|
+
.get(claimSlug);
|
|
2130
|
+
if (!row || String(row.status) !== "invited" || !row.target_email)
|
|
2131
|
+
return null;
|
|
2132
|
+
if (Date.parse(String(row.expires_at)) <= Date.now())
|
|
2133
|
+
return null;
|
|
2134
|
+
const workspace = this.getWorkspace(String(row.workspace_id));
|
|
2135
|
+
if (!workspace)
|
|
2136
|
+
return null;
|
|
2137
|
+
return {
|
|
2138
|
+
claimSlug,
|
|
2139
|
+
workspace,
|
|
2140
|
+
targetEmail: String(row.target_email),
|
|
2141
|
+
status: "invited",
|
|
2142
|
+
expiresAt: String(row.expires_at),
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
/** Transfers workspace ownership to the verified claimer and closes the claim. */
|
|
2146
|
+
acceptAgentClaim(claimSlug, userId) {
|
|
2147
|
+
const claim = this.getOpenAgentClaim(claimSlug);
|
|
2148
|
+
if (!claim || !this.getUser(userId))
|
|
2149
|
+
return null;
|
|
2150
|
+
const now = new Date().toISOString();
|
|
2151
|
+
this.db.exec("BEGIN");
|
|
2152
|
+
try {
|
|
2153
|
+
const closed = this.db
|
|
2154
|
+
.prepare("UPDATE agent_claims SET status = 'claimed', claimed_at = ? WHERE claim_slug = ? AND status = 'invited'")
|
|
2155
|
+
.run(now, claimSlug);
|
|
2156
|
+
if (closed.changes === 0) {
|
|
2157
|
+
this.db.exec("ROLLBACK");
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2160
|
+
this.db
|
|
2161
|
+
.prepare("UPDATE workspaces SET owner_user_id = ?, updated_at = ? WHERE id = ?")
|
|
2162
|
+
.run(userId, now, claim.workspace.id);
|
|
2163
|
+
this.db
|
|
2164
|
+
.prepare("UPDATE users SET workspace_id = ?, role = 'owner' WHERE id = ?")
|
|
2165
|
+
.run(claim.workspace.id, userId);
|
|
2166
|
+
this.ensureWorkspaceMembership({ workspaceId: claim.workspace.id, userId, role: "owner" });
|
|
2167
|
+
this.db.exec("COMMIT");
|
|
2168
|
+
}
|
|
2169
|
+
catch (err) {
|
|
2170
|
+
this.db.exec("ROLLBACK");
|
|
2171
|
+
throw err;
|
|
2172
|
+
}
|
|
2173
|
+
return this.getWorkspace(claim.workspace.id);
|
|
2174
|
+
}
|
|
2175
|
+
// --- Row Mappers ---
|
|
2176
|
+
mapUser(row) {
|
|
2177
|
+
return {
|
|
2178
|
+
id: String(row.id),
|
|
2179
|
+
email: String(row.email),
|
|
2180
|
+
name: String(row.name),
|
|
2181
|
+
workspaceId: String(row.workspace_id),
|
|
2182
|
+
role: String(row.role),
|
|
2183
|
+
createdAt: String(row.created_at),
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
mapWorkspace(row) {
|
|
2187
|
+
return {
|
|
2188
|
+
id: String(row.id),
|
|
2189
|
+
name: String(row.name),
|
|
2190
|
+
slug: String(row.slug),
|
|
2191
|
+
ownerUserId: String(row.owner_user_id),
|
|
2192
|
+
tier: String(row.tier),
|
|
2193
|
+
monthlyActionsLimit: Number(row.monthly_actions_limit),
|
|
2194
|
+
monthlyActionsUsed: Number(row.monthly_actions_used),
|
|
2195
|
+
creditsBalance: Number(row.credits_balance),
|
|
2196
|
+
stripeCustomerId: row.stripe_customer_id ? String(row.stripe_customer_id) : null,
|
|
2197
|
+
stripeSubscriptionId: row.stripe_subscription_id
|
|
2198
|
+
? String(row.stripe_subscription_id)
|
|
2199
|
+
: null,
|
|
2200
|
+
projectsEnvName: String(row.projects_env_name),
|
|
2201
|
+
policy: JSON.parse(String(row.policy_json || "{}")),
|
|
2202
|
+
createdAt: String(row.created_at),
|
|
2203
|
+
updatedAt: String(row.updated_at),
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
mapConnection(row) {
|
|
2207
|
+
return {
|
|
2208
|
+
id: String(row.id),
|
|
2209
|
+
workspaceId: String(row.workspace_id),
|
|
2210
|
+
lane: String(row.lane),
|
|
2211
|
+
providerSlug: String(row.provider_slug),
|
|
2212
|
+
providerName: String(row.provider_name),
|
|
2213
|
+
status: String(row.status),
|
|
2214
|
+
externalAccountId: row.external_account_id ? String(row.external_account_id) : null,
|
|
2215
|
+
redirectUrl: row.redirect_url ? String(row.redirect_url) : null,
|
|
2216
|
+
metadata: JSON.parse(String(row.metadata_json || "{}")),
|
|
2217
|
+
updatedAt: String(row.updated_at),
|
|
2218
|
+
createdAt: String(row.created_at),
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
mapResource(row) {
|
|
2222
|
+
return {
|
|
2223
|
+
id: String(row.id),
|
|
2224
|
+
workspaceId: String(row.workspace_id),
|
|
2225
|
+
resourceName: String(row.resource_name),
|
|
2226
|
+
providerSlug: String(row.provider_slug),
|
|
2227
|
+
providerName: String(row.provider_name),
|
|
2228
|
+
serviceSlug: String(row.service_slug),
|
|
2229
|
+
planSlug: row.plan_slug ? String(row.plan_slug) : null,
|
|
2230
|
+
status: String(row.status),
|
|
2231
|
+
envVarKeys: JSON.parse(String(row.env_var_keys_json || "[]")),
|
|
2232
|
+
externalResourceId: row.external_resource_id
|
|
2233
|
+
? String(row.external_resource_id)
|
|
2234
|
+
: null,
|
|
2235
|
+
metadata: JSON.parse(String(row.metadata_json || "{}")),
|
|
2236
|
+
createdAt: String(row.created_at),
|
|
2237
|
+
updatedAt: String(row.updated_at),
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
mapPlan(row) {
|
|
2241
|
+
const lane = String(row.lane);
|
|
2242
|
+
const toolSlug = String(row.tool_slug);
|
|
2243
|
+
const creditCost = Number(row.credit_cost);
|
|
2244
|
+
const approvalStatus = String(row.approval_status);
|
|
2245
|
+
return {
|
|
2246
|
+
id: String(row.id),
|
|
2247
|
+
workspaceId: String(row.workspace_id),
|
|
2248
|
+
actorId: String(row.actor_id),
|
|
2249
|
+
toolSlug,
|
|
2250
|
+
upstreamSlug: String(row.upstream_slug),
|
|
2251
|
+
lane,
|
|
2252
|
+
riskLevel: String(row.risk_level),
|
|
2253
|
+
input: JSON.parse(String(row.input_json || "{}")),
|
|
2254
|
+
reason: String(row.reason),
|
|
2255
|
+
creditCost,
|
|
2256
|
+
approvalStatus,
|
|
2257
|
+
approvedBy: row.approved_by ? String(row.approved_by) : null,
|
|
2258
|
+
approvedAt: row.approved_at ? String(row.approved_at) : null,
|
|
2259
|
+
runId: row.run_id ? String(row.run_id) : null,
|
|
2260
|
+
decisionRecord: row.decision_record_json
|
|
2261
|
+
? JSON.parse(String(row.decision_record_json))
|
|
2262
|
+
: null,
|
|
2263
|
+
memoryContext: row.memory_context_json
|
|
2264
|
+
? JSON.parse(String(row.memory_context_json))
|
|
2265
|
+
: null,
|
|
2266
|
+
supplierSettlement: this.computeSupplierSettlement(lane, toolSlug, creditCost, approvalStatus === "executed"
|
|
2267
|
+
? "settled_by_muxo_master_wallet"
|
|
2268
|
+
: "planned_muxo_master_wallet", String(row.workspace_id)),
|
|
2269
|
+
createdAt: String(row.created_at),
|
|
2270
|
+
expiresAt: String(row.expires_at),
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
mapRun(row) {
|
|
2274
|
+
return {
|
|
2275
|
+
id: String(row.id),
|
|
2276
|
+
planId: String(row.plan_id),
|
|
2277
|
+
workspaceId: String(row.workspace_id),
|
|
2278
|
+
actorId: String(row.actor_id),
|
|
2279
|
+
toolSlug: String(row.tool_slug),
|
|
2280
|
+
lane: String(row.lane),
|
|
2281
|
+
riskLevel: String(row.risk_level),
|
|
2282
|
+
status: String(row.status),
|
|
2283
|
+
input: JSON.parse(String(row.input_json || "{}")),
|
|
2284
|
+
output: row.output_json ? JSON.parse(String(row.output_json)) : null,
|
|
2285
|
+
error: row.error ? String(row.error) : null,
|
|
2286
|
+
errorMessage: row.error ? String(row.error) : null,
|
|
2287
|
+
actionsMetered: Number(row.actions_metered),
|
|
2288
|
+
creditsMetered: Number(row.credits_metered),
|
|
2289
|
+
durationMs: row.duration_ms !== null ? Number(row.duration_ms) : null,
|
|
2290
|
+
receiptId: row.receipt_id ? String(row.receipt_id) : null,
|
|
2291
|
+
startedAt: String(row.started_at),
|
|
2292
|
+
completedAt: row.completed_at ? String(row.completed_at) : null,
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
mapReceipt(row) {
|
|
2296
|
+
const lane = String(row.lane);
|
|
2297
|
+
const toolSlug = String(row.tool_slug);
|
|
2298
|
+
const creditsMetered = Number(row.credits_metered);
|
|
2299
|
+
return {
|
|
2300
|
+
id: String(row.id),
|
|
2301
|
+
runId: String(row.run_id),
|
|
2302
|
+
planId: String(row.plan_id),
|
|
2303
|
+
workspaceId: String(row.workspace_id),
|
|
2304
|
+
actorId: String(row.actor_id),
|
|
2305
|
+
toolSlug,
|
|
2306
|
+
lane,
|
|
2307
|
+
riskLevel: String(row.risk_level),
|
|
2308
|
+
status: String(row.status),
|
|
2309
|
+
actionsMetered: Number(row.actions_metered),
|
|
2310
|
+
creditsMetered,
|
|
2311
|
+
inputHash: String(row.input_hash),
|
|
2312
|
+
outputHash: String(row.output_hash),
|
|
2313
|
+
prevReceiptHash: String(row.prev_receipt_hash),
|
|
2314
|
+
receiptHash: String(row.receipt_hash),
|
|
2315
|
+
signature: String(row.signature),
|
|
2316
|
+
decisionRecord: row.decision_record_json
|
|
2317
|
+
? JSON.parse(String(row.decision_record_json))
|
|
2318
|
+
: null,
|
|
2319
|
+
supplierSettlement: this.computeSupplierSettlement(lane, toolSlug, creditsMetered, "settled_by_muxo_master_wallet", String(row.workspace_id)),
|
|
2320
|
+
issuedAt: String(row.issued_at),
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
}
|