@mastra/auth-cloud 0.0.1 → 1.1.0
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 +63 -0
- package/LICENSE.md +30 -0
- package/README.md +65 -1
- package/dist/auth-provider.d.ts +198 -0
- package/dist/auth-provider.d.ts.map +1 -0
- package/dist/client.d.ts +110 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/error.d.ts +65 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/index.cjs +855 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +850 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/index.d.ts +9 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/network.d.ts +20 -0
- package/dist/oauth/network.d.ts.map +1 -0
- package/dist/oauth/oauth.d.ts +68 -0
- package/dist/oauth/oauth.d.ts.map +1 -0
- package/dist/oauth/state.d.ts +47 -0
- package/dist/oauth/state.d.ts.map +1 -0
- package/dist/pkce/cookie.d.ts +42 -0
- package/dist/pkce/cookie.d.ts.map +1 -0
- package/dist/pkce/error.d.ts +31 -0
- package/dist/pkce/error.d.ts.map +1 -0
- package/dist/pkce/index.d.ts +10 -0
- package/dist/pkce/index.d.ts.map +1 -0
- package/dist/pkce/pkce.d.ts +26 -0
- package/dist/pkce/pkce.d.ts.map +1 -0
- package/dist/rbac/index.d.ts +2 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/rbac-provider.d.ts +124 -0
- package/dist/rbac/rbac-provider.d.ts.map +1 -0
- package/dist/session/cookie.d.ts +32 -0
- package/dist/session/cookie.d.ts.map +1 -0
- package/dist/session/index.d.ts +9 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/session.d.ts +56 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +54 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto$1 = require('crypto');
|
|
4
|
+
var server = require('@mastra/core/server');
|
|
5
|
+
var ee = require('@mastra/core/auth/ee');
|
|
6
|
+
|
|
7
|
+
// src/error.ts
|
|
8
|
+
var AuthError = class _AuthError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
cause;
|
|
11
|
+
cloudCode;
|
|
12
|
+
cloudMessage;
|
|
13
|
+
constructor(code, message, options) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "AuthError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.cause = options?.cause;
|
|
18
|
+
this.cloudCode = options?.cloudCode;
|
|
19
|
+
this.cloudMessage = options?.cloudMessage;
|
|
20
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Factory: OAuth state parameter is invalid or malformed.
|
|
24
|
+
*/
|
|
25
|
+
static invalidState() {
|
|
26
|
+
return new _AuthError("invalid_state", "OAuth state parameter is invalid or malformed.");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Factory: OAuth state parameter does not match expected value.
|
|
30
|
+
*/
|
|
31
|
+
static stateMismatch() {
|
|
32
|
+
return new _AuthError("state_mismatch", "OAuth state parameter does not match. Possible CSRF attack.");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Factory: Authorization code is missing from callback.
|
|
36
|
+
*/
|
|
37
|
+
static missingCode() {
|
|
38
|
+
return new _AuthError("missing_code", "Authorization code is missing from OAuth callback.");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Factory: Token exchange with Cloud API failed.
|
|
42
|
+
*/
|
|
43
|
+
static tokenExchangeFailed(options) {
|
|
44
|
+
return new _AuthError("token_exchange_failed", "Failed to exchange authorization code for tokens.", options);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Factory: Token verification failed.
|
|
48
|
+
*/
|
|
49
|
+
static verificationFailed() {
|
|
50
|
+
return new _AuthError("verification_failed", "Token verification failed.");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Factory: Session is invalid.
|
|
54
|
+
*/
|
|
55
|
+
static sessionInvalid() {
|
|
56
|
+
return new _AuthError("session_invalid", "Session is invalid or has been revoked.");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Factory: Session has expired.
|
|
60
|
+
*/
|
|
61
|
+
static sessionExpired() {
|
|
62
|
+
return new _AuthError("session_expired", "Session has expired. Please log in again.");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Factory: Network error during API call.
|
|
66
|
+
*/
|
|
67
|
+
static networkError(cause) {
|
|
68
|
+
return new _AuthError("network_error", "Network error occurred while communicating with Cloud API.", { cause });
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Factory: Cloud API returned an error.
|
|
72
|
+
*/
|
|
73
|
+
static cloudApiError(options) {
|
|
74
|
+
const message = options?.cloudMessage ?? "Cloud API returned an error.";
|
|
75
|
+
return new _AuthError("cloud_api_error", message, options);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/oauth/state.ts
|
|
80
|
+
function encodeState(csrf, returnTo) {
|
|
81
|
+
const data = { csrf, returnTo };
|
|
82
|
+
const json = JSON.stringify(data);
|
|
83
|
+
return Buffer.from(json).toString("base64url");
|
|
84
|
+
}
|
|
85
|
+
function decodeState(state) {
|
|
86
|
+
try {
|
|
87
|
+
const json = Buffer.from(state, "base64url").toString();
|
|
88
|
+
const data = JSON.parse(json);
|
|
89
|
+
if (typeof data.csrf !== "string" || typeof data.returnTo !== "string") {
|
|
90
|
+
throw new Error("Missing required fields");
|
|
91
|
+
}
|
|
92
|
+
return data;
|
|
93
|
+
} catch {
|
|
94
|
+
throw AuthError.invalidState();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function validateReturnTo(returnTo, requestOrigin) {
|
|
98
|
+
if (!returnTo) {
|
|
99
|
+
return "/";
|
|
100
|
+
}
|
|
101
|
+
if (returnTo.startsWith("/") && !returnTo.startsWith("//")) {
|
|
102
|
+
return returnTo;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(returnTo);
|
|
106
|
+
const origin = new URL(requestOrigin);
|
|
107
|
+
if (parsed.origin === origin.origin) {
|
|
108
|
+
return returnTo;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
return "/";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/oauth/network.ts
|
|
116
|
+
async function fetchWithRetry(url, options) {
|
|
117
|
+
try {
|
|
118
|
+
return await fetch(url, options);
|
|
119
|
+
} catch {
|
|
120
|
+
try {
|
|
121
|
+
return await fetch(url, options);
|
|
122
|
+
} catch (retryError) {
|
|
123
|
+
throw AuthError.networkError(retryError instanceof Error ? retryError : void 0);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/pkce/error.ts
|
|
129
|
+
var PKCEError = class _PKCEError extends Error {
|
|
130
|
+
code;
|
|
131
|
+
cause;
|
|
132
|
+
constructor(code, message, cause) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "PKCEError";
|
|
135
|
+
this.code = code;
|
|
136
|
+
this.cause = cause;
|
|
137
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Factory: PKCE verifier cookie not found.
|
|
141
|
+
*/
|
|
142
|
+
static missingVerifier() {
|
|
143
|
+
return new _PKCEError(
|
|
144
|
+
"MISSING_VERIFIER",
|
|
145
|
+
"PKCE verifier cookie not found. Authorization flow may have expired or was not initiated properly."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Factory: PKCE verifier has expired.
|
|
150
|
+
*/
|
|
151
|
+
static expired() {
|
|
152
|
+
return new _PKCEError("EXPIRED", "PKCE verifier has expired. Please restart the authorization flow.");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Factory: PKCE verifier cookie is malformed.
|
|
156
|
+
*/
|
|
157
|
+
static invalid(cause) {
|
|
158
|
+
return new _PKCEError("INVALID", "PKCE verifier cookie is malformed or invalid.", cause);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/pkce/cookie.ts
|
|
163
|
+
var PKCE_COOKIE_NAME = "mastra_pkce_verifier";
|
|
164
|
+
function setPKCECookie(verifier, state, isProduction) {
|
|
165
|
+
const ttlSeconds = 5 * 60;
|
|
166
|
+
const data = {
|
|
167
|
+
verifier,
|
|
168
|
+
state,
|
|
169
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
170
|
+
};
|
|
171
|
+
const encoded = encodeURIComponent(JSON.stringify(data));
|
|
172
|
+
let cookie = `${PKCE_COOKIE_NAME}=${encoded}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${ttlSeconds}`;
|
|
173
|
+
if (isProduction) {
|
|
174
|
+
cookie += "; Secure";
|
|
175
|
+
}
|
|
176
|
+
return cookie;
|
|
177
|
+
}
|
|
178
|
+
function parsePKCECookie(cookieHeader) {
|
|
179
|
+
if (!cookieHeader) {
|
|
180
|
+
throw PKCEError.missingVerifier();
|
|
181
|
+
}
|
|
182
|
+
const match = cookieHeader.match(new RegExp(`${PKCE_COOKIE_NAME}=([^;]+)`));
|
|
183
|
+
if (!match?.[1]) {
|
|
184
|
+
throw PKCEError.missingVerifier();
|
|
185
|
+
}
|
|
186
|
+
let data;
|
|
187
|
+
try {
|
|
188
|
+
data = JSON.parse(decodeURIComponent(match[1]));
|
|
189
|
+
} catch (e) {
|
|
190
|
+
throw PKCEError.invalid(e instanceof Error ? e : void 0);
|
|
191
|
+
}
|
|
192
|
+
if (data.expiresAt < Date.now()) {
|
|
193
|
+
throw PKCEError.expired();
|
|
194
|
+
}
|
|
195
|
+
return data;
|
|
196
|
+
}
|
|
197
|
+
function clearPKCECookie() {
|
|
198
|
+
return `${PKCE_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`;
|
|
199
|
+
}
|
|
200
|
+
function generateCodeVerifier() {
|
|
201
|
+
return crypto$1.randomBytes(32).toString("base64url");
|
|
202
|
+
}
|
|
203
|
+
function computeCodeChallenge(verifier) {
|
|
204
|
+
return crypto$1.createHash("sha256").update(verifier).digest("base64url");
|
|
205
|
+
}
|
|
206
|
+
function generateState() {
|
|
207
|
+
return crypto$1.randomBytes(16).toString("base64url");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/oauth/oauth.ts
|
|
211
|
+
function getLoginUrl(options) {
|
|
212
|
+
const { projectId, cloudBaseUrl, callbackUrl, returnTo, requestOrigin, isProduction } = options;
|
|
213
|
+
const verifier = generateCodeVerifier();
|
|
214
|
+
const challenge = computeCodeChallenge(verifier);
|
|
215
|
+
const csrf = generateState();
|
|
216
|
+
const validatedReturnTo = validateReturnTo(returnTo, requestOrigin);
|
|
217
|
+
const state = encodeState(csrf, validatedReturnTo);
|
|
218
|
+
const url = new URL("/auth/oss", cloudBaseUrl);
|
|
219
|
+
url.searchParams.set("project_id", projectId);
|
|
220
|
+
url.searchParams.set("code_challenge", challenge);
|
|
221
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
222
|
+
url.searchParams.set("redirect_uri", callbackUrl);
|
|
223
|
+
url.searchParams.set("state", state);
|
|
224
|
+
const isProductionEnv = isProduction ?? process.env.NODE_ENV === "production";
|
|
225
|
+
const pkceCookie = setPKCECookie(verifier, csrf, isProductionEnv);
|
|
226
|
+
return {
|
|
227
|
+
url: url.toString(),
|
|
228
|
+
cookies: [pkceCookie]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function handleCallback(options) {
|
|
232
|
+
const { projectId, cloudBaseUrl, redirectUri, code, state, cookieHeader } = options;
|
|
233
|
+
const pkceData = parsePKCECookie(cookieHeader);
|
|
234
|
+
const stateData = decodeState(state);
|
|
235
|
+
if (stateData.csrf !== pkceData.state) {
|
|
236
|
+
throw AuthError.stateMismatch();
|
|
237
|
+
}
|
|
238
|
+
const response = await fetchWithRetry(`${cloudBaseUrl}/auth/callback`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: {
|
|
241
|
+
"Content-Type": "application/json",
|
|
242
|
+
"X-Project-ID": projectId
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
code,
|
|
246
|
+
redirect_uri: redirectUri,
|
|
247
|
+
code_verifier: pkceData.verifier
|
|
248
|
+
})
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
let cloudCode;
|
|
252
|
+
let cloudMessage;
|
|
253
|
+
try {
|
|
254
|
+
const errorBody = await response.json();
|
|
255
|
+
cloudCode = errorBody.code;
|
|
256
|
+
cloudMessage = errorBody.message;
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
throw AuthError.tokenExchangeFailed({ cloudCode, cloudMessage });
|
|
260
|
+
}
|
|
261
|
+
const body = await response.json();
|
|
262
|
+
const verifyResponse = await fetchWithRetry(`${cloudBaseUrl}/auth/verify`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
Authorization: `Bearer ${body.access_token}`,
|
|
266
|
+
"X-Project-ID": projectId
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
if (!verifyResponse.ok) {
|
|
270
|
+
throw AuthError.verificationFailed();
|
|
271
|
+
}
|
|
272
|
+
const verifyBody = await verifyResponse.json();
|
|
273
|
+
const clearCookie = clearPKCECookie();
|
|
274
|
+
return {
|
|
275
|
+
user: {
|
|
276
|
+
id: verifyBody.sub,
|
|
277
|
+
email: verifyBody.email,
|
|
278
|
+
name: verifyBody.name,
|
|
279
|
+
avatar: verifyBody.avatar_url,
|
|
280
|
+
role: verifyBody.role
|
|
281
|
+
},
|
|
282
|
+
accessToken: body.access_token,
|
|
283
|
+
returnTo: stateData.returnTo,
|
|
284
|
+
cookies: [clearCookie]
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/session/cookie.ts
|
|
289
|
+
var SESSION_COOKIE_NAME = "mastra_cloud_session";
|
|
290
|
+
function setSessionCookie(token, isProduction) {
|
|
291
|
+
const ttlSeconds = 24 * 60 * 60;
|
|
292
|
+
let cookie = `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${ttlSeconds}`;
|
|
293
|
+
if (isProduction) {
|
|
294
|
+
cookie += "; Secure";
|
|
295
|
+
}
|
|
296
|
+
return cookie;
|
|
297
|
+
}
|
|
298
|
+
function parseSessionCookie(cookieHeader) {
|
|
299
|
+
if (!cookieHeader) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const match = cookieHeader.match(new RegExp(`${SESSION_COOKIE_NAME}=([^;]+)`));
|
|
303
|
+
return match?.[1] ?? null;
|
|
304
|
+
}
|
|
305
|
+
function clearSessionCookie() {
|
|
306
|
+
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/session/session.ts
|
|
310
|
+
async function verifyToken(options) {
|
|
311
|
+
const { projectId, cloudBaseUrl, token } = options;
|
|
312
|
+
const response = await fetchWithRetry(`${cloudBaseUrl}/auth/verify`, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: {
|
|
315
|
+
Authorization: `Bearer ${token}`,
|
|
316
|
+
"X-Project-ID": projectId
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
throw AuthError.verificationFailed();
|
|
321
|
+
}
|
|
322
|
+
const body = await response.json();
|
|
323
|
+
if (body.token_type === "project_api_token") {
|
|
324
|
+
return {
|
|
325
|
+
user: {
|
|
326
|
+
id: "api-token",
|
|
327
|
+
email: void 0,
|
|
328
|
+
name: void 0,
|
|
329
|
+
avatar: void 0
|
|
330
|
+
},
|
|
331
|
+
role: body.role
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
user: {
|
|
336
|
+
id: body.sub,
|
|
337
|
+
email: body.email,
|
|
338
|
+
name: body.name,
|
|
339
|
+
avatar: body.avatar_url
|
|
340
|
+
},
|
|
341
|
+
role: body.role
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function validateSession(options) {
|
|
345
|
+
const { projectId, cloudBaseUrl, sessionToken } = options;
|
|
346
|
+
try {
|
|
347
|
+
const response = await fetchWithRetry(`${cloudBaseUrl}/auth/session/validate`, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: {
|
|
350
|
+
"Content-Type": "application/json",
|
|
351
|
+
Authorization: `Bearer ${sessionToken}`,
|
|
352
|
+
"X-Project-ID": projectId
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return await response.json();
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function destroySession(options) {
|
|
364
|
+
const { cloudBaseUrl, sessionToken } = options;
|
|
365
|
+
await fetchWithRetry(`${cloudBaseUrl}/auth/session/destroy`, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
Authorization: `Bearer ${sessionToken}`
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function getLogoutUrl(cloudBaseUrl, postLogoutRedirectUri, idTokenHint) {
|
|
373
|
+
const url = new URL("/auth/logout", cloudBaseUrl);
|
|
374
|
+
url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectUri);
|
|
375
|
+
url.searchParams.set("id_token_hint", idTokenHint);
|
|
376
|
+
return url.toString();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/client.ts
|
|
380
|
+
var MastraCloudAuth = class {
|
|
381
|
+
config;
|
|
382
|
+
constructor(config) {
|
|
383
|
+
this.config = config;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Generate login URL for OAuth authorization.
|
|
387
|
+
*
|
|
388
|
+
* @param options - Login options
|
|
389
|
+
* @returns URL to redirect to and cookies to set
|
|
390
|
+
*/
|
|
391
|
+
getLoginUrl(options) {
|
|
392
|
+
return getLoginUrl({
|
|
393
|
+
projectId: this.config.projectId,
|
|
394
|
+
cloudBaseUrl: this.config.cloudBaseUrl,
|
|
395
|
+
callbackUrl: this.config.callbackUrl,
|
|
396
|
+
returnTo: options.returnTo,
|
|
397
|
+
requestOrigin: options.requestOrigin,
|
|
398
|
+
isProduction: this.config.isProduction
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Handle OAuth callback after authorization.
|
|
403
|
+
*
|
|
404
|
+
* @param options - Callback parameters
|
|
405
|
+
* @returns User info, tokens, and redirect URL
|
|
406
|
+
*/
|
|
407
|
+
handleCallback(options) {
|
|
408
|
+
return handleCallback({
|
|
409
|
+
projectId: this.config.projectId,
|
|
410
|
+
cloudBaseUrl: this.config.cloudBaseUrl,
|
|
411
|
+
redirectUri: this.config.callbackUrl,
|
|
412
|
+
...options
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Verify an access token.
|
|
417
|
+
*
|
|
418
|
+
* @param token - Access token to verify
|
|
419
|
+
* @returns User and role information
|
|
420
|
+
*/
|
|
421
|
+
verifyToken(token) {
|
|
422
|
+
return verifyToken({ projectId: this.config.projectId, cloudBaseUrl: this.config.cloudBaseUrl, token });
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Validate an existing session.
|
|
426
|
+
*
|
|
427
|
+
* @param sessionToken - Session token to validate
|
|
428
|
+
* @returns Session data if valid, null otherwise
|
|
429
|
+
*/
|
|
430
|
+
validateSession(sessionToken) {
|
|
431
|
+
return validateSession({ projectId: this.config.projectId, cloudBaseUrl: this.config.cloudBaseUrl, sessionToken });
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Destroy a session (server-side logout).
|
|
435
|
+
*
|
|
436
|
+
* @param sessionToken - Session token to destroy
|
|
437
|
+
*/
|
|
438
|
+
destroySession(sessionToken) {
|
|
439
|
+
return destroySession({ cloudBaseUrl: this.config.cloudBaseUrl, sessionToken });
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get the logout URL for client-side redirect.
|
|
443
|
+
*
|
|
444
|
+
* @param postLogoutRedirectUri - URL to redirect to after logout
|
|
445
|
+
* @param idTokenHint - The access token
|
|
446
|
+
* @returns Full logout URL with redirect and token parameters
|
|
447
|
+
*/
|
|
448
|
+
getLogoutUrl(postLogoutRedirectUri, idTokenHint) {
|
|
449
|
+
return getLogoutUrl(this.config.cloudBaseUrl, postLogoutRedirectUri, idTokenHint);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Create Set-Cookie header value for session token.
|
|
453
|
+
*
|
|
454
|
+
* @param token - Session token to store
|
|
455
|
+
* @returns Set-Cookie header value
|
|
456
|
+
*/
|
|
457
|
+
setSessionCookie(token) {
|
|
458
|
+
return setSessionCookie(token, this.config.isProduction ?? process.env.NODE_ENV === "production");
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Create Set-Cookie header value to clear session cookie.
|
|
462
|
+
*
|
|
463
|
+
* @returns Set-Cookie header value
|
|
464
|
+
*/
|
|
465
|
+
clearSessionCookie() {
|
|
466
|
+
return clearSessionCookie();
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var MastraCloudAuthProvider = class extends server.MastraAuthProvider {
|
|
470
|
+
client;
|
|
471
|
+
/** Marker for EE license exemption - MastraCloudAuth is exempt */
|
|
472
|
+
isMastraCloudAuth = true;
|
|
473
|
+
/**
|
|
474
|
+
* Cookie header for handleCallback PKCE validation.
|
|
475
|
+
* Set via setCallbackCookieHeader() before handleCallback() is called.
|
|
476
|
+
* @internal
|
|
477
|
+
*/
|
|
478
|
+
_lastCallbackCookieHeader = null;
|
|
479
|
+
constructor(options) {
|
|
480
|
+
super({ name: options?.name ?? "cloud" });
|
|
481
|
+
this.client = new MastraCloudAuth({
|
|
482
|
+
projectId: options.projectId,
|
|
483
|
+
cloudBaseUrl: options.cloudBaseUrl,
|
|
484
|
+
callbackUrl: options.callbackUrl,
|
|
485
|
+
isProduction: options.isProduction
|
|
486
|
+
});
|
|
487
|
+
this.registerOptions(options);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Set cookie header for handleCallback PKCE validation.
|
|
491
|
+
* Must be called before handleCallback() to pass cookie header.
|
|
492
|
+
*
|
|
493
|
+
* @param cookieHeader - Cookie header from original request
|
|
494
|
+
*/
|
|
495
|
+
setCallbackCookieHeader(cookieHeader) {
|
|
496
|
+
this._lastCallbackCookieHeader = cookieHeader;
|
|
497
|
+
}
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// MastraAuthProvider Implementation
|
|
500
|
+
// ============================================================================
|
|
501
|
+
/**
|
|
502
|
+
* Authenticate a bearer token or session cookie.
|
|
503
|
+
*
|
|
504
|
+
* Checks session cookie first, falls back to bearer token for API clients.
|
|
505
|
+
*
|
|
506
|
+
* @param token - Bearer token (from Authorization header)
|
|
507
|
+
* @param request - Hono or raw Request
|
|
508
|
+
* @returns Authenticated user with role, or null if invalid
|
|
509
|
+
*/
|
|
510
|
+
async authenticateToken(token, request) {
|
|
511
|
+
try {
|
|
512
|
+
const rawRequest = "raw" in request ? request.raw : request;
|
|
513
|
+
const cookieHeader = rawRequest.headers.get("cookie");
|
|
514
|
+
const sessionToken = parseSessionCookie(cookieHeader);
|
|
515
|
+
if (sessionToken) {
|
|
516
|
+
const { user, role } = await this.client.verifyToken(sessionToken);
|
|
517
|
+
return { ...user, role };
|
|
518
|
+
}
|
|
519
|
+
if (token) {
|
|
520
|
+
const { user, role } = await this.client.verifyToken(token);
|
|
521
|
+
return { ...user, role };
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
} catch {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Authorize a user for access.
|
|
530
|
+
*
|
|
531
|
+
* Simple validation - detailed permission checking happens in server
|
|
532
|
+
* middleware via checkRoutePermission(), not authorizeUser().
|
|
533
|
+
*
|
|
534
|
+
* @param user - Authenticated user
|
|
535
|
+
* @returns True if user has valid id
|
|
536
|
+
*/
|
|
537
|
+
authorizeUser(user) {
|
|
538
|
+
return !!user?.id;
|
|
539
|
+
}
|
|
540
|
+
// ============================================================================
|
|
541
|
+
// ISSOProvider Implementation
|
|
542
|
+
// ============================================================================
|
|
543
|
+
/**
|
|
544
|
+
* Cached login result for getLoginCookies() to retrieve cookies.
|
|
545
|
+
* @internal
|
|
546
|
+
*/
|
|
547
|
+
_lastLoginResult = null;
|
|
548
|
+
/**
|
|
549
|
+
* Get URL to redirect user to for SSO login.
|
|
550
|
+
*
|
|
551
|
+
* @param redirectUri - Callback URL after authentication
|
|
552
|
+
* @param state - State parameter (format: uuid|encodedPostLoginRedirect)
|
|
553
|
+
* @returns Full authorization URL
|
|
554
|
+
*/
|
|
555
|
+
getLoginUrl(redirectUri, state) {
|
|
556
|
+
let postLoginRedirect = "/";
|
|
557
|
+
if (state && state.includes("|")) {
|
|
558
|
+
const parts = state.split("|", 2);
|
|
559
|
+
const encodedRedirect = parts[1];
|
|
560
|
+
if (encodedRedirect) {
|
|
561
|
+
try {
|
|
562
|
+
postLoginRedirect = decodeURIComponent(encodedRedirect);
|
|
563
|
+
} catch {
|
|
564
|
+
postLoginRedirect = "/";
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const redirectUrl = new URL(redirectUri);
|
|
569
|
+
const origin = redirectUrl.origin;
|
|
570
|
+
const result = this.client.getLoginUrl({
|
|
571
|
+
returnTo: postLoginRedirect,
|
|
572
|
+
requestOrigin: origin
|
|
573
|
+
});
|
|
574
|
+
this._lastLoginResult = result;
|
|
575
|
+
return result.url;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get cookies to set during login redirect (PKCE verifier).
|
|
579
|
+
* Must be called after getLoginUrl() in same request.
|
|
580
|
+
*
|
|
581
|
+
* @returns Array of Set-Cookie header values
|
|
582
|
+
*/
|
|
583
|
+
getLoginCookies() {
|
|
584
|
+
const cookies = this._lastLoginResult?.cookies;
|
|
585
|
+
this._lastLoginResult = null;
|
|
586
|
+
return cookies;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Handle OAuth callback, exchange code for tokens and user.
|
|
590
|
+
*
|
|
591
|
+
* @param code - Authorization code from callback
|
|
592
|
+
* @param state - State parameter for CSRF validation
|
|
593
|
+
* @returns User, tokens, and session cookies
|
|
594
|
+
*/
|
|
595
|
+
async handleCallback(code, state) {
|
|
596
|
+
const cookieHeader = this._lastCallbackCookieHeader;
|
|
597
|
+
this._lastCallbackCookieHeader = null;
|
|
598
|
+
const result = await this.client.handleCallback({
|
|
599
|
+
code,
|
|
600
|
+
state,
|
|
601
|
+
cookieHeader
|
|
602
|
+
});
|
|
603
|
+
const sessionCookie = this.client.setSessionCookie(result.accessToken);
|
|
604
|
+
return {
|
|
605
|
+
user: result.user,
|
|
606
|
+
// Already has role from handleCallback
|
|
607
|
+
tokens: {
|
|
608
|
+
accessToken: result.accessToken
|
|
609
|
+
},
|
|
610
|
+
cookies: [...result.cookies, sessionCookie]
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Get configuration for rendering login button in UI.
|
|
615
|
+
*
|
|
616
|
+
* @returns Login button configuration
|
|
617
|
+
*/
|
|
618
|
+
getLoginButtonConfig() {
|
|
619
|
+
return {
|
|
620
|
+
provider: "mastra",
|
|
621
|
+
text: "Sign in with Mastra Cloud"
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get logout URL for client-side redirect.
|
|
626
|
+
* Requires the request to extract the session token for id_token_hint.
|
|
627
|
+
*
|
|
628
|
+
* @param redirectUri - URL to redirect to after logout
|
|
629
|
+
* @param request - Request to extract session token from
|
|
630
|
+
* @returns Logout URL with redirect and token parameters, or null if no session
|
|
631
|
+
*/
|
|
632
|
+
getLogoutUrl(redirectUri, request) {
|
|
633
|
+
const sessionToken = request ? this.getSessionIdFromRequest(request) : null;
|
|
634
|
+
if (!sessionToken) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
return this.client.getLogoutUrl(redirectUri, sessionToken);
|
|
638
|
+
}
|
|
639
|
+
// ============================================================================
|
|
640
|
+
// ISessionProvider Implementation
|
|
641
|
+
// ============================================================================
|
|
642
|
+
/**
|
|
643
|
+
* Create a new session for a user.
|
|
644
|
+
*
|
|
645
|
+
* For Cloud auth, sessions are created via handleCallback.
|
|
646
|
+
* This method builds a Session object for interface compatibility.
|
|
647
|
+
*
|
|
648
|
+
* @param userId - User to create session for
|
|
649
|
+
* @param metadata - Optional metadata (accessToken can be passed here)
|
|
650
|
+
* @returns Session object
|
|
651
|
+
*/
|
|
652
|
+
async createSession(userId, metadata) {
|
|
653
|
+
const now = /* @__PURE__ */ new Date();
|
|
654
|
+
const expiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
|
|
655
|
+
return {
|
|
656
|
+
id: metadata?.accessToken ?? crypto.randomUUID(),
|
|
657
|
+
userId,
|
|
658
|
+
createdAt: now,
|
|
659
|
+
expiresAt,
|
|
660
|
+
metadata
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Validate a session and return it if valid.
|
|
665
|
+
*
|
|
666
|
+
* @param sessionId - Session token to validate
|
|
667
|
+
* @returns Session object or null if invalid/expired
|
|
668
|
+
*/
|
|
669
|
+
async validateSession(sessionId) {
|
|
670
|
+
const session = await this.client.validateSession(sessionId);
|
|
671
|
+
if (!session) return null;
|
|
672
|
+
return {
|
|
673
|
+
id: sessionId,
|
|
674
|
+
userId: session.userId,
|
|
675
|
+
createdAt: new Date(session.createdAt),
|
|
676
|
+
expiresAt: new Date(session.expiresAt)
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Destroy a session (logout).
|
|
681
|
+
*
|
|
682
|
+
* @param sessionId - Session token to destroy
|
|
683
|
+
*/
|
|
684
|
+
async destroySession(sessionId) {
|
|
685
|
+
await this.client.destroySession(sessionId);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Refresh a session, extending its expiry.
|
|
689
|
+
* Cloud handles refresh internally, so just validate.
|
|
690
|
+
*
|
|
691
|
+
* @param sessionId - Session token to refresh
|
|
692
|
+
* @returns Session object or null if invalid
|
|
693
|
+
*/
|
|
694
|
+
async refreshSession(sessionId) {
|
|
695
|
+
return this.validateSession(sessionId);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Extract session ID from an incoming request.
|
|
699
|
+
*
|
|
700
|
+
* @param request - Incoming HTTP request
|
|
701
|
+
* @returns Session token or null if not present
|
|
702
|
+
*/
|
|
703
|
+
getSessionIdFromRequest(request) {
|
|
704
|
+
return parseSessionCookie(request.headers.get("cookie"));
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Create response headers to set session cookie.
|
|
708
|
+
*
|
|
709
|
+
* @param session - Session to encode (id is the access token)
|
|
710
|
+
* @returns Headers object with Set-Cookie
|
|
711
|
+
*/
|
|
712
|
+
getSessionHeaders(session) {
|
|
713
|
+
return { "Set-Cookie": this.client.setSessionCookie(session.id) };
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Create response headers to clear session (for logout).
|
|
717
|
+
*
|
|
718
|
+
* @returns Headers object to clear session cookie
|
|
719
|
+
*/
|
|
720
|
+
getClearSessionHeaders() {
|
|
721
|
+
return { "Set-Cookie": this.client.clearSessionCookie() };
|
|
722
|
+
}
|
|
723
|
+
// ============================================================================
|
|
724
|
+
// IUserProvider Implementation
|
|
725
|
+
// ============================================================================
|
|
726
|
+
/**
|
|
727
|
+
* Get current user from request (session cookie).
|
|
728
|
+
*
|
|
729
|
+
* @param request - Incoming HTTP request
|
|
730
|
+
* @returns User with role or null if not authenticated
|
|
731
|
+
*/
|
|
732
|
+
async getCurrentUser(request) {
|
|
733
|
+
const sessionToken = this.getSessionIdFromRequest(request);
|
|
734
|
+
if (!sessionToken) return null;
|
|
735
|
+
try {
|
|
736
|
+
const { user, role } = await this.client.verifyToken(sessionToken);
|
|
737
|
+
return { ...user, role };
|
|
738
|
+
} catch {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Get user by ID.
|
|
744
|
+
* Cloud API doesn't have a /users/:id endpoint.
|
|
745
|
+
*
|
|
746
|
+
* @returns Always null (not supported)
|
|
747
|
+
*/
|
|
748
|
+
async getUser(_userId) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
var MastraRBACCloud = class {
|
|
753
|
+
options;
|
|
754
|
+
/**
|
|
755
|
+
* Expose roleMapping for middleware access.
|
|
756
|
+
* This allows the authorization middleware to resolve permissions
|
|
757
|
+
* without needing to call the async methods.
|
|
758
|
+
*/
|
|
759
|
+
get roleMapping() {
|
|
760
|
+
return this.options.roleMapping;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Create a new Mastra Cloud RBAC provider.
|
|
764
|
+
*
|
|
765
|
+
* @param options - RBAC configuration options
|
|
766
|
+
*/
|
|
767
|
+
constructor(options) {
|
|
768
|
+
this.options = options;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get all roles for a user.
|
|
772
|
+
*
|
|
773
|
+
* Returns the user's role as a single-element array, or empty array if no role.
|
|
774
|
+
* Cloud uses a single-role model (role is attached via verifyToken()).
|
|
775
|
+
*
|
|
776
|
+
* @param user - Cloud user to get roles for
|
|
777
|
+
* @returns Array containing user's role, or empty array
|
|
778
|
+
*/
|
|
779
|
+
async getRoles(user) {
|
|
780
|
+
return user.role ? [user.role] : [];
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Check if a user has a specific role.
|
|
784
|
+
*
|
|
785
|
+
* @param user - Cloud user to check
|
|
786
|
+
* @param role - Role name to check for
|
|
787
|
+
* @returns True if user has the role
|
|
788
|
+
*/
|
|
789
|
+
async hasRole(user, role) {
|
|
790
|
+
const roles = await this.getRoles(user);
|
|
791
|
+
return roles.includes(role);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Get all permissions for a user by mapping their role.
|
|
795
|
+
*
|
|
796
|
+
* Uses the configured roleMapping to translate the user's role
|
|
797
|
+
* into Mastra permission strings.
|
|
798
|
+
*
|
|
799
|
+
* If the user has no role, the _default permissions from the
|
|
800
|
+
* role mapping are applied.
|
|
801
|
+
*
|
|
802
|
+
* @param user - Cloud user to get permissions for
|
|
803
|
+
* @returns Array of permission strings
|
|
804
|
+
*/
|
|
805
|
+
async getPermissions(user) {
|
|
806
|
+
const roles = await this.getRoles(user);
|
|
807
|
+
if (roles.length === 0) {
|
|
808
|
+
return this.options.roleMapping["_default"] ?? [];
|
|
809
|
+
}
|
|
810
|
+
return ee.resolvePermissionsFromMapping(roles, this.options.roleMapping);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Check if a user has a specific permission.
|
|
814
|
+
*
|
|
815
|
+
* Uses wildcard matching to check if the user's permissions
|
|
816
|
+
* grant access to the required permission.
|
|
817
|
+
*
|
|
818
|
+
* @param user - Cloud user to check
|
|
819
|
+
* @param permission - Permission to check for (e.g., 'agents:read')
|
|
820
|
+
* @returns True if user has the permission
|
|
821
|
+
*/
|
|
822
|
+
async hasPermission(user, permission) {
|
|
823
|
+
const permissions = await this.getPermissions(user);
|
|
824
|
+
return permissions.some((p) => ee.matchesPermission(p, permission));
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Check if a user has ALL of the specified permissions.
|
|
828
|
+
*
|
|
829
|
+
* @param user - Cloud user to check
|
|
830
|
+
* @param permissions - Array of permissions to check for
|
|
831
|
+
* @returns True if user has all permissions
|
|
832
|
+
*/
|
|
833
|
+
async hasAllPermissions(user, permissions) {
|
|
834
|
+
const userPermissions = await this.getPermissions(user);
|
|
835
|
+
return permissions.every((required) => userPermissions.some((p) => ee.matchesPermission(p, required)));
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Check if a user has ANY of the specified permissions.
|
|
839
|
+
*
|
|
840
|
+
* @param user - Cloud user to check
|
|
841
|
+
* @param permissions - Array of permissions to check for
|
|
842
|
+
* @returns True if user has at least one permission
|
|
843
|
+
*/
|
|
844
|
+
async hasAnyPermission(user, permissions) {
|
|
845
|
+
const userPermissions = await this.getPermissions(user);
|
|
846
|
+
return permissions.some((required) => userPermissions.some((p) => ee.matchesPermission(p, required)));
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
exports.AuthError = AuthError;
|
|
851
|
+
exports.MastraCloudAuth = MastraCloudAuth;
|
|
852
|
+
exports.MastraCloudAuthProvider = MastraCloudAuthProvider;
|
|
853
|
+
exports.MastraRBACCloud = MastraRBACCloud;
|
|
854
|
+
//# sourceMappingURL=index.cjs.map
|
|
855
|
+
//# sourceMappingURL=index.cjs.map
|