@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +401 -314
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/connection.js +1 -1
  7. package/dist/capabilities/intake.js +1 -1
  8. package/dist/capabilities/version-control.js +1 -1
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +441 -39455
  11. package/dist/factory.js.map +1 -1
  12. package/dist/index.js +2 -39466
  13. package/dist/integrations/base.js +1 -1
  14. package/dist/integrations/github/config.js +36 -22
  15. package/dist/integrations/github/config.js.map +1 -1
  16. package/dist/integrations/github/factory-session.js +33 -37
  17. package/dist/integrations/github/factory-session.js.map +1 -1
  18. package/dist/integrations/github/integration.js +1037 -3624
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-triage.js +71 -71
  21. package/dist/integrations/github/issue-triage.js.map +1 -1
  22. package/dist/integrations/github/pat.js +47 -36
  23. package/dist/integrations/github/pat.js.map +1 -1
  24. package/dist/integrations/github/provenance.js +72 -101
  25. package/dist/integrations/github/provenance.js.map +1 -1
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +1272 -1833
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/rules.js +203 -471
  30. package/dist/integrations/github/rules.js.map +1 -1
  31. package/dist/integrations/github/sandbox.d.ts +2 -0
  32. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  33. package/dist/integrations/github/sandbox.js +401 -326
  34. package/dist/integrations/github/sandbox.js.map +1 -1
  35. package/dist/integrations/github/session-subscriptions.js +165 -223
  36. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  37. package/dist/integrations/github/subscriptions.js +43 -51
  38. package/dist/integrations/github/subscriptions.js.map +1 -1
  39. package/dist/integrations/github/token-refresh.js +15 -18
  40. package/dist/integrations/github/token-refresh.js.map +1 -1
  41. package/dist/integrations/github/webhook.js +359 -343
  42. package/dist/integrations/github/webhook.js.map +1 -1
  43. package/dist/integrations/linear/agent-tools.js +77 -100
  44. package/dist/integrations/linear/agent-tools.js.map +1 -1
  45. package/dist/integrations/linear/integration.js +611 -1309
  46. package/dist/integrations/linear/integration.js.map +1 -1
  47. package/dist/integrations/linear/routes.js +227 -230
  48. package/dist/integrations/linear/routes.js.map +1 -1
  49. package/dist/integrations/linear/rules.js +126 -346
  50. package/dist/integrations/linear/rules.js.map +1 -1
  51. package/dist/integrations/linear/storage.js +1 -1
  52. package/dist/integrations/platform/api-client.js +131 -135
  53. package/dist/integrations/platform/api-client.js.map +1 -1
  54. package/dist/integrations/platform/github/event-worker.js +275 -573
  55. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  56. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  57. package/dist/integrations/platform/github/integration.js +845 -3942
  58. package/dist/integrations/platform/github/integration.js.map +1 -1
  59. package/dist/integrations/platform/linear/integration.js +457 -1309
  60. package/dist/integrations/platform/linear/integration.js.map +1 -1
  61. package/dist/integrations/workos/integration.js +80 -85
  62. package/dist/integrations/workos/integration.js.map +1 -1
  63. package/dist/routes/config.d.ts.map +1 -1
  64. package/dist/routes/config.js +933 -915
  65. package/dist/routes/config.js.map +1 -1
  66. package/dist/routes/custom-provider-source.js +110 -93
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.js +401 -349
  69. package/dist/routes/fs.js.map +1 -1
  70. package/dist/routes/intake.js +165 -165
  71. package/dist/routes/intake.js.map +1 -1
  72. package/dist/routes/oauth.js +447 -25166
  73. package/dist/routes/oauth.js.map +1 -1
  74. package/dist/routes/projects.js +437 -376
  75. package/dist/routes/projects.js.map +1 -1
  76. package/dist/routes/provider-credentials.js +76 -58
  77. package/dist/routes/provider-credentials.js.map +1 -1
  78. package/dist/routes/route.js +12 -8
  79. package/dist/routes/route.js.map +1 -1
  80. package/dist/routes/skills.js +160 -173
  81. package/dist/routes/skills.js.map +1 -1
  82. package/dist/routes/surface.js +250 -28618
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js +141 -166
  85. package/dist/routes/tenant-credentials.js.map +1 -1
  86. package/dist/routes/work-items.js +654 -862
  87. package/dist/routes/work-items.js.map +1 -1
  88. package/dist/rules/binding-context.js +23 -35
  89. package/dist/rules/binding-context.js.map +1 -1
  90. package/dist/rules/defaults.js +200 -309
  91. package/dist/rules/defaults.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +438 -677
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/index.js +5 -599
  96. package/dist/rules/processor.js +344 -596
  97. package/dist/rules/processor.js.map +1 -1
  98. package/dist/rules/resolve.js +21 -18
  99. package/dist/rules/resolve.js.map +1 -1
  100. package/dist/rules/start-coordinator.js +168 -157
  101. package/dist/rules/start-coordinator.js.map +1 -1
  102. package/dist/rules/tools.js +50 -85
  103. package/dist/rules/tools.js.map +1 -1
  104. package/dist/rules/transition-service.js +183 -454
  105. package/dist/rules/transition-service.js.map +1 -1
  106. package/dist/rules/types.js +33 -29
  107. package/dist/rules/types.js.map +1 -1
  108. package/dist/rules/validation.js +266 -281
  109. package/dist/rules/validation.js.map +1 -1
  110. package/dist/sandbox/fleet.js +272 -250
  111. package/dist/sandbox/fleet.js.map +1 -1
  112. package/dist/sandbox/reattach.js +13 -6
  113. package/dist/sandbox/reattach.js.map +1 -1
  114. package/dist/server-error.js +12 -16
  115. package/dist/server-error.js.map +1 -1
  116. package/dist/skills/service.js +30 -35
  117. package/dist/skills/service.js.map +1 -1
  118. package/dist/spa-static.d.ts.map +1 -1
  119. package/dist/spa-static.js +91 -64
  120. package/dist/spa-static.js.map +1 -1
  121. package/dist/state-signing.js +73 -44
  122. package/dist/state-signing.js.map +1 -1
  123. package/dist/storage/domains/audit/agent-audit.js +60 -59
  124. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  125. package/dist/storage/domains/audit/base.js +169 -112
  126. package/dist/storage/domains/audit/base.js.map +1 -1
  127. package/dist/storage/domains/audit/domain.js +155 -153
  128. package/dist/storage/domains/audit/domain.js.map +1 -1
  129. package/dist/storage/domains/credentials/base.js +259 -243
  130. package/dist/storage/domains/credentials/base.js.map +1 -1
  131. package/dist/storage/domains/custom-providers/base.js +138 -143
  132. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  133. package/dist/storage/domains/intake/base.js +75 -57
  134. package/dist/storage/domains/intake/base.js.map +1 -1
  135. package/dist/storage/domains/integrations/base.js +310 -245
  136. package/dist/storage/domains/integrations/base.js.map +1 -1
  137. package/dist/storage/domains/memory-settings/base.js +117 -102
  138. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  139. package/dist/storage/domains/model-packs/base.js +89 -88
  140. package/dist/storage/domains/model-packs/base.js.map +1 -1
  141. package/dist/storage/domains/projects/base.js +97 -92
  142. package/dist/storage/domains/projects/base.js.map +1 -1
  143. package/dist/storage/domains/queue-health/base.js +111 -76
  144. package/dist/storage/domains/queue-health/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +9 -0
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +777 -702
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +322 -344
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  154. package/dist/storage/domains/work-items/base.js +1266 -1132
  155. package/dist/storage/domains/work-items/base.js.map +1 -1
  156. package/dist/storage/domains/work-items/metrics.js +202 -184
  157. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  158. package/dist/timing.d.ts +15 -0
  159. package/dist/timing.d.ts.map +1 -0
  160. package/dist/timing.js +34 -0
  161. package/dist/timing.js.map +1 -0
  162. package/dist/workspace.d.ts +2 -3
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +236 -438
  165. package/dist/workspace.js.map +1 -1
  166. package/factory-skills/factory-review/SKILL.md +9 -2
  167. package/package.json +9 -9
  168. package/dist/capabilities/connection.js.map +0 -1
  169. package/dist/capabilities/intake.js.map +0 -1
  170. package/dist/capabilities/version-control.js.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/integrations/base.js.map +0 -1
  173. package/dist/integrations/linear/storage.js.map +0 -1
  174. package/dist/rules/index.js.map +0 -1
package/dist/auth.js CHANGED
@@ -1,370 +1,457 @@
1
- // src/auth.ts
1
+ import { timedAboveThreshold } from "./timing.js";
2
2
  import { MastraAuthWorkos } from "@mastra/auth-workos";
3
- import {
4
- registerApiRoute,
5
- isAuthHttpHandler,
6
- isCredentialsProvider,
7
- isOrganizationsProvider,
8
- isSessionProvider,
9
- isSSOProvider
10
- } from "@mastra/core/server";
3
+ import { isAuthHttpHandler, isCredentialsProvider, isOrganizationsProvider, isSSOProvider, isSessionProvider, registerApiRoute } from "@mastra/core/server";
4
+ //#region src/auth.ts
5
+ /**
6
+ * Validate that a `returnTo` value is a safe same-site path, to prevent
7
+ * open-redirect attacks. Only absolute local paths (`/foo`) are allowed;
8
+ * protocol-relative (`//evil.com`) and absolute URLs are rejected.
9
+ */
11
10
  function sanitizeReturnTo(raw) {
12
- if (!raw) return "/";
13
- if (!raw.startsWith("/")) return "/";
14
- if (raw.startsWith("//") || raw.startsWith("/\\")) return "/";
15
- return raw;
11
+ if (!raw) return "/";
12
+ if (!raw.startsWith("/")) return "/";
13
+ if (raw.startsWith("//") || raw.startsWith("/\\")) return "/";
14
+ return raw;
16
15
  }
16
+ /** Extract a bearer token from the Authorization header, if present. */
17
17
  function getBearerToken(authorization) {
18
- if (!authorization) return "";
19
- const match = /^Bearer\s+(.+)$/i.exec(authorization);
20
- return match?.[1] ?? "";
18
+ if (!authorization) return "";
19
+ return /^Bearer\s+(.+)$/i.exec(authorization)?.[1] ?? "";
21
20
  }
21
+ /**
22
+ * Whether the SPA is served cross-origin from this API (platform deploy). When
23
+ * `MASTRACODE_ALLOWED_ORIGINS` is set the browser talks to us cross-site, so
24
+ * session cookies must be `SameSite=None; Secure` for the browser to send them.
25
+ * Same-origin local dev leaves this unset and keeps the stricter `SameSite=Lax`.
26
+ */
22
27
  function isCrossSiteAuth() {
23
- return Boolean(process.env.MASTRACODE_ALLOWED_ORIGINS?.trim());
28
+ return Boolean(process.env.MASTRACODE_ALLOWED_ORIGINS?.trim());
24
29
  }
25
- var FACTORY_AUTH_USER_KEY = "factoryAuthUser";
30
+ /** Context key under which the gate stashes the authenticated user. */
31
+ const FACTORY_AUTH_USER_KEY = "factoryAuthUser";
32
+ /**
33
+ * Read the authenticated user the gate stashed on the context, or
34
+ * `undefined` when unauthenticated / auth disabled. Used by downstream routes
35
+ * (e.g. GitHub) to scope rows per user.
36
+ */
26
37
  function getFactoryAuthUser(c) {
27
- return c.get(FACTORY_AUTH_USER_KEY);
38
+ return c.get(FACTORY_AUTH_USER_KEY);
28
39
  }
40
+ /** Resolve the stable user id from an authenticated user shape. */
29
41
  function getFactoryAuthUserId(user) {
30
- return user?.workosId ?? user?.id;
42
+ return user?.workosId ?? user?.id;
31
43
  }
44
+ /** Resolve the organization id from a user shape, if present. */
32
45
  function getFactoryAuthOrgId(user) {
33
- return user?.organizationId;
46
+ return user?.organizationId;
34
47
  }
48
+ /**
49
+ * Resolve the tenant identity `(orgId, userId)` from the authenticated user on
50
+ * the context. Returns `undefined` when there is no signed-in user (auth
51
+ * disabled or unauthenticated). `orgId` is `undefined` for personal accounts;
52
+ * callers gate org-scoped GitHub features on its presence while agent state
53
+ * falls back to a user-only tenant.
54
+ */
35
55
  function factoryAuthTenant(c) {
36
- const user = getFactoryAuthUser(c);
37
- const userId = getFactoryAuthUserId(user);
38
- if (!userId) return void 0;
39
- return { orgId: getFactoryAuthOrgId(user), userId };
56
+ const user = getFactoryAuthUser(c);
57
+ const userId = getFactoryAuthUserId(user);
58
+ if (!userId) return void 0;
59
+ return {
60
+ orgId: getFactoryAuthOrgId(user),
61
+ userId
62
+ };
40
63
  }
64
+ /** True when both WorkOS credential env vars are present (legacy env gate). */
41
65
  function envWorkosConfigured() {
42
- return Boolean(process.env.WORKOS_API_KEY && process.env.WORKOS_CLIENT_ID);
66
+ return Boolean(process.env.WORKOS_API_KEY && process.env.WORKOS_CLIENT_ID);
43
67
  }
68
+ /**
69
+ * WorkOS provider implied by the `WORKOS_*` env vars — back-compat for test
70
+ * suites exercised without booting the factory (route suites set `WORKOS_*`
71
+ * directly and call {@link mountFactoryAuth} without an explicit provider).
72
+ * `fetchMemberships: true` lets `authenticateToken` resolve `organizationId`
73
+ * from a single membership when the JWT has no org claim — required so a
74
+ * bootstrapped personal org resolves without re-auth.
75
+ */
44
76
  function envFallbackAuthProvider(redirectUri) {
45
- if (!envWorkosConfigured()) return void 0;
46
- return new MastraAuthWorkos({
47
- redirectUri: redirectUri ?? process.env.WORKOS_REDIRECT_URI,
48
- fetchMemberships: true
49
- });
77
+ if (!envWorkosConfigured()) return void 0;
78
+ return new MastraAuthWorkos({
79
+ redirectUri: redirectUri ?? process.env.WORKOS_REDIRECT_URI,
80
+ fetchMemberships: true
81
+ });
50
82
  }
83
+ /**
84
+ * Map a provider `authenticateToken` result onto the neutral SPA user shape.
85
+ *
86
+ * Two result families exist today:
87
+ * - flat provider users (WorkOS `WorkOSUser` et al.): `id`/`workosId`/`email`/
88
+ * `name`/`organizationId` directly on the object;
89
+ * - session-shaped results (better-auth `BetterAuthUser`): `{ session, user }`
90
+ * with the active org on the session.
91
+ */
51
92
  function toFactoryAuthUser(result) {
52
- if (!result || typeof result !== "object") return null;
53
- const record = result;
54
- if (record.user && typeof record.user === "object" && record.session && typeof record.session === "object") {
55
- const user = record.user;
56
- const session = record.session;
57
- if (typeof user.id !== "string") return null;
58
- return {
59
- id: user.id,
60
- email: typeof user.email === "string" ? user.email : void 0,
61
- name: typeof user.name === "string" ? user.name : void 0,
62
- organizationId: typeof session.activeOrganizationId === "string" ? session.activeOrganizationId : void 0
63
- };
64
- }
65
- const flat = record;
66
- const id = typeof flat.id === "string" ? flat.id : void 0;
67
- const workosId = typeof flat.workosId === "string" ? flat.workosId : void 0;
68
- if (!id && !workosId) return null;
69
- return {
70
- id,
71
- workosId,
72
- email: typeof flat.email === "string" ? flat.email : void 0,
73
- name: typeof flat.name === "string" ? flat.name : void 0,
74
- organizationId: typeof flat.organizationId === "string" ? flat.organizationId : void 0
75
- };
93
+ if (!result || typeof result !== "object") return null;
94
+ const record = result;
95
+ if (record.user && typeof record.user === "object" && record.session && typeof record.session === "object") {
96
+ const user = record.user;
97
+ const session = record.session;
98
+ if (typeof user.id !== "string") return null;
99
+ return {
100
+ id: user.id,
101
+ email: typeof user.email === "string" ? user.email : void 0,
102
+ name: typeof user.name === "string" ? user.name : void 0,
103
+ organizationId: typeof session.activeOrganizationId === "string" ? session.activeOrganizationId : void 0
104
+ };
105
+ }
106
+ const flat = record;
107
+ const id = typeof flat.id === "string" ? flat.id : void 0;
108
+ const workosId = typeof flat.workosId === "string" ? flat.workosId : void 0;
109
+ if (!id && !workosId) return null;
110
+ return {
111
+ id,
112
+ workosId,
113
+ email: typeof flat.email === "string" ? flat.email : void 0,
114
+ name: typeof flat.name === "string" ? flat.name : void 0,
115
+ organizationId: typeof flat.organizationId === "string" ? flat.organizationId : void 0
116
+ };
76
117
  }
118
+ /**
119
+ * Resolve the authenticated user for a request via the provider. Never throws:
120
+ * ordinary invalid/expired sessions resolve to `null`.
121
+ */
77
122
  async function authenticateRequest(provider, token, raw) {
78
- try {
79
- const result = await provider.authenticateToken(token, raw);
80
- return toFactoryAuthUser(result);
81
- } catch {
82
- return null;
83
- }
123
+ try {
124
+ return toFactoryAuthUser(await provider.authenticateToken(token, raw));
125
+ } catch {
126
+ return null;
127
+ }
84
128
  }
129
+ /**
130
+ * Bootstrap a personal org for no-org accounts so org-scoped features (GitHub
131
+ * connect) work without leaving the app. Mutates the resolved user so the
132
+ * current request sees the org immediately; subsequent requests resolve it via
133
+ * the provider's own session/membership lookup (providers cache internally).
134
+ * Best-effort: providers swallow their own bootstrap failures, and any
135
+ * unexpected throw leaves the user no-org.
136
+ */
85
137
  async function ensureUserOrg(provider, user) {
86
- if (getFactoryAuthOrgId(user)) return;
87
- if (!isOrganizationsProvider(provider)) return;
88
- const userId = getFactoryAuthUserId(user);
89
- if (!userId) return;
90
- try {
91
- const orgId = await provider.ensureOrganization(userId);
92
- if (orgId) user.organizationId = orgId;
93
- } catch {
94
- }
138
+ if (getFactoryAuthOrgId(user)) return;
139
+ if (!isOrganizationsProvider(provider)) return;
140
+ const userId = getFactoryAuthUserId(user);
141
+ if (!userId) return;
142
+ try {
143
+ const orgId = await provider.ensureOrganization(userId);
144
+ if (orgId) user.organizationId = orgId;
145
+ } catch {}
95
146
  }
147
+ /**
148
+ * `Set-Cookie` values that clear the provider's session cookie(s), from the
149
+ * provider's (possibly partial) `ISessionProvider.getClearSessionHeaders`.
150
+ */
96
151
  function providerClearCookies(provider) {
97
- const getClearSessionHeaders = provider.getClearSessionHeaders;
98
- if (typeof getClearSessionHeaders !== "function") return [];
99
- const headers = getClearSessionHeaders.call(provider) ?? {};
100
- const setCookie = headers["Set-Cookie"];
101
- if (!setCookie) return [];
102
- return setCookie.split(/,(?=\s*[^;=,\s]+=)/).map((cookie) => cookie.trim());
152
+ const getClearSessionHeaders = provider.getClearSessionHeaders;
153
+ if (typeof getClearSessionHeaders !== "function") return [];
154
+ const setCookie = (getClearSessionHeaders.call(provider) ?? {})["Set-Cookie"];
155
+ if (!setCookie) return [];
156
+ return setCookie.split(/,(?=\s*[^;=,\s]+=)/).map((cookie) => cookie.trim());
103
157
  }
158
+ /**
159
+ * Fail-closed authorization for organization-level administrative mutations.
160
+ * The caller must belong to the same active organization and the provider must
161
+ * explicitly confirm an admin/owner role.
162
+ */
104
163
  async function isOrganizationAdmin(provider, c, organizationId) {
105
- const user = await ensureFactoryAuthUser(provider, c);
106
- if (!user || user.organizationId !== organizationId || !provider || !isOrganizationsProvider(provider)) {
107
- return false;
108
- }
109
- const userId = getFactoryAuthUserId(user);
110
- if (!userId) return false;
111
- try {
112
- return await provider.isOrganizationAdmin(organizationId, userId);
113
- } catch {
114
- return false;
115
- }
164
+ const user = await ensureFactoryAuthUser(provider, c);
165
+ if (!user || user.organizationId !== organizationId || !provider || !isOrganizationsProvider(provider)) return false;
166
+ const userId = getFactoryAuthUserId(user);
167
+ if (!userId) return false;
168
+ try {
169
+ return await provider.isOrganizationAdmin(organizationId, userId);
170
+ } catch {
171
+ return false;
172
+ }
116
173
  }
174
+ /**
175
+ * Build the factory's implementation of the `RouteAuth` seam over the
176
+ * resolved provider (`undefined` = auth disabled). Constructed once per boot
177
+ * by `MastraFactory.prepare()` and handed to factory route modules at
178
+ * construction — they never import the factory auth module directly.
179
+ */
117
180
  function createFactoryRouteAuth(provider) {
118
- return {
119
- enabled: () => provider !== void 0,
120
- ensureUser: (c) => ensureFactoryAuthUser(provider, c),
121
- tenant: (c) => factoryAuthTenant(c),
122
- isOrganizationAdmin: (c, organizationId) => isOrganizationAdmin(provider, c, organizationId)
123
- };
181
+ return {
182
+ enabled: () => provider !== void 0,
183
+ ensureUser: (c) => ensureFactoryAuthUser(provider, c),
184
+ tenant: (c) => factoryAuthTenant(c),
185
+ isOrganizationAdmin: (c, organizationId) => isOrganizationAdmin(provider, c, organizationId)
186
+ };
124
187
  }
188
+ /** True when the given provider is WorkOS. Gates WorkOS-only capabilities. */
125
189
  function isWorkOSAuth(provider) {
126
- return provider instanceof MastraAuthWorkos;
190
+ return provider instanceof MastraAuthWorkos;
127
191
  }
192
+ /**
193
+ * The raw WorkOS provider, for features that need the WorkOS client directly
194
+ * (audit-log export, Admin Portal links). Callers must gate on
195
+ * {@link isWorkOSAuth} first — throws when the provider is not WorkOS.
196
+ */
128
197
  function getWorkOSProvider(provider) {
129
- if (provider instanceof MastraAuthWorkos) return provider;
130
- throw new Error("WorkOS provider requested but the active factory auth provider is not WorkOS");
198
+ if (provider instanceof MastraAuthWorkos) return provider;
199
+ throw new Error("WorkOS provider requested but the active factory auth provider is not WorkOS");
131
200
  }
201
+ /**
202
+ * Resolve the authenticated user for a request, stashing it on the context.
203
+ *
204
+ * The gate only authenticates non-`/auth/*` requests via the `Authorization`
205
+ * header, so cookie-based browser navigations to public `/auth/*` routes (the
206
+ * GitHub connect/callback flow) arrive without a gate-stashed user. This reads
207
+ * the session cookie from the raw request the same way `/auth/me` does,
208
+ * caches the result on the context, and returns it so downstream helpers like
209
+ * {@link factoryAuthTenant} work uniformly on both gated and public routes.
210
+ *
211
+ * Returns `undefined` when there is no valid session (or auth is disabled).
212
+ */
132
213
  async function ensureFactoryAuthUser(provider, c) {
133
- const existing = getFactoryAuthUser(c);
134
- if (existing) return existing;
135
- if (!provider) return void 0;
136
- const token = getBearerToken(c.req.header("Authorization"));
137
- const user = await authenticateRequest(provider, token, c.req.raw);
138
- if (!user) return void 0;
139
- await ensureUserOrg(provider, user);
140
- c.set(FACTORY_AUTH_USER_KEY, user);
141
- return user;
214
+ const existing = getFactoryAuthUser(c);
215
+ if (existing) return existing;
216
+ if (!provider) return void 0;
217
+ const user = await authenticateRequest(provider, getBearerToken(c.req.header("Authorization")), c.req.raw);
218
+ if (!user) return void 0;
219
+ await ensureUserOrg(provider, user);
220
+ c.set(FACTORY_AUTH_USER_KEY, user);
221
+ return user;
142
222
  }
223
+ /**
224
+ * Decide whether a request is a top-level browser navigation (which should be
225
+ * redirected to `/signin`) versus an API/XHR call (which should get a 401 JSON
226
+ * response the SPA can react to).
227
+ */
143
228
  function isNavigationRequest(path, accept) {
144
- if (path.startsWith("/api/")) return false;
145
- return (accept ?? "").includes("text/html");
229
+ if (path.startsWith("/api/")) return false;
230
+ return (accept ?? "").includes("text/html");
146
231
  }
232
+ /**
233
+ * Handle the provider-neutral `/auth/me` route: validate the session with the
234
+ * active provider and report the signed-in user (no tokens) to the SPA.
235
+ * `/auth/me` is public (the gate skips `/auth/*`), so it validates the session
236
+ * itself rather than reading a value the gate would have stashed.
237
+ */
147
238
  async function handleAuthMe(provider, c) {
148
- const token = getBearerToken(c.req.header("Authorization"));
149
- const user = await authenticateRequest(provider, token, c.req.raw);
150
- const signUpDisabled = isCredentialsProvider(provider) && provider.isSignUpEnabled?.() === false;
151
- const meta = { provider: provider.name, ...signUpDisabled ? { signUpDisabled: true } : {} };
152
- if (!user) {
153
- return c.json({ authenticated: false, user: null, ...meta });
154
- }
155
- await ensureUserOrg(provider, user);
156
- return c.json({
157
- authenticated: true,
158
- user: {
159
- userId: getFactoryAuthUserId(user),
160
- email: user.email,
161
- name: user.name,
162
- organizationId: user.organizationId
163
- },
164
- ...meta
165
- });
239
+ const user = await authenticateRequest(provider, getBearerToken(c.req.header("Authorization")), c.req.raw);
240
+ const signUpDisabled = isCredentialsProvider(provider) && provider.isSignUpEnabled?.() === false;
241
+ const meta = {
242
+ provider: provider.name,
243
+ ...signUpDisabled ? { signUpDisabled: true } : {}
244
+ };
245
+ if (!user) return c.json({
246
+ authenticated: false,
247
+ user: null,
248
+ ...meta
249
+ });
250
+ await ensureUserOrg(provider, user);
251
+ return c.json({
252
+ authenticated: true,
253
+ user: {
254
+ userId: getFactoryAuthUserId(user),
255
+ email: user.email,
256
+ name: user.name,
257
+ organizationId: user.organizationId
258
+ },
259
+ ...meta
260
+ });
166
261
  }
262
+ /** Encode a validated returnTo path into the OAuth `state` parameter. */
167
263
  function encodeState(returnTo) {
168
- return Buffer.from(JSON.stringify({ returnTo }), "utf8").toString("base64url");
264
+ return Buffer.from(JSON.stringify({ returnTo }), "utf8").toString("base64url");
169
265
  }
266
+ /** Decode the OAuth `state` parameter back into a sanitized returnTo path. */
170
267
  function decodeState(state) {
171
- if (!state) return "/";
172
- try {
173
- const parsed = JSON.parse(Buffer.from(state, "base64url").toString("utf8"));
174
- return sanitizeReturnTo(parsed.returnTo);
175
- } catch {
176
- return "/";
177
- }
268
+ if (!state) return "/";
269
+ try {
270
+ return sanitizeReturnTo(JSON.parse(Buffer.from(state, "base64url").toString("utf8")).returnTo);
271
+ } catch {
272
+ return "/";
273
+ }
178
274
  }
275
+ /**
276
+ * Derive the public `/auth/*` routes from the provider's capabilities:
277
+ *
278
+ * - `IAuthHttpHandler` → `ALL /auth/api/*` proxy to the provider's own HTTP
279
+ * surface (better-auth sign-in/up/out/session — what the SPA's
280
+ * email/password form posts to).
281
+ * - `ISSOProvider` → hosted-login `GET /auth/login` / `GET /auth/callback` /
282
+ * `GET /auth/logout` (returnTo preserved through the OAuth `state` param).
283
+ * - handler-shaped, non-SSO providers → `GET /auth/login` redirects to the
284
+ * SPA's `/signin` form, `GET /auth/logout` revokes via the provider's
285
+ * sign-out endpoint and clears the session cookie.
286
+ */
179
287
  function providerAuthRoutes(provider, publicUrl) {
180
- const routes = [];
181
- if (isAuthHttpHandler(provider)) {
182
- routes.push({
183
- path: "/auth/api/*",
184
- method: "ALL",
185
- handler: (c) => provider.handleAuthRequest(c.req.raw)
186
- });
187
- }
188
- if (isSSOProvider(provider)) {
189
- routes.push(
190
- {
191
- path: "/auth/login",
192
- method: "GET",
193
- handler: async (c) => {
194
- const returnTo = sanitizeReturnTo(c.req.query("returnTo"));
195
- const state = encodeState(returnTo);
196
- const redirectUri = publicUrl ? new URL("/auth/callback", publicUrl).toString() : "";
197
- const loginUrl = await provider.getLoginUrl(redirectUri, state);
198
- for (const cookie of await provider.getLoginCookies?.(redirectUri, state) ?? []) {
199
- c.header("Set-Cookie", cookie, { append: true });
200
- }
201
- return c.redirect(loginUrl);
202
- }
203
- },
204
- {
205
- path: "/auth/callback",
206
- method: "GET",
207
- handler: async (c) => {
208
- const code = c.req.query("code");
209
- const returnTo = decodeState(c.req.query("state"));
210
- if (!code) {
211
- return c.redirect("/auth/login");
212
- }
213
- try {
214
- const result = await provider.handleCallback(code, c.req.query("state") ?? "");
215
- if (result.cookies?.length) {
216
- for (const cookie of result.cookies) {
217
- c.header("Set-Cookie", cookie, { append: true });
218
- }
219
- } else if (isSessionProvider(provider) && result.tokens) {
220
- const resultUser = result.user;
221
- const session = await provider.createSession(resultUser.id, {
222
- accessToken: result.tokens.accessToken,
223
- refreshToken: result.tokens.refreshToken,
224
- expiresAt: result.tokens.expiresAt,
225
- organizationId: resultUser.organizationId
226
- });
227
- for (const [key, value] of Object.entries(provider.getSessionHeaders(session))) {
228
- c.header(key, value, { append: true });
229
- }
230
- }
231
- return c.redirect(returnTo);
232
- } catch {
233
- return c.redirect("/auth/login");
234
- }
235
- }
236
- },
237
- {
238
- path: "/auth/logout",
239
- method: "GET",
240
- handler: async (c) => {
241
- let logoutUrl = null;
242
- try {
243
- logoutUrl = await provider.getLogoutUrl?.("/", c.req.raw) ?? null;
244
- } catch {
245
- logoutUrl = null;
246
- }
247
- for (const cookie of providerClearCookies(provider)) {
248
- c.header("Set-Cookie", cookie, { append: true });
249
- }
250
- return c.redirect(logoutUrl ?? "/");
251
- }
252
- }
253
- );
254
- } else if (isAuthHttpHandler(provider)) {
255
- routes.push(
256
- {
257
- // Hosted-login equivalent: no hosted page, so send the browser to the
258
- // SPA's /signin form, preserving returnTo.
259
- path: "/auth/login",
260
- method: "GET",
261
- handler: (c) => {
262
- const returnTo = sanitizeReturnTo(c.req.query("returnTo"));
263
- return c.redirect(`/signin?returnTo=${encodeURIComponent(returnTo)}`);
264
- }
265
- },
266
- {
267
- path: "/auth/logout",
268
- method: "GET",
269
- handler: async (c) => {
270
- try {
271
- const origin = new URL(c.req.url).origin;
272
- const response = await provider.handleAuthRequest(
273
- new Request(`${origin}/auth/api/sign-out`, { method: "POST", headers: c.req.raw.headers })
274
- );
275
- for (const cookie of response.headers.getSetCookie()) {
276
- c.header("Set-Cookie", cookie, { append: true });
277
- }
278
- } catch {
279
- }
280
- for (const cookie of providerClearCookies(provider)) {
281
- c.header("Set-Cookie", cookie, { append: true });
282
- }
283
- return c.redirect("/");
284
- }
285
- }
286
- );
287
- }
288
- return routes;
288
+ const routes = [];
289
+ if (isAuthHttpHandler(provider)) routes.push({
290
+ path: "/auth/api/*",
291
+ method: "ALL",
292
+ handler: (c) => provider.handleAuthRequest(c.req.raw)
293
+ });
294
+ if (isSSOProvider(provider)) routes.push({
295
+ path: "/auth/login",
296
+ method: "GET",
297
+ handler: async (c) => {
298
+ const state = encodeState(sanitizeReturnTo(c.req.query("returnTo")));
299
+ const redirectUri = publicUrl ? new URL("/auth/callback", publicUrl).toString() : "";
300
+ const loginUrl = await provider.getLoginUrl(redirectUri, state);
301
+ for (const cookie of await provider.getLoginCookies?.(redirectUri, state) ?? []) c.header("Set-Cookie", cookie, { append: true });
302
+ return c.redirect(loginUrl);
303
+ }
304
+ }, {
305
+ path: "/auth/callback",
306
+ method: "GET",
307
+ handler: async (c) => {
308
+ const code = c.req.query("code");
309
+ const returnTo = decodeState(c.req.query("state"));
310
+ if (!code) return c.redirect("/auth/login");
311
+ try {
312
+ const result = await provider.handleCallback(code, c.req.query("state") ?? "");
313
+ if (result.cookies?.length) for (const cookie of result.cookies) c.header("Set-Cookie", cookie, { append: true });
314
+ else if (isSessionProvider(provider) && result.tokens) {
315
+ const resultUser = result.user;
316
+ const session = await provider.createSession(resultUser.id, {
317
+ accessToken: result.tokens.accessToken,
318
+ refreshToken: result.tokens.refreshToken,
319
+ expiresAt: result.tokens.expiresAt,
320
+ organizationId: resultUser.organizationId
321
+ });
322
+ for (const [key, value] of Object.entries(provider.getSessionHeaders(session))) c.header(key, value, { append: true });
323
+ }
324
+ return c.redirect(returnTo);
325
+ } catch {
326
+ return c.redirect("/auth/login");
327
+ }
328
+ }
329
+ }, {
330
+ path: "/auth/logout",
331
+ method: "GET",
332
+ handler: async (c) => {
333
+ let logoutUrl = null;
334
+ try {
335
+ logoutUrl = await provider.getLogoutUrl?.("/", c.req.raw) ?? null;
336
+ } catch {
337
+ logoutUrl = null;
338
+ }
339
+ for (const cookie of providerClearCookies(provider)) c.header("Set-Cookie", cookie, { append: true });
340
+ return c.redirect(logoutUrl ?? "/");
341
+ }
342
+ });
343
+ else if (isAuthHttpHandler(provider)) routes.push({
344
+ path: "/auth/login",
345
+ method: "GET",
346
+ handler: (c) => {
347
+ const returnTo = sanitizeReturnTo(c.req.query("returnTo"));
348
+ return c.redirect(`/signin?returnTo=${encodeURIComponent(returnTo)}`);
349
+ }
350
+ }, {
351
+ path: "/auth/logout",
352
+ method: "GET",
353
+ handler: async (c) => {
354
+ try {
355
+ const origin = new URL(c.req.url).origin;
356
+ const response = await provider.handleAuthRequest(new Request(`${origin}/auth/api/sign-out`, {
357
+ method: "POST",
358
+ headers: c.req.raw.headers
359
+ }));
360
+ for (const cookie of response.headers.getSetCookie()) c.header("Set-Cookie", cookie, { append: true });
361
+ } catch {}
362
+ for (const cookie of providerClearCookies(provider)) c.header("Set-Cookie", cookie, { append: true });
363
+ return c.redirect("/");
364
+ }
365
+ });
366
+ return routes;
289
367
  }
368
+ /**
369
+ * Register the public `/auth/*` routes on a Hono app: the capability-derived
370
+ * provider routes (login/callback/logout/provider APIs) plus the
371
+ * provider-neutral `/auth/me`. Split out from `mountFactoryAuth` so both the local
372
+ * Hono server and the platform Mastra entry can reuse the exact same handlers.
373
+ */
290
374
  function registerAuthRoutes(app, provider, options = {}) {
291
- for (const route of providerAuthRoutes(provider, options.publicUrl)) {
292
- const methods = route.method === "ALL" ? ["GET", "POST", "PUT", "DELETE", "PATCH"] : [route.method];
293
- app.on(methods, route.path, (c) => route.handler(c));
294
- }
295
- app.get("/auth/me", (c) => handleAuthMe(provider, c));
375
+ for (const route of providerAuthRoutes(provider, options.publicUrl)) {
376
+ const methods = route.method === "ALL" ? [
377
+ "GET",
378
+ "POST",
379
+ "PUT",
380
+ "DELETE",
381
+ "PATCH"
382
+ ] : [route.method];
383
+ app.on(methods, route.path, (c) => route.handler(c));
384
+ }
385
+ app.get("/auth/me", (c) => handleAuthMe(provider, c));
296
386
  }
387
+ /**
388
+ * Build the public `/auth/*` routes (provider routes + `/auth/me`) as Mastra
389
+ * `server.apiRoutes`. Used by the platform Mastra entry (`src/mastra/index.ts`),
390
+ * which can't register plain Hono routes on the deployer-generated app the way
391
+ * the local server does via {@link registerAuthRoutes}.
392
+ *
393
+ * Handlers are identical to {@link registerAuthRoutes}. All are `requiresAuth: false`
394
+ * (they must be reachable while unauthenticated), and the gate middleware skips
395
+ * `/auth/*` so it never blocks them. `/auth/*` is not under `/api`, so it is a
396
+ * valid custom-route path.
397
+ */
297
398
  function buildAuthRoutes(provider, options = {}) {
298
- return [
299
- // `registerApiRoute` handlers see @mastra/core's bundled hono Context type,
300
- // which is structurally identical to (but nominally distinct from) the
301
- // local hono version the route handlers are typed against — cast across
302
- // the seam.
303
- ...providerAuthRoutes(provider, options.publicUrl).map(
304
- (route) => registerApiRoute(route.path, {
305
- method: route.method,
306
- requiresAuth: false,
307
- handler: (c) => route.handler(c)
308
- })
309
- ),
310
- registerApiRoute("/auth/me", {
311
- method: "GET",
312
- requiresAuth: false,
313
- handler: (c) => handleAuthMe(provider, c)
314
- })
315
- ];
399
+ return [...providerAuthRoutes(provider, options.publicUrl).map((route) => registerApiRoute(route.path, {
400
+ method: route.method,
401
+ requiresAuth: false,
402
+ handler: (c) => route.handler(c)
403
+ })), registerApiRoute("/auth/me", {
404
+ method: "GET",
405
+ requiresAuth: false,
406
+ handler: (c) => handleAuthMe(provider, c)
407
+ })];
316
408
  }
409
+ /**
410
+ * Build the auth gate as a plain Hono middleware handler `(c, next)`. Protects
411
+ * everything that is not a public `/auth/*` route: authenticated requests stash
412
+ * the user on the context and continue; unauthenticated navigations redirect to
413
+ * login and XHR/API calls get a 401 JSON. Shared by the local Hono server
414
+ * (`mountFactoryAuth`) and the platform Mastra entry (`server.middleware`).
415
+ */
317
416
  function createFactoryAuthGate(provider) {
318
- return async (c, next) => {
319
- const path = c.req.path;
320
- if (path.startsWith("/auth/")) {
321
- return next();
322
- }
323
- if (c.req.method === "POST" && path === "/web/github/webhook") {
324
- return next();
325
- }
326
- if (path === "/signin" || path.startsWith("/assets/")) {
327
- return next();
328
- }
329
- const token = getBearerToken(c.req.header("Authorization"));
330
- const user = await authenticateRequest(provider, token, c.req.raw);
331
- if (user) {
332
- await ensureUserOrg(provider, user);
333
- c.set(FACTORY_AUTH_USER_KEY, user);
334
- c.get("requestContext")?.set("user", user);
335
- return next();
336
- }
337
- if (isNavigationRequest(path, c.req.header("Accept"))) {
338
- const url = new URL(c.req.url);
339
- const returnTo = sanitizeReturnTo(url.pathname + url.search);
340
- return c.redirect(`/signin?returnTo=${encodeURIComponent(returnTo)}`);
341
- }
342
- return c.json({ error: "unauthorized" }, 401);
343
- };
417
+ return async (c, next) => {
418
+ const path = c.req.path;
419
+ if (path.startsWith("/auth/")) return next();
420
+ if (c.req.method === "POST" && path === "/web/github/webhook") return next();
421
+ if (path === "/signin" || path.startsWith("/assets/") || path === "/manifest.webmanifest" || path === "/mastra.svg") return next();
422
+ const token = getBearerToken(c.req.header("Authorization"));
423
+ const user = await timedAboveThreshold("auth.gate.authenticate", 1e3, () => authenticateRequest(provider, token, c.req.raw));
424
+ if (user) {
425
+ await ensureUserOrg(provider, user);
426
+ c.set(FACTORY_AUTH_USER_KEY, user);
427
+ c.get("requestContext")?.set("user", user);
428
+ return next();
429
+ }
430
+ if (isNavigationRequest(path, c.req.header("Accept"))) {
431
+ const url = new URL(c.req.url);
432
+ const returnTo = sanitizeReturnTo(url.pathname + url.search);
433
+ return c.redirect(`/signin?returnTo=${encodeURIComponent(returnTo)}`);
434
+ }
435
+ return c.json({ error: "unauthorized" }, 401);
436
+ };
344
437
  }
438
+ /**
439
+ * Mount factory auth gating onto the host app. No-op when auth is disabled
440
+ * (no provider active).
441
+ *
442
+ * Must be called before the Mastra adapter routes, the `/web/*` routes, and
443
+ * the static UI handlers so the gate covers every request. Composes the shared
444
+ * `registerAuthRoutes` + `createFactoryAuthGate` factories so the local Hono server
445
+ * and the platform Mastra entry stay behavior-identical.
446
+ */
345
447
  function mountFactoryAuth(app, options = {}) {
346
- const provider = options.provider ?? envFallbackAuthProvider(options.redirectUri);
347
- if (!provider) return false;
348
- registerAuthRoutes(app, provider, { publicUrl: options.publicUrl });
349
- app.use("*", createFactoryAuthGate(provider));
350
- return true;
448
+ const provider = options.provider ?? envFallbackAuthProvider(options.redirectUri);
449
+ if (!provider) return false;
450
+ registerAuthRoutes(app, provider, { publicUrl: options.publicUrl });
451
+ app.use("*", createFactoryAuthGate(provider));
452
+ return true;
351
453
  }
352
- export {
353
- buildAuthRoutes,
354
- createFactoryAuthGate,
355
- createFactoryRouteAuth,
356
- ensureFactoryAuthUser,
357
- factoryAuthTenant,
358
- getBearerToken,
359
- getFactoryAuthOrgId,
360
- getFactoryAuthUser,
361
- getFactoryAuthUserId,
362
- getWorkOSProvider,
363
- isCrossSiteAuth,
364
- isOrganizationAdmin,
365
- isWorkOSAuth,
366
- mountFactoryAuth,
367
- registerAuthRoutes,
368
- sanitizeReturnTo
369
- };
454
+ //#endregion
455
+ export { buildAuthRoutes, createFactoryAuthGate, createFactoryRouteAuth, ensureFactoryAuthUser, factoryAuthTenant, getBearerToken, getFactoryAuthOrgId, getFactoryAuthUser, getFactoryAuthUserId, getWorkOSProvider, isCrossSiteAuth, isOrganizationAdmin, isWorkOSAuth, mountFactoryAuth, registerAuthRoutes, sanitizeReturnTo };
456
+
370
457
  //# sourceMappingURL=auth.js.map