@mastra/factory 0.0.0 → 0.1.0-alpha.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.
Files changed (287) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE.md +30 -0
  3. package/dist/auth.d.ts +181 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +370 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/capabilities/connection.d.ts +9 -0
  8. package/dist/capabilities/connection.d.ts.map +1 -0
  9. package/dist/capabilities/connection.js +1 -0
  10. package/dist/capabilities/connection.js.map +1 -0
  11. package/dist/capabilities/intake.d.ts +93 -0
  12. package/dist/capabilities/intake.d.ts.map +1 -0
  13. package/dist/capabilities/intake.js +1 -0
  14. package/dist/capabilities/intake.js.map +1 -0
  15. package/dist/capabilities/version-control.d.ts +239 -0
  16. package/dist/capabilities/version-control.d.ts.map +1 -0
  17. package/dist/capabilities/version-control.js +1 -0
  18. package/dist/capabilities/version-control.js.map +1 -0
  19. package/dist/factory.d.ts +145 -0
  20. package/dist/factory.d.ts.map +1 -0
  21. package/dist/factory.js +38245 -0
  22. package/dist/factory.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +38245 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/integrations/base.d.ts +197 -0
  28. package/dist/integrations/base.d.ts.map +1 -0
  29. package/dist/integrations/base.js +1 -0
  30. package/dist/integrations/base.js.map +1 -0
  31. package/dist/integrations/github/config.d.ts +60 -0
  32. package/dist/integrations/github/config.d.ts.map +1 -0
  33. package/dist/integrations/github/config.js +28 -0
  34. package/dist/integrations/github/config.js.map +1 -0
  35. package/dist/integrations/github/factory-session.d.ts +12 -0
  36. package/dist/integrations/github/factory-session.d.ts.map +1 -0
  37. package/dist/integrations/github/factory-session.js +41 -0
  38. package/dist/integrations/github/factory-session.js.map +1 -0
  39. package/dist/integrations/github/integration.d.ts +191 -0
  40. package/dist/integrations/github/integration.d.ts.map +1 -0
  41. package/dist/integrations/github/integration.js +3001 -0
  42. package/dist/integrations/github/integration.js.map +1 -0
  43. package/dist/integrations/github/issue-triage.d.ts +27 -0
  44. package/dist/integrations/github/issue-triage.d.ts.map +1 -0
  45. package/dist/integrations/github/issue-triage.js +82 -0
  46. package/dist/integrations/github/issue-triage.js.map +1 -0
  47. package/dist/integrations/github/project-lock.d.ts +78 -0
  48. package/dist/integrations/github/project-lock.d.ts.map +1 -0
  49. package/dist/integrations/github/project-lock.js +70 -0
  50. package/dist/integrations/github/project-lock.js.map +1 -0
  51. package/dist/integrations/github/provenance.d.ts +27 -0
  52. package/dist/integrations/github/provenance.d.ts.map +1 -0
  53. package/dist/integrations/github/provenance.js +104 -0
  54. package/dist/integrations/github/provenance.js.map +1 -0
  55. package/dist/integrations/github/routes.d.ts +72 -0
  56. package/dist/integrations/github/routes.d.ts.map +1 -0
  57. package/dist/integrations/github/routes.js +1834 -0
  58. package/dist/integrations/github/routes.js.map +1 -0
  59. package/dist/integrations/github/sandbox.d.ts +268 -0
  60. package/dist/integrations/github/sandbox.d.ts.map +1 -0
  61. package/dist/integrations/github/sandbox.js +381 -0
  62. package/dist/integrations/github/sandbox.js.map +1 -0
  63. package/dist/integrations/github/session-subscriptions.d.ts +23 -0
  64. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
  65. package/dist/integrations/github/session-subscriptions.js +190 -0
  66. package/dist/integrations/github/session-subscriptions.js.map +1 -0
  67. package/dist/integrations/github/subscriptions.d.ts +54 -0
  68. package/dist/integrations/github/subscriptions.d.ts.map +1 -0
  69. package/dist/integrations/github/subscriptions.js +70 -0
  70. package/dist/integrations/github/subscriptions.js.map +1 -0
  71. package/dist/integrations/github/webhook.d.ts +87 -0
  72. package/dist/integrations/github/webhook.d.ts.map +1 -0
  73. package/dist/integrations/github/webhook.js +381 -0
  74. package/dist/integrations/github/webhook.js.map +1 -0
  75. package/dist/integrations/linear/agent-tools.d.ts +34 -0
  76. package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
  77. package/dist/integrations/linear/agent-tools.js +108 -0
  78. package/dist/integrations/linear/agent-tools.js.map +1 -0
  79. package/dist/integrations/linear/integration.d.ts +233 -0
  80. package/dist/integrations/linear/integration.d.ts.map +1 -0
  81. package/dist/integrations/linear/integration.js +962 -0
  82. package/dist/integrations/linear/integration.js.map +1 -0
  83. package/dist/integrations/linear/routes.d.ts +61 -0
  84. package/dist/integrations/linear/routes.d.ts.map +1 -0
  85. package/dist/integrations/linear/routes.js +242 -0
  86. package/dist/integrations/linear/routes.js.map +1 -0
  87. package/dist/integrations/linear/storage.d.ts +65 -0
  88. package/dist/integrations/linear/storage.d.ts.map +1 -0
  89. package/dist/integrations/linear/storage.js +1 -0
  90. package/dist/integrations/linear/storage.js.map +1 -0
  91. package/dist/integrations/platform/api-client.d.ts +24 -0
  92. package/dist/integrations/platform/api-client.d.ts.map +1 -0
  93. package/dist/integrations/platform/api-client.js +153 -0
  94. package/dist/integrations/platform/api-client.js.map +1 -0
  95. package/dist/integrations/platform/github/event-worker.d.ts +44 -0
  96. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
  97. package/dist/integrations/platform/github/event-worker.js +585 -0
  98. package/dist/integrations/platform/github/event-worker.js.map +1 -0
  99. package/dist/integrations/platform/github/integration.d.ts +36 -0
  100. package/dist/integrations/platform/github/integration.d.ts.map +1 -0
  101. package/dist/integrations/platform/github/integration.js +3253 -0
  102. package/dist/integrations/platform/github/integration.js.map +1 -0
  103. package/dist/integrations/platform/linear/integration.d.ts +46 -0
  104. package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
  105. package/dist/integrations/platform/linear/integration.js +891 -0
  106. package/dist/integrations/platform/linear/integration.js.map +1 -0
  107. package/dist/integrations/workos/integration.d.ts +39 -0
  108. package/dist/integrations/workos/integration.d.ts.map +1 -0
  109. package/dist/integrations/workos/integration.js +96 -0
  110. package/dist/integrations/workos/integration.js.map +1 -0
  111. package/dist/routes/config.d.ts +220 -0
  112. package/dist/routes/config.d.ts.map +1 -0
  113. package/dist/routes/config.js +770 -0
  114. package/dist/routes/config.js.map +1 -0
  115. package/dist/routes/fs.d.ts +105 -0
  116. package/dist/routes/fs.d.ts.map +1 -0
  117. package/dist/routes/fs.js +294 -0
  118. package/dist/routes/fs.js.map +1 -0
  119. package/dist/routes/intake.d.ts +23 -0
  120. package/dist/routes/intake.d.ts.map +1 -0
  121. package/dist/routes/intake.js +179 -0
  122. package/dist/routes/intake.js.map +1 -0
  123. package/dist/routes/oauth.d.ts +43 -0
  124. package/dist/routes/oauth.d.ts.map +1 -0
  125. package/dist/routes/oauth.js +25058 -0
  126. package/dist/routes/oauth.js.map +1 -0
  127. package/dist/routes/projects.d.ts +19 -0
  128. package/dist/routes/projects.d.ts.map +1 -0
  129. package/dist/routes/projects.js +395 -0
  130. package/dist/routes/projects.js.map +1 -0
  131. package/dist/routes/provider-credentials.d.ts +81 -0
  132. package/dist/routes/provider-credentials.d.ts.map +1 -0
  133. package/dist/routes/provider-credentials.js +68 -0
  134. package/dist/routes/provider-credentials.js.map +1 -0
  135. package/dist/routes/route.d.ts +48 -0
  136. package/dist/routes/route.d.ts.map +1 -0
  137. package/dist/routes/route.js +11 -0
  138. package/dist/routes/route.js.map +1 -0
  139. package/dist/routes/skills.d.ts +30 -0
  140. package/dist/routes/skills.d.ts.map +1 -0
  141. package/dist/routes/skills.js +181 -0
  142. package/dist/routes/skills.js.map +1 -0
  143. package/dist/routes/surface.d.ts +83 -0
  144. package/dist/routes/surface.d.ts.map +1 -0
  145. package/dist/routes/surface.js +28519 -0
  146. package/dist/routes/surface.js.map +1 -0
  147. package/dist/routes/tenant-credentials.d.ts +74 -0
  148. package/dist/routes/tenant-credentials.d.ts.map +1 -0
  149. package/dist/routes/tenant-credentials.js +181 -0
  150. package/dist/routes/tenant-credentials.js.map +1 -0
  151. package/dist/routes/work-items.d.ts +40 -0
  152. package/dist/routes/work-items.d.ts.map +1 -0
  153. package/dist/routes/work-items.js +902 -0
  154. package/dist/routes/work-items.js.map +1 -0
  155. package/dist/rules/binding-context.d.ts +5 -0
  156. package/dist/rules/binding-context.d.ts.map +1 -0
  157. package/dist/rules/binding-context.js +40 -0
  158. package/dist/rules/binding-context.js.map +1 -0
  159. package/dist/rules/defaults.d.ts +9 -0
  160. package/dist/rules/defaults.d.ts.map +1 -0
  161. package/dist/rules/defaults.js +334 -0
  162. package/dist/rules/defaults.js.map +1 -0
  163. package/dist/rules/dispatcher.d.ts +40 -0
  164. package/dist/rules/dispatcher.d.ts.map +1 -0
  165. package/dist/rules/dispatcher.js +695 -0
  166. package/dist/rules/dispatcher.js.map +1 -0
  167. package/dist/rules/github-service.d.ts +29 -0
  168. package/dist/rules/github-service.d.ts.map +1 -0
  169. package/dist/rules/github-service.js +474 -0
  170. package/dist/rules/github-service.js.map +1 -0
  171. package/dist/rules/index.d.ts +7 -0
  172. package/dist/rules/index.d.ts.map +1 -0
  173. package/dist/rules/index.js +583 -0
  174. package/dist/rules/index.js.map +1 -0
  175. package/dist/rules/linear-service.d.ts +27 -0
  176. package/dist/rules/linear-service.d.ts.map +1 -0
  177. package/dist/rules/linear-service.js +343 -0
  178. package/dist/rules/linear-service.js.map +1 -0
  179. package/dist/rules/processor.d.ts +54 -0
  180. package/dist/rules/processor.d.ts.map +1 -0
  181. package/dist/rules/processor.js +629 -0
  182. package/dist/rules/processor.js.map +1 -0
  183. package/dist/rules/resolve.d.ts +17 -0
  184. package/dist/rules/resolve.d.ts.map +1 -0
  185. package/dist/rules/resolve.js +27 -0
  186. package/dist/rules/resolve.js.map +1 -0
  187. package/dist/rules/start-coordinator.d.ts +58 -0
  188. package/dist/rules/start-coordinator.d.ts.map +1 -0
  189. package/dist/rules/start-coordinator.js +136 -0
  190. package/dist/rules/start-coordinator.js.map +1 -0
  191. package/dist/rules/tools.d.ts +10 -0
  192. package/dist/rules/tools.d.ts.map +1 -0
  193. package/dist/rules/tools.js +93 -0
  194. package/dist/rules/tools.js.map +1 -0
  195. package/dist/rules/transition-service.d.ts +32 -0
  196. package/dist/rules/transition-service.d.ts.map +1 -0
  197. package/dist/rules/transition-service.js +474 -0
  198. package/dist/rules/transition-service.js.map +1 -0
  199. package/dist/rules/types.d.ts +225 -0
  200. package/dist/rules/types.d.ts.map +1 -0
  201. package/dist/rules/types.js +33 -0
  202. package/dist/rules/types.js.map +1 -0
  203. package/dist/rules/validation.d.ts +11 -0
  204. package/dist/rules/validation.d.ts.map +1 -0
  205. package/dist/rules/validation.js +310 -0
  206. package/dist/rules/validation.js.map +1 -0
  207. package/dist/sandbox/fleet.d.ts +208 -0
  208. package/dist/sandbox/fleet.d.ts.map +1 -0
  209. package/dist/sandbox/fleet.js +259 -0
  210. package/dist/sandbox/fleet.js.map +1 -0
  211. package/dist/sandbox/reattach.d.ts +3 -0
  212. package/dist/sandbox/reattach.d.ts.map +1 -0
  213. package/dist/sandbox/reattach.js +9 -0
  214. package/dist/sandbox/reattach.js.map +1 -0
  215. package/dist/server-error.d.ts +12 -0
  216. package/dist/server-error.d.ts.map +1 -0
  217. package/dist/server-error.js +20 -0
  218. package/dist/server-error.js.map +1 -0
  219. package/dist/skills/service.d.ts +48 -0
  220. package/dist/skills/service.d.ts.map +1 -0
  221. package/dist/skills/service.js +45 -0
  222. package/dist/skills/service.js.map +1 -0
  223. package/dist/spa-static.d.ts +34 -0
  224. package/dist/spa-static.d.ts.map +1 -0
  225. package/dist/spa-static.js +73 -0
  226. package/dist/spa-static.js.map +1 -0
  227. package/dist/state-signing.d.ts +48 -0
  228. package/dist/state-signing.d.ts.map +1 -0
  229. package/dist/state-signing.js +48 -0
  230. package/dist/state-signing.js.map +1 -0
  231. package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
  232. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
  233. package/dist/storage/domains/audit/agent-audit.js +65 -0
  234. package/dist/storage/domains/audit/agent-audit.js.map +1 -0
  235. package/dist/storage/domains/audit/base.d.ts +140 -0
  236. package/dist/storage/domains/audit/base.d.ts.map +1 -0
  237. package/dist/storage/domains/audit/base.js +127 -0
  238. package/dist/storage/domains/audit/base.js.map +1 -0
  239. package/dist/storage/domains/audit/domain.d.ts +69 -0
  240. package/dist/storage/domains/audit/domain.d.ts.map +1 -0
  241. package/dist/storage/domains/audit/domain.js +165 -0
  242. package/dist/storage/domains/audit/domain.js.map +1 -0
  243. package/dist/storage/domains/credentials/base.d.ts +134 -0
  244. package/dist/storage/domains/credentials/base.d.ts.map +1 -0
  245. package/dist/storage/domains/credentials/base.js +257 -0
  246. package/dist/storage/domains/credentials/base.js.map +1 -0
  247. package/dist/storage/domains/intake/base.d.ts +38 -0
  248. package/dist/storage/domains/intake/base.d.ts.map +1 -0
  249. package/dist/storage/domains/intake/base.js +62 -0
  250. package/dist/storage/domains/intake/base.js.map +1 -0
  251. package/dist/storage/domains/integrations/base.d.ts +126 -0
  252. package/dist/storage/domains/integrations/base.d.ts.map +1 -0
  253. package/dist/storage/domains/integrations/base.js +252 -0
  254. package/dist/storage/domains/integrations/base.js.map +1 -0
  255. package/dist/storage/domains/model-packs/base.d.ts +49 -0
  256. package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
  257. package/dist/storage/domains/model-packs/base.js +95 -0
  258. package/dist/storage/domains/model-packs/base.js.map +1 -0
  259. package/dist/storage/domains/projects/base.d.ts +55 -0
  260. package/dist/storage/domains/projects/base.d.ts.map +1 -0
  261. package/dist/storage/domains/projects/base.js +99 -0
  262. package/dist/storage/domains/projects/base.js.map +1 -0
  263. package/dist/storage/domains/queue-health/base.d.ts +52 -0
  264. package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
  265. package/dist/storage/domains/queue-health/base.js +87 -0
  266. package/dist/storage/domains/queue-health/base.js.map +1 -0
  267. package/dist/storage/domains/source-control/base.d.ts +288 -0
  268. package/dist/storage/domains/source-control/base.d.ts.map +1 -0
  269. package/dist/storage/domains/source-control/base.js +722 -0
  270. package/dist/storage/domains/source-control/base.js.map +1 -0
  271. package/dist/storage/domains/source-control/inmemory.d.ts +158 -0
  272. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
  273. package/dist/storage/domains/source-control/inmemory.js +348 -0
  274. package/dist/storage/domains/source-control/inmemory.js.map +1 -0
  275. package/dist/storage/domains/work-items/base.d.ts +412 -0
  276. package/dist/storage/domains/work-items/base.d.ts.map +1 -0
  277. package/dist/storage/domains/work-items/base.js +1174 -0
  278. package/dist/storage/domains/work-items/base.js.map +1 -0
  279. package/dist/storage/domains/work-items/metrics.d.ts +101 -0
  280. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
  281. package/dist/storage/domains/work-items/metrics.js +203 -0
  282. package/dist/storage/domains/work-items/metrics.js.map +1 -0
  283. package/dist/workspace.d.ts +20 -0
  284. package/dist/workspace.d.ts.map +1 -0
  285. package/dist/workspace.js +364 -0
  286. package/dist/workspace.js.map +1 -0
  287. package/package.json +19 -19
@@ -0,0 +1,165 @@
1
+ // src/storage/domains/audit/domain.ts
2
+ import { registerApiRoute } from "@mastra/core/server";
3
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
+ var 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
+ var AuditDomain = class {
27
+ #auth;
28
+ #audit;
29
+ #projects;
30
+ #sinks;
31
+ #agentTenant;
32
+ constructor({ auth, audit, projects, sinks = [], agentTenant }) {
33
+ this.#auth = auth;
34
+ this.#audit = audit;
35
+ this.#projects = projects;
36
+ this.#sinks = sinks;
37
+ this.#agentTenant = agentTenant;
38
+ const ids = /* @__PURE__ */ new Set();
39
+ for (const sink of this.#sinks) {
40
+ if (!sink.id) throw new Error("Audit integration id must not be empty");
41
+ if (ids.has(sink.id)) throw new Error(`Duplicate audit integration id '${sink.id}'`);
42
+ ids.add(sink.id);
43
+ }
44
+ }
45
+ async record(input) {
46
+ try {
47
+ await this.#audit.ensureReady();
48
+ const row = await this.#audit.record(input);
49
+ for (const sink of this.#sinks) {
50
+ if (!sink.audit) continue;
51
+ void Promise.resolve().then(() => sink.audit?.({ event: row })).catch((err) => {
52
+ console.warn("[Audit] Audit integration failed", {
53
+ integration: sink.id,
54
+ action: row.action,
55
+ error: err instanceof Error ? err.message : String(err)
56
+ });
57
+ });
58
+ }
59
+ return row;
60
+ } catch (err) {
61
+ console.warn("[Audit] Failed to record audit event", {
62
+ action: input.action,
63
+ error: err instanceof Error ? err.message : String(err)
64
+ });
65
+ return null;
66
+ }
67
+ }
68
+ async list(input) {
69
+ await this.#audit.ensureReady();
70
+ return this.#audit.list(input);
71
+ }
72
+ async emit({ context, input }) {
73
+ try {
74
+ const tenant = this.#auth.tenant(context);
75
+ if (!tenant?.orgId) return;
76
+ await this.record({
77
+ orgId: tenant.orgId,
78
+ actorId: tenant.userId,
79
+ action: input.action,
80
+ targets: input.targets,
81
+ metadata: input.metadata,
82
+ factoryProjectId: input.factoryProjectId,
83
+ projectRepositoryId: input.projectRepositoryId,
84
+ context: auditRequestContext(context)
85
+ });
86
+ } catch (err) {
87
+ console.warn("[Audit] Failed to emit audit event", {
88
+ action: input.action,
89
+ error: err instanceof Error ? err.message : String(err)
90
+ });
91
+ }
92
+ }
93
+ async emitAgent({
94
+ requestContext,
95
+ input
96
+ }) {
97
+ try {
98
+ const context = requestContext.get("controller");
99
+ const tenant = this.#agentTenant?.(requestContext);
100
+ const orgId = tenant?.orgId;
101
+ const userId = tenant?.userId;
102
+ const threadId = context?.threadId;
103
+ const state = context?.getState();
104
+ if (!orgId || !userId || !threadId || !state?.factoryProjectId) return;
105
+ await this.record({
106
+ orgId,
107
+ actorId: `agent:${threadId}`,
108
+ actorType: "agent",
109
+ action: input.action,
110
+ targets: input.targets,
111
+ metadata: { ...input.metadata, startedBy: userId },
112
+ factoryProjectId: state.factoryProjectId,
113
+ projectRepositoryId: state.projectRepositoryId,
114
+ context: {}
115
+ });
116
+ } catch (err) {
117
+ console.warn("[Audit] Failed to emit agent audit event", {
118
+ action: input.action,
119
+ error: err instanceof Error ? err.message : String(err)
120
+ });
121
+ }
122
+ }
123
+ routes() {
124
+ return [
125
+ 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
+ const project = await this.#projects.get({ orgId: tenant.orgId, id: projectId });
135
+ if (!project) return c.json({ error: "Project not found" }, 404);
136
+ const page = await this.list({
137
+ orgId: tenant.orgId,
138
+ factoryProjectId: projectId,
139
+ actions: parseActionsParam(c.req.query("actions")),
140
+ actorId: c.req.query("actor") || void 0,
141
+ before: c.req.query("before") || void 0,
142
+ limit: parseLimitParam(c.req.query("limit"))
143
+ });
144
+ return c.json(page);
145
+ }
146
+ })
147
+ ];
148
+ }
149
+ async #resolveTenant(c) {
150
+ await this.#auth.ensureUser(c);
151
+ const tenant = this.#auth.tenant(c);
152
+ if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
153
+ if (!tenant.orgId) {
154
+ return {
155
+ response: c.json({ error: "organization_required", message: "The audit trail requires an organization." }, 403)
156
+ };
157
+ }
158
+ return { orgId: tenant.orgId, userId: tenant.userId };
159
+ }
160
+ };
161
+ export {
162
+ AuditDomain,
163
+ auditRequestContext
164
+ };
165
+ //# sourceMappingURL=domain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"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> | 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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","names":[]}
@@ -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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"}
@@ -0,0 +1,257 @@
1
+ // src/storage/domains/credentials/base.ts
2
+ import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
3
+ function isOAuthCredentialExpired(credential, now = Date.now()) {
4
+ return now >= credential.expires;
5
+ }
6
+ function assertCredentialScope(tenant, credential) {
7
+ if (credential.type === "oauth" && !tenant.userId) {
8
+ throw new Error("OAuth credentials must be user-scoped");
9
+ }
10
+ }
11
+ var MODEL_CREDENTIALS_SCHEMA = {
12
+ name: "model_provider_credentials",
13
+ columns: {
14
+ id: { type: "uuid-pk" },
15
+ org_id: { type: "text" },
16
+ user_id: { type: "text", nullable: true },
17
+ provider: { type: "text" },
18
+ type: { type: "text" },
19
+ data: { type: "json" },
20
+ created_at: { type: "timestamp" },
21
+ updated_at: { type: "timestamp" }
22
+ },
23
+ uniqueIndexes: [
24
+ // Scope is encoded in `user_id` nullability (NULL = org-scoped shared
25
+ // row), enforced by two partial unique indexes.
26
+ {
27
+ name: "model_provider_credentials_user_unique",
28
+ columns: ["org_id", "user_id", "provider"],
29
+ whereNotNull: "user_id"
30
+ },
31
+ { name: "model_provider_credentials_org_unique", columns: ["org_id", "provider"], whereNull: "user_id" }
32
+ ]
33
+ };
34
+ var OAUTH_LOGIN_SESSIONS_SCHEMA = {
35
+ name: "oauth_login_sessions",
36
+ columns: {
37
+ session_id: { type: "text", primaryKey: true },
38
+ org_id: { type: "text" },
39
+ user_id: { type: "text" },
40
+ provider: { type: "text" },
41
+ kind: { type: "text" },
42
+ pending: { type: "json" },
43
+ expires_at: { type: "timestamp" },
44
+ next_poll_at: { type: "timestamp", nullable: true },
45
+ created_at: { type: "timestamp" }
46
+ }
47
+ };
48
+ function toSessionRow(db) {
49
+ return {
50
+ sessionId: db.session_id,
51
+ orgId: db.org_id,
52
+ userId: db.user_id,
53
+ provider: db.provider,
54
+ kind: db.kind,
55
+ pending: db.pending,
56
+ expiresAt: db.expires_at,
57
+ nextPollAt: db.next_poll_at,
58
+ createdAt: db.created_at
59
+ };
60
+ }
61
+ function tenantWhere(tenant, provider) {
62
+ return { org_id: tenant.orgId, provider, user_id: tenant.userId ?? null };
63
+ }
64
+ var ModelCredentialsStorage = class extends FactoryStorageDomain {
65
+ constructor() {
66
+ super("model-credentials");
67
+ }
68
+ async init() {
69
+ await this.ensureCollections([MODEL_CREDENTIALS_SCHEMA, OAUTH_LOGIN_SESSIONS_SCHEMA]);
70
+ }
71
+ async dangerouslyClearAll() {
72
+ await this.ops.deleteMany("oauth_login_sessions", {});
73
+ await this.ops.deleteMany("model_provider_credentials", {});
74
+ }
75
+ get #db() {
76
+ return this.ops;
77
+ }
78
+ /** Read the tenant's credential for a provider at exactly that scope. */
79
+ async getCredential(tenant, provider) {
80
+ const row = await this.#db.findOne("model_provider_credentials", tenantWhere(tenant, provider));
81
+ return row?.data;
82
+ }
83
+ /** Upsert the tenant's credential (`created_at` is preserved on update). */
84
+ async setCredential(tenant, provider, credential) {
85
+ assertCredentialScope(tenant, credential);
86
+ const where = tenantWhere(tenant, provider);
87
+ const update = async () => this.#db.updateMany("model_provider_credentials", where, {
88
+ type: credential.type,
89
+ data: credential,
90
+ updated_at: /* @__PURE__ */ new Date()
91
+ });
92
+ let lastError;
93
+ for (let attempt = 0; attempt < 2; attempt++) {
94
+ if (await update() > 0) return;
95
+ const now = /* @__PURE__ */ new Date();
96
+ try {
97
+ await this.#db.insertOne("model_provider_credentials", {
98
+ org_id: tenant.orgId,
99
+ user_id: tenant.userId ?? null,
100
+ provider,
101
+ type: credential.type,
102
+ data: credential,
103
+ created_at: now,
104
+ updated_at: now
105
+ });
106
+ return;
107
+ } catch (error) {
108
+ if (!(error instanceof UniqueViolationError)) throw error;
109
+ lastError = error;
110
+ }
111
+ }
112
+ throw lastError;
113
+ }
114
+ /** Delete the tenant's credential at exactly that scope. True when a row was removed. */
115
+ async removeCredential(tenant, provider) {
116
+ const deleted = await this.#db.deleteMany("model_provider_credentials", tenantWhere(tenant, provider));
117
+ return deleted > 0;
118
+ }
119
+ /** All credentials visible to a user: their own rows plus the org's shared rows. */
120
+ async listCredentials(orgId, userId) {
121
+ const [userRows, orgRows] = await Promise.all([
122
+ this.#db.findMany("model_provider_credentials", { org_id: orgId, user_id: userId }),
123
+ this.#db.findMany("model_provider_credentials", { org_id: orgId, user_id: null })
124
+ ]);
125
+ return [
126
+ ...userRows.map((row) => ({
127
+ provider: row.provider,
128
+ scope: "user",
129
+ credential: row.data,
130
+ updatedAt: row.updated_at
131
+ })),
132
+ ...orgRows.map((row) => ({
133
+ provider: row.provider,
134
+ scope: "org",
135
+ credential: row.data,
136
+ updatedAt: row.updated_at
137
+ }))
138
+ ];
139
+ }
140
+ /**
141
+ * Resolve the credential a caller should use for a provider: the user's own
142
+ * row wins over the org's shared row.
143
+ */
144
+ async resolveCredential(orgId, userId, provider) {
145
+ const userRow = await this.#db.findOne("model_provider_credentials", {
146
+ org_id: orgId,
147
+ provider,
148
+ user_id: userId
149
+ });
150
+ if (userRow) return { provider: userRow.provider, scope: "user", credential: userRow.data };
151
+ const orgRow = await this.#db.findOne("model_provider_credentials", {
152
+ org_id: orgId,
153
+ provider,
154
+ user_id: null
155
+ });
156
+ if (orgRow) return { provider: orgRow.provider, scope: "org", credential: orgRow.data };
157
+ return void 0;
158
+ }
159
+ /**
160
+ * Refresh the tenant's OAuth credential under the backend's atomic
161
+ * read-modify-write so concurrent replicas don't invalidate each other's
162
+ * rotating refresh tokens. The expiry is re-checked under the lock —
163
+ * another replica may have refreshed already, in which case `refreshFn` is
164
+ * skipped and the fresh credential is returned. Returns `undefined` when no
165
+ * OAuth row exists for the tenant.
166
+ */
167
+ async refreshOAuth(tenant, provider, refreshFn) {
168
+ let result;
169
+ await this.#db.updateAtomic(
170
+ "model_provider_credentials",
171
+ tenantWhere(tenant, provider),
172
+ async (row) => {
173
+ if (row.data.type !== "oauth") return null;
174
+ const current = row.data;
175
+ if (!isOAuthCredentialExpired(current)) {
176
+ result = current;
177
+ return null;
178
+ }
179
+ const next = await refreshFn(current);
180
+ result = next;
181
+ return { data: next, updated_at: /* @__PURE__ */ new Date() };
182
+ }
183
+ );
184
+ return result;
185
+ }
186
+ /** Persist a pending login flow started by a web route. */
187
+ async createLoginSession(input) {
188
+ const inserted = await this.#db.insertOne("oauth_login_sessions", {
189
+ session_id: input.sessionId,
190
+ org_id: input.orgId,
191
+ user_id: input.userId,
192
+ provider: input.provider,
193
+ kind: input.kind,
194
+ pending: input.pending,
195
+ expires_at: input.expiresAt,
196
+ next_poll_at: input.nextPollAt ?? null,
197
+ created_at: /* @__PURE__ */ new Date()
198
+ });
199
+ return toSessionRow(inserted);
200
+ }
201
+ /**
202
+ * Read a pending login flow. An expired session is deleted on read (TTL
203
+ * cleanup) and reported as absent.
204
+ */
205
+ async getLoginSession(sessionId) {
206
+ const row = await this.#db.findOne("oauth_login_sessions", { session_id: sessionId });
207
+ if (!row) return void 0;
208
+ if (row.expires_at.getTime() <= Date.now()) {
209
+ await this.#db.deleteMany("oauth_login_sessions", { session_id: sessionId });
210
+ return void 0;
211
+ }
212
+ return toSessionRow(row);
213
+ }
214
+ /**
215
+ * Atomically claim a due login session for one upstream completion/poll attempt.
216
+ * Returns undefined when the session is missing, expired, owned by another
217
+ * tenant/provider, or already claimed by a concurrent request.
218
+ */
219
+ async claimLoginSession(sessionId, owner) {
220
+ const now = Date.now();
221
+ let claimed = false;
222
+ const updated = await this.#db.updateAtomic(
223
+ "oauth_login_sessions",
224
+ { session_id: sessionId },
225
+ (row) => {
226
+ if (row.org_id !== owner.orgId || row.user_id !== owner.userId || row.provider !== owner.provider || row.kind !== owner.kind) {
227
+ return null;
228
+ }
229
+ if (row.expires_at.getTime() <= now) return null;
230
+ if (row.next_poll_at && row.next_poll_at.getTime() > now) return null;
231
+ claimed = true;
232
+ return { next_poll_at: row.expires_at };
233
+ }
234
+ );
235
+ return claimed && updated ? toSessionRow(updated) : void 0;
236
+ }
237
+ /** Update a pending flow's serialized state and/or next allowed poll time. */
238
+ async touchLoginSession(sessionId, updates) {
239
+ const set = {};
240
+ if (updates.pending !== void 0) set.pending = updates.pending;
241
+ if (updates.nextPollAt !== void 0) set.next_poll_at = updates.nextPollAt;
242
+ if (Object.keys(set).length === 0) return;
243
+ await this.#db.updateMany("oauth_login_sessions", { session_id: sessionId }, set);
244
+ }
245
+ /** Remove a pending login flow (completed, cancelled, or failed). */
246
+ async deleteLoginSession(sessionId) {
247
+ await this.#db.deleteMany("oauth_login_sessions", { session_id: sessionId });
248
+ }
249
+ };
250
+ export {
251
+ MODEL_CREDENTIALS_SCHEMA,
252
+ ModelCredentialsStorage,
253
+ OAUTH_LOGIN_SESSIONS_SCHEMA,
254
+ assertCredentialScope,
255
+ isOAuthCredentialExpired
256
+ };
257
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/storage/domains/credentials/base.ts"],"sourcesContent":["/**\n * Model provider credentials domain — tenant-scoped OAuth tokens and API keys\n * for model providers (Anthropic/Claude Max, OpenAI Codex, GitHub Copilot,\n * xAI, plus plain API-key providers).\n *\n * Tenancy is two-level:\n * - **user-scoped** rows (`userId` present): personal plan OAuth tokens and\n * personal API keys. OAuth plan tokens are ONLY ever user-scoped — they are\n * personal subscriptions, and sharing one would bill/rate-limit a single\n * account for the whole org.\n * - **org-scoped** rows (`userId` absent): shared API keys set by an org\n * admin, inherited by every member.\n *\n * Resolution order at model-call time is user > org (the caller layers server\n * env vars underneath as a final fallback).\n *\n * Secrets posture: credentials are stored server-side only and never returned\n * to the client (same as `linear_connections.access_token`). Encryption at\n * rest is out of scope, matching the existing posture.\n *\n * The domain also owns `oauth_login_sessions`: pending web login flows\n * (paste-code PKCE verifiers, device-code poll state) persisted server-side so\n * any replica can complete or poll a flow started on another.\n */\n\nimport type { AuthCredential, OAuthCredential } from '@mastra/code-sdk/auth/types';\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, CollectionWhere, FactoryStorageOps } from '@mastra/core/storage';\n\n/** Owning tenant of a credential row. `userId` absent = org-scoped row. */\nexport interface CredentialTenant {\n orgId: string;\n userId?: string;\n}\n\n/** Which tenancy level a credential row lives at. */\nexport type CredentialScope = 'user' | 'org';\n\n/** One stored credential, annotated with its scope. */\nexport interface CredentialRecord {\n provider: string;\n scope: CredentialScope;\n credential: AuthCredential;\n updatedAt: Date;\n}\n\n/** Result of per-caller resolution: the winning credential and its scope. */\nexport interface ResolvedCredential {\n provider: string;\n scope: CredentialScope;\n credential: AuthCredential;\n}\n\n/** Kind of pending web login flow a session row tracks. */\nexport type LoginSessionKind = 'paste-code' | 'device-code';\n\n/** One pending OAuth login flow, persisted so any replica can continue it. */\nexport interface LoginSessionRow {\n sessionId: string;\n orgId: string;\n userId: string;\n provider: string;\n kind: LoginSessionKind;\n /** Serialized flow state (PKCE verifier, device-code pending state, ...). */\n pending: Record<string, unknown>;\n expiresAt: Date;\n /** Earliest time the next upstream poll is allowed (device-code flows). */\n nextPollAt: Date | null;\n createdAt: Date;\n}\n\nexport interface CreateLoginSessionInput {\n sessionId: string;\n orgId: string;\n userId: string;\n provider: string;\n kind: LoginSessionKind;\n pending: Record<string, unknown>;\n expiresAt: Date;\n nextPollAt?: Date | null;\n}\n\n/** Mirror of `AuthStorage.getApiKey()`'s expiry check. */\nexport function isOAuthCredentialExpired(credential: OAuthCredential, now = Date.now()): boolean {\n return now >= credential.expires;\n}\n\n/** OAuth plan credentials are personal and must always have an owning user. */\nexport function assertCredentialScope(tenant: CredentialTenant, credential: AuthCredential): void {\n if (credential.type === 'oauth' && !tenant.userId) {\n throw new Error('OAuth credentials must be user-scoped');\n }\n}\n\nexport const MODEL_CREDENTIALS_SCHEMA: CollectionSchema = {\n name: 'model_provider_credentials',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n user_id: { type: 'text', nullable: true },\n provider: { type: 'text' },\n type: { type: 'text' },\n data: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [\n // Scope is encoded in `user_id` nullability (NULL = org-scoped shared\n // row), enforced by two partial unique indexes.\n {\n name: 'model_provider_credentials_user_unique',\n columns: ['org_id', 'user_id', 'provider'],\n whereNotNull: 'user_id',\n },\n { name: 'model_provider_credentials_org_unique', columns: ['org_id', 'provider'], whereNull: 'user_id' },\n ],\n};\n\nexport const OAUTH_LOGIN_SESSIONS_SCHEMA: CollectionSchema = {\n name: 'oauth_login_sessions',\n columns: {\n session_id: { type: 'text', primaryKey: true },\n org_id: { type: 'text' },\n user_id: { type: 'text' },\n provider: { type: 'text' },\n kind: { type: 'text' },\n pending: { type: 'json' },\n expires_at: { type: 'timestamp' },\n next_poll_at: { type: 'timestamp', nullable: true },\n created_at: { type: 'timestamp' },\n },\n};\n\n/** Column shape of one `model_provider_credentials` row as returned by ops. */\ninterface CredentialDbRow extends Record<string, unknown> {\n id: string;\n provider: string;\n user_id: string | null;\n data: AuthCredential;\n updated_at: Date;\n}\n\n/** Column shape of one `oauth_login_sessions` row as returned by ops. */\ninterface LoginSessionDbRow extends Record<string, unknown> {\n session_id: string;\n org_id: string;\n user_id: string;\n provider: string;\n kind: LoginSessionKind;\n pending: Record<string, unknown>;\n expires_at: Date;\n next_poll_at: Date | null;\n created_at: Date;\n}\n\nfunction toSessionRow(db: LoginSessionDbRow): LoginSessionRow {\n return {\n sessionId: db.session_id,\n orgId: db.org_id,\n userId: db.user_id,\n provider: db.provider,\n kind: db.kind,\n pending: db.pending,\n expiresAt: db.expires_at,\n nextPollAt: db.next_poll_at,\n createdAt: db.created_at,\n };\n}\n\n/** Filter selecting exactly the tenant's row for a provider (`null` = org row). */\nfunction tenantWhere(tenant: CredentialTenant, provider: string): CollectionWhere {\n return { org_id: tenant.orgId, provider, user_id: tenant.userId ?? null };\n}\n\n/**\n * Model-credentials storage, written once against the generic\n * `FactoryStorageOps` surface. `refreshOAuth()` and `claimLoginSession()`\n * ride `updateAtomic` so concurrent replicas serialize instead of\n * invalidating each other's rotating tokens / double-claiming a flow.\n */\nexport class ModelCredentialsStorage extends FactoryStorageDomain {\n constructor() {\n super('model-credentials');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([MODEL_CREDENTIALS_SCHEMA, OAUTH_LOGIN_SESSIONS_SCHEMA]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('oauth_login_sessions', {});\n await this.ops.deleteMany('model_provider_credentials', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n /** Read the tenant's credential for a provider at exactly that scope. */\n async getCredential(tenant: CredentialTenant, provider: string): Promise<AuthCredential | undefined> {\n const row = await this.#db.findOne<CredentialDbRow>('model_provider_credentials', tenantWhere(tenant, provider));\n return row?.data;\n }\n\n /** Upsert the tenant's credential (`created_at` is preserved on update). */\n async setCredential(tenant: CredentialTenant, provider: string, credential: AuthCredential): Promise<void> {\n assertCredentialScope(tenant, credential);\n const where = tenantWhere(tenant, provider);\n const update = async () =>\n this.#db.updateMany('model_provider_credentials', where, {\n type: credential.type,\n data: credential,\n updated_at: new Date(),\n });\n\n let lastError: unknown;\n for (let attempt = 0; attempt < 2; attempt++) {\n if ((await update()) > 0) return;\n\n const now = new Date();\n try {\n await this.#db.insertOne<CredentialDbRow>('model_provider_credentials', {\n org_id: tenant.orgId,\n user_id: tenant.userId ?? null,\n provider,\n type: credential.type,\n data: credential,\n created_at: now,\n updated_at: now,\n });\n return;\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n lastError = error;\n }\n }\n throw lastError;\n }\n\n /** Delete the tenant's credential at exactly that scope. True when a row was removed. */\n async removeCredential(tenant: CredentialTenant, provider: string): Promise<boolean> {\n const deleted = await this.#db.deleteMany('model_provider_credentials', tenantWhere(tenant, provider));\n return deleted > 0;\n }\n\n /** All credentials visible to a user: their own rows plus the org's shared rows. */\n async listCredentials(orgId: string, userId: string): Promise<CredentialRecord[]> {\n // SQL `IN` never matches NULL, so the org rows need their own query.\n const [userRows, orgRows] = await Promise.all([\n this.#db.findMany<CredentialDbRow>('model_provider_credentials', { org_id: orgId, user_id: userId }),\n this.#db.findMany<CredentialDbRow>('model_provider_credentials', { org_id: orgId, user_id: null }),\n ]);\n return [\n ...userRows.map(row => ({\n provider: row.provider,\n scope: 'user' as const,\n credential: row.data,\n updatedAt: row.updated_at,\n })),\n ...orgRows.map(row => ({\n provider: row.provider,\n scope: 'org' as const,\n credential: row.data,\n updatedAt: row.updated_at,\n })),\n ];\n }\n\n /**\n * Resolve the credential a caller should use for a provider: the user's own\n * row wins over the org's shared row.\n */\n async resolveCredential(orgId: string, userId: string, provider: string): Promise<ResolvedCredential | undefined> {\n const userRow = await this.#db.findOne<CredentialDbRow>('model_provider_credentials', {\n org_id: orgId,\n provider,\n user_id: userId,\n });\n if (userRow) return { provider: userRow.provider, scope: 'user', credential: userRow.data };\n const orgRow = await this.#db.findOne<CredentialDbRow>('model_provider_credentials', {\n org_id: orgId,\n provider,\n user_id: null,\n });\n if (orgRow) return { provider: orgRow.provider, scope: 'org', credential: orgRow.data };\n return undefined;\n }\n\n /**\n * Refresh the tenant's OAuth credential under the backend's atomic\n * read-modify-write so concurrent replicas don't invalidate each other's\n * rotating refresh tokens. The expiry is re-checked under the lock —\n * another replica may have refreshed already, in which case `refreshFn` is\n * skipped and the fresh credential is returned. Returns `undefined` when no\n * OAuth row exists for the tenant.\n */\n async refreshOAuth(\n tenant: CredentialTenant,\n provider: string,\n refreshFn: (current: OAuthCredential) => Promise<OAuthCredential>,\n ): Promise<OAuthCredential | undefined> {\n let result: OAuthCredential | undefined;\n await this.#db.updateAtomic<CredentialDbRow>(\n 'model_provider_credentials',\n tenantWhere(tenant, provider),\n async row => {\n if (row.data.type !== 'oauth') return null;\n const current = row.data;\n // Re-check under the lock: another replica may have refreshed while we waited.\n if (!isOAuthCredentialExpired(current)) {\n result = current;\n return null;\n }\n const next = await refreshFn(current);\n result = next;\n return { data: next, updated_at: new Date() };\n },\n );\n return result;\n }\n\n /** Persist a pending login flow started by a web route. */\n async createLoginSession(input: CreateLoginSessionInput): Promise<LoginSessionRow> {\n const inserted = await this.#db.insertOne<LoginSessionDbRow>('oauth_login_sessions', {\n session_id: input.sessionId,\n org_id: input.orgId,\n user_id: input.userId,\n provider: input.provider,\n kind: input.kind,\n pending: input.pending,\n expires_at: input.expiresAt,\n next_poll_at: input.nextPollAt ?? null,\n created_at: new Date(),\n });\n return toSessionRow(inserted);\n }\n\n /**\n * Read a pending login flow. An expired session is deleted on read (TTL\n * cleanup) and reported as absent.\n */\n async getLoginSession(sessionId: string): Promise<LoginSessionRow | undefined> {\n const row = await this.#db.findOne<LoginSessionDbRow>('oauth_login_sessions', { session_id: sessionId });\n if (!row) return undefined;\n if (row.expires_at.getTime() <= Date.now()) {\n await this.#db.deleteMany('oauth_login_sessions', { session_id: sessionId });\n return undefined;\n }\n return toSessionRow(row);\n }\n\n /**\n * Atomically claim a due login session for one upstream completion/poll attempt.\n * Returns undefined when the session is missing, expired, owned by another\n * tenant/provider, or already claimed by a concurrent request.\n */\n async claimLoginSession(\n sessionId: string,\n owner: Pick<LoginSessionRow, 'orgId' | 'userId' | 'provider' | 'kind'>,\n ): Promise<LoginSessionRow | undefined> {\n const now = Date.now();\n let claimed = false;\n const updated = await this.#db.updateAtomic<LoginSessionDbRow>(\n 'oauth_login_sessions',\n { session_id: sessionId },\n row => {\n if (\n row.org_id !== owner.orgId ||\n row.user_id !== owner.userId ||\n row.provider !== owner.provider ||\n row.kind !== owner.kind\n ) {\n return null;\n }\n if (row.expires_at.getTime() <= now) return null;\n if (row.next_poll_at && row.next_poll_at.getTime() > now) return null;\n claimed = true;\n // Park the next poll at expiry so a concurrent claim loses until the\n // claimer either touches the session forward or deletes it.\n return { next_poll_at: row.expires_at };\n },\n );\n return claimed && updated ? toSessionRow(updated) : undefined;\n }\n\n /** Update a pending flow's serialized state and/or next allowed poll time. */\n async touchLoginSession(\n sessionId: string,\n updates: { pending?: Record<string, unknown>; nextPollAt?: Date | null },\n ): Promise<void> {\n const set: Record<string, unknown> = {};\n if (updates.pending !== undefined) set.pending = updates.pending;\n if (updates.nextPollAt !== undefined) set.next_poll_at = updates.nextPollAt;\n if (Object.keys(set).length === 0) return;\n await this.#db.updateMany('oauth_login_sessions', { session_id: sessionId }, set);\n }\n\n /** Remove a pending login flow (completed, cancelled, or failed). */\n async deleteLoginSession(sessionId: string): Promise<void> {\n await this.#db.deleteMany('oauth_login_sessions', { session_id: sessionId });\n }\n}\n"],"mappings":"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","names":[]}