@mastra/factory 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150
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/CHANGELOG.md +695 -0
- package/LICENSE.md +30 -0
- package/README.md +27 -0
- package/dist/auth.d.ts +181 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +505 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities/connection.d.ts +9 -0
- package/dist/capabilities/connection.d.ts.map +1 -0
- package/dist/capabilities/connection.js +1 -0
- package/dist/capabilities/intake.d.ts +140 -0
- package/dist/capabilities/intake.d.ts.map +1 -0
- package/dist/capabilities/intake.js +1 -0
- package/dist/capabilities/version-control.d.ts +245 -0
- package/dist/capabilities/version-control.d.ts.map +1 -0
- package/dist/capabilities/version-control.js +1 -0
- package/dist/factory.d.ts +156 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +483 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/integrations/base.d.ts +210 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +1 -0
- package/dist/integrations/github/config.d.ts +60 -0
- package/dist/integrations/github/config.d.ts.map +1 -0
- package/dist/integrations/github/config.js +42 -0
- package/dist/integrations/github/config.js.map +1 -0
- package/dist/integrations/github/factory-session.d.ts +12 -0
- package/dist/integrations/github/factory-session.d.ts.map +1 -0
- package/dist/integrations/github/factory-session.js +37 -0
- package/dist/integrations/github/factory-session.js.map +1 -0
- package/dist/integrations/github/integration.d.ts +191 -0
- package/dist/integrations/github/integration.d.ts.map +1 -0
- package/dist/integrations/github/integration.js +1077 -0
- package/dist/integrations/github/integration.js.map +1 -0
- package/dist/integrations/github/issue-triage.d.ts +27 -0
- package/dist/integrations/github/issue-triage.d.ts.map +1 -0
- package/dist/integrations/github/issue-triage.js +82 -0
- package/dist/integrations/github/issue-triage.js.map +1 -0
- package/dist/integrations/github/pat.d.ts +38 -0
- package/dist/integrations/github/pat.d.ts.map +1 -0
- package/dist/integrations/github/pat.js +59 -0
- package/dist/integrations/github/pat.js.map +1 -0
- package/dist/integrations/github/provenance.d.ts +27 -0
- package/dist/integrations/github/provenance.d.ts.map +1 -0
- package/dist/integrations/github/provenance.js +75 -0
- package/dist/integrations/github/provenance.js.map +1 -0
- package/dist/integrations/github/routes.d.ts +69 -0
- package/dist/integrations/github/routes.d.ts.map +1 -0
- package/dist/integrations/github/routes.js +1324 -0
- package/dist/integrations/github/routes.js.map +1 -0
- package/dist/integrations/github/rules.d.ts +78 -0
- package/dist/integrations/github/rules.d.ts.map +1 -0
- package/dist/integrations/github/rules.js +362 -0
- package/dist/integrations/github/rules.js.map +1 -0
- package/dist/integrations/github/sandbox-release.d.ts +38 -0
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
- package/dist/integrations/github/sandbox-release.js +74 -0
- package/dist/integrations/github/sandbox-release.js.map +1 -0
- package/dist/integrations/github/sandbox.d.ts +281 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -0
- package/dist/integrations/github/sandbox.js +547 -0
- package/dist/integrations/github/sandbox.js.map +1 -0
- package/dist/integrations/github/session-subscriptions.d.ts +26 -0
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/session-subscriptions.js +192 -0
- package/dist/integrations/github/session-subscriptions.js.map +1 -0
- package/dist/integrations/github/subscriptions.d.ts +54 -0
- package/dist/integrations/github/subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/subscriptions.js +62 -0
- package/dist/integrations/github/subscriptions.js.map +1 -0
- package/dist/integrations/github/token-refresh.d.ts +12 -0
- package/dist/integrations/github/token-refresh.d.ts.map +1 -0
- package/dist/integrations/github/token-refresh.js +24 -0
- package/dist/integrations/github/token-refresh.js.map +1 -0
- package/dist/integrations/github/webhook.d.ts +87 -0
- package/dist/integrations/github/webhook.d.ts.map +1 -0
- package/dist/integrations/github/webhook.js +397 -0
- package/dist/integrations/github/webhook.js.map +1 -0
- package/dist/integrations/linear/agent-tools.d.ts +34 -0
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
- package/dist/integrations/linear/agent-tools.js +85 -0
- package/dist/integrations/linear/agent-tools.js.map +1 -0
- package/dist/integrations/linear/integration.d.ts +233 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -0
- package/dist/integrations/linear/integration.js +672 -0
- package/dist/integrations/linear/integration.js.map +1 -0
- package/dist/integrations/linear/routes.d.ts +61 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -0
- package/dist/integrations/linear/routes.js +239 -0
- package/dist/integrations/linear/routes.js.map +1 -0
- package/dist/integrations/linear/rules.d.ts +42 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -0
- package/dist/integrations/linear/rules.js +133 -0
- package/dist/integrations/linear/rules.js.map +1 -0
- package/dist/integrations/linear/storage.d.ts +65 -0
- package/dist/integrations/linear/storage.d.ts.map +1 -0
- package/dist/integrations/linear/storage.js +1 -0
- package/dist/integrations/platform/api-client.d.ts +26 -0
- package/dist/integrations/platform/api-client.d.ts.map +1 -0
- package/dist/integrations/platform/api-client.js +156 -0
- package/dist/integrations/platform/api-client.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.d.ts +49 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/github/event-worker.js +348 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -0
- package/dist/integrations/platform/github/integration.d.ts +50 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -0
- package/dist/integrations/platform/github/integration.js +961 -0
- package/dist/integrations/platform/github/integration.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +46 -0
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
- package/dist/integrations/platform/linear/integration.js +483 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -0
- package/dist/integrations/workos/integration.d.ts +39 -0
- package/dist/integrations/workos/integration.d.ts.map +1 -0
- package/dist/integrations/workos/integration.js +91 -0
- package/dist/integrations/workos/integration.js.map +1 -0
- package/dist/routes/config.d.ts +235 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +972 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/custom-provider-source.d.ts +52 -0
- package/dist/routes/custom-provider-source.d.ts.map +1 -0
- package/dist/routes/custom-provider-source.js +124 -0
- package/dist/routes/custom-provider-source.js.map +1 -0
- package/dist/routes/fs.d.ts +151 -0
- package/dist/routes/fs.d.ts.map +1 -0
- package/dist/routes/fs.js +708 -0
- package/dist/routes/fs.js.map +1 -0
- package/dist/routes/intake.d.ts +23 -0
- package/dist/routes/intake.d.ts.map +1 -0
- package/dist/routes/intake.js +179 -0
- package/dist/routes/intake.js.map +1 -0
- package/dist/routes/oauth.d.ts +43 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +463 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/projects.d.ts +19 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +461 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/provider-credentials.d.ts +81 -0
- package/dist/routes/provider-credentials.d.ts.map +1 -0
- package/dist/routes/provider-credentials.js +86 -0
- package/dist/routes/provider-credentials.js.map +1 -0
- package/dist/routes/route.d.ts +48 -0
- package/dist/routes/route.d.ts.map +1 -0
- package/dist/routes/route.js +15 -0
- package/dist/routes/route.js.map +1 -0
- package/dist/routes/skills.d.ts +30 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +168 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/surface.d.ts +91 -0
- package/dist/routes/surface.d.ts.map +1 -0
- package/dist/routes/surface.js +260 -0
- package/dist/routes/surface.js.map +1 -0
- package/dist/routes/tenant-credentials.d.ts +74 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -0
- package/dist/routes/tenant-credentials.js +156 -0
- package/dist/routes/tenant-credentials.js.map +1 -0
- package/dist/routes/work-items.d.ts +40 -0
- package/dist/routes/work-items.d.ts.map +1 -0
- package/dist/routes/work-items.js +695 -0
- package/dist/routes/work-items.js.map +1 -0
- package/dist/rules/binding-context.d.ts +5 -0
- package/dist/rules/binding-context.d.ts.map +1 -0
- package/dist/rules/binding-context.js +28 -0
- package/dist/rules/binding-context.js.map +1 -0
- package/dist/rules/defaults.d.ts +9 -0
- package/dist/rules/defaults.d.ts.map +1 -0
- package/dist/rules/defaults.js +260 -0
- package/dist/rules/defaults.js.map +1 -0
- package/dist/rules/dispatcher.d.ts +42 -0
- package/dist/rules/dispatcher.d.ts.map +1 -0
- package/dist/rules/dispatcher.js +518 -0
- package/dist/rules/dispatcher.js.map +1 -0
- package/dist/rules/index.d.ts +7 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +5 -0
- package/dist/rules/processor.d.ts +54 -0
- package/dist/rules/processor.d.ts.map +1 -0
- package/dist/rules/processor.js +378 -0
- package/dist/rules/processor.js.map +1 -0
- package/dist/rules/resolve.d.ts +17 -0
- package/dist/rules/resolve.d.ts.map +1 -0
- package/dist/rules/resolve.js +30 -0
- package/dist/rules/resolve.js.map +1 -0
- package/dist/rules/start-coordinator.d.ts +60 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -0
- package/dist/rules/start-coordinator.js +194 -0
- package/dist/rules/start-coordinator.js.map +1 -0
- package/dist/rules/tools.d.ts +10 -0
- package/dist/rules/tools.d.ts.map +1 -0
- package/dist/rules/tools.js +58 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/transition-service.d.ts +49 -0
- package/dist/rules/transition-service.d.ts.map +1 -0
- package/dist/rules/transition-service.js +232 -0
- package/dist/rules/transition-service.js.map +1 -0
- package/dist/rules/types.d.ts +234 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +38 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/validation.d.ts +11 -0
- package/dist/rules/validation.d.ts.map +1 -0
- package/dist/rules/validation.js +308 -0
- package/dist/rules/validation.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +222 -0
- package/dist/sandbox/fleet.d.ts.map +1 -0
- package/dist/sandbox/fleet.js +322 -0
- package/dist/sandbox/fleet.js.map +1 -0
- package/dist/sandbox/reattach.d.ts +3 -0
- package/dist/sandbox/reattach.d.ts.map +1 -0
- package/dist/sandbox/reattach.js +16 -0
- package/dist/sandbox/reattach.js.map +1 -0
- package/dist/server-error.d.ts +12 -0
- package/dist/server-error.d.ts.map +1 -0
- package/dist/server-error.js +16 -0
- package/dist/server-error.js.map +1 -0
- package/dist/skills/service.d.ts +48 -0
- package/dist/skills/service.d.ts.map +1 -0
- package/dist/skills/service.js +40 -0
- package/dist/skills/service.js.map +1 -0
- package/dist/spa-static.d.ts +33 -0
- package/dist/spa-static.d.ts.map +1 -0
- package/dist/spa-static.js +104 -0
- package/dist/spa-static.js.map +1 -0
- package/dist/state-signing.d.ts +59 -0
- package/dist/state-signing.d.ts.map +1 -0
- package/dist/state-signing.js +83 -0
- package/dist/state-signing.js.map +1 -0
- package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
- package/dist/storage/domains/audit/agent-audit.js +66 -0
- package/dist/storage/domains/audit/agent-audit.js.map +1 -0
- package/dist/storage/domains/audit/base.d.ts +140 -0
- package/dist/storage/domains/audit/base.d.ts.map +1 -0
- package/dist/storage/domains/audit/base.js +184 -0
- package/dist/storage/domains/audit/base.js.map +1 -0
- package/dist/storage/domains/audit/domain.d.ts +69 -0
- package/dist/storage/domains/audit/domain.d.ts.map +1 -0
- package/dist/storage/domains/audit/domain.js +167 -0
- package/dist/storage/domains/audit/domain.js.map +1 -0
- package/dist/storage/domains/channel-identity/base.d.ts +96 -0
- package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
- package/dist/storage/domains/channel-identity/base.js +149 -0
- package/dist/storage/domains/channel-identity/base.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +134 -0
- package/dist/storage/domains/credentials/base.d.ts.map +1 -0
- package/dist/storage/domains/credentials/base.js +273 -0
- package/dist/storage/domains/credentials/base.js.map +1 -0
- package/dist/storage/domains/custom-providers/base.d.ts +57 -0
- package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
- package/dist/storage/domains/custom-providers/base.js +145 -0
- package/dist/storage/domains/custom-providers/base.js.map +1 -0
- package/dist/storage/domains/intake/base.d.ts +38 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -0
- package/dist/storage/domains/intake/base.js +80 -0
- package/dist/storage/domains/intake/base.js.map +1 -0
- package/dist/storage/domains/integrations/base.d.ts +126 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -0
- package/dist/storage/domains/integrations/base.js +317 -0
- package/dist/storage/domains/integrations/base.js.map +1 -0
- package/dist/storage/domains/memory-settings/base.d.ts +62 -0
- package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
- package/dist/storage/domains/memory-settings/base.js +126 -0
- package/dist/storage/domains/memory-settings/base.js.map +1 -0
- package/dist/storage/domains/model-packs/base.d.ts +49 -0
- package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
- package/dist/storage/domains/model-packs/base.js +96 -0
- package/dist/storage/domains/model-packs/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +58 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -0
- package/dist/storage/domains/projects/base.js +111 -0
- package/dist/storage/domains/projects/base.js.map +1 -0
- package/dist/storage/domains/queue-health/base.d.ts +52 -0
- package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
- package/dist/storage/domains/queue-health/base.js +122 -0
- package/dist/storage/domains/queue-health/base.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +358 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -0
- package/dist/storage/domains/source-control/base.js +908 -0
- package/dist/storage/domains/source-control/base.js.map +1 -0
- package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
- package/dist/storage/domains/source-control/inmemory.js +377 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -0
- package/dist/storage/domains/work-items/base.d.ts +412 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -0
- package/dist/storage/domains/work-items/base.js +1309 -0
- package/dist/storage/domains/work-items/base.js.map +1 -0
- package/dist/storage/domains/work-items/metrics.d.ts +101 -0
- package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
- package/dist/storage/domains/work-items/metrics.js +221 -0
- package/dist/storage/domains/work-items/metrics.js.map +1 -0
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +24 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +286 -0
- package/dist/workspace.js.map +1 -0
- package/factory-skills/configure-factory-rules/SKILL.md +67 -0
- package/factory-skills/factory-plan/SKILL.md +61 -0
- package/factory-skills/factory-review/SKILL.md +155 -0
- package/factory-skills/factory-triage/SKILL.md +77 -0
- package/package.json +81 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
2
|
+
//#region src/storage/domains/audit/domain.ts
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const MAX_ACTION_FILTERS = 16;
|
|
5
|
+
function loose(c) {
|
|
6
|
+
return c;
|
|
7
|
+
}
|
|
8
|
+
function parseActionsParam(raw) {
|
|
9
|
+
if (!raw) return void 0;
|
|
10
|
+
const actions = raw.split(",").map((action) => action.trim()).filter(Boolean).slice(0, MAX_ACTION_FILTERS);
|
|
11
|
+
return actions.length > 0 ? actions : void 0;
|
|
12
|
+
}
|
|
13
|
+
function parseLimitParam(raw) {
|
|
14
|
+
if (!raw) return void 0;
|
|
15
|
+
const limit = Number.parseInt(raw, 10);
|
|
16
|
+
return Number.isFinite(limit) ? limit : void 0;
|
|
17
|
+
}
|
|
18
|
+
function auditRequestContext(c) {
|
|
19
|
+
const location = c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
|
|
20
|
+
const userAgent = c.req.header("user-agent");
|
|
21
|
+
return {
|
|
22
|
+
...location ? { location } : {},
|
|
23
|
+
...userAgent ? { userAgent } : {}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Factory-owned audit behavior backed by the audit storage domain. */
|
|
27
|
+
var AuditDomain = class {
|
|
28
|
+
#auth;
|
|
29
|
+
#audit;
|
|
30
|
+
#projects;
|
|
31
|
+
#sinks;
|
|
32
|
+
#agentTenant;
|
|
33
|
+
constructor({ auth, audit, projects, sinks = [], agentTenant }) {
|
|
34
|
+
this.#auth = auth;
|
|
35
|
+
this.#audit = audit;
|
|
36
|
+
this.#projects = projects;
|
|
37
|
+
this.#sinks = sinks;
|
|
38
|
+
this.#agentTenant = agentTenant;
|
|
39
|
+
const ids = /* @__PURE__ */ new Set();
|
|
40
|
+
for (const sink of this.#sinks) {
|
|
41
|
+
if (!sink.id) throw new Error("Audit integration id must not be empty");
|
|
42
|
+
if (ids.has(sink.id)) throw new Error(`Duplicate audit integration id '${sink.id}'`);
|
|
43
|
+
ids.add(sink.id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async record(input) {
|
|
47
|
+
try {
|
|
48
|
+
await this.#audit.ensureReady();
|
|
49
|
+
const row = await this.#audit.record(input);
|
|
50
|
+
for (const sink of this.#sinks) {
|
|
51
|
+
if (!sink.audit) continue;
|
|
52
|
+
Promise.resolve().then(() => sink.audit?.({ event: row })).catch((err) => {
|
|
53
|
+
console.warn("[Audit] Audit integration failed", {
|
|
54
|
+
integration: sink.id,
|
|
55
|
+
action: row.action,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err)
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return row;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.warn("[Audit] Failed to record audit event", {
|
|
63
|
+
action: input.action,
|
|
64
|
+
error: err instanceof Error ? err.message : String(err)
|
|
65
|
+
});
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async list(input) {
|
|
70
|
+
await this.#audit.ensureReady();
|
|
71
|
+
return this.#audit.list(input);
|
|
72
|
+
}
|
|
73
|
+
async emit({ context, input }) {
|
|
74
|
+
try {
|
|
75
|
+
const tenant = this.#auth.tenant(context);
|
|
76
|
+
if (!tenant?.orgId) return;
|
|
77
|
+
await this.record({
|
|
78
|
+
orgId: tenant.orgId,
|
|
79
|
+
actorId: tenant.userId,
|
|
80
|
+
action: input.action,
|
|
81
|
+
targets: input.targets,
|
|
82
|
+
metadata: input.metadata,
|
|
83
|
+
factoryProjectId: input.factoryProjectId,
|
|
84
|
+
projectRepositoryId: input.projectRepositoryId,
|
|
85
|
+
context: auditRequestContext(context)
|
|
86
|
+
});
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.warn("[Audit] Failed to emit audit event", {
|
|
89
|
+
action: input.action,
|
|
90
|
+
error: err instanceof Error ? err.message : String(err)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async emitAgent({ requestContext, input }) {
|
|
95
|
+
try {
|
|
96
|
+
const context = requestContext.get("controller");
|
|
97
|
+
const tenant = this.#agentTenant?.(requestContext);
|
|
98
|
+
const orgId = tenant?.orgId;
|
|
99
|
+
const userId = tenant?.userId;
|
|
100
|
+
const threadId = context?.threadId;
|
|
101
|
+
const state = context?.getState();
|
|
102
|
+
if (!orgId || !userId || !threadId || !state?.factoryProjectId) return;
|
|
103
|
+
await this.record({
|
|
104
|
+
orgId,
|
|
105
|
+
actorId: `agent:${threadId}`,
|
|
106
|
+
actorType: "agent",
|
|
107
|
+
action: input.action,
|
|
108
|
+
targets: input.targets,
|
|
109
|
+
metadata: {
|
|
110
|
+
...input.metadata,
|
|
111
|
+
startedBy: userId
|
|
112
|
+
},
|
|
113
|
+
factoryProjectId: state.factoryProjectId,
|
|
114
|
+
projectRepositoryId: state.projectRepositoryId,
|
|
115
|
+
context: {}
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.warn("[Audit] Failed to emit agent audit event", {
|
|
119
|
+
action: input.action,
|
|
120
|
+
error: err instanceof Error ? err.message : String(err)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
routes() {
|
|
125
|
+
return [registerApiRoute("/web/factory/projects/:id/audit", {
|
|
126
|
+
method: "GET",
|
|
127
|
+
handler: async (cc) => {
|
|
128
|
+
const c = loose(cc);
|
|
129
|
+
const tenant = await this.#resolveTenant(c);
|
|
130
|
+
if ("response" in tenant) return tenant.response;
|
|
131
|
+
const projectId = c.req.param("id");
|
|
132
|
+
if (!projectId || !UUID_RE.test(projectId)) return c.json({ error: "Project not found" }, 404);
|
|
133
|
+
await this.#projects.ensureReady();
|
|
134
|
+
if (!await this.#projects.get({
|
|
135
|
+
orgId: tenant.orgId,
|
|
136
|
+
id: projectId
|
|
137
|
+
})) return c.json({ error: "Project not found" }, 404);
|
|
138
|
+
const page = await this.list({
|
|
139
|
+
orgId: tenant.orgId,
|
|
140
|
+
factoryProjectId: projectId,
|
|
141
|
+
actions: parseActionsParam(c.req.query("actions")),
|
|
142
|
+
actorId: c.req.query("actor") || void 0,
|
|
143
|
+
before: c.req.query("before") || void 0,
|
|
144
|
+
limit: parseLimitParam(c.req.query("limit"))
|
|
145
|
+
});
|
|
146
|
+
return c.json(page);
|
|
147
|
+
}
|
|
148
|
+
})];
|
|
149
|
+
}
|
|
150
|
+
async #resolveTenant(c) {
|
|
151
|
+
await this.#auth.ensureUser(c);
|
|
152
|
+
const tenant = this.#auth.tenant(c);
|
|
153
|
+
if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
|
|
154
|
+
if (!tenant.orgId) return { response: c.json({
|
|
155
|
+
error: "organization_required",
|
|
156
|
+
message: "The audit trail requires an organization."
|
|
157
|
+
}, 403) };
|
|
158
|
+
return {
|
|
159
|
+
orgId: tenant.orgId,
|
|
160
|
+
userId: tenant.userId
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
export { AuditDomain, auditRequestContext };
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.js","names":["#auth","#audit","#projects","#sinks","#agentTenant","#resolveTenant"],"sources":["../../../../src/storage/domains/audit/domain.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../../routes/route.js';\nimport type { FactoryProjectsStorage } from '../projects/base.js';\nimport type {\n AuditContext,\n AuditEventPage,\n AuditEventRow,\n AuditStorage,\n AuditTarget,\n ListAuditEventsInput,\n RecordAuditEventInput,\n} from './base.js';\n\nexport interface EmitAuditInput {\n action: string;\n factoryProjectId?: string;\n projectRepositoryId?: string;\n targets: AuditTarget[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface EmitAgentAuditInput {\n action: string;\n targets: AuditTarget[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface AuditEmitter {\n emit(args: { context: Context; input: EmitAuditInput }): Promise<void>;\n}\n\nexport interface AuditAgentEmitter {\n emitAgent(args: { requestContext: RequestContext; input: EmitAgentAuditInput }): Promise<void>;\n}\n\n/** Best-effort destination for locally persisted audit events (e.g. an integration's audit log). */\nexport interface AuditSink {\n id: string;\n audit?(args: { event: AuditEventRow }): Promise<void>;\n}\n\ninterface FactorySessionState {\n factoryProjectId?: string;\n projectRepositoryId?: string;\n}\n\nexport interface AuditDomainOptions {\n auth: RouteAuth;\n /** Audit storage domain handle. */\n audit: AuditStorage;\n /** Projects domain handle, used to scope the audit trail route. */\n projects: FactoryProjectsStorage;\n /** Best-effort fan-out destinations notified after each recorded event. */\n sinks?: AuditSink[];\n /** Resolve the acting tenant for agent-emitted events from the request context. */\n agentTenant?: (requestContext: RequestContext) => { orgId?: string; userId?: string } | undefined;\n}\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst MAX_ACTION_FILTERS = 16;\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nfunction parseActionsParam(raw: string | undefined): string[] | undefined {\n if (!raw) return undefined;\n const actions = raw\n .split(',')\n .map(action => action.trim())\n .filter(Boolean)\n .slice(0, MAX_ACTION_FILTERS);\n return actions.length > 0 ? actions : undefined;\n}\n\nfunction parseLimitParam(raw: string | undefined): number | undefined {\n if (!raw) return undefined;\n const limit = Number.parseInt(raw, 10);\n return Number.isFinite(limit) ? limit : undefined;\n}\n\nexport function auditRequestContext(c: Context): AuditContext {\n const location = c.req.header('x-forwarded-for')?.split(',')[0]?.trim();\n const userAgent = c.req.header('user-agent');\n return {\n ...(location ? { location } : {}),\n ...(userAgent ? { userAgent } : {}),\n };\n}\n\n/** Factory-owned audit behavior backed by the audit storage domain. */\nexport class AuditDomain implements AuditEmitter, AuditAgentEmitter {\n readonly #auth: RouteAuth;\n readonly #audit: AuditStorage;\n readonly #projects: FactoryProjectsStorage;\n readonly #sinks: AuditSink[];\n readonly #agentTenant: AuditDomainOptions['agentTenant'];\n\n constructor({ auth, audit, projects, sinks = [], agentTenant }: AuditDomainOptions) {\n this.#auth = auth;\n this.#audit = audit;\n this.#projects = projects;\n this.#sinks = sinks;\n this.#agentTenant = agentTenant;\n\n const ids = new Set<string>();\n for (const sink of this.#sinks) {\n if (!sink.id) throw new Error('Audit integration id must not be empty');\n if (ids.has(sink.id)) throw new Error(`Duplicate audit integration id '${sink.id}'`);\n ids.add(sink.id);\n }\n }\n\n async record(input: RecordAuditEventInput): Promise<AuditEventRow | null> {\n try {\n await this.#audit.ensureReady();\n const row = await this.#audit.record(input);\n for (const sink of this.#sinks) {\n if (!sink.audit) continue;\n void Promise.resolve()\n .then(() => sink.audit?.({ event: row }))\n .catch(err => {\n console.warn('[Audit] Audit integration failed', {\n integration: sink.id,\n action: row.action,\n error: err instanceof Error ? err.message : String(err),\n });\n });\n }\n return row;\n } catch (err) {\n console.warn('[Audit] Failed to record audit event', {\n action: input.action,\n error: err instanceof Error ? err.message : String(err),\n });\n return null;\n }\n }\n\n async list(input: ListAuditEventsInput): Promise<AuditEventPage> {\n await this.#audit.ensureReady();\n return this.#audit.list(input);\n }\n\n async emit({ context, input }: { context: Context; input: EmitAuditInput }): Promise<void> {\n try {\n const tenant = this.#auth.tenant(context);\n if (!tenant?.orgId) return;\n await this.record({\n orgId: tenant.orgId,\n actorId: tenant.userId,\n action: input.action,\n targets: input.targets,\n metadata: input.metadata,\n factoryProjectId: input.factoryProjectId,\n projectRepositoryId: input.projectRepositoryId,\n context: auditRequestContext(context),\n });\n } catch (err) {\n console.warn('[Audit] Failed to emit audit event', {\n action: input.action,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n async emitAgent({\n requestContext,\n input,\n }: {\n requestContext: RequestContext;\n input: EmitAgentAuditInput;\n }): Promise<void> {\n try {\n const context = requestContext.get('controller') as\n | AgentControllerRequestContext<FactorySessionState>\n | undefined;\n const tenant = this.#agentTenant?.(requestContext);\n const orgId = tenant?.orgId;\n const userId = tenant?.userId;\n const threadId = context?.threadId;\n const state = context?.getState();\n if (!orgId || !userId || !threadId || !state?.factoryProjectId) return;\n\n await this.record({\n orgId,\n actorId: `agent:${threadId}`,\n actorType: 'agent',\n action: input.action,\n targets: input.targets,\n metadata: { ...input.metadata, startedBy: userId },\n factoryProjectId: state.factoryProjectId,\n projectRepositoryId: state.projectRepositoryId,\n context: {},\n });\n } catch (err) {\n console.warn('[Audit] Failed to emit agent audit event', {\n action: input.action,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n routes(): ApiRoute[] {\n return [\n registerApiRoute('/web/factory/projects/:id/audit', {\n method: 'GET',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await this.#resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const projectId = c.req.param('id');\n if (!projectId || !UUID_RE.test(projectId)) return c.json({ error: 'Project not found' }, 404);\n await this.#projects.ensureReady();\n const project = await this.#projects.get({ orgId: tenant.orgId, id: projectId });\n if (!project) return c.json({ error: 'Project not found' }, 404);\n\n const page = await this.list({\n orgId: tenant.orgId,\n factoryProjectId: projectId,\n actions: parseActionsParam(c.req.query('actions')),\n actorId: c.req.query('actor') || undefined,\n before: c.req.query('before') || undefined,\n limit: parseLimitParam(c.req.query('limit')),\n });\n return c.json(page);\n },\n }),\n ];\n }\n\n async #resolveTenant(c: Context): Promise<{ orgId: string; userId: string } | { response: Response }> {\n await this.#auth.ensureUser(c);\n const tenant = this.#auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json({ error: 'organization_required', message: 'The audit trail requires an organization.' }, 403),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { FactoryStorageDomain } from '@mastra/core/storage';
|
|
2
|
+
import type { CollectionSchema } from '@mastra/core/storage';
|
|
3
|
+
/**
|
|
4
|
+
* A reverse index from a platform sender identity to a Mastra tenant.
|
|
5
|
+
*
|
|
6
|
+
* Inbound channel events (a Slack DM, a Discord mention, ...) carry only the
|
|
7
|
+
* platform-side sender ids — there is no Mastra `(orgId, userId)` on the event.
|
|
8
|
+
* Model credentials, however, resolve **per-tenant**, so a channel run cannot
|
|
9
|
+
* load the sender's stored model credentials until we know which tenant the
|
|
10
|
+
* sender is. This domain holds that mapping: an authenticated web user links
|
|
11
|
+
* their platform account, and the dispatch seam reads it back to stamp the
|
|
12
|
+
* tenant onto the run's request context.
|
|
13
|
+
*
|
|
14
|
+
* One row per `(platform, external_team_id, external_user_id)` — the same
|
|
15
|
+
* platform user in two workspaces (two team ids) is two independent links, so
|
|
16
|
+
* multi-workspace falls out of the key. `platform` is generic (`'slack'` today,
|
|
17
|
+
* reusable for Discord/Telegram later). The `external_` prefix disambiguates
|
|
18
|
+
* the platform-side ids from the tenant ids (`org_id` / `user_id`) that cohabit
|
|
19
|
+
* the row.
|
|
20
|
+
*/
|
|
21
|
+
export interface ChannelAccountLink {
|
|
22
|
+
/** Undefined for personal accounts (no organization). */
|
|
23
|
+
orgId?: string;
|
|
24
|
+
userId: string;
|
|
25
|
+
/**
|
|
26
|
+
* Which Factory project this sender's channel runs route to. Unset until
|
|
27
|
+
* the user picks one (or the dispatch path auto-stamps their only factory).
|
|
28
|
+
*/
|
|
29
|
+
defaultFactoryProjectId?: string;
|
|
30
|
+
linkedAt: Date;
|
|
31
|
+
}
|
|
32
|
+
/** The reverse-lookup key: a platform sender identity. */
|
|
33
|
+
export interface ChannelAccountLinkKey {
|
|
34
|
+
platform: string;
|
|
35
|
+
externalTeamId: string;
|
|
36
|
+
externalUserId: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Human-readable labels for a linked identity, captured at link time (e.g.
|
|
40
|
+
* from Slack OIDC id_token claims). Display-only — never part of the key.
|
|
41
|
+
*/
|
|
42
|
+
export interface ChannelAccountLinkNames {
|
|
43
|
+
externalTeamName?: string;
|
|
44
|
+
externalUserName?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare const CHANNEL_ACCOUNT_LINKS_SCHEMA: CollectionSchema;
|
|
47
|
+
/** A link row with its platform sender key, as listed for a tenant user. */
|
|
48
|
+
export interface ChannelAccountLinkEntry extends ChannelAccountLink, ChannelAccountLinkKey, ChannelAccountLinkNames {
|
|
49
|
+
}
|
|
50
|
+
export declare class ChannelIdentityStorage extends FactoryStorageDomain {
|
|
51
|
+
#private;
|
|
52
|
+
constructor();
|
|
53
|
+
init(): Promise<void>;
|
|
54
|
+
dangerouslyClearAll(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Bind a platform sender identity to a Mastra tenant. Last-write-wins: a
|
|
57
|
+
* re-link (e.g. the same Slack user connecting a different tenant) replaces
|
|
58
|
+
* the stored tenant and refreshes `linkedAt`.
|
|
59
|
+
*/
|
|
60
|
+
saveAccountLink({ platform, externalTeamId, externalUserId, orgId, userId, externalTeamName, externalUserName, }: ChannelAccountLinkKey & ChannelAccountLinkNames & {
|
|
61
|
+
orgId?: string;
|
|
62
|
+
userId: string;
|
|
63
|
+
}): Promise<ChannelAccountLink>;
|
|
64
|
+
/**
|
|
65
|
+
* Set (or clear, with `null`) which Factory project a link's channel runs
|
|
66
|
+
* route to. The `userId` guard makes this self-service only — a caller can
|
|
67
|
+
* never repoint another tenant's link. Returns whether a row was updated.
|
|
68
|
+
*
|
|
69
|
+
* Note: `saveAccountLink` deliberately omits this column from its upsert
|
|
70
|
+
* row, so a re-link keeps the stored default.
|
|
71
|
+
*/
|
|
72
|
+
setDefaultFactory({ platform, externalTeamId, externalUserId, userId, factoryProjectId, }: ChannelAccountLinkKey & {
|
|
73
|
+
userId: string;
|
|
74
|
+
factoryProjectId: string | null;
|
|
75
|
+
}): Promise<boolean>;
|
|
76
|
+
/** Resolve the tenant a platform sender is linked to, or `null` if unlinked. */
|
|
77
|
+
getAccountLink({ platform, externalTeamId, externalUserId, }: ChannelAccountLinkKey): Promise<ChannelAccountLink | null>;
|
|
78
|
+
/** Remove a platform sender's link. Returns whether a row was deleted. */
|
|
79
|
+
deleteAccountLink({ platform, externalTeamId, externalUserId }: ChannelAccountLinkKey): Promise<boolean>;
|
|
80
|
+
/**
|
|
81
|
+
* All platform identities linked to a tenant user, for the "Connected
|
|
82
|
+
* accounts" settings surface. Keyed by the tenant `userId` alone — a link
|
|
83
|
+
* belongs to the person, and their personal/org context at link time does
|
|
84
|
+
* not change who may see or sever it.
|
|
85
|
+
*/
|
|
86
|
+
listAccountLinksForUser(userId: string): Promise<ChannelAccountLinkEntry[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Remove one of the tenant user's own links, addressed by its sender key.
|
|
89
|
+
* The `userId` guard makes the delete self-service only: a caller can never
|
|
90
|
+
* sever another tenant's link even with a known sender key.
|
|
91
|
+
*/
|
|
92
|
+
deleteAccountLinkForUser({ userId, platform, externalTeamId, externalUserId, }: ChannelAccountLinkKey & {
|
|
93
|
+
userId: string;
|
|
94
|
+
}): Promise<boolean>;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/channel-identity/base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAqB,MAAM,sBAAsB,CAAC;AAEhF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,EAAE,IAAI,CAAC;CAChB;AAED,0DAA0D;AAC1D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,4BAA4B,EAAE,gBAoB1C,CAAC;AAwBF,4EAA4E;AAC5E,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB,EAAE,qBAAqB,EAAE,uBAAuB;CAAG;AAatH,qBAAa,sBAAuB,SAAQ,oBAAoB;;;IAKxD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ1C;;;;OAIG;IACG,eAAe,CAAC,EACpB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,KAAK,EACL,MAAM,EACN,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,qBAAqB,GACtB,uBAAuB,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAkB5F;;;;;;;OAOG;IACG,iBAAiB,CAAC,EACtB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,MAAM,EACN,gBAAgB,GACjB,EAAE,qBAAqB,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAcjG,gFAAgF;IAC1E,cAAc,CAAC,EACnB,QAAQ,EACR,cAAc,EACd,cAAc,GACf,EAAE,qBAAqB,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAS7D,0EAA0E;IACpE,iBAAiB,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC;IAS9G;;;;;OAKG;IACG,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAKjF;;;;OAIG;IACG,wBAAwB,CAAC,EAC7B,MAAM,EACN,QAAQ,EACR,cAAc,EACd,cAAc,GACf,EAAE,qBAAqB,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CASjE"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { FactoryStorageDomain } from "@mastra/core/storage";
|
|
2
|
+
//#region src/storage/domains/channel-identity/base.ts
|
|
3
|
+
const CHANNEL_ACCOUNT_LINKS_SCHEMA = {
|
|
4
|
+
name: "channel_account_links",
|
|
5
|
+
columns: {
|
|
6
|
+
id: { type: "uuid-pk" },
|
|
7
|
+
platform: { type: "text" },
|
|
8
|
+
external_team_id: { type: "text" },
|
|
9
|
+
external_user_id: { type: "text" },
|
|
10
|
+
org_id: {
|
|
11
|
+
type: "text",
|
|
12
|
+
nullable: true
|
|
13
|
+
},
|
|
14
|
+
user_id: { type: "text" },
|
|
15
|
+
external_team_name: {
|
|
16
|
+
type: "text",
|
|
17
|
+
nullable: true
|
|
18
|
+
},
|
|
19
|
+
external_user_name: {
|
|
20
|
+
type: "text",
|
|
21
|
+
nullable: true
|
|
22
|
+
},
|
|
23
|
+
default_factory_project_id: {
|
|
24
|
+
type: "text",
|
|
25
|
+
nullable: true
|
|
26
|
+
},
|
|
27
|
+
linked_at: { type: "timestamp" }
|
|
28
|
+
},
|
|
29
|
+
uniqueIndexes: [{
|
|
30
|
+
name: "channel_account_links_sender_key",
|
|
31
|
+
columns: [
|
|
32
|
+
"platform",
|
|
33
|
+
"external_team_id",
|
|
34
|
+
"external_user_id"
|
|
35
|
+
]
|
|
36
|
+
}]
|
|
37
|
+
};
|
|
38
|
+
function toLink(row) {
|
|
39
|
+
return {
|
|
40
|
+
...row.org_id ? { orgId: row.org_id } : {},
|
|
41
|
+
userId: row.user_id,
|
|
42
|
+
...row.default_factory_project_id ? { defaultFactoryProjectId: row.default_factory_project_id } : {},
|
|
43
|
+
linkedAt: row.linked_at
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function toEntry(row) {
|
|
47
|
+
return {
|
|
48
|
+
...toLink(row),
|
|
49
|
+
platform: row.platform,
|
|
50
|
+
externalTeamId: row.external_team_id,
|
|
51
|
+
externalUserId: row.external_user_id,
|
|
52
|
+
...row.external_team_name ? { externalTeamName: row.external_team_name } : {},
|
|
53
|
+
...row.external_user_name ? { externalUserName: row.external_user_name } : {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var ChannelIdentityStorage = class extends FactoryStorageDomain {
|
|
57
|
+
constructor() {
|
|
58
|
+
super("channel-identity");
|
|
59
|
+
}
|
|
60
|
+
async init() {
|
|
61
|
+
await this.ensureCollections([CHANNEL_ACCOUNT_LINKS_SCHEMA]);
|
|
62
|
+
}
|
|
63
|
+
async dangerouslyClearAll() {
|
|
64
|
+
await this.ops.deleteMany("channel_account_links", {});
|
|
65
|
+
}
|
|
66
|
+
get #db() {
|
|
67
|
+
return this.ops;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Bind a platform sender identity to a Mastra tenant. Last-write-wins: a
|
|
71
|
+
* re-link (e.g. the same Slack user connecting a different tenant) replaces
|
|
72
|
+
* the stored tenant and refreshes `linkedAt`.
|
|
73
|
+
*/
|
|
74
|
+
async saveAccountLink({ platform, externalTeamId, externalUserId, orgId, userId, externalTeamName, externalUserName }) {
|
|
75
|
+
return toLink(await this.#db.upsertOne("channel_account_links", [
|
|
76
|
+
"platform",
|
|
77
|
+
"external_team_id",
|
|
78
|
+
"external_user_id"
|
|
79
|
+
], {
|
|
80
|
+
platform,
|
|
81
|
+
external_team_id: externalTeamId,
|
|
82
|
+
external_user_id: externalUserId,
|
|
83
|
+
org_id: orgId ?? null,
|
|
84
|
+
user_id: userId,
|
|
85
|
+
external_team_name: externalTeamName ?? null,
|
|
86
|
+
external_user_name: externalUserName ?? null,
|
|
87
|
+
linked_at: /* @__PURE__ */ new Date()
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Set (or clear, with `null`) which Factory project a link's channel runs
|
|
92
|
+
* route to. The `userId` guard makes this self-service only — a caller can
|
|
93
|
+
* never repoint another tenant's link. Returns whether a row was updated.
|
|
94
|
+
*
|
|
95
|
+
* Note: `saveAccountLink` deliberately omits this column from its upsert
|
|
96
|
+
* row, so a re-link keeps the stored default.
|
|
97
|
+
*/
|
|
98
|
+
async setDefaultFactory({ platform, externalTeamId, externalUserId, userId, factoryProjectId }) {
|
|
99
|
+
return await this.#db.updateMany("channel_account_links", {
|
|
100
|
+
user_id: userId,
|
|
101
|
+
platform,
|
|
102
|
+
external_team_id: externalTeamId,
|
|
103
|
+
external_user_id: externalUserId
|
|
104
|
+
}, { default_factory_project_id: factoryProjectId }) > 0;
|
|
105
|
+
}
|
|
106
|
+
/** Resolve the tenant a platform sender is linked to, or `null` if unlinked. */
|
|
107
|
+
async getAccountLink({ platform, externalTeamId, externalUserId }) {
|
|
108
|
+
const row = await this.#db.findOne("channel_account_links", {
|
|
109
|
+
platform,
|
|
110
|
+
external_team_id: externalTeamId,
|
|
111
|
+
external_user_id: externalUserId
|
|
112
|
+
});
|
|
113
|
+
return row ? toLink(row) : null;
|
|
114
|
+
}
|
|
115
|
+
/** Remove a platform sender's link. Returns whether a row was deleted. */
|
|
116
|
+
async deleteAccountLink({ platform, externalTeamId, externalUserId }) {
|
|
117
|
+
return await this.#db.deleteMany("channel_account_links", {
|
|
118
|
+
platform,
|
|
119
|
+
external_team_id: externalTeamId,
|
|
120
|
+
external_user_id: externalUserId
|
|
121
|
+
}) > 0;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* All platform identities linked to a tenant user, for the "Connected
|
|
125
|
+
* accounts" settings surface. Keyed by the tenant `userId` alone — a link
|
|
126
|
+
* belongs to the person, and their personal/org context at link time does
|
|
127
|
+
* not change who may see or sever it.
|
|
128
|
+
*/
|
|
129
|
+
async listAccountLinksForUser(userId) {
|
|
130
|
+
return (await this.#db.findMany("channel_account_links", { user_id: userId })).map(toEntry);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Remove one of the tenant user's own links, addressed by its sender key.
|
|
134
|
+
* The `userId` guard makes the delete self-service only: a caller can never
|
|
135
|
+
* sever another tenant's link even with a known sender key.
|
|
136
|
+
*/
|
|
137
|
+
async deleteAccountLinkForUser({ userId, platform, externalTeamId, externalUserId }) {
|
|
138
|
+
return await this.#db.deleteMany("channel_account_links", {
|
|
139
|
+
user_id: userId,
|
|
140
|
+
platform,
|
|
141
|
+
external_team_id: externalTeamId,
|
|
142
|
+
external_user_id: externalUserId
|
|
143
|
+
}) > 0;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
//#endregion
|
|
147
|
+
export { CHANNEL_ACCOUNT_LINKS_SCHEMA, ChannelIdentityStorage };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","names":["#db"],"sources":["../../../../src/storage/domains/channel-identity/base.ts"],"sourcesContent":["import { FactoryStorageDomain } from '@mastra/core/storage';\nimport type { CollectionSchema, FactoryStorageOps } from '@mastra/core/storage';\n\n/**\n * A reverse index from a platform sender identity to a Mastra tenant.\n *\n * Inbound channel events (a Slack DM, a Discord mention, ...) carry only the\n * platform-side sender ids — there is no Mastra `(orgId, userId)` on the event.\n * Model credentials, however, resolve **per-tenant**, so a channel run cannot\n * load the sender's stored model credentials until we know which tenant the\n * sender is. This domain holds that mapping: an authenticated web user links\n * their platform account, and the dispatch seam reads it back to stamp the\n * tenant onto the run's request context.\n *\n * One row per `(platform, external_team_id, external_user_id)` — the same\n * platform user in two workspaces (two team ids) is two independent links, so\n * multi-workspace falls out of the key. `platform` is generic (`'slack'` today,\n * reusable for Discord/Telegram later). The `external_` prefix disambiguates\n * the platform-side ids from the tenant ids (`org_id` / `user_id`) that cohabit\n * the row.\n */\nexport interface ChannelAccountLink {\n /** Undefined for personal accounts (no organization). */\n orgId?: string;\n userId: string;\n /**\n * Which Factory project this sender's channel runs route to. Unset until\n * the user picks one (or the dispatch path auto-stamps their only factory).\n */\n defaultFactoryProjectId?: string;\n linkedAt: Date;\n}\n\n/** The reverse-lookup key: a platform sender identity. */\nexport interface ChannelAccountLinkKey {\n platform: string;\n externalTeamId: string;\n externalUserId: string;\n}\n\n/**\n * Human-readable labels for a linked identity, captured at link time (e.g.\n * from Slack OIDC id_token claims). Display-only — never part of the key.\n */\nexport interface ChannelAccountLinkNames {\n externalTeamName?: string;\n externalUserName?: string;\n}\n\nexport const CHANNEL_ACCOUNT_LINKS_SCHEMA: CollectionSchema = {\n name: 'channel_account_links',\n columns: {\n id: { type: 'uuid-pk' },\n platform: { type: 'text' },\n external_team_id: { type: 'text' },\n external_user_id: { type: 'text' },\n org_id: { type: 'text', nullable: true },\n user_id: { type: 'text' },\n external_team_name: { type: 'text', nullable: true },\n external_user_name: { type: 'text', nullable: true },\n default_factory_project_id: { type: 'text', nullable: true },\n linked_at: { type: 'timestamp' },\n },\n uniqueIndexes: [\n {\n name: 'channel_account_links_sender_key',\n columns: ['platform', 'external_team_id', 'external_user_id'],\n },\n ],\n};\n\ninterface ChannelAccountLinkDbRow extends Record<string, unknown> {\n id: string;\n platform: string;\n external_team_id: string;\n external_user_id: string;\n org_id: string | null;\n user_id: string;\n external_team_name: string | null;\n external_user_name: string | null;\n default_factory_project_id: string | null;\n linked_at: Date;\n}\n\nfunction toLink(row: ChannelAccountLinkDbRow): ChannelAccountLink {\n return {\n ...(row.org_id ? { orgId: row.org_id } : {}),\n userId: row.user_id,\n ...(row.default_factory_project_id ? { defaultFactoryProjectId: row.default_factory_project_id } : {}),\n linkedAt: row.linked_at,\n };\n}\n\n/** A link row with its platform sender key, as listed for a tenant user. */\nexport interface ChannelAccountLinkEntry extends ChannelAccountLink, ChannelAccountLinkKey, ChannelAccountLinkNames {}\n\nfunction toEntry(row: ChannelAccountLinkDbRow): ChannelAccountLinkEntry {\n return {\n ...toLink(row),\n platform: row.platform,\n externalTeamId: row.external_team_id,\n externalUserId: row.external_user_id,\n ...(row.external_team_name ? { externalTeamName: row.external_team_name } : {}),\n ...(row.external_user_name ? { externalUserName: row.external_user_name } : {}),\n };\n}\n\nexport class ChannelIdentityStorage extends FactoryStorageDomain {\n constructor() {\n super('channel-identity');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([CHANNEL_ACCOUNT_LINKS_SCHEMA]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('channel_account_links', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n /**\n * Bind a platform sender identity to a Mastra tenant. Last-write-wins: a\n * re-link (e.g. the same Slack user connecting a different tenant) replaces\n * the stored tenant and refreshes `linkedAt`.\n */\n async saveAccountLink({\n platform,\n externalTeamId,\n externalUserId,\n orgId,\n userId,\n externalTeamName,\n externalUserName,\n }: ChannelAccountLinkKey &\n ChannelAccountLinkNames & { orgId?: string; userId: string }): Promise<ChannelAccountLink> {\n const row = await this.#db.upsertOne<ChannelAccountLinkDbRow>(\n 'channel_account_links',\n ['platform', 'external_team_id', 'external_user_id'],\n {\n platform,\n external_team_id: externalTeamId,\n external_user_id: externalUserId,\n org_id: orgId ?? null,\n user_id: userId,\n external_team_name: externalTeamName ?? null,\n external_user_name: externalUserName ?? null,\n linked_at: new Date(),\n },\n );\n return toLink(row);\n }\n\n /**\n * Set (or clear, with `null`) which Factory project a link's channel runs\n * route to. The `userId` guard makes this self-service only — a caller can\n * never repoint another tenant's link. Returns whether a row was updated.\n *\n * Note: `saveAccountLink` deliberately omits this column from its upsert\n * row, so a re-link keeps the stored default.\n */\n async setDefaultFactory({\n platform,\n externalTeamId,\n externalUserId,\n userId,\n factoryProjectId,\n }: ChannelAccountLinkKey & { userId: string; factoryProjectId: string | null }): Promise<boolean> {\n const updated = await this.#db.updateMany(\n 'channel_account_links',\n {\n user_id: userId,\n platform,\n external_team_id: externalTeamId,\n external_user_id: externalUserId,\n },\n { default_factory_project_id: factoryProjectId },\n );\n return updated > 0;\n }\n\n /** Resolve the tenant a platform sender is linked to, or `null` if unlinked. */\n async getAccountLink({\n platform,\n externalTeamId,\n externalUserId,\n }: ChannelAccountLinkKey): Promise<ChannelAccountLink | null> {\n const row = await this.#db.findOne<ChannelAccountLinkDbRow>('channel_account_links', {\n platform,\n external_team_id: externalTeamId,\n external_user_id: externalUserId,\n });\n return row ? toLink(row) : null;\n }\n\n /** Remove a platform sender's link. Returns whether a row was deleted. */\n async deleteAccountLink({ platform, externalTeamId, externalUserId }: ChannelAccountLinkKey): Promise<boolean> {\n const deleted = await this.#db.deleteMany('channel_account_links', {\n platform,\n external_team_id: externalTeamId,\n external_user_id: externalUserId,\n });\n return deleted > 0;\n }\n\n /**\n * All platform identities linked to a tenant user, for the \"Connected\n * accounts\" settings surface. Keyed by the tenant `userId` alone — a link\n * belongs to the person, and their personal/org context at link time does\n * not change who may see or sever it.\n */\n async listAccountLinksForUser(userId: string): Promise<ChannelAccountLinkEntry[]> {\n const rows = await this.#db.findMany<ChannelAccountLinkDbRow>('channel_account_links', { user_id: userId });\n return rows.map(toEntry);\n }\n\n /**\n * Remove one of the tenant user's own links, addressed by its sender key.\n * The `userId` guard makes the delete self-service only: a caller can never\n * sever another tenant's link even with a known sender key.\n */\n async deleteAccountLinkForUser({\n userId,\n platform,\n externalTeamId,\n externalUserId,\n }: ChannelAccountLinkKey & { userId: string }): Promise<boolean> {\n const deleted = await this.#db.deleteMany('channel_account_links', {\n user_id: userId,\n platform,\n external_team_id: externalTeamId,\n external_user_id: externalUserId,\n });\n return deleted > 0;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model provider credentials domain — tenant-scoped OAuth tokens and API keys
|
|
3
|
+
* for model providers (Anthropic/Claude Max, OpenAI Codex, GitHub Copilot,
|
|
4
|
+
* xAI, plus plain API-key providers).
|
|
5
|
+
*
|
|
6
|
+
* Tenancy is two-level:
|
|
7
|
+
* - **user-scoped** rows (`userId` present): personal plan OAuth tokens and
|
|
8
|
+
* personal API keys. OAuth plan tokens are ONLY ever user-scoped — they are
|
|
9
|
+
* personal subscriptions, and sharing one would bill/rate-limit a single
|
|
10
|
+
* account for the whole org.
|
|
11
|
+
* - **org-scoped** rows (`userId` absent): shared API keys set by an org
|
|
12
|
+
* admin, inherited by every member.
|
|
13
|
+
*
|
|
14
|
+
* Resolution order at model-call time is user > org (the caller layers server
|
|
15
|
+
* env vars underneath as a final fallback).
|
|
16
|
+
*
|
|
17
|
+
* Secrets posture: credentials are stored server-side only and never returned
|
|
18
|
+
* to the client (same as `linear_connections.access_token`). Encryption at
|
|
19
|
+
* rest is out of scope, matching the existing posture.
|
|
20
|
+
*
|
|
21
|
+
* The domain also owns `oauth_login_sessions`: pending web login flows
|
|
22
|
+
* (paste-code PKCE verifiers, device-code poll state) persisted server-side so
|
|
23
|
+
* any replica can complete or poll a flow started on another.
|
|
24
|
+
*/
|
|
25
|
+
import type { AuthCredential, OAuthCredential } from '@mastra/code-sdk/auth/types';
|
|
26
|
+
import { FactoryStorageDomain } from '@mastra/core/storage';
|
|
27
|
+
import type { CollectionSchema } from '@mastra/core/storage';
|
|
28
|
+
/** Owning tenant of a credential row. `userId` absent = org-scoped row. */
|
|
29
|
+
export interface CredentialTenant {
|
|
30
|
+
orgId: string;
|
|
31
|
+
userId?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Which tenancy level a credential row lives at. */
|
|
34
|
+
export type CredentialScope = 'user' | 'org';
|
|
35
|
+
/** One stored credential, annotated with its scope. */
|
|
36
|
+
export interface CredentialRecord {
|
|
37
|
+
provider: string;
|
|
38
|
+
scope: CredentialScope;
|
|
39
|
+
credential: AuthCredential;
|
|
40
|
+
updatedAt: Date;
|
|
41
|
+
}
|
|
42
|
+
/** Result of per-caller resolution: the winning credential and its scope. */
|
|
43
|
+
export interface ResolvedCredential {
|
|
44
|
+
provider: string;
|
|
45
|
+
scope: CredentialScope;
|
|
46
|
+
credential: AuthCredential;
|
|
47
|
+
}
|
|
48
|
+
/** Kind of pending web login flow a session row tracks. */
|
|
49
|
+
export type LoginSessionKind = 'paste-code' | 'device-code';
|
|
50
|
+
/** One pending OAuth login flow, persisted so any replica can continue it. */
|
|
51
|
+
export interface LoginSessionRow {
|
|
52
|
+
sessionId: string;
|
|
53
|
+
orgId: string;
|
|
54
|
+
userId: string;
|
|
55
|
+
provider: string;
|
|
56
|
+
kind: LoginSessionKind;
|
|
57
|
+
/** Serialized flow state (PKCE verifier, device-code pending state, ...). */
|
|
58
|
+
pending: Record<string, unknown>;
|
|
59
|
+
expiresAt: Date;
|
|
60
|
+
/** Earliest time the next upstream poll is allowed (device-code flows). */
|
|
61
|
+
nextPollAt: Date | null;
|
|
62
|
+
createdAt: Date;
|
|
63
|
+
}
|
|
64
|
+
export interface CreateLoginSessionInput {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
orgId: string;
|
|
67
|
+
userId: string;
|
|
68
|
+
provider: string;
|
|
69
|
+
kind: LoginSessionKind;
|
|
70
|
+
pending: Record<string, unknown>;
|
|
71
|
+
expiresAt: Date;
|
|
72
|
+
nextPollAt?: Date | null;
|
|
73
|
+
}
|
|
74
|
+
/** Mirror of `AuthStorage.getApiKey()`'s expiry check. */
|
|
75
|
+
export declare function isOAuthCredentialExpired(credential: OAuthCredential, now?: number): boolean;
|
|
76
|
+
/** OAuth plan credentials are personal and must always have an owning user. */
|
|
77
|
+
export declare function assertCredentialScope(tenant: CredentialTenant, credential: AuthCredential): void;
|
|
78
|
+
export declare const MODEL_CREDENTIALS_SCHEMA: CollectionSchema;
|
|
79
|
+
export declare const OAUTH_LOGIN_SESSIONS_SCHEMA: CollectionSchema;
|
|
80
|
+
/**
|
|
81
|
+
* Model-credentials storage, written once against the generic
|
|
82
|
+
* `FactoryStorageOps` surface. `refreshOAuth()` and `claimLoginSession()`
|
|
83
|
+
* ride `updateAtomic` so concurrent replicas serialize instead of
|
|
84
|
+
* invalidating each other's rotating tokens / double-claiming a flow.
|
|
85
|
+
*/
|
|
86
|
+
export declare class ModelCredentialsStorage extends FactoryStorageDomain {
|
|
87
|
+
#private;
|
|
88
|
+
constructor();
|
|
89
|
+
init(): Promise<void>;
|
|
90
|
+
dangerouslyClearAll(): Promise<void>;
|
|
91
|
+
/** Read the tenant's credential for a provider at exactly that scope. */
|
|
92
|
+
getCredential(tenant: CredentialTenant, provider: string): Promise<AuthCredential | undefined>;
|
|
93
|
+
/** Upsert the tenant's credential (`created_at` is preserved on update). */
|
|
94
|
+
setCredential(tenant: CredentialTenant, provider: string, credential: AuthCredential): Promise<void>;
|
|
95
|
+
/** Delete the tenant's credential at exactly that scope. True when a row was removed. */
|
|
96
|
+
removeCredential(tenant: CredentialTenant, provider: string): Promise<boolean>;
|
|
97
|
+
/** All credentials visible to a user: their own rows plus the org's shared rows. */
|
|
98
|
+
listCredentials(orgId: string, userId: string): Promise<CredentialRecord[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the credential a caller should use for a provider: the user's own
|
|
101
|
+
* row wins over the org's shared row.
|
|
102
|
+
*/
|
|
103
|
+
resolveCredential(orgId: string, userId: string, provider: string): Promise<ResolvedCredential | undefined>;
|
|
104
|
+
/**
|
|
105
|
+
* Refresh the tenant's OAuth credential under the backend's atomic
|
|
106
|
+
* read-modify-write so concurrent replicas don't invalidate each other's
|
|
107
|
+
* rotating refresh tokens. The expiry is re-checked under the lock —
|
|
108
|
+
* another replica may have refreshed already, in which case `refreshFn` is
|
|
109
|
+
* skipped and the fresh credential is returned. Returns `undefined` when no
|
|
110
|
+
* OAuth row exists for the tenant.
|
|
111
|
+
*/
|
|
112
|
+
refreshOAuth(tenant: CredentialTenant, provider: string, refreshFn: (current: OAuthCredential) => Promise<OAuthCredential>): Promise<OAuthCredential | undefined>;
|
|
113
|
+
/** Persist a pending login flow started by a web route. */
|
|
114
|
+
createLoginSession(input: CreateLoginSessionInput): Promise<LoginSessionRow>;
|
|
115
|
+
/**
|
|
116
|
+
* Read a pending login flow. An expired session is deleted on read (TTL
|
|
117
|
+
* cleanup) and reported as absent.
|
|
118
|
+
*/
|
|
119
|
+
getLoginSession(sessionId: string): Promise<LoginSessionRow | undefined>;
|
|
120
|
+
/**
|
|
121
|
+
* Atomically claim a due login session for one upstream completion/poll attempt.
|
|
122
|
+
* Returns undefined when the session is missing, expired, owned by another
|
|
123
|
+
* tenant/provider, or already claimed by a concurrent request.
|
|
124
|
+
*/
|
|
125
|
+
claimLoginSession(sessionId: string, owner: Pick<LoginSessionRow, 'orgId' | 'userId' | 'provider' | 'kind'>): Promise<LoginSessionRow | undefined>;
|
|
126
|
+
/** Update a pending flow's serialized state and/or next allowed poll time. */
|
|
127
|
+
touchLoginSession(sessionId: string, updates: {
|
|
128
|
+
pending?: Record<string, unknown>;
|
|
129
|
+
nextPollAt?: Date | null;
|
|
130
|
+
}): Promise<void>;
|
|
131
|
+
/** Remove a pending login flow (completed, cancelled, or failed). */
|
|
132
|
+
deleteLoginSession(sessionId: string): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/credentials/base.ts"],"names":[],"mappings":"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"}
|