@muxoai/sdk 1.3.0 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +19 -4
- package/.codex/config.toml +9 -2
- package/ARCHITECTURE.md +16 -6
- package/README.md +69 -24
- package/dist/clerk.d.ts +35 -0
- package/dist/clerk.js +201 -0
- package/dist/cli.js +1677 -103
- package/dist/composio-catalog.d.ts +234 -0
- package/dist/composio-catalog.js +1894 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +27 -1
- package/dist/durable-workflow.d.ts +112 -0
- package/dist/durable-workflow.js +284 -0
- package/dist/engine.d.ts +377 -6
- package/dist/engine.js +1919 -94
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp-schema.d.ts +286 -0
- package/dist/mcp-schema.js +2580 -0
- package/dist/mcp.d.ts +2 -25
- package/dist/mcp.js +1008 -507
- package/dist/memory-graph.d.ts +45 -0
- package/dist/memory-graph.js +293 -0
- package/dist/nl.js +214 -35
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/oss-suite.d.ts +217 -0
- package/dist/oss-suite.js +836 -0
- package/dist/policy.d.ts +190 -0
- package/dist/policy.js +1597 -0
- package/dist/providers/composio-instant.d.ts +4 -0
- package/dist/providers/composio-instant.js +890 -535
- package/dist/providers/composio-sdk.d.ts +126 -5
- package/dist/providers/composio-sdk.js +902 -79
- package/dist/providers/microsoft-governance.d.ts +89 -3
- package/dist/providers/microsoft-governance.js +673 -26
- package/dist/providers/stripe-billing.d.ts +42 -0
- package/dist/providers/stripe-billing.js +244 -5
- package/dist/providers/stripe-projects.d.ts +459 -4
- package/dist/providers/stripe-projects.js +2674 -122
- package/dist/providers/supermemory.d.ts +102 -6
- package/dist/providers/supermemory.js +609 -55
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +728 -7
- package/dist/sdk.js +539 -10
- package/dist/server.js +2744 -321
- package/dist/store.d.ts +138 -6
- package/dist/store.js +932 -58
- package/dist/types.d.ts +330 -4
- package/dist/ui/console-html.d.ts +62 -0
- package/dist/ui/console-html.js +3742 -2228
- 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 +6 -5
- package/plugins/muxo/.app.json +1 -1
- package/plugins/muxo/.claude-plugin/plugin.json +3 -3
- package/plugins/muxo/.codex-plugin/plugin.json +19 -10
- package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
- package/plugins/muxo/.mcp.json +1 -33
- package/plugins/muxo/assets/logo.png +0 -0
- package/plugins/muxo/hooks/session-start.sh +16 -4
- package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
- package/plugins/muxo/mcp.json +0 -10
- package/plugins/muxo/plugin.json +65 -8
- package/plugins/muxo/skills/muxo/SKILL.md +142 -14
- package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
- package/plugins/muxo/skills/muxo/references/cli.md +9 -3
- package/plugins/muxo/skills/muxo/references/mcp.md +22 -10
- package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
- package/plugins/muxo/skills/setup/SKILL.md +17 -3
- package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
- package/src/catalogues/stripe-projects-catalog.json +140 -0
package/dist/store.js
CHANGED
|
@@ -18,16 +18,58 @@ const DEFAULT_POLICY = {
|
|
|
18
18
|
allowProvisioning: true,
|
|
19
19
|
maxCreditsPerRun: 250,
|
|
20
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
|
+
`;
|
|
21
61
|
export class MuxoStore {
|
|
22
62
|
db;
|
|
23
63
|
vaultKey;
|
|
24
64
|
constructor(dbPath, vaultKey) {
|
|
25
65
|
this.db = new DatabaseSync(dbPath);
|
|
66
|
+
this.db.exec("PRAGMA busy_timeout = 10000;");
|
|
26
67
|
this.vaultKey = vaultKey;
|
|
27
68
|
this.initSchema();
|
|
28
69
|
}
|
|
29
70
|
initSchema() {
|
|
30
71
|
this.db.exec(`
|
|
72
|
+
PRAGMA busy_timeout = 10000;
|
|
31
73
|
PRAGMA journal_mode = WAL;
|
|
32
74
|
PRAGMA foreign_keys = ON;
|
|
33
75
|
|
|
@@ -153,26 +195,7 @@ export class MuxoStore {
|
|
|
153
195
|
completed_at TEXT
|
|
154
196
|
);
|
|
155
197
|
|
|
156
|
-
CREATE TABLE IF NOT EXISTS receipts (
|
|
157
|
-
id TEXT PRIMARY KEY,
|
|
158
|
-
run_id TEXT NOT NULL UNIQUE,
|
|
159
|
-
plan_id TEXT NOT NULL,
|
|
160
|
-
workspace_id TEXT NOT NULL,
|
|
161
|
-
actor_id TEXT NOT NULL,
|
|
162
|
-
tool_slug TEXT NOT NULL,
|
|
163
|
-
lane TEXT NOT NULL,
|
|
164
|
-
risk_level TEXT NOT NULL,
|
|
165
|
-
status TEXT NOT NULL,
|
|
166
|
-
actions_metered INTEGER NOT NULL,
|
|
167
|
-
credits_metered INTEGER NOT NULL,
|
|
168
|
-
input_hash TEXT NOT NULL,
|
|
169
|
-
output_hash TEXT NOT NULL,
|
|
170
|
-
prev_receipt_hash TEXT NOT NULL,
|
|
171
|
-
receipt_hash TEXT NOT NULL,
|
|
172
|
-
signature TEXT NOT NULL,
|
|
173
|
-
decision_record_json TEXT,
|
|
174
|
-
issued_at TEXT NOT NULL
|
|
175
|
-
);
|
|
198
|
+
CREATE TABLE IF NOT EXISTS receipts (${RECEIPTS_TABLE_COLUMNS});
|
|
176
199
|
|
|
177
200
|
CREATE TABLE IF NOT EXISTS memories (
|
|
178
201
|
id TEXT PRIMARY KEY,
|
|
@@ -186,6 +209,23 @@ export class MuxoStore {
|
|
|
186
209
|
created_at TEXT NOT NULL
|
|
187
210
|
);
|
|
188
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
|
+
|
|
189
229
|
CREATE TABLE IF NOT EXISTS agent_claims (
|
|
190
230
|
claim_slug TEXT PRIMARY KEY,
|
|
191
231
|
workspace_id TEXT NOT NULL,
|
|
@@ -223,12 +263,42 @@ export class MuxoStore {
|
|
|
223
263
|
updated_at TEXT NOT NULL,
|
|
224
264
|
PRIMARY KEY (workspace_id, provider)
|
|
225
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
|
+
);
|
|
226
293
|
`);
|
|
227
294
|
// Backwards-compatible column migrations for existing databases
|
|
228
295
|
for (const stmt of [
|
|
229
296
|
"ALTER TABLE plans ADD COLUMN decision_record_json TEXT",
|
|
230
297
|
"ALTER TABLE plans ADD COLUMN memory_context_json TEXT",
|
|
231
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",
|
|
232
302
|
]) {
|
|
233
303
|
try {
|
|
234
304
|
this.db.exec(stmt);
|
|
@@ -237,6 +307,38 @@ export class MuxoStore {
|
|
|
237
307
|
// Column already exists
|
|
238
308
|
}
|
|
239
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
|
+
}
|
|
240
342
|
}
|
|
241
343
|
// --- Encryption Vault (AES-256-GCM) ---
|
|
242
344
|
encryptSecrets(workspaceId, secrets) {
|
|
@@ -332,11 +434,24 @@ export class MuxoStore {
|
|
|
332
434
|
referenceId: workspaceId,
|
|
333
435
|
description: "Free Plan initial monthly allowance (1,000 actions + 250 trial credits)",
|
|
334
436
|
});
|
|
437
|
+
this.ensureWorkspaceMembership({
|
|
438
|
+
workspaceId,
|
|
439
|
+
userId,
|
|
440
|
+
role: "owner",
|
|
441
|
+
});
|
|
335
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
|
+
});
|
|
336
450
|
return {
|
|
337
451
|
user: this.getUser(userId),
|
|
338
452
|
workspace: this.getWorkspace(workspaceId),
|
|
339
453
|
sessionToken,
|
|
454
|
+
apiKey: defaultKey.secretKey,
|
|
340
455
|
};
|
|
341
456
|
}
|
|
342
457
|
loginUser(email, password) {
|
|
@@ -349,6 +464,11 @@ export class MuxoStore {
|
|
|
349
464
|
return null;
|
|
350
465
|
const user = this.mapUser(row);
|
|
351
466
|
const workspace = this.getWorkspace(user.workspaceId);
|
|
467
|
+
this.ensureWorkspaceMembership({
|
|
468
|
+
workspaceId: workspace.id,
|
|
469
|
+
userId: user.id,
|
|
470
|
+
role: user.role,
|
|
471
|
+
});
|
|
352
472
|
const sessionToken = this.createSession(user.id, workspace.id);
|
|
353
473
|
return { user, workspace, sessionToken };
|
|
354
474
|
}
|
|
@@ -362,7 +482,7 @@ export class MuxoStore {
|
|
|
362
482
|
.run(this.hashToken(rawToken), userId, workspaceId, expires.toISOString(), now.toISOString());
|
|
363
483
|
return rawToken;
|
|
364
484
|
}
|
|
365
|
-
resolveAuthToken(token) {
|
|
485
|
+
resolveAuthToken(token, workspaceOverrideId) {
|
|
366
486
|
const trimmed = token.trim();
|
|
367
487
|
if (!trimmed)
|
|
368
488
|
return null;
|
|
@@ -400,11 +520,22 @@ export class MuxoStore {
|
|
|
400
520
|
return null;
|
|
401
521
|
}
|
|
402
522
|
const user = this.getUser(String(sessionRow.user_id));
|
|
403
|
-
|
|
404
|
-
|
|
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)
|
|
405
536
|
return null;
|
|
406
537
|
return {
|
|
407
|
-
user,
|
|
538
|
+
user: { ...user, workspaceId: workspace.id },
|
|
408
539
|
workspace,
|
|
409
540
|
authType: "session",
|
|
410
541
|
actorId: user.id,
|
|
@@ -422,6 +553,326 @@ export class MuxoStore {
|
|
|
422
553
|
.get(workspaceId);
|
|
423
554
|
return row ? this.mapWorkspace(row) : null;
|
|
424
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
|
+
}
|
|
425
876
|
updateWorkspacePolicy(workspaceId, policy) {
|
|
426
877
|
const ws = this.getWorkspace(workspaceId);
|
|
427
878
|
if (!ws)
|
|
@@ -443,12 +894,16 @@ export class MuxoStore {
|
|
|
443
894
|
const rawSecret = `mk_${env}_${id.slice(4)}_${crypto.randomBytes(16).toString("hex")}`;
|
|
444
895
|
const keyPrefix = `${rawSecret.slice(0, 16)}...${rawSecret.slice(-4)}`;
|
|
445
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;
|
|
446
900
|
const now = new Date().toISOString();
|
|
447
901
|
this.db
|
|
448
902
|
.prepare(`INSERT INTO api_keys (
|
|
449
|
-
id, workspace_id, name, key_prefix, key_hash, environment, scopes_json,
|
|
450
|
-
|
|
451
|
-
|
|
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);
|
|
452
907
|
return {
|
|
453
908
|
record: {
|
|
454
909
|
id,
|
|
@@ -457,6 +912,9 @@ export class MuxoStore {
|
|
|
457
912
|
keyPrefix,
|
|
458
913
|
environment: env,
|
|
459
914
|
scopes,
|
|
915
|
+
teamName,
|
|
916
|
+
role,
|
|
917
|
+
createdByEmail,
|
|
460
918
|
lastUsedAt: null,
|
|
461
919
|
revokedAt: null,
|
|
462
920
|
createdAt: now,
|
|
@@ -475,6 +933,9 @@ export class MuxoStore {
|
|
|
475
933
|
keyPrefix: String(r.key_prefix),
|
|
476
934
|
environment: String(r.environment),
|
|
477
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,
|
|
478
939
|
lastUsedAt: r.last_used_at ? String(r.last_used_at) : null,
|
|
479
940
|
revokedAt: r.revoked_at ? String(r.revoked_at) : null,
|
|
480
941
|
createdAt: String(r.created_at),
|
|
@@ -485,8 +946,95 @@ export class MuxoStore {
|
|
|
485
946
|
const res = this.db
|
|
486
947
|
.prepare("UPDATE api_keys SET revoked_at = ? WHERE id = ? AND workspace_id = ?")
|
|
487
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
|
+
}
|
|
488
952
|
return res.changes > 0;
|
|
489
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
|
+
}
|
|
490
1038
|
// --- Connections ---
|
|
491
1039
|
upsertConnection(params) {
|
|
492
1040
|
const now = new Date().toISOString();
|
|
@@ -510,7 +1058,23 @@ export class MuxoStore {
|
|
|
510
1058
|
metadata_json = excluded.metadata_json,
|
|
511
1059
|
updated_at = excluded.updated_at`)
|
|
512
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);
|
|
513
|
-
|
|
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;
|
|
514
1078
|
}
|
|
515
1079
|
getConnection(id) {
|
|
516
1080
|
const row = this.db
|
|
@@ -560,14 +1124,46 @@ export class MuxoStore {
|
|
|
560
1124
|
metadata_json = excluded.metadata_json,
|
|
561
1125
|
updated_at = excluded.updated_at`)
|
|
562
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);
|
|
563
|
-
|
|
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;
|
|
564
1146
|
}
|
|
565
1147
|
getResource(workspaceId, resourceNameOrId) {
|
|
566
1148
|
const row = this.db
|
|
567
|
-
.prepare("SELECT * FROM resources WHERE workspace_id = ? AND (resource_name = ? OR id = ?)")
|
|
568
|
-
.get(workspaceId, resourceNameOrId, resourceNameOrId);
|
|
1149
|
+
.prepare("SELECT * FROM resources WHERE workspace_id = ? AND (resource_name = ? OR id = ? OR external_resource_id = ?)")
|
|
1150
|
+
.get(workspaceId, resourceNameOrId, resourceNameOrId, resourceNameOrId);
|
|
569
1151
|
return row ? this.mapResource(row) : null;
|
|
570
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
|
+
}
|
|
571
1167
|
listResources(workspaceId) {
|
|
572
1168
|
const rows = this.db
|
|
573
1169
|
.prepare("SELECT * FROM resources WHERE workspace_id = ? ORDER BY updated_at DESC")
|
|
@@ -742,6 +1338,12 @@ export class MuxoStore {
|
|
|
742
1338
|
.get(planId, workspaceId);
|
|
743
1339
|
return row ? this.mapPlan(row) : null;
|
|
744
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
|
+
}
|
|
745
1347
|
listPlans(workspaceId, limit = 50) {
|
|
746
1348
|
const rows = this.db
|
|
747
1349
|
.prepare("SELECT * FROM plans WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
@@ -787,7 +1389,9 @@ export class MuxoStore {
|
|
|
787
1389
|
SET monthly_actions_used = ?, credits_balance = ?, updated_at = ?
|
|
788
1390
|
WHERE id = ?`)
|
|
789
1391
|
.run(newActionsUsed, newCreditsBalance, now, params.workspaceId);
|
|
790
|
-
const
|
|
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);
|
|
791
1395
|
this.addLedgerEntry({
|
|
792
1396
|
workspaceId: params.workspaceId,
|
|
793
1397
|
kind: "run_deduction",
|
|
@@ -795,7 +1399,9 @@ export class MuxoStore {
|
|
|
795
1399
|
creditsDelta: -params.creditsMetered,
|
|
796
1400
|
balanceAfter: newCreditsBalance,
|
|
797
1401
|
referenceId: run.id,
|
|
798
|
-
description:
|
|
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)})`,
|
|
799
1405
|
});
|
|
800
1406
|
}
|
|
801
1407
|
const receipt = this.issueReceipt({
|
|
@@ -818,10 +1424,41 @@ export class MuxoStore {
|
|
|
818
1424
|
receipt,
|
|
819
1425
|
};
|
|
820
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
|
+
}
|
|
821
1458
|
issueReceipt(params) {
|
|
822
1459
|
const id = `rcpt_${crypto.randomBytes(8).toString("hex")}`;
|
|
823
1460
|
const lastReceipt = this.db
|
|
824
|
-
.prepare("SELECT receipt_hash FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC LIMIT 1")
|
|
1461
|
+
.prepare("SELECT receipt_hash FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC, rowid DESC LIMIT 1")
|
|
825
1462
|
.get(params.run.workspaceId);
|
|
826
1463
|
const prevReceiptHash = lastReceipt?.receipt_hash || "genesis_0000000000000000";
|
|
827
1464
|
const inputHash = crypto
|
|
@@ -903,14 +1540,17 @@ export class MuxoStore {
|
|
|
903
1540
|
return rows.map((r) => this.mapRun(r));
|
|
904
1541
|
}
|
|
905
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.
|
|
906
1545
|
const row = this.db
|
|
907
|
-
.prepare(
|
|
908
|
-
|
|
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);
|
|
909
1549
|
return row ? this.mapReceipt(row) : null;
|
|
910
1550
|
}
|
|
911
1551
|
listReceipts(workspaceId, limit = 50) {
|
|
912
1552
|
const rows = this.db
|
|
913
|
-
.prepare("SELECT * FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC LIMIT ?")
|
|
1553
|
+
.prepare("SELECT * FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC, rowid DESC LIMIT ?")
|
|
914
1554
|
.all(workspaceId, limit);
|
|
915
1555
|
return rows.map((r) => this.mapReceipt(r));
|
|
916
1556
|
}
|
|
@@ -1004,7 +1644,9 @@ export class MuxoStore {
|
|
|
1004
1644
|
computeSupplierSettlement(lane, toolSlug, creditCost, settlementStatus = "settled_by_muxo_master_wallet", workspaceId) {
|
|
1005
1645
|
const useByok = settlementStatus === "settled_by_customer_byok" ||
|
|
1006
1646
|
settlementStatus === "planned_customer_byok" ||
|
|
1007
|
-
(
|
|
1647
|
+
(Number(creditCost || 0) === 0 && workspaceId
|
|
1648
|
+
? this.hasByokForLane(workspaceId, lane, toolSlug)
|
|
1649
|
+
: false);
|
|
1008
1650
|
const upstreamProvider = toolSlug.split(".")[1] || lane;
|
|
1009
1651
|
if (useByok) {
|
|
1010
1652
|
const isPlanned = settlementStatus === "planned_muxo_master_wallet" ||
|
|
@@ -1127,6 +1769,17 @@ export class MuxoStore {
|
|
|
1127
1769
|
totalSupplierWholesalePaidCents,
|
|
1128
1770
|
totalMuxoNetMarginCents,
|
|
1129
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
|
+
},
|
|
1130
1783
|
};
|
|
1131
1784
|
}
|
|
1132
1785
|
listLedgerEntries(workspaceId, limit = 50) {
|
|
@@ -1141,7 +1794,12 @@ export class MuxoStore {
|
|
|
1141
1794
|
if (kind === "run_deduction" && refId) {
|
|
1142
1795
|
const run = this.getRun(workspaceId, refId);
|
|
1143
1796
|
if (run) {
|
|
1144
|
-
|
|
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);
|
|
1145
1803
|
}
|
|
1146
1804
|
}
|
|
1147
1805
|
return {
|
|
@@ -1204,6 +1862,15 @@ export class MuxoStore {
|
|
|
1204
1862
|
this.db
|
|
1205
1863
|
.prepare("DELETE FROM memories WHERE id = ? AND workspace_id = ?")
|
|
1206
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);
|
|
1207
1874
|
return {
|
|
1208
1875
|
id: String(row.id),
|
|
1209
1876
|
workspaceId: String(row.workspace_id),
|
|
@@ -1216,6 +1883,166 @@ export class MuxoStore {
|
|
|
1216
1883
|
createdAt: String(row.created_at),
|
|
1217
1884
|
};
|
|
1218
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
|
+
}
|
|
1219
2046
|
// --- Autonomous Agent Onboarding (agents.composio.dev pattern) ---
|
|
1220
2047
|
createAutonomousAgentSignup(params) {
|
|
1221
2048
|
const suffix = crypto.randomBytes(4).toString("hex");
|
|
@@ -1259,46 +2086,92 @@ export class MuxoStore {
|
|
|
1259
2086
|
expiresAt: String(row.expires_at),
|
|
1260
2087
|
};
|
|
1261
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
|
+
*/
|
|
1262
2093
|
createAgentClaim(params) {
|
|
1263
|
-
const
|
|
2094
|
+
const targetEmail = (params.email || "").trim().toLowerCase();
|
|
1264
2095
|
const now = new Date();
|
|
1265
2096
|
const expires = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
1266
|
-
|
|
1267
|
-
.
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
+
}
|
|
1271
2117
|
return {
|
|
1272
2118
|
claimSlug,
|
|
1273
2119
|
claimUrl: `${params.baseUrl.replace(/\/$/, "")}/claim/${claimSlug}`,
|
|
1274
|
-
targetEmail
|
|
2120
|
+
targetEmail,
|
|
1275
2121
|
status: "invited",
|
|
1276
2122
|
expiresAt: expires.toISOString(),
|
|
1277
2123
|
};
|
|
1278
2124
|
}
|
|
1279
|
-
|
|
2125
|
+
/** Returns a claim only while it can still be accepted (invited and not expired). */
|
|
2126
|
+
getOpenAgentClaim(claimSlug) {
|
|
1280
2127
|
const row = this.db
|
|
1281
2128
|
.prepare("SELECT * FROM agent_claims WHERE claim_slug = ?")
|
|
1282
2129
|
.get(claimSlug);
|
|
1283
|
-
if (!row)
|
|
2130
|
+
if (!row || String(row.status) !== "invited" || !row.target_email)
|
|
1284
2131
|
return null;
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
this.db
|
|
1289
|
-
.prepare("UPDATE agent_claims SET status = 'claimed', claimed_at = ? WHERE claim_slug = ?")
|
|
1290
|
-
.run(now, claimSlug);
|
|
1291
|
-
const workspace = this.getWorkspace(workspaceId);
|
|
2132
|
+
if (Date.parse(String(row.expires_at)) <= Date.now())
|
|
2133
|
+
return null;
|
|
2134
|
+
const workspace = this.getWorkspace(String(row.workspace_id));
|
|
1292
2135
|
if (!workspace)
|
|
1293
2136
|
return null;
|
|
1294
2137
|
return {
|
|
1295
2138
|
claimSlug,
|
|
1296
2139
|
workspace,
|
|
1297
|
-
targetEmail,
|
|
1298
|
-
status: "
|
|
1299
|
-
|
|
2140
|
+
targetEmail: String(row.target_email),
|
|
2141
|
+
status: "invited",
|
|
2142
|
+
expiresAt: String(row.expires_at),
|
|
1300
2143
|
};
|
|
1301
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
|
+
}
|
|
1302
2175
|
// --- Row Mappers ---
|
|
1303
2176
|
mapUser(row) {
|
|
1304
2177
|
return {
|
|
@@ -1410,6 +2283,7 @@ export class MuxoStore {
|
|
|
1410
2283
|
input: JSON.parse(String(row.input_json || "{}")),
|
|
1411
2284
|
output: row.output_json ? JSON.parse(String(row.output_json)) : null,
|
|
1412
2285
|
error: row.error ? String(row.error) : null,
|
|
2286
|
+
errorMessage: row.error ? String(row.error) : null,
|
|
1413
2287
|
actionsMetered: Number(row.actions_metered),
|
|
1414
2288
|
creditsMetered: Number(row.credits_metered),
|
|
1415
2289
|
durationMs: row.duration_ms !== null ? Number(row.duration_ms) : null,
|