@mastra/factory 0.2.2-alpha.1 → 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.
- package/CHANGELOG.md +10 -0
- package/dist/auth.js +401 -334
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.js +441 -39622
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39633
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3672
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1881
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +400 -345
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -4022
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -912
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28615
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.js +438 -683
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js +91 -65
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -706
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -348
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1148
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/timing.js +29 -22
- package/dist/timing.js.map +1 -1
- package/dist/workspace.js +236 -477
- package/dist/workspace.js.map +1 -1
- package/package.json +8 -8
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
package/dist/auth.js
CHANGED
|
@@ -1,390 +1,457 @@
|
|
|
1
|
-
|
|
1
|
+
import { timedAboveThreshold } from "./timing.js";
|
|
2
2
|
import { MastraAuthWorkos } from "@mastra/auth-workos";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "@mastra/core/server";
|
|
11
|
-
|
|
12
|
-
// src/timing.ts
|
|
13
|
-
async function timedAboveThreshold(phase, thresholdMs, fn) {
|
|
14
|
-
const start = performance.now();
|
|
15
|
-
try {
|
|
16
|
-
return await fn();
|
|
17
|
-
} finally {
|
|
18
|
-
const elapsed = performance.now() - start;
|
|
19
|
-
if (elapsed > thresholdMs) {
|
|
20
|
-
process.stderr.write(`[factory:timing] slow ${phase} ${Math.round(elapsed)}ms
|
|
21
|
-
`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// src/auth.ts
|
|
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
|
+
*/
|
|
27
10
|
function sanitizeReturnTo(raw) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
11
|
+
if (!raw) return "/";
|
|
12
|
+
if (!raw.startsWith("/")) return "/";
|
|
13
|
+
if (raw.startsWith("//") || raw.startsWith("/\\")) return "/";
|
|
14
|
+
return raw;
|
|
32
15
|
}
|
|
16
|
+
/** Extract a bearer token from the Authorization header, if present. */
|
|
33
17
|
function getBearerToken(authorization) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return match?.[1] ?? "";
|
|
18
|
+
if (!authorization) return "";
|
|
19
|
+
return /^Bearer\s+(.+)$/i.exec(authorization)?.[1] ?? "";
|
|
37
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
|
+
*/
|
|
38
27
|
function isCrossSiteAuth() {
|
|
39
|
-
|
|
28
|
+
return Boolean(process.env.MASTRACODE_ALLOWED_ORIGINS?.trim());
|
|
40
29
|
}
|
|
41
|
-
|
|
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
|
+
*/
|
|
42
37
|
function getFactoryAuthUser(c) {
|
|
43
|
-
|
|
38
|
+
return c.get(FACTORY_AUTH_USER_KEY);
|
|
44
39
|
}
|
|
40
|
+
/** Resolve the stable user id from an authenticated user shape. */
|
|
45
41
|
function getFactoryAuthUserId(user) {
|
|
46
|
-
|
|
42
|
+
return user?.workosId ?? user?.id;
|
|
47
43
|
}
|
|
44
|
+
/** Resolve the organization id from a user shape, if present. */
|
|
48
45
|
function getFactoryAuthOrgId(user) {
|
|
49
|
-
|
|
46
|
+
return user?.organizationId;
|
|
50
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
|
+
*/
|
|
51
55
|
function factoryAuthTenant(c) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
};
|
|
56
63
|
}
|
|
64
|
+
/** True when both WorkOS credential env vars are present (legacy env gate). */
|
|
57
65
|
function envWorkosConfigured() {
|
|
58
|
-
|
|
66
|
+
return Boolean(process.env.WORKOS_API_KEY && process.env.WORKOS_CLIENT_ID);
|
|
59
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
|
+
*/
|
|
60
76
|
function envFallbackAuthProvider(redirectUri) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
if (!envWorkosConfigured()) return void 0;
|
|
78
|
+
return new MastraAuthWorkos({
|
|
79
|
+
redirectUri: redirectUri ?? process.env.WORKOS_REDIRECT_URI,
|
|
80
|
+
fetchMemberships: true
|
|
81
|
+
});
|
|
66
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
|
+
*/
|
|
67
92
|
function toFactoryAuthUser(result) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
};
|
|
92
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the authenticated user for a request via the provider. Never throws:
|
|
120
|
+
* ordinary invalid/expired sessions resolve to `null`.
|
|
121
|
+
*/
|
|
93
122
|
async function authenticateRequest(provider, token, raw) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
123
|
+
try {
|
|
124
|
+
return toFactoryAuthUser(await provider.authenticateToken(token, raw));
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
100
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
|
+
*/
|
|
101
137
|
async function ensureUserOrg(provider, user) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
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 {}
|
|
111
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* `Set-Cookie` values that clear the provider's session cookie(s), from the
|
|
149
|
+
* provider's (possibly partial) `ISessionProvider.getClearSessionHeaders`.
|
|
150
|
+
*/
|
|
112
151
|
function providerClearCookies(provider) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
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());
|
|
119
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
|
+
*/
|
|
120
163
|
async function isOrganizationAdmin(provider, c, organizationId) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
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
|
+
}
|
|
132
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
|
+
*/
|
|
133
180
|
function createFactoryRouteAuth(provider) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
};
|
|
140
187
|
}
|
|
188
|
+
/** True when the given provider is WorkOS. Gates WorkOS-only capabilities. */
|
|
141
189
|
function isWorkOSAuth(provider) {
|
|
142
|
-
|
|
190
|
+
return provider instanceof MastraAuthWorkos;
|
|
143
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
|
+
*/
|
|
144
197
|
function getWorkOSProvider(provider) {
|
|
145
|
-
|
|
146
|
-
|
|
198
|
+
if (provider instanceof MastraAuthWorkos) return provider;
|
|
199
|
+
throw new Error("WorkOS provider requested but the active factory auth provider is not WorkOS");
|
|
147
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
|
+
*/
|
|
148
213
|
async function ensureFactoryAuthUser(provider, c) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
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;
|
|
158
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
|
+
*/
|
|
159
228
|
function isNavigationRequest(path, accept) {
|
|
160
|
-
|
|
161
|
-
|
|
229
|
+
if (path.startsWith("/api/")) return false;
|
|
230
|
+
return (accept ?? "").includes("text/html");
|
|
162
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
|
+
*/
|
|
163
238
|
async function handleAuthMe(provider, c) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
});
|
|
182
261
|
}
|
|
262
|
+
/** Encode a validated returnTo path into the OAuth `state` parameter. */
|
|
183
263
|
function encodeState(returnTo) {
|
|
184
|
-
|
|
264
|
+
return Buffer.from(JSON.stringify({ returnTo }), "utf8").toString("base64url");
|
|
185
265
|
}
|
|
266
|
+
/** Decode the OAuth `state` parameter back into a sanitized returnTo path. */
|
|
186
267
|
function decodeState(state) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
268
|
+
if (!state) return "/";
|
|
269
|
+
try {
|
|
270
|
+
return sanitizeReturnTo(JSON.parse(Buffer.from(state, "base64url").toString("utf8")).returnTo);
|
|
271
|
+
} catch {
|
|
272
|
+
return "/";
|
|
273
|
+
}
|
|
194
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
|
+
*/
|
|
195
287
|
function providerAuthRoutes(provider, publicUrl) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
path: "/auth/login",
|
|
276
|
-
method: "GET",
|
|
277
|
-
handler: (c) => {
|
|
278
|
-
const returnTo = sanitizeReturnTo(c.req.query("returnTo"));
|
|
279
|
-
return c.redirect(`/signin?returnTo=${encodeURIComponent(returnTo)}`);
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
path: "/auth/logout",
|
|
284
|
-
method: "GET",
|
|
285
|
-
handler: async (c) => {
|
|
286
|
-
try {
|
|
287
|
-
const origin = new URL(c.req.url).origin;
|
|
288
|
-
const response = await provider.handleAuthRequest(
|
|
289
|
-
new Request(`${origin}/auth/api/sign-out`, { method: "POST", headers: c.req.raw.headers })
|
|
290
|
-
);
|
|
291
|
-
for (const cookie of response.headers.getSetCookie()) {
|
|
292
|
-
c.header("Set-Cookie", cookie, { append: true });
|
|
293
|
-
}
|
|
294
|
-
} catch {
|
|
295
|
-
}
|
|
296
|
-
for (const cookie of providerClearCookies(provider)) {
|
|
297
|
-
c.header("Set-Cookie", cookie, { append: true });
|
|
298
|
-
}
|
|
299
|
-
return c.redirect("/");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
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;
|
|
305
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
|
+
*/
|
|
306
374
|
function registerAuthRoutes(app, provider, options = {}) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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));
|
|
312
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
|
+
*/
|
|
313
398
|
function buildAuthRoutes(provider, options = {}) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
handler: (c) => route.handler(c)
|
|
324
|
-
})
|
|
325
|
-
),
|
|
326
|
-
registerApiRoute("/auth/me", {
|
|
327
|
-
method: "GET",
|
|
328
|
-
requiresAuth: false,
|
|
329
|
-
handler: (c) => handleAuthMe(provider, c)
|
|
330
|
-
})
|
|
331
|
-
];
|
|
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
|
+
})];
|
|
332
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
|
+
*/
|
|
333
416
|
function createFactoryAuthGate(provider) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
c.get("requestContext")?.set("user", user);
|
|
355
|
-
return next();
|
|
356
|
-
}
|
|
357
|
-
if (isNavigationRequest(path, c.req.header("Accept"))) {
|
|
358
|
-
const url = new URL(c.req.url);
|
|
359
|
-
const returnTo = sanitizeReturnTo(url.pathname + url.search);
|
|
360
|
-
return c.redirect(`/signin?returnTo=${encodeURIComponent(returnTo)}`);
|
|
361
|
-
}
|
|
362
|
-
return c.json({ error: "unauthorized" }, 401);
|
|
363
|
-
};
|
|
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
|
+
};
|
|
364
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
|
+
*/
|
|
365
447
|
function mountFactoryAuth(app, options = {}) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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;
|
|
371
453
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
createFactoryRouteAuth,
|
|
376
|
-
ensureFactoryAuthUser,
|
|
377
|
-
factoryAuthTenant,
|
|
378
|
-
getBearerToken,
|
|
379
|
-
getFactoryAuthOrgId,
|
|
380
|
-
getFactoryAuthUser,
|
|
381
|
-
getFactoryAuthUserId,
|
|
382
|
-
getWorkOSProvider,
|
|
383
|
-
isCrossSiteAuth,
|
|
384
|
-
isOrganizationAdmin,
|
|
385
|
-
isWorkOSAuth,
|
|
386
|
-
mountFactoryAuth,
|
|
387
|
-
registerAuthRoutes,
|
|
388
|
-
sanitizeReturnTo
|
|
389
|
-
};
|
|
454
|
+
//#endregion
|
|
455
|
+
export { buildAuthRoutes, createFactoryAuthGate, createFactoryRouteAuth, ensureFactoryAuthUser, factoryAuthTenant, getBearerToken, getFactoryAuthOrgId, getFactoryAuthUser, getFactoryAuthUserId, getWorkOSProvider, isCrossSiteAuth, isOrganizationAdmin, isWorkOSAuth, mountFactoryAuth, registerAuthRoutes, sanitizeReturnTo };
|
|
456
|
+
|
|
390
457
|
//# sourceMappingURL=auth.js.map
|