better-auth 0.0.2-beta.7 → 0.0.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 (96) hide show
  1. package/dist/access.d.ts +4 -0
  2. package/dist/access.js +126 -0
  3. package/dist/access.js.map +1 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +553 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/client/plugins.d.ts +2436 -0
  8. package/dist/client/plugins.js +411 -0
  9. package/dist/client/plugins.js.map +1 -0
  10. package/dist/client-A2Mt04KQ.d.ts +3503 -0
  11. package/dist/client.d.ts +1433 -0
  12. package/dist/client.js +693 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/helper-B5_2Vzba.d.ts +14 -0
  15. package/dist/index-Dg4eEXZW.d.ts +24 -0
  16. package/dist/index-W5nXvJ-p.d.ts +1498 -0
  17. package/dist/index.d.ts +6 -4
  18. package/dist/index.js +2195 -1191
  19. package/dist/index.js.map +1 -1
  20. package/dist/next-js.d.ts +14 -0
  21. package/dist/next-js.js +14 -0
  22. package/dist/next-js.js.map +1 -0
  23. package/dist/plugins.d.ts +892 -49
  24. package/dist/plugins.js +3951 -253
  25. package/dist/plugins.js.map +1 -1
  26. package/dist/preact.d.ts +8 -0
  27. package/dist/preact.js +294 -0
  28. package/dist/preact.js.map +1 -0
  29. package/dist/react.d.ts +14 -0
  30. package/dist/react.js +314 -0
  31. package/dist/react.js.map +1 -0
  32. package/dist/schema-BOszzrbQ.d.ts +792 -0
  33. package/dist/social.d.ts +4 -0
  34. package/dist/social.js +509 -0
  35. package/dist/social.js.map +1 -0
  36. package/dist/solid-start.d.ts +18 -0
  37. package/dist/solid-start.js +14 -0
  38. package/dist/solid-start.js.map +1 -0
  39. package/dist/solid.d.ts +2790 -0
  40. package/dist/solid.js +306 -0
  41. package/dist/solid.js.map +1 -0
  42. package/dist/statement-COylZd3J.d.ts +81 -0
  43. package/dist/svelte-kit.d.ts +10 -7
  44. package/dist/svelte-kit.js +12 -17
  45. package/dist/svelte-kit.js.map +1 -1
  46. package/dist/svelte.d.ts +2791 -0
  47. package/dist/svelte.js +304 -0
  48. package/dist/svelte.js.map +1 -0
  49. package/dist/type-DbMyI3b5.d.ts +5724 -0
  50. package/dist/types.d.ts +7 -0
  51. package/dist/types.js +1 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/vue.d.ts +14 -0
  54. package/dist/vue.js +311 -0
  55. package/dist/vue.js.map +1 -0
  56. package/package.json +80 -54
  57. package/LICENSE +0 -21
  58. package/dist/actions.d.ts +0 -33
  59. package/dist/actions.js +0 -1373
  60. package/dist/actions.js.map +0 -1
  61. package/dist/adapters/drizzle-adapter.d.ts +0 -10
  62. package/dist/adapters/drizzle-adapter.js +0 -1095
  63. package/dist/adapters/drizzle-adapter.js.map +0 -1
  64. package/dist/adapters/memory.d.ts +0 -8
  65. package/dist/adapters/memory.js +0 -136
  66. package/dist/adapters/memory.js.map +0 -1
  67. package/dist/adapters/mongodb-adapter.d.ts +0 -9
  68. package/dist/adapters/mongodb-adapter.js +0 -97
  69. package/dist/adapters/mongodb-adapter.js.map +0 -1
  70. package/dist/adapters/prisma-adapter.d.ts +0 -7
  71. package/dist/adapters/prisma-adapter.js +0 -144
  72. package/dist/adapters/prisma-adapter.js.map +0 -1
  73. package/dist/adapters/redis-adapter.d.ts +0 -7
  74. package/dist/adapters/redis-adapter.js +0 -65
  75. package/dist/adapters/redis-adapter.js.map +0 -1
  76. package/dist/adapters.d.ts +0 -3
  77. package/dist/adapters.js +0 -206
  78. package/dist/adapters.js.map +0 -1
  79. package/dist/h3.d.ts +0 -10
  80. package/dist/h3.js +0 -326
  81. package/dist/h3.js.map +0 -1
  82. package/dist/hono.d.ts +0 -10
  83. package/dist/hono.js +0 -25
  84. package/dist/hono.js.map +0 -1
  85. package/dist/index-UcTu1vUg.d.ts +0 -107
  86. package/dist/next.d.ts +0 -17
  87. package/dist/next.js +0 -26
  88. package/dist/next.js.map +0 -1
  89. package/dist/options-CH15FEBw.d.ts +0 -1562
  90. package/dist/providers.d.ts +0 -3
  91. package/dist/providers.js +0 -653
  92. package/dist/providers.js.map +0 -1
  93. package/dist/routes/session.d.ts +0 -39
  94. package/dist/routes/session.js +0 -128
  95. package/dist/routes/session.js.map +0 -1
  96. package/dist/types-DAxaMWCy.d.ts +0 -136
package/dist/plugins.js CHANGED
@@ -1,311 +1,4009 @@
1
- // src/plugins/email-verification.ts
2
- import { base64url } from "jose";
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/plugins/organization/organization.ts
8
+ import { APIError as APIError5 } from "better-call";
9
+ import {
10
+ z as z11
11
+ } from "zod";
12
+
13
+ // src/api/call.ts
14
+ import {
15
+ createEndpointCreator,
16
+ createMiddleware,
17
+ createMiddlewareCreator
18
+ } from "better-call";
19
+ var optionsMiddleware = createMiddleware(async () => {
20
+ return {};
21
+ });
22
+ var createAuthMiddleware = createMiddlewareCreator({
23
+ use: [optionsMiddleware]
24
+ });
25
+ var createAuthEndpoint = createEndpointCreator({
26
+ use: [optionsMiddleware]
27
+ });
28
+
29
+ // src/api/routes/sign-in.ts
30
+ import { APIError } from "better-call";
31
+ import { generateCodeVerifier } from "oslo/oauth2";
32
+ import { Argon2id } from "oslo/password";
3
33
  import { z } from "zod";
4
34
 
5
- // src/jwt/index.ts
6
- import { SignJWT, decodeJwt, jwtVerify } from "jose";
7
- function parseJWT(jwt) {
8
- const decoded = decodeJwt(jwt);
9
- return decoded;
35
+ // src/social-providers/apple.ts
36
+ import "arctic";
37
+ import { parseJWT } from "oslo/jwt";
38
+ import { betterFetch } from "@better-fetch/fetch";
39
+
40
+ // src/error/better-auth-error.ts
41
+ var BetterAuthError = class extends Error {
42
+ constructor(message) {
43
+ super(message);
44
+ }
45
+ };
46
+
47
+ // src/utils/base-url.ts
48
+ function checkHasPath(url) {
49
+ try {
50
+ const parsedUrl = new URL(url);
51
+ return parsedUrl.pathname !== "/";
52
+ } catch (error) {
53
+ console.error("Invalid URL:", error);
54
+ return false;
55
+ }
56
+ }
57
+ function withPath(url, path = "/api/auth") {
58
+ const hasPath = checkHasPath(url);
59
+ if (hasPath) {
60
+ return {
61
+ baseURL: new URL(url).origin,
62
+ withPath: url
63
+ };
64
+ }
65
+ path = path.startsWith("/") ? path : `/${path}`;
66
+ return {
67
+ baseURL: url,
68
+ withPath: `${url}${path}`
69
+ };
10
70
  }
11
- function validateJWT(jwt, secret) {
12
- return jwtVerify(jwt, new TextEncoder().encode(secret));
71
+ function getBaseURL(url, path) {
72
+ if (url) {
73
+ return withPath(url, path);
74
+ }
75
+ const env = typeof process !== "undefined" ? process.env : typeof import.meta !== "undefined" ? (
76
+ //@ts-ignore
77
+ import.meta.env
78
+ ) : {};
79
+ const fromEnv = env.BETTER_AUTH_URL || env.AUTH_URL || env.NEXT_PUBLIC_AUTH_URL || env.NEXT_PUBLIC_BETTER_AUTH_URL || env.PUBLIC_AUTH_URL || env.PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_AUTH_URL;
80
+ if (fromEnv) {
81
+ return withPath(fromEnv, path);
82
+ }
83
+ const isDev = !fromEnv && (env.NODE_ENV === "development" || env.NODE_ENV === "test");
84
+ if (isDev) {
85
+ return {
86
+ baseURL: "http://localhost:3000",
87
+ withPath: "http://localhost:3000/api/auth"
88
+ };
89
+ }
90
+ throw new BetterAuthError(
91
+ "Could not infer baseURL from environment variables"
92
+ );
13
93
  }
14
- function createJWT({
15
- payload,
16
- secret,
17
- expiresIn,
18
- algorithm
19
- }) {
20
- return new SignJWT(payload).setProtectedHeader({
21
- alg: algorithm || "HS256"
22
- }).setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn).sign(new TextEncoder().encode(secret));
94
+
95
+ // src/social-providers/utils.ts
96
+ function getRedirectURI(providerId, redirectURI) {
97
+ return redirectURI || `${getBaseURL()}/api/auth/callback/${providerId}`;
23
98
  }
24
99
 
25
- // src/plugins/email-verification.ts
26
- var emailVerification = (opts) => {
27
- const options = {
28
- expiresIn: 60 * 60 * 24,
29
- providers: ["credential"],
30
- ...opts
100
+ // src/social-providers/apple.ts
101
+ var apple = ({
102
+ clientId,
103
+ clientSecret,
104
+ redirectURI
105
+ }) => {
106
+ const tokenEndpoint = "https://appleid.apple.com/auth/token";
107
+ redirectURI = getRedirectURI("apple", redirectURI);
108
+ return {
109
+ id: "apple",
110
+ name: "Apple",
111
+ createAuthorizationURL({ state, scopes }) {
112
+ const _scope = scopes || ["email", "name", "openid"];
113
+ return new URL(
114
+ `https://appleid.apple.com/auth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectURI}&scope=${_scope.join(
115
+ " "
116
+ )}&state=${state}`
117
+ );
118
+ },
119
+ validateAuthorizationCode: async (code) => {
120
+ const data = await betterFetch(tokenEndpoint, {
121
+ method: "POST",
122
+ body: new URLSearchParams({
123
+ client_id: clientId,
124
+ client_secret: clientSecret,
125
+ grant_type: "authorization_code",
126
+ code
127
+ }),
128
+ headers: {
129
+ "Content-Type": "application/x-www-form-urlencoded"
130
+ }
131
+ });
132
+ if (data.error) {
133
+ throw new BetterAuthError(data.error?.message || "");
134
+ }
135
+ return data.data;
136
+ },
137
+ async getUserInfo(token) {
138
+ const data = parseJWT(token.idToken())?.payload;
139
+ if (!data) {
140
+ return null;
141
+ }
142
+ return {
143
+ user: {
144
+ id: data.sub,
145
+ name: data.name,
146
+ email: data.email,
147
+ emailVerified: data.email_verified === "true"
148
+ },
149
+ data
150
+ };
151
+ }
31
152
  };
153
+ };
154
+
155
+ // src/social-providers/discord.ts
156
+ import { betterFetch as betterFetch2 } from "@better-fetch/fetch";
157
+ import { Discord } from "arctic";
158
+ var discord = ({
159
+ clientId,
160
+ clientSecret,
161
+ redirectURI
162
+ }) => {
163
+ const discordArctic = new Discord(
164
+ clientId,
165
+ clientSecret,
166
+ getRedirectURI("discord", redirectURI)
167
+ );
32
168
  return {
33
- id: "verify-email",
34
- name: "Email Verification",
35
- version: "0.0.1",
36
- hooks: {
37
- matcher(context) {
38
- const check = context.request.action === "signup" && options.providers.includes(context.request.body.provider);
39
- return check;
40
- },
41
- async after(context, response) {
42
- if (response.metadata?.isError) {
43
- return null;
44
- }
45
- const {
46
- data: { email }
47
- } = z.object({
48
- data: z.object({
49
- email: z.string()
50
- })
51
- }).parse(context.request.body);
52
- const token = await createJWT({
53
- payload: {
54
- email
55
- },
56
- secret: context.secret,
57
- expiresIn: options.expiresIn || 60 * 60 * 24
58
- });
59
- const encoded = base64url.encode(token);
60
- const url = `${context.request.url.toString()}/verify-email?token=${encoded}`;
61
- await options.sendEmail(email, url);
169
+ id: "discord",
170
+ name: "Discord",
171
+ createAuthorizationURL({ state, scopes }) {
172
+ const _scope = scopes || ["email"];
173
+ return discordArctic.createAuthorizationURL(state, _scope);
174
+ },
175
+ validateAuthorizationCode: discordArctic.validateAuthorizationCode,
176
+ async getUserInfo(token) {
177
+ const { data: profile, error } = await betterFetch2(
178
+ "https://discord.com/api/users/@me",
179
+ {
180
+ auth: {
181
+ type: "Bearer",
182
+ token: token.accessToken
183
+ }
184
+ }
185
+ );
186
+ if (error) {
62
187
  return null;
63
188
  }
189
+ return {
190
+ user: {
191
+ id: profile.id,
192
+ name: profile.display_name || profile.username || "",
193
+ email: profile.email,
194
+ emailVerified: profile.verified
195
+ },
196
+ data: profile
197
+ };
198
+ }
199
+ };
200
+ };
201
+
202
+ // src/social-providers/facebook.ts
203
+ import { betterFetch as betterFetch3 } from "@better-fetch/fetch";
204
+ import { Facebook } from "arctic";
205
+ var facebook = ({
206
+ clientId,
207
+ clientSecret,
208
+ redirectURI
209
+ }) => {
210
+ const facebookArctic = new Facebook(
211
+ clientId,
212
+ clientSecret,
213
+ getRedirectURI("facebook", redirectURI)
214
+ );
215
+ return {
216
+ id: "facebook",
217
+ name: "Facebook",
218
+ createAuthorizationURL({ state, scopes }) {
219
+ const _scopes = scopes || ["email", "public_profile"];
220
+ return facebookArctic.createAuthorizationURL(state, _scopes);
64
221
  },
65
- handler: async (context) => {
66
- const token = context.request.url.searchParams.get("token");
67
- if (!token) {
68
- return {
69
- status: 302,
70
- headers: {
71
- Location: `${options.redirectURL.error}?error=Token not found`
72
- },
73
- metadata: {
74
- isError: true
222
+ validateAuthorizationCode: facebookArctic.validateAuthorizationCode,
223
+ async getUserInfo(token) {
224
+ const { data: profile, error } = await betterFetch3(
225
+ "https://graph.facebook.com/me",
226
+ {
227
+ auth: {
228
+ type: "Bearer",
229
+ token: token.accessToken
75
230
  }
76
- };
231
+ }
232
+ );
233
+ if (error) {
234
+ return null;
77
235
  }
78
- const decoded = new TextDecoder().decode(base64url.decode(token));
79
- const isValid = await validateJWT(decoded, context.secret);
80
- if (!isValid) {
81
- return {
82
- status: 302,
236
+ return {
237
+ user: {
238
+ id: profile.id,
239
+ name: profile.name,
240
+ email: profile.email,
241
+ emailVerified: profile.email_verified
242
+ },
243
+ data: profile
244
+ };
245
+ }
246
+ };
247
+ };
248
+
249
+ // src/social-providers/github.ts
250
+ import { betterFetch as betterFetch4 } from "@better-fetch/fetch";
251
+ import { GitHub } from "arctic";
252
+ var github = ({
253
+ clientId,
254
+ clientSecret,
255
+ redirectURI
256
+ }) => {
257
+ const githubArctic = new GitHub(
258
+ clientId,
259
+ clientSecret,
260
+ getRedirectURI("github", redirectURI)
261
+ );
262
+ return {
263
+ id: "github",
264
+ name: "Github",
265
+ createAuthorizationURL({ state, scopes }) {
266
+ const _scopes = scopes || ["user:email"];
267
+ return githubArctic.createAuthorizationURL(state, _scopes);
268
+ },
269
+ validateAuthorizationCode: githubArctic.validateAuthorizationCode,
270
+ async getUserInfo(token) {
271
+ const { data: profile, error } = await betterFetch4(
272
+ "https://api.github.com/user",
273
+ {
274
+ method: "GET",
83
275
  headers: {
84
- Location: `${options.redirectURL.error}?error=Invalid token`
85
- },
86
- metadata: {
87
- isError: true
276
+ Authorization: `Bearer ${token.accessToken}`
88
277
  }
89
- };
278
+ }
279
+ );
280
+ if (error) {
281
+ return null;
90
282
  }
91
- const payload = parseJWT(decoded);
92
- if (payload.exp < Date.now() / 1e3) {
93
- return {
94
- status: 302,
283
+ let emailVerified = false;
284
+ if (!profile.email) {
285
+ const { data, error: error2 } = await betterFetch4("https://api.github.com/user/emails", {
95
286
  headers: {
96
- Location: `${options.redirectURL.error}?error=Token expired`
97
- },
98
- metadata: {
99
- isError: true
287
+ Authorization: `Bearer ${token.accessToken}`,
288
+ "User-Agent": "better-auth"
100
289
  }
101
- };
290
+ });
291
+ if (!error2) {
292
+ profile.email = (data.find((e) => e.primary) ?? data[0])?.email;
293
+ emailVerified = data.find((e) => e.email === profile.email)?.verified ?? false;
294
+ }
102
295
  }
103
- await context.adapter.updateUserByEmail(
104
- payload.email,
105
- {
106
- emailVerified: true
107
- },
108
- context
109
- );
110
296
  return {
111
- status: 302,
112
- headers: {
113
- Location: options.redirectURL.success
114
- }
297
+ user: {
298
+ id: profile.id,
299
+ name: profile.name,
300
+ email: profile.email,
301
+ image: profile.avatar_url,
302
+ emailVerified,
303
+ createdAt: /* @__PURE__ */ new Date(),
304
+ updatedAt: /* @__PURE__ */ new Date()
305
+ },
306
+ data: profile
115
307
  };
116
308
  }
117
309
  };
118
310
  };
119
311
 
120
- // src/crypto/hmac.ts
121
- async function hmac(secretKey, message) {
122
- const enc = new TextEncoder();
123
- const algorithm = { name: "HMAC", hash: "SHA-256" };
124
- const key = await crypto.subtle.importKey(
125
- "raw",
126
- enc.encode(secretKey),
127
- algorithm,
128
- false,
129
- ["sign", "verify"]
130
- );
131
- const signature = await crypto.subtle.sign(
132
- algorithm.name,
133
- key,
134
- enc.encode(message)
135
- );
136
- return btoa(String.fromCharCode(...new Uint8Array(signature)));
137
- }
138
-
139
- // src/crypto/random.ts
140
- function generateRandomString(size) {
141
- const i2hex = (i) => `0${i.toString(16)}`.slice(-2);
142
- const r = (a, i) => a + i2hex(i);
143
- const bytes = crypto.getRandomValues(new Uint8Array(size));
144
- return Array.from(bytes).reduce(r, "");
145
- }
146
-
147
- // src/plugins/csrf-check.ts
148
- var csrfHandler = async (context) => {
149
- const csrfToken = context.request.cookies.get(context.cookies.csrfToken.name);
150
- if (csrfToken) {
151
- return {
152
- status: 200,
153
- body: {
154
- csrfToken
155
- }
156
- };
157
- }
158
- const token = generateRandomString(32);
159
- const hash = await hmac(context.secret, token);
160
- const cookie = `${token}!${hash}`;
161
- context.request.cookies.set(
162
- context.cookies.csrfToken.name,
163
- cookie,
164
- context.cookies.csrfToken.options
312
+ // src/social-providers/google.ts
313
+ import { Google } from "arctic";
314
+ import { parseJWT as parseJWT2 } from "oslo/jwt";
315
+ var google = ({
316
+ clientId,
317
+ clientSecret,
318
+ redirectURI
319
+ }) => {
320
+ const googleArctic = new Google(
321
+ clientId,
322
+ clientSecret,
323
+ getRedirectURI("google", redirectURI)
165
324
  );
166
325
  return {
167
- status: 200,
168
- body: {
169
- csrfToken: cookie
326
+ id: "google",
327
+ name: "Google",
328
+ createAuthorizationURL({ state, scopes, codeVerifier }) {
329
+ if (!codeVerifier) {
330
+ throw new BetterAuthError("codeVerifier is required for Google");
331
+ }
332
+ const _scopes = scopes || ["email", "profile"];
333
+ return googleArctic.createAuthorizationURL(state, codeVerifier, _scopes);
334
+ },
335
+ validateAuthorizationCode: async (code, codeVerifier) => {
336
+ if (!codeVerifier) {
337
+ throw new BetterAuthError("codeVerifier is required for Google");
338
+ }
339
+ return googleArctic.validateAuthorizationCode(code, codeVerifier);
340
+ },
341
+ async getUserInfo(token) {
342
+ if (!token.idToken) {
343
+ return null;
344
+ }
345
+ const user = parseJWT2(token.idToken())?.payload;
346
+ return {
347
+ user: {
348
+ id: user.sub,
349
+ name: user.name,
350
+ email: user.email,
351
+ image: user.picture,
352
+ emailVerified: user.email_verified
353
+ },
354
+ data: user
355
+ };
170
356
  }
171
357
  };
172
358
  };
173
- var CSRFCheckPlugin = () => {
359
+
360
+ // src/social-providers/spotify.ts
361
+ import { betterFetch as betterFetch5 } from "@better-fetch/fetch";
362
+ import { Spotify } from "arctic";
363
+ var spotify = ({
364
+ clientId,
365
+ clientSecret,
366
+ redirectURI
367
+ }) => {
368
+ const spotifyArctic = new Spotify(
369
+ clientId,
370
+ clientSecret,
371
+ getRedirectURI("spotify", redirectURI)
372
+ );
174
373
  return {
175
- id: "csrf",
176
- name: "CSRF Check",
177
- version: "1.0.0",
178
- hooks: {
179
- matcher: (context) => !context.disableCSRF,
180
- before: async (context) => {
181
- const csrfToken = context.request.body.csrfToken;
182
- const csrfCookie = context.request.cookies.get(
183
- context.cookies.csrfToken.name
184
- );
185
- const [token, hash] = csrfCookie?.split("!") || [null, null];
186
- if (!csrfToken || !csrfCookie || !token || !hash || csrfCookie !== csrfToken) {
187
- context.request.cookies.set(context.cookies.csrfToken.name, "", {
188
- ...context.cookies.csrfToken.options,
189
- maxAge: 0
190
- });
191
- return {
192
- response: {
193
- status: 403,
194
- statusText: "Invalid CSRF Token"
195
- }
196
- };
197
- }
198
- const expectedHash = await hmac(context.secret, token);
199
- if (hash !== expectedHash) {
200
- context.request.cookies.set(context.cookies.csrfToken.name, "", {
201
- ...context.cookies.csrfToken.options,
202
- maxAge: 0
203
- });
204
- return {
205
- response: {
206
- status: 403,
207
- statusText: "Invalid CSRF Token"
208
- }
209
- };
374
+ id: "spotify",
375
+ name: "Spotify",
376
+ createAuthorizationURL({ state, scopes }) {
377
+ const _scopes = scopes || ["user-read-email"];
378
+ return spotifyArctic.createAuthorizationURL(state, _scopes);
379
+ },
380
+ validateAuthorizationCode: spotifyArctic.validateAuthorizationCode,
381
+ async getUserInfo(token) {
382
+ const { data: profile, error } = await betterFetch5(
383
+ "https://api.spotify.com/v1/me",
384
+ {
385
+ method: "GET",
386
+ headers: {
387
+ Authorization: `Bearer ${token.accessToken}`
388
+ }
210
389
  }
390
+ );
391
+ if (error) {
211
392
  return null;
212
393
  }
213
- },
214
- handler: csrfHandler
394
+ return {
395
+ user: {
396
+ id: profile.id,
397
+ name: profile.display_name,
398
+ email: profile.email,
399
+ image: profile.images[0]?.url,
400
+ emailVerified: false
401
+ },
402
+ data: profile
403
+ };
404
+ }
215
405
  };
216
406
  };
217
407
 
218
- // src/plugins/utils.ts
219
- var getPlugins = (options) => {
220
- const plugins = {
221
- post: [],
222
- pre: [],
223
- unordered: []
224
- };
225
- for (const plugin of options.plugins || []) {
226
- plugins[plugin.order || "unordered"].push(plugin);
227
- }
228
- const internalPlugins = [CSRFCheckPlugin()];
229
- return [
230
- ...plugins.pre,
231
- ...plugins.unordered,
232
- ...internalPlugins,
233
- ...plugins.post
234
- ];
235
- };
236
- var usePlugins = (context, ignorePlugins) => {
237
- const plugins = context.plugins.filter(
238
- (pl) => pl.hooks && !ignorePlugins?.includes(pl.id)
408
+ // src/social-providers/twitch.ts
409
+ import { betterFetch as betterFetch6 } from "@better-fetch/fetch";
410
+ import { Twitch } from "arctic";
411
+ var twitch = ({
412
+ clientId,
413
+ clientSecret,
414
+ redirectURI
415
+ }) => {
416
+ const twitchArctic = new Twitch(
417
+ clientId,
418
+ clientSecret,
419
+ getRedirectURI("twitch", redirectURI)
239
420
  );
240
- const hooks = plugins.map((plugin) => {
241
- return plugin.hooks;
242
- });
243
- const before = [];
244
- const after = [];
245
- for (const hook of hooks) {
246
- if (hook.matcher(context)) {
247
- hook.before && before.push(hook.before);
248
- hook.after && after.push(hook.after);
249
- }
250
- }
251
421
  return {
252
- before: async (context2) => {
253
- let ctx = context2;
254
- let response;
255
- for (const hook of before) {
256
- const res = await hook(ctx);
257
- if (res?.context) {
258
- ctx = res.context;
259
- }
260
- if (res?.response) {
261
- response = res.response;
262
- break;
422
+ id: "twitch",
423
+ name: "Twitch",
424
+ createAuthorizationURL({ state, scopes }) {
425
+ const _scopes = scopes || ["activity:write", "read"];
426
+ return twitchArctic.createAuthorizationURL(state, _scopes);
427
+ },
428
+ validateAuthorizationCode: twitchArctic.validateAuthorizationCode,
429
+ async getUserInfo(token) {
430
+ const { data: profile, error } = await betterFetch6(
431
+ "https://api.twitch.tv/helix/users",
432
+ {
433
+ method: "GET",
434
+ headers: {
435
+ Authorization: `Bearer ${token.accessToken}`
436
+ }
263
437
  }
438
+ );
439
+ if (error) {
440
+ return null;
264
441
  }
265
442
  return {
266
- context: ctx,
267
- response
443
+ user: {
444
+ id: profile.sub,
445
+ name: profile.preferred_username,
446
+ email: profile.email,
447
+ image: profile.picture,
448
+ emailVerified: false
449
+ },
450
+ data: profile
268
451
  };
452
+ }
453
+ };
454
+ };
455
+
456
+ // src/social-providers/twitter.ts
457
+ import { betterFetch as betterFetch7 } from "@better-fetch/fetch";
458
+ import { Twitter } from "arctic";
459
+ var twitter = ({
460
+ clientId,
461
+ clientSecret,
462
+ redirectURI
463
+ }) => {
464
+ const twitterArctic = new Twitter(
465
+ clientId,
466
+ clientSecret,
467
+ getRedirectURI("twitter", redirectURI)
468
+ );
469
+ return {
470
+ id: "twitter",
471
+ name: "Twitter",
472
+ createAuthorizationURL(data) {
473
+ const _scopes = data.scopes || ["account_info.read"];
474
+ return twitterArctic.createAuthorizationURL(
475
+ data.state,
476
+ data.codeVerifier,
477
+ _scopes
478
+ );
269
479
  },
270
- after: async (context2, fnResponse) => {
271
- let ctx = context2;
272
- let response;
273
- for (const hook of after) {
274
- const res = await hook(ctx, fnResponse);
275
- if (res?.context) {
276
- ctx = res.context;
277
- }
278
- if (res?.response) {
279
- response = res.response;
280
- break;
480
+ validateAuthorizationCode: async (code, codeVerifier) => {
481
+ if (!codeVerifier) {
482
+ throw new BetterAuthError("codeVerifier is required for Twitter");
483
+ }
484
+ return twitterArctic.validateAuthorizationCode(code, codeVerifier);
485
+ },
486
+ async getUserInfo(token) {
487
+ const { data: profile, error } = await betterFetch7(
488
+ "https://api.x.com/2/users/me?user.fields=profile_image_url",
489
+ {
490
+ method: "GET",
491
+ headers: {
492
+ Authorization: `Bearer ${token.accessToken}`
493
+ }
281
494
  }
495
+ );
496
+ if (error) {
497
+ return null;
498
+ }
499
+ if (!profile.data.email) {
500
+ return null;
282
501
  }
283
502
  return {
284
- context: ctx,
285
- response
503
+ user: {
504
+ id: profile.data.id,
505
+ name: profile.data.name,
506
+ email: profile.data.email,
507
+ image: profile.data.profile_image_url,
508
+ emailVerified: profile.data.verified || false
509
+ },
510
+ data: profile
286
511
  };
287
512
  }
288
513
  };
289
514
  };
290
- var withPlugins = (fn, ignorePlugins) => {
291
- return async (ctx) => {
292
- const { before, after } = usePlugins(ctx, ignorePlugins);
293
- const { context, response } = await before(ctx);
294
- if (response) {
295
- return response;
515
+
516
+ // src/types/provider.ts
517
+ import "arctic";
518
+
519
+ // src/social-providers/index.ts
520
+ var oAuthProviders = {
521
+ apple,
522
+ discord,
523
+ facebook,
524
+ github,
525
+ google,
526
+ spotify,
527
+ twitch,
528
+ twitter
529
+ };
530
+ var oAuthProviderList = Object.keys(oAuthProviders);
531
+
532
+ // src/utils/state.ts
533
+ import { generateState as generateStateOAuth } from "oslo/oauth2";
534
+ function generateState(callbackURL, currentURL) {
535
+ const code = generateStateOAuth();
536
+ const state = `${code}!${callbackURL}!${currentURL}`;
537
+ return { state, code };
538
+ }
539
+ function parseState(state) {
540
+ const [code, callbackURL, currentURL] = state.split("!");
541
+ return { code, callbackURL, currentURL };
542
+ }
543
+
544
+ // src/api/routes/session.ts
545
+ var getSession = createAuthEndpoint(
546
+ "/session",
547
+ {
548
+ method: "GET",
549
+ requireHeaders: true
550
+ },
551
+ async (ctx) => {
552
+ const sessionCookieToken = await ctx.getSignedCookie(
553
+ ctx.context.authCookies.sessionToken.name,
554
+ ctx.context.secret
555
+ );
556
+ if (!sessionCookieToken) {
557
+ return ctx.json(null, {
558
+ status: 401
559
+ });
560
+ }
561
+ const session = await ctx.context.internalAdapter.findSession(sessionCookieToken);
562
+ if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) {
563
+ ctx.setSignedCookie(
564
+ ctx.context.authCookies.sessionToken.name,
565
+ "",
566
+ ctx.context.secret,
567
+ {
568
+ maxAge: 0
569
+ }
570
+ );
571
+ return ctx.json(null, {
572
+ status: 401
573
+ });
574
+ }
575
+ const updatedSession = await ctx.context.internalAdapter.updateSession(
576
+ session.session
577
+ );
578
+ await ctx.setSignedCookie(
579
+ ctx.context.authCookies.sessionToken.name,
580
+ updatedSession.id,
581
+ ctx.context.secret,
582
+ {
583
+ ...ctx.context.authCookies.sessionToken.options,
584
+ maxAge: updatedSession.expiresAt.valueOf() - Date.now()
585
+ }
586
+ );
587
+ return ctx.json({
588
+ session: updatedSession,
589
+ user: session.user
590
+ });
591
+ }
592
+ );
593
+ var getSessionFromCtx = async (ctx) => {
594
+ const session = await getSession({
595
+ ...ctx,
596
+ //@ts-expect-error: By default since this request context comes from a router it'll have a `router` flag which force it to be a request object
597
+ _flag: void 0
598
+ });
599
+ return session;
600
+ };
601
+
602
+ // src/api/routes/sign-in.ts
603
+ var signInOAuth = createAuthEndpoint(
604
+ "/sign-in/social",
605
+ {
606
+ method: "POST",
607
+ requireHeaders: true,
608
+ query: z.object({
609
+ /**
610
+ * Redirect to the current URL after the
611
+ * user has signed in.
612
+ */
613
+ currentURL: z.string().optional()
614
+ }).optional(),
615
+ body: z.object({
616
+ /**
617
+ * Callback URL to redirect to after the user has signed in.
618
+ */
619
+ callbackURL: z.string().optional(),
620
+ /**
621
+ * OAuth2 provider to use`
622
+ */
623
+ provider: z.enum(oAuthProviderList)
624
+ })
625
+ },
626
+ async (c) => {
627
+ const provider = c.context.options.socialProvider?.find(
628
+ (p) => p.id === c.body.provider
629
+ );
630
+ if (!provider) {
631
+ c.context.logger.error(
632
+ "Provider not found. Make sure to add the provider to your auth config",
633
+ {
634
+ provider: c.body.provider
635
+ }
636
+ );
637
+ throw new APIError("NOT_FOUND");
638
+ }
639
+ const cookie = c.context.authCookies;
640
+ const currentURL = c.query?.currentURL ? new URL(c.query?.currentURL) : null;
641
+ const state = generateState(
642
+ c.body.callbackURL || currentURL?.origin || c.context.baseURL,
643
+ c.query?.currentURL
644
+ );
645
+ try {
646
+ await c.setSignedCookie(
647
+ cookie.state.name,
648
+ state.code,
649
+ c.context.secret,
650
+ cookie.state.options
651
+ );
652
+ const codeVerifier = generateCodeVerifier();
653
+ await c.setSignedCookie(
654
+ cookie.pkCodeVerifier.name,
655
+ codeVerifier,
656
+ c.context.secret,
657
+ cookie.pkCodeVerifier.options
658
+ );
659
+ const url = provider.createAuthorizationURL({
660
+ state: state.state,
661
+ codeVerifier
662
+ });
663
+ return {
664
+ url: url.toString(),
665
+ state: state.state,
666
+ codeVerifier,
667
+ redirect: true
668
+ };
669
+ } catch (e) {
670
+ throw new APIError("INTERNAL_SERVER_ERROR");
671
+ }
672
+ }
673
+ );
674
+ var signInEmail = createAuthEndpoint(
675
+ "/sign-in/email",
676
+ {
677
+ method: "POST",
678
+ body: z.object({
679
+ email: z.string().email(),
680
+ password: z.string(),
681
+ callbackURL: z.string().optional(),
682
+ /**
683
+ * If this is true the session will only be valid for the current browser session
684
+ * @default false
685
+ */
686
+ dontRememberMe: z.boolean().default(false).optional()
687
+ })
688
+ },
689
+ async (ctx) => {
690
+ if (!ctx.context.options?.emailAndPassword?.enabled) {
691
+ ctx.context.logger.error("Email and password is not enabled");
692
+ throw new APIError("BAD_REQUEST", {
693
+ message: "Email and password is not enabled"
694
+ });
695
+ }
696
+ const currentSession = await getSessionFromCtx(ctx);
697
+ if (currentSession) {
698
+ return ctx.json({
699
+ user: currentSession.user,
700
+ session: currentSession.session,
701
+ redirect: !!ctx.body.callbackURL,
702
+ url: ctx.body.callbackURL
703
+ });
704
+ }
705
+ const { email, password } = ctx.body;
706
+ const argon2id = new Argon2id();
707
+ const user = await ctx.context.internalAdapter.findUserByEmail(email);
708
+ if (!user) {
709
+ await argon2id.hash(password);
710
+ ctx.context.logger.error("User not found", { email });
711
+ throw new APIError("UNAUTHORIZED", {
712
+ message: "Invalid email or password"
713
+ });
714
+ }
715
+ const credentialAccount = user.accounts.find(
716
+ (a) => a.providerId === "credential"
717
+ );
718
+ if (!credentialAccount) {
719
+ ctx.context.logger.error("Credential account not found", { email });
720
+ throw new APIError("UNAUTHORIZED", {
721
+ message: "Invalid email or password"
722
+ });
723
+ }
724
+ const currentPassword = credentialAccount?.password;
725
+ if (!currentPassword) {
726
+ ctx.context.logger.error("Password not found", { email });
727
+ throw new APIError("UNAUTHORIZED", {
728
+ message: "Unexpected error"
729
+ });
730
+ }
731
+ const validPassword = await argon2id.verify(currentPassword, password);
732
+ if (!validPassword) {
733
+ ctx.context.logger.error("Invalid password");
734
+ throw new APIError("UNAUTHORIZED", {
735
+ message: "Invalid email or password"
736
+ });
737
+ }
738
+ const session = await ctx.context.internalAdapter.createSession(
739
+ user.user.id,
740
+ ctx.request
741
+ );
742
+ await ctx.setSignedCookie(
743
+ ctx.context.authCookies.sessionToken.name,
744
+ session.id,
745
+ ctx.context.secret,
746
+ ctx.body.dontRememberMe ? {
747
+ ...ctx.context.authCookies.sessionToken.options,
748
+ maxAge: void 0
749
+ } : ctx.context.authCookies.sessionToken.options
750
+ );
751
+ return ctx.json({
752
+ user: user.user,
753
+ session,
754
+ redirect: !!ctx.body.callbackURL,
755
+ url: ctx.body.callbackURL
756
+ });
757
+ }
758
+ );
759
+
760
+ // src/api/routes/callback.ts
761
+ import { APIError as APIError2 } from "better-call";
762
+ import { z as z3 } from "zod";
763
+
764
+ // src/adapters/schema.ts
765
+ import { z as z2 } from "zod";
766
+ var accountSchema = z2.object({
767
+ id: z2.string(),
768
+ providerId: z2.string(),
769
+ accountId: z2.string(),
770
+ userId: z2.string(),
771
+ accessToken: z2.string().nullable().optional(),
772
+ refreshToken: z2.string().nullable().optional(),
773
+ idToken: z2.string().nullable().optional(),
774
+ accessTokenExpiresAt: z2.date().nullable().optional(),
775
+ refreshTokenExpiresAt: z2.date().nullable().optional(),
776
+ /**
777
+ * Password is only stored in the credential provider
778
+ */
779
+ password: z2.string().optional().nullable()
780
+ });
781
+ var userSchema = z2.object({
782
+ id: z2.string(),
783
+ email: z2.string().transform((val) => val.toLowerCase()),
784
+ emailVerified: z2.boolean().default(false),
785
+ name: z2.string(),
786
+ image: z2.string().optional(),
787
+ createdAt: z2.date().default(/* @__PURE__ */ new Date()),
788
+ updatedAt: z2.date().default(/* @__PURE__ */ new Date())
789
+ });
790
+ var sessionSchema = z2.object({
791
+ id: z2.string(),
792
+ userId: z2.string(),
793
+ expiresAt: z2.date(),
794
+ ipAddress: z2.string().optional(),
795
+ userAgent: z2.string().optional()
796
+ });
797
+
798
+ // src/client/client-utils.ts
799
+ var HIDE_ON_CLIENT_METADATA = {
800
+ onClient: "hide"
801
+ };
802
+
803
+ // src/utils/id.ts
804
+ import { alphabet, generateRandomString } from "oslo/crypto";
805
+ var generateId = () => {
806
+ return generateRandomString(36, alphabet("a-z", "0-9"));
807
+ };
808
+
809
+ // src/api/routes/callback.ts
810
+ var callbackOAuth = createAuthEndpoint(
811
+ "/callback/:id",
812
+ {
813
+ method: "GET",
814
+ query: z3.object({
815
+ state: z3.string(),
816
+ code: z3.string(),
817
+ code_verifier: z3.string().optional()
818
+ }),
819
+ metadata: HIDE_ON_CLIENT_METADATA
820
+ },
821
+ async (c) => {
822
+ const provider = c.context.options.socialProvider?.find(
823
+ (p) => p.id === c.params.id
824
+ );
825
+ if (!provider) {
826
+ c.context.logger.error(
827
+ "Oauth provider with id",
828
+ c.params.id,
829
+ "not found"
830
+ );
831
+ throw new APIError2("NOT_FOUND");
832
+ }
833
+ const tokens = await provider.validateAuthorizationCode(
834
+ c.query.code,
835
+ c.query.code_verifier || ""
836
+ );
837
+ if (!tokens) {
838
+ c.context.logger.error("Code verification failed");
839
+ throw new APIError2("UNAUTHORIZED");
840
+ }
841
+ const user = await provider.getUserInfo(tokens).then((res) => res?.user);
842
+ const id = generateId();
843
+ const data = userSchema.safeParse({
844
+ ...user,
845
+ id
846
+ });
847
+ const { callbackURL, currentURL } = parseState(c.query.state);
848
+ if (!user || data.success === false) {
849
+ if (currentURL) {
850
+ throw c.redirect(`${currentURL}?error=oauth_validation_failed`);
851
+ } else {
852
+ throw new APIError2("BAD_REQUEST");
853
+ }
854
+ }
855
+ if (!callbackURL) {
856
+ c.context.logger.error("Callback URL not found");
857
+ throw new APIError2("FORBIDDEN");
858
+ }
859
+ const dbUser = await c.context.internalAdapter.findUserByEmail(user.email);
860
+ const userId = dbUser?.user.id;
861
+ if (dbUser) {
862
+ const hasBeenLinked = dbUser.accounts.find(
863
+ (a) => a.providerId === provider.id
864
+ );
865
+ if (!hasBeenLinked && !user.emailVerified) {
866
+ c.context.logger.error("User already exists");
867
+ const url = new URL(currentURL || callbackURL);
868
+ url.searchParams.set("error", "user_already_exists");
869
+ throw c.redirect(url.toString());
870
+ }
871
+ if (!hasBeenLinked && user.emailVerified) {
872
+ await c.context.internalAdapter.linkAccount({
873
+ providerId: provider.id,
874
+ accountId: user.id,
875
+ id: `${provider.id}:${user.id}`,
876
+ userId: dbUser.user.id,
877
+ ...tokens
878
+ });
879
+ }
880
+ } else {
881
+ try {
882
+ await c.context.internalAdapter.createOAuthUser(data.data, {
883
+ ...tokens,
884
+ id: `${provider.id}:${user.id}`,
885
+ providerId: provider.id,
886
+ accountId: user.id,
887
+ userId: id
888
+ });
889
+ } catch (e) {
890
+ const url = new URL(currentURL || callbackURL);
891
+ url.searchParams.set("error", "unable_to_create_user");
892
+ c.setHeader("Location", url.toString());
893
+ throw c.redirect(url.toString());
894
+ }
895
+ }
896
+ if (!userId && !id)
897
+ throw new APIError2("INTERNAL_SERVER_ERROR", {
898
+ message: "Unable to create user"
899
+ });
900
+ const session = await c.context.internalAdapter.createSession(
901
+ userId || id,
902
+ c.request
903
+ );
904
+ try {
905
+ await c.setSignedCookie(
906
+ c.context.authCookies.sessionToken.name,
907
+ session.id,
908
+ c.context.secret,
909
+ c.context.authCookies.sessionToken.options
910
+ );
911
+ } catch (e) {
912
+ c.context.logger.error("Unable to set session cookie", e);
913
+ const url = new URL(currentURL || callbackURL);
914
+ url.searchParams.set("error", "unable_to_create_session");
915
+ throw c.redirect(url.toString());
916
+ }
917
+ throw c.redirect(callbackURL);
918
+ }
919
+ );
920
+
921
+ // src/api/routes/sign-out.ts
922
+ import { z as z4 } from "zod";
923
+ var signOut = createAuthEndpoint(
924
+ "/sign-out",
925
+ {
926
+ method: "POST",
927
+ body: z4.object({
928
+ callbackURL: z4.string().optional()
929
+ }).optional()
930
+ },
931
+ async (ctx) => {
932
+ const sessionCookieToken = await ctx.getSignedCookie(
933
+ ctx.context.authCookies.sessionToken.name,
934
+ ctx.context.secret
935
+ );
936
+ if (!sessionCookieToken) {
937
+ return ctx.json(null);
938
+ }
939
+ await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
940
+ ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
941
+ maxAge: 0
942
+ });
943
+ return ctx.json(null, {
944
+ body: {
945
+ redirect: !!ctx.body?.callbackURL,
946
+ url: ctx.body?.callbackURL
947
+ }
948
+ });
949
+ }
950
+ );
951
+
952
+ // src/api/routes/forget-password.ts
953
+ import { TimeSpan } from "oslo";
954
+ import { createJWT } from "oslo/jwt";
955
+ import { validateJWT } from "oslo/jwt";
956
+ import { Argon2id as Argon2id2 } from "oslo/password";
957
+ import { z as z5 } from "zod";
958
+ var forgetPassword = createAuthEndpoint(
959
+ "/forget-password",
960
+ {
961
+ method: "POST",
962
+ body: z5.object({
963
+ /**
964
+ * The email address of the user to send a password reset email to.
965
+ */
966
+ email: z5.string().email()
967
+ })
968
+ },
969
+ async (ctx) => {
970
+ if (!ctx.context.options.emailAndPassword?.sendResetPasswordToken) {
971
+ ctx.context.logger.error(
972
+ "Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function to your auth config!"
973
+ );
974
+ return ctx.json(null, {
975
+ status: 400,
976
+ statusText: "RESET_PASSWORD_EMAIL_NOT_SENT",
977
+ body: {
978
+ message: "Reset password isn't enabled"
979
+ }
980
+ });
296
981
  }
297
- const res = await fn(context);
298
- const { response: afterResponse } = await after(context, res);
299
- if (afterResponse) {
300
- return afterResponse;
982
+ const { email } = ctx.body;
983
+ const user = await ctx.context.internalAdapter.findUserByEmail(email);
984
+ if (!user) {
985
+ return ctx.json(
986
+ {
987
+ error: "User not found"
988
+ },
989
+ {
990
+ status: 400,
991
+ statusText: "USER_NOT_FOUND",
992
+ body: {
993
+ message: "User not found"
994
+ }
995
+ }
996
+ );
997
+ }
998
+ const token = await createJWT(
999
+ "HS256",
1000
+ Buffer.from(ctx.context.secret),
1001
+ {
1002
+ email: user.user.email
1003
+ },
1004
+ {
1005
+ expiresIn: new TimeSpan(1, "h"),
1006
+ issuer: "better-auth",
1007
+ subject: "forget-password",
1008
+ audiences: [user.user.email],
1009
+ includeIssuedTimestamp: true
1010
+ }
1011
+ );
1012
+ await ctx.context.options.emailAndPassword.sendResetPasswordToken(
1013
+ token,
1014
+ user.user
1015
+ );
1016
+ return ctx.json({
1017
+ status: true
1018
+ });
1019
+ }
1020
+ );
1021
+ var resetPassword = createAuthEndpoint(
1022
+ "/reset-password",
1023
+ {
1024
+ method: "POST",
1025
+ body: z5.object({
1026
+ token: z5.string(),
1027
+ newPassword: z5.string(),
1028
+ callbackURL: z5.string().optional()
1029
+ })
1030
+ },
1031
+ async (ctx) => {
1032
+ const { token, newPassword } = ctx.body;
1033
+ try {
1034
+ const jwt = await validateJWT(
1035
+ "HS256",
1036
+ Buffer.from(ctx.context.secret),
1037
+ token
1038
+ );
1039
+ const email = z5.string().email().parse(jwt.payload.email);
1040
+ const user = await ctx.context.internalAdapter.findUserByEmail(email);
1041
+ if (!user) {
1042
+ return ctx.json(null, {
1043
+ status: 400,
1044
+ statusText: "USER_NOT_FOUND",
1045
+ body: {
1046
+ message: "User not found"
1047
+ }
1048
+ });
1049
+ }
1050
+ if (newPassword.length < (ctx.context.options.emailAndPassword?.minPasswordLength || 8) || newPassword.length > (ctx.context.options.emailAndPassword?.maxPasswordLength || 32)) {
1051
+ return ctx.json(null, {
1052
+ status: 400,
1053
+ statusText: "INVALID_PASSWORD_LENGTH",
1054
+ body: {
1055
+ message: "Password length must be between 8 and 32"
1056
+ }
1057
+ });
1058
+ }
1059
+ const argon2id = new Argon2id2();
1060
+ const hashedPassword = await argon2id.hash(newPassword);
1061
+ const updatedUser = await ctx.context.internalAdapter.updatePassword(
1062
+ user.user.id,
1063
+ hashedPassword
1064
+ );
1065
+ if (!updatedUser) {
1066
+ return ctx.json(null, {
1067
+ status: 500,
1068
+ statusText: "INTERNAL_SERVER_ERROR",
1069
+ body: {
1070
+ message: "Internal server error"
1071
+ }
1072
+ });
1073
+ }
1074
+ return ctx.json({
1075
+ status: true,
1076
+ url: ctx.body.callbackURL,
1077
+ redirect: !!ctx.body.callbackURL
1078
+ });
1079
+ } catch (e) {
1080
+ console.log(e);
1081
+ return ctx.json(null, {
1082
+ status: 400,
1083
+ statusText: "INVALID_TOKEN",
1084
+ body: {
1085
+ message: "Invalid token"
1086
+ }
1087
+ });
1088
+ }
1089
+ }
1090
+ );
1091
+
1092
+ // src/api/routes/verify-email.ts
1093
+ import { TimeSpan as TimeSpan2 } from "oslo";
1094
+ import { createJWT as createJWT2, validateJWT as validateJWT2 } from "oslo/jwt";
1095
+ import { z as z6 } from "zod";
1096
+ var sendVerificationEmail = createAuthEndpoint(
1097
+ "/send-verification-email",
1098
+ {
1099
+ method: "POST",
1100
+ body: z6.object({
1101
+ email: z6.string().email(),
1102
+ callbackURL: z6.string().optional()
1103
+ })
1104
+ },
1105
+ async (ctx) => {
1106
+ if (!ctx.context.options.emailAndPassword?.sendVerificationEmail) {
1107
+ return ctx.json(null, {
1108
+ status: 400,
1109
+ statusText: "VERIFICATION_EMAIL_NOT_SENT",
1110
+ body: {
1111
+ message: "Verification email isn't enabled"
1112
+ }
1113
+ });
1114
+ }
1115
+ const { email } = ctx.body;
1116
+ const token = await createJWT2(
1117
+ "HS256",
1118
+ Buffer.from(ctx.context.secret),
1119
+ {
1120
+ email: email.toLowerCase()
1121
+ },
1122
+ {
1123
+ expiresIn: new TimeSpan2(1, "h"),
1124
+ issuer: "better-auth",
1125
+ subject: "verify-email",
1126
+ audiences: [email],
1127
+ includeIssuedTimestamp: true
1128
+ }
1129
+ );
1130
+ const url = `${ctx.context.baseURL}/verify-email?token=${token}?callbackURL=${ctx.body.callbackURL}`;
1131
+ await ctx.context.options.emailAndPassword.sendVerificationEmail(
1132
+ email,
1133
+ url
1134
+ );
1135
+ return ctx.json({
1136
+ status: true
1137
+ });
1138
+ }
1139
+ );
1140
+ var verifyEmail = createAuthEndpoint(
1141
+ "/verify-email",
1142
+ {
1143
+ method: "GET",
1144
+ query: z6.object({
1145
+ token: z6.string(),
1146
+ callbackURL: z6.string()
1147
+ })
1148
+ },
1149
+ async (ctx) => {
1150
+ const { token } = ctx.query;
1151
+ try {
1152
+ const jwt = await validateJWT2(
1153
+ "HS256",
1154
+ Buffer.from(ctx.context.secret),
1155
+ token
1156
+ );
1157
+ const schema = z6.object({
1158
+ email: z6.string().email()
1159
+ });
1160
+ const parsed = schema.parse(jwt.payload);
1161
+ const user = await ctx.context.internalAdapter.findUserByEmail(
1162
+ parsed.email
1163
+ );
1164
+ if (!user) {
1165
+ return ctx.json(null, {
1166
+ status: 400,
1167
+ statusText: "USER_NOT_FOUND",
1168
+ body: {
1169
+ message: "User not found"
1170
+ }
1171
+ });
1172
+ }
1173
+ const account = user.accounts.find((a) => a.providerId === "credential");
1174
+ if (!account) {
1175
+ return ctx.json(null, {
1176
+ status: 400,
1177
+ statusText: "ACCOUNT_NOT_FOUND",
1178
+ body: {
1179
+ message: "Account not found"
1180
+ }
1181
+ });
1182
+ }
1183
+ await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
1184
+ emailVerified: true
1185
+ });
1186
+ if (ctx.query.callbackURL) {
1187
+ throw ctx.redirect(ctx.query.callbackURL);
1188
+ }
1189
+ return ctx.json({
1190
+ status: true
1191
+ });
1192
+ } catch (e) {
1193
+ return ctx.json(null, {
1194
+ status: 400,
1195
+ statusText: "INVALID_TOKEN",
1196
+ body: {
1197
+ message: "Invalid token"
1198
+ }
1199
+ });
1200
+ }
1201
+ }
1202
+ );
1203
+
1204
+ // src/utils/shim.ts
1205
+ var shimContext = (originalObject, newContext) => {
1206
+ const shimmedObj = {};
1207
+ for (const [key, value] of Object.entries(originalObject)) {
1208
+ shimmedObj[key] = (ctx) => {
1209
+ return value({
1210
+ ...ctx,
1211
+ context: {
1212
+ ...newContext,
1213
+ ...ctx.context
1214
+ }
1215
+ });
1216
+ };
1217
+ shimmedObj[key].path = value.path;
1218
+ shimmedObj[key].method = value.method;
1219
+ shimmedObj[key].options = value.options;
1220
+ shimmedObj[key].headers = value.headers;
1221
+ }
1222
+ return shimmedObj;
1223
+ };
1224
+
1225
+ // src/plugins/organization/access/index.ts
1226
+ var access_exports = {};
1227
+ __export(access_exports, {
1228
+ AccessControl: () => AccessControl,
1229
+ ParsingError: () => ParsingError,
1230
+ Role: () => Role,
1231
+ adminAc: () => adminAc,
1232
+ createAccessControl: () => createAccessControl,
1233
+ defaultAc: () => defaultAc,
1234
+ defaultRoles: () => defaultRoles,
1235
+ defaultStatements: () => defaultStatements,
1236
+ memberAc: () => memberAc,
1237
+ ownerAc: () => ownerAc,
1238
+ permissionFromString: () => permissionFromString
1239
+ });
1240
+
1241
+ // src/plugins/organization/access/src/access.ts
1242
+ var ParsingError = class extends Error {
1243
+ path;
1244
+ constructor(message, path) {
1245
+ super(message);
1246
+ this.path = path;
1247
+ }
1248
+ };
1249
+ var AccessControl = class {
1250
+ constructor(s) {
1251
+ this.s = s;
1252
+ this.statements = s;
1253
+ }
1254
+ statements;
1255
+ newRole(statements) {
1256
+ return new Role(statements);
1257
+ }
1258
+ };
1259
+ var Role = class _Role {
1260
+ statements;
1261
+ constructor(statements) {
1262
+ this.statements = statements;
1263
+ }
1264
+ authorize(request, connector) {
1265
+ for (const [requestedResource, requestedActions] of Object.entries(
1266
+ request
1267
+ )) {
1268
+ const allowedActions = this.statements[requestedResource];
1269
+ if (!allowedActions) {
1270
+ return {
1271
+ success: false,
1272
+ error: `You are not allowed to access resource: ${requestedResource}`
1273
+ };
1274
+ }
1275
+ const success = connector === "OR" ? requestedActions.some(
1276
+ (requestedAction) => allowedActions.includes(requestedAction)
1277
+ ) : requestedActions.every(
1278
+ (requestedAction) => allowedActions.includes(requestedAction)
1279
+ );
1280
+ if (success) {
1281
+ return { success };
1282
+ }
1283
+ return {
1284
+ success: false,
1285
+ error: `unauthorized to access resource "${requestedResource}"`
1286
+ };
1287
+ }
1288
+ return {
1289
+ success: false,
1290
+ error: "Not authorized"
1291
+ };
1292
+ }
1293
+ static fromString(s) {
1294
+ const statements = JSON.parse(s);
1295
+ if (typeof statements !== "object") {
1296
+ throw new ParsingError("statements is not an object", ".");
1297
+ }
1298
+ for (const [resource, actions] of Object.entries(statements)) {
1299
+ if (typeof resource !== "string") {
1300
+ throw new ParsingError("invalid resource identifier", resource);
1301
+ }
1302
+ if (!Array.isArray(actions)) {
1303
+ throw new ParsingError("actions is not an array", resource);
1304
+ }
1305
+ for (let i = 0; i < actions.length; i++) {
1306
+ if (typeof actions[i] !== "string") {
1307
+ throw new ParsingError("action is not a string", `${resource}[${i}]`);
1308
+ }
1309
+ }
1310
+ }
1311
+ return new _Role(statements);
1312
+ }
1313
+ toString() {
1314
+ return JSON.stringify(this.statements);
1315
+ }
1316
+ };
1317
+
1318
+ // src/plugins/organization/access/statement.ts
1319
+ var createAccessControl = (statements) => {
1320
+ return new AccessControl(statements);
1321
+ };
1322
+ var defaultStatements = {
1323
+ organization: ["update", "delete"],
1324
+ member: ["create", "update", "delete"],
1325
+ invitation: ["create", "cancel"]
1326
+ };
1327
+ var defaultAc = createAccessControl(defaultStatements);
1328
+ var adminAc = defaultAc.newRole({
1329
+ organization: ["update"],
1330
+ invitation: ["create", "cancel"],
1331
+ member: ["create", "update", "delete"]
1332
+ });
1333
+ var ownerAc = defaultAc.newRole({
1334
+ organization: ["update", "delete"],
1335
+ member: ["create", "update", "delete"],
1336
+ invitation: ["create", "cancel"]
1337
+ });
1338
+ var memberAc = defaultAc.newRole({
1339
+ organization: [],
1340
+ member: [],
1341
+ invitation: []
1342
+ });
1343
+ var defaultRoles = {
1344
+ admin: adminAc,
1345
+ owner: ownerAc,
1346
+ member: memberAc
1347
+ };
1348
+
1349
+ // src/plugins/organization/access/utils.ts
1350
+ var permissionFromString = (permission) => {
1351
+ return Role.fromString(permission ?? "");
1352
+ };
1353
+
1354
+ // src/utils/date.ts
1355
+ var getDate = (span) => {
1356
+ const date = /* @__PURE__ */ new Date();
1357
+ return new Date(date.getTime() + span);
1358
+ };
1359
+
1360
+ // src/plugins/organization/adapter.ts
1361
+ var getOrgAdapter = (adapter, options) => {
1362
+ return {
1363
+ findOrganizationBySlug: async (slug) => {
1364
+ const organization2 = await adapter.findOne({
1365
+ model: "organization",
1366
+ where: [
1367
+ {
1368
+ field: "slug",
1369
+ value: slug
1370
+ }
1371
+ ]
1372
+ });
1373
+ return organization2;
1374
+ },
1375
+ createOrganization: async (data) => {
1376
+ const organization2 = await adapter.create({
1377
+ model: "organization",
1378
+ data: data.organization
1379
+ });
1380
+ const member = await adapter.create({
1381
+ model: "member",
1382
+ data: {
1383
+ id: generateId(),
1384
+ name: data.user.name,
1385
+ organizationId: organization2.id,
1386
+ userId: data.user.id,
1387
+ email: data.user.email,
1388
+ role: options?.creatorRole || "owner"
1389
+ }
1390
+ });
1391
+ return {
1392
+ ...organization2,
1393
+ members: [member]
1394
+ };
1395
+ },
1396
+ findMemberByEmail: async (data) => {
1397
+ const member = await adapter.findOne({
1398
+ model: "member",
1399
+ where: [
1400
+ {
1401
+ field: "email",
1402
+ value: data.email
1403
+ },
1404
+ {
1405
+ field: "organizationId",
1406
+ value: data.organizationId
1407
+ }
1408
+ ]
1409
+ });
1410
+ return member;
1411
+ },
1412
+ findMemberByOrgId: async (data) => {
1413
+ const member = await adapter.findOne({
1414
+ model: "member",
1415
+ where: [
1416
+ {
1417
+ field: "userId",
1418
+ value: data.userId
1419
+ },
1420
+ {
1421
+ field: "organizationId",
1422
+ value: data.organizationId
1423
+ }
1424
+ ]
1425
+ });
1426
+ return member;
1427
+ },
1428
+ findMemberById: async (memberId) => {
1429
+ const member = await adapter.findOne({
1430
+ model: "member",
1431
+ where: [
1432
+ {
1433
+ field: "id",
1434
+ value: memberId
1435
+ }
1436
+ ]
1437
+ });
1438
+ return member;
1439
+ },
1440
+ createMember: async (data) => {
1441
+ const member = await adapter.create({
1442
+ model: "member",
1443
+ data
1444
+ });
1445
+ return member;
1446
+ },
1447
+ updateMember: async (memberId, role2) => {
1448
+ const member = await adapter.update({
1449
+ model: "member",
1450
+ where: [
1451
+ {
1452
+ field: "id",
1453
+ value: memberId
1454
+ }
1455
+ ],
1456
+ update: {
1457
+ role: role2
1458
+ }
1459
+ });
1460
+ return member;
1461
+ },
1462
+ deleteMember: async (memberId) => {
1463
+ const member = await adapter.delete({
1464
+ model: "member",
1465
+ where: [
1466
+ {
1467
+ field: "id",
1468
+ value: memberId
1469
+ }
1470
+ ]
1471
+ });
1472
+ return member;
1473
+ },
1474
+ updateOrganization: async (orgId, data) => {
1475
+ const organization2 = await adapter.update({
1476
+ model: "organization",
1477
+ where: [
1478
+ {
1479
+ field: "id",
1480
+ value: orgId
1481
+ }
1482
+ ],
1483
+ update: data
1484
+ });
1485
+ return organization2;
1486
+ },
1487
+ deleteOrganization: async (orgId) => {
1488
+ const organization2 = await adapter.delete({
1489
+ model: "organization",
1490
+ where: [
1491
+ {
1492
+ field: "id",
1493
+ value: orgId
1494
+ }
1495
+ ]
1496
+ });
1497
+ return organization2;
1498
+ },
1499
+ setActiveOrganization: async (sessionId, orgId) => {
1500
+ const session = await adapter.update({
1501
+ model: "session",
1502
+ where: [
1503
+ {
1504
+ field: "id",
1505
+ value: sessionId
1506
+ }
1507
+ ],
1508
+ update: {
1509
+ activeOrganizationId: orgId
1510
+ }
1511
+ });
1512
+ return session;
1513
+ },
1514
+ findOrganizationById: async (orgId) => {
1515
+ const organization2 = await adapter.findOne({
1516
+ model: "organization",
1517
+ where: [
1518
+ {
1519
+ field: "id",
1520
+ value: orgId
1521
+ }
1522
+ ]
1523
+ });
1524
+ return organization2;
1525
+ },
1526
+ findFullOrganization: async (orgId) => {
1527
+ const organization2 = await adapter.findOne({
1528
+ model: "organization",
1529
+ where: [
1530
+ {
1531
+ field: "id",
1532
+ value: orgId
1533
+ }
1534
+ ]
1535
+ });
1536
+ if (!organization2) {
1537
+ return null;
1538
+ }
1539
+ const members = await adapter.findMany({
1540
+ model: "member",
1541
+ where: [
1542
+ {
1543
+ field: "organizationId",
1544
+ value: orgId
1545
+ }
1546
+ ]
1547
+ });
1548
+ const invitations = await adapter.findMany({
1549
+ model: "invitation",
1550
+ where: [
1551
+ {
1552
+ field: "organizationId",
1553
+ value: orgId
1554
+ }
1555
+ ]
1556
+ });
1557
+ return {
1558
+ ...organization2,
1559
+ members,
1560
+ invitations
1561
+ };
1562
+ },
1563
+ listOrganizations: async (userId) => {
1564
+ const members = await adapter.findMany({
1565
+ model: "member",
1566
+ where: [
1567
+ {
1568
+ field: "userId",
1569
+ value: userId
1570
+ }
1571
+ ]
1572
+ });
1573
+ const organizationIds = members?.map((member) => member.organizationId);
1574
+ console.log({ organizationIds });
1575
+ if (!organizationIds) {
1576
+ return [];
1577
+ }
1578
+ const organizations = [];
1579
+ for (const id of organizationIds) {
1580
+ const organization2 = await adapter.findOne({
1581
+ model: "organization",
1582
+ where: [
1583
+ {
1584
+ field: "id",
1585
+ value: id
1586
+ }
1587
+ ]
1588
+ });
1589
+ if (organization2) {
1590
+ organizations.push(organization2);
1591
+ }
1592
+ }
1593
+ return organizations;
1594
+ },
1595
+ createInvitation: async ({
1596
+ invitation,
1597
+ user
1598
+ }) => {
1599
+ const defaultExpiration = 1e3 * 60 * 60 * 48;
1600
+ const expiresAt = getDate(
1601
+ options?.invitationExpiresIn || defaultExpiration
1602
+ );
1603
+ const invite = await adapter.create({
1604
+ model: "invitation",
1605
+ data: {
1606
+ id: generateId(),
1607
+ email: invitation.email,
1608
+ role: invitation.role,
1609
+ organizationId: invitation.organizationId,
1610
+ status: "pending",
1611
+ expiresAt,
1612
+ inviterId: user.id
1613
+ }
1614
+ });
1615
+ return invite;
1616
+ },
1617
+ findInvitationById: async (id) => {
1618
+ const invitation = await adapter.findOne({
1619
+ model: "invitation",
1620
+ where: [
1621
+ {
1622
+ field: "id",
1623
+ value: id
1624
+ }
1625
+ ]
1626
+ });
1627
+ return invitation;
1628
+ },
1629
+ findPendingInvitation: async (data) => {
1630
+ const invitation = await adapter.findMany({
1631
+ model: "invitation",
1632
+ where: [
1633
+ {
1634
+ field: "email",
1635
+ value: data.email
1636
+ },
1637
+ {
1638
+ field: "organizationId",
1639
+ value: data.organizationId
1640
+ },
1641
+ {
1642
+ field: "status",
1643
+ value: "pending"
1644
+ }
1645
+ ]
1646
+ });
1647
+ return invitation.filter(
1648
+ (invite) => new Date(invite.expiresAt) > /* @__PURE__ */ new Date()
1649
+ );
1650
+ },
1651
+ updateInvitation: async (data) => {
1652
+ const invitation = await adapter.update({
1653
+ model: "invitation",
1654
+ where: [
1655
+ {
1656
+ field: "id",
1657
+ value: data.invitationId
1658
+ }
1659
+ ],
1660
+ update: {
1661
+ status: data.status
1662
+ }
1663
+ });
1664
+ return invitation;
1665
+ }
1666
+ };
1667
+ };
1668
+
1669
+ // src/plugins/organization/call.ts
1670
+ import "better-call";
1671
+
1672
+ // src/api/middlewares/session.ts
1673
+ import { APIError as APIError3 } from "better-call";
1674
+ var sessionMiddleware = createAuthMiddleware(async (ctx) => {
1675
+ const session = await getSessionFromCtx(ctx);
1676
+ if (!session?.session) {
1677
+ throw new APIError3("UNAUTHORIZED");
1678
+ }
1679
+ return {
1680
+ session
1681
+ };
1682
+ });
1683
+
1684
+ // src/plugins/organization/call.ts
1685
+ var orgMiddleware = createAuthMiddleware(async (ctx) => {
1686
+ return {};
1687
+ });
1688
+ var orgSessionMiddleware = createAuthMiddleware(
1689
+ {
1690
+ use: [sessionMiddleware]
1691
+ },
1692
+ async (ctx) => {
1693
+ const session = ctx.context.session;
1694
+ return {
1695
+ session
1696
+ };
1697
+ }
1698
+ );
1699
+
1700
+ // src/plugins/organization/routes/crud-invites.ts
1701
+ import { z as z8 } from "zod";
1702
+
1703
+ // src/plugins/organization/schema.ts
1704
+ import { z as z7 } from "zod";
1705
+ var role = z7.enum(["admin", "member", "owner"]);
1706
+ var invitationStatus = z7.enum(["pending", "accepted", "rejected", "canceled"]).default("pending");
1707
+ var organizationSchema = z7.object({
1708
+ id: z7.string(),
1709
+ name: z7.string(),
1710
+ slug: z7.string()
1711
+ });
1712
+ var memberSchema = z7.object({
1713
+ id: z7.string(),
1714
+ name: z7.string(),
1715
+ email: z7.string(),
1716
+ organizationId: z7.string(),
1717
+ userId: z7.string(),
1718
+ role
1719
+ });
1720
+ var invitationSchema = z7.object({
1721
+ id: z7.string(),
1722
+ organizationId: z7.string(),
1723
+ email: z7.string(),
1724
+ role,
1725
+ status: invitationStatus,
1726
+ /**
1727
+ * The id of the user who invited the user.
1728
+ */
1729
+ inviterId: z7.string(),
1730
+ expiresAt: z7.date()
1731
+ });
1732
+
1733
+ // src/plugins/organization/routes/crud-invites.ts
1734
+ var createInvitation = createAuthEndpoint(
1735
+ "/organization/invite-member",
1736
+ {
1737
+ method: "POST",
1738
+ use: [orgMiddleware, orgSessionMiddleware],
1739
+ body: z8.object({
1740
+ email: z8.string(),
1741
+ role,
1742
+ organizationId: z8.string().optional(),
1743
+ resend: z8.boolean().optional()
1744
+ })
1745
+ },
1746
+ async (ctx) => {
1747
+ const session = ctx.context.session;
1748
+ const orgId = ctx.body.organizationId || session.session.activeOrganizationId;
1749
+ if (!orgId) {
1750
+ return ctx.json(null, {
1751
+ status: 400,
1752
+ body: {
1753
+ message: "Organization id not found!"
1754
+ }
1755
+ });
1756
+ }
1757
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
1758
+ const member = await adapter.findMemberByOrgId({
1759
+ userId: session.user.id,
1760
+ organizationId: orgId
1761
+ });
1762
+ if (!member) {
1763
+ return ctx.json(null, {
1764
+ status: 400,
1765
+ body: {
1766
+ message: "User is not a member of this organization!"
1767
+ }
1768
+ });
1769
+ }
1770
+ const role2 = ctx.context.roles[member.role];
1771
+ if (!role2) {
1772
+ return ctx.json(null, {
1773
+ status: 400,
1774
+ body: {
1775
+ message: "Role not found!"
1776
+ }
1777
+ });
1778
+ }
1779
+ const canInvite = role2.authorize({
1780
+ invitation: ["create"]
1781
+ });
1782
+ if (canInvite.error) {
1783
+ return ctx.json(null, {
1784
+ body: {
1785
+ message: "You are not allowed to invite users to this organization"
1786
+ },
1787
+ status: 403
1788
+ });
1789
+ }
1790
+ const alreadyMember = await adapter.findMemberByEmail({
1791
+ email: ctx.body.email,
1792
+ organizationId: orgId
1793
+ });
1794
+ if (alreadyMember) {
1795
+ return ctx.json(null, {
1796
+ status: 400,
1797
+ body: {
1798
+ message: "User is already a member of this organization"
1799
+ }
1800
+ });
1801
+ }
1802
+ const alreadyInvited = await adapter.findPendingInvitation({
1803
+ email: ctx.body.email,
1804
+ organizationId: orgId
1805
+ });
1806
+ if (alreadyInvited.length && !ctx.body.resend) {
1807
+ return ctx.json(null, {
1808
+ status: 400,
1809
+ body: {
1810
+ message: "User is already invited to this organization"
1811
+ }
1812
+ });
1813
+ }
1814
+ const invitation = await adapter.createInvitation({
1815
+ invitation: {
1816
+ role: ctx.body.role,
1817
+ email: ctx.body.email,
1818
+ organizationId: orgId
1819
+ },
1820
+ user: session.user
1821
+ });
1822
+ await ctx.context.orgOptions.sendInvitationEmail?.(
1823
+ invitation,
1824
+ ctx.body.email
1825
+ );
1826
+ return ctx.json(invitation);
1827
+ }
1828
+ );
1829
+ var acceptInvitation = createAuthEndpoint(
1830
+ "/organization/accept-invitation",
1831
+ {
1832
+ method: "POST",
1833
+ body: z8.object({
1834
+ invitationId: z8.string()
1835
+ }),
1836
+ use: [orgMiddleware, orgSessionMiddleware]
1837
+ },
1838
+ async (ctx) => {
1839
+ const session = ctx.context.session;
1840
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
1841
+ const invitation = await adapter.findInvitationById(ctx.body.invitationId);
1842
+ if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") {
1843
+ return ctx.json(null, {
1844
+ status: 400,
1845
+ body: {
1846
+ message: "Invitation not found!"
1847
+ }
1848
+ });
1849
+ }
1850
+ if (invitation.email !== session.user.email) {
1851
+ return ctx.json(null, {
1852
+ status: 400,
1853
+ body: {
1854
+ message: "You are not the repentant of the invitation"
1855
+ }
1856
+ });
1857
+ }
1858
+ const acceptedI = await adapter.updateInvitation({
1859
+ invitationId: ctx.body.invitationId,
1860
+ status: "accepted"
1861
+ });
1862
+ const member = await adapter.createMember({
1863
+ id: generateId(),
1864
+ organizationId: invitation.organizationId,
1865
+ userId: session.user.id,
1866
+ email: invitation.email,
1867
+ role: invitation.role,
1868
+ name: session.user.name
1869
+ });
1870
+ return ctx.json({
1871
+ invitation: acceptedI,
1872
+ member
1873
+ });
1874
+ }
1875
+ );
1876
+ var rejectInvitation = createAuthEndpoint(
1877
+ "/organization/reject-invitation",
1878
+ {
1879
+ method: "POST",
1880
+ body: z8.object({
1881
+ invitationId: z8.string()
1882
+ }),
1883
+ use: [orgMiddleware, orgSessionMiddleware]
1884
+ },
1885
+ async (ctx) => {
1886
+ const session = ctx.context.session;
1887
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
1888
+ const invitation = await adapter.findInvitationById(ctx.body.invitationId);
1889
+ if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") {
1890
+ return ctx.json(null, {
1891
+ status: 400,
1892
+ body: {
1893
+ message: "Invitation not found!"
1894
+ }
1895
+ });
1896
+ }
1897
+ if (invitation.email !== session.user.email) {
1898
+ return ctx.json(null, {
1899
+ status: 400,
1900
+ body: {
1901
+ message: "You are not the repentant of the invitation"
1902
+ }
1903
+ });
1904
+ }
1905
+ const rejectedI = await adapter.updateInvitation({
1906
+ invitationId: ctx.body.invitationId,
1907
+ status: "rejected"
1908
+ });
1909
+ return ctx.json({
1910
+ invitation: rejectedI,
1911
+ member: null
1912
+ });
1913
+ }
1914
+ );
1915
+ var cancelInvitation = createAuthEndpoint(
1916
+ "/organization/cancel-invitation",
1917
+ {
1918
+ method: "POST",
1919
+ body: z8.object({
1920
+ invitationId: z8.string()
1921
+ }),
1922
+ use: [orgMiddleware, orgSessionMiddleware]
1923
+ },
1924
+ async (ctx) => {
1925
+ const session = ctx.context.session;
1926
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
1927
+ const invitation = await adapter.findInvitationById(ctx.body.invitationId);
1928
+ if (!invitation) {
1929
+ return ctx.json(null, {
1930
+ status: 400,
1931
+ body: {
1932
+ message: "Invitation not found!"
1933
+ }
1934
+ });
1935
+ }
1936
+ const member = await adapter.findMemberByOrgId({
1937
+ userId: session.user.id,
1938
+ organizationId: invitation.organizationId
1939
+ });
1940
+ if (!member) {
1941
+ return ctx.json(null, {
1942
+ status: 400,
1943
+ body: {
1944
+ message: "User is not a member of this organization"
1945
+ }
1946
+ });
1947
+ }
1948
+ const canCancel = ctx.context.roles[member.role].authorize({
1949
+ invitation: ["cancel"]
1950
+ });
1951
+ if (canCancel.error) {
1952
+ return ctx.json(null, {
1953
+ status: 403,
1954
+ body: {
1955
+ message: "You are not allowed to cancel this invitation"
1956
+ }
1957
+ });
1958
+ }
1959
+ const canceledI = await adapter.updateInvitation({
1960
+ invitationId: ctx.body.invitationId,
1961
+ status: "canceled"
1962
+ });
1963
+ return ctx.json(canceledI);
1964
+ }
1965
+ );
1966
+ var getActiveInvitation = createAuthEndpoint(
1967
+ "/organization/get-active-invitation",
1968
+ {
1969
+ method: "GET",
1970
+ use: [orgMiddleware],
1971
+ query: z8.object({
1972
+ id: z8.string()
1973
+ })
1974
+ },
1975
+ async (ctx) => {
1976
+ const session = await getSessionFromCtx(ctx);
1977
+ if (!session) {
1978
+ return ctx.json(null, {
1979
+ status: 400,
1980
+ body: {
1981
+ message: "User not logged in"
1982
+ }
1983
+ });
1984
+ }
1985
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
1986
+ const invitation = await adapter.findInvitationById(ctx.query.id);
1987
+ if (!invitation || invitation.status !== "pending" || invitation.expiresAt < /* @__PURE__ */ new Date()) {
1988
+ return ctx.json(null, {
1989
+ status: 400,
1990
+ body: {
1991
+ message: "Invitation not found!"
1992
+ }
1993
+ });
1994
+ }
1995
+ if (invitation.email !== session.user.email) {
1996
+ return ctx.json(null, {
1997
+ status: 400,
1998
+ body: {
1999
+ message: "You are not the repentant of the invitation"
2000
+ }
2001
+ });
2002
+ }
2003
+ const organization2 = await adapter.findOrganizationById(
2004
+ invitation.organizationId
2005
+ );
2006
+ if (!organization2) {
2007
+ return ctx.json(null, {
2008
+ status: 400,
2009
+ body: {
2010
+ message: "Organization not found!"
2011
+ }
2012
+ });
2013
+ }
2014
+ const member = await adapter.findMemberByOrgId({
2015
+ userId: invitation.inviterId,
2016
+ organizationId: invitation.organizationId
2017
+ });
2018
+ if (!member) {
2019
+ return ctx.json(null, {
2020
+ status: 400,
2021
+ body: {
2022
+ message: "Inviter is no longer a member of this organization"
2023
+ }
2024
+ });
2025
+ }
2026
+ return ctx.json({
2027
+ ...invitation,
2028
+ organizationName: organization2.name,
2029
+ organizationSlug: organization2.slug,
2030
+ inviterEmail: member.email,
2031
+ inviterName: member.name
2032
+ });
2033
+ }
2034
+ );
2035
+
2036
+ // src/plugins/organization/routes/crud-members.ts
2037
+ import { z as z9 } from "zod";
2038
+ var removeMember = createAuthEndpoint(
2039
+ "/organization/remove-member",
2040
+ {
2041
+ method: "POST",
2042
+ body: z9.object({
2043
+ memberId: z9.string()
2044
+ }),
2045
+ use: [orgMiddleware, orgSessionMiddleware]
2046
+ },
2047
+ async (ctx) => {
2048
+ const session = ctx.context.session;
2049
+ const orgId = session.session.activeOrganizationId;
2050
+ if (!orgId) {
2051
+ return ctx.json(null, {
2052
+ status: 400,
2053
+ body: {
2054
+ message: "No active organization found!"
2055
+ }
2056
+ });
2057
+ }
2058
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
2059
+ const member = await adapter.findMemberByOrgId({
2060
+ userId: session.user.id,
2061
+ organizationId: orgId
2062
+ });
2063
+ if (!member) {
2064
+ return ctx.json(null, {
2065
+ status: 400,
2066
+ body: {
2067
+ message: "Member not found!"
2068
+ }
2069
+ });
2070
+ }
2071
+ const role2 = ctx.context.roles[member.role];
2072
+ if (!role2) {
2073
+ return ctx.json(null, {
2074
+ status: 400,
2075
+ body: {
2076
+ message: "Role not found!"
2077
+ }
2078
+ });
2079
+ }
2080
+ if (session.user.id === member.userId && member.role === (ctx.context.orgOptions?.creatorRole || "owner")) {
2081
+ return ctx.json(null, {
2082
+ status: 400,
2083
+ body: {
2084
+ message: "You cannot delete yourself"
2085
+ }
2086
+ });
2087
+ }
2088
+ const canDeleteMember = role2.authorize({
2089
+ member: ["delete"]
2090
+ });
2091
+ if (canDeleteMember.error) {
2092
+ return ctx.json(null, {
2093
+ body: {
2094
+ message: "You are not allowed to delete this member"
2095
+ },
2096
+ status: 403
2097
+ });
2098
+ }
2099
+ const existing = await adapter.findMemberById(ctx.body.memberId);
2100
+ if (existing?.organizationId !== orgId) {
2101
+ return ctx.json(null, {
2102
+ status: 400,
2103
+ body: {
2104
+ message: "Member not found!"
2105
+ }
2106
+ });
2107
+ }
2108
+ const deletedMember = await adapter.deleteMember(ctx.body.memberId);
2109
+ if (session.user.id === existing.userId && session.session.activeOrganizationId === existing.organizationId) {
2110
+ await adapter.setActiveOrganization(session.session.id, null);
2111
+ }
2112
+ return ctx.json(deletedMember);
2113
+ }
2114
+ );
2115
+ var updateMember = createAuthEndpoint(
2116
+ "/organization/update-member",
2117
+ {
2118
+ method: "POST",
2119
+ body: z9.object({
2120
+ memberId: z9.string(),
2121
+ role: z9.string()
2122
+ }),
2123
+ use: [orgMiddleware, orgSessionMiddleware]
2124
+ },
2125
+ async (ctx) => {
2126
+ const session = ctx.context.session;
2127
+ const orgId = session.session.activeOrganizationId;
2128
+ if (!orgId) {
2129
+ return ctx.json(null, {
2130
+ status: 400,
2131
+ body: {
2132
+ message: "No active organization found!"
2133
+ }
2134
+ });
2135
+ }
2136
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
2137
+ const member = await adapter.findMemberByOrgId({
2138
+ userId: session.user.id,
2139
+ organizationId: orgId
2140
+ });
2141
+ if (!member) {
2142
+ return ctx.json(null, {
2143
+ status: 400,
2144
+ body: {
2145
+ message: "Member not found!"
2146
+ }
2147
+ });
2148
+ }
2149
+ const role2 = ctx.context.roles[member.role];
2150
+ if (!role2) {
2151
+ return ctx.json(null, {
2152
+ status: 400,
2153
+ body: {
2154
+ message: "Role not found!"
2155
+ }
2156
+ });
2157
+ }
2158
+ const canUpdateMember = role2.authorize({
2159
+ member: ["update"]
2160
+ });
2161
+ if (canUpdateMember.error) {
2162
+ return ctx.json(null, {
2163
+ body: {
2164
+ message: "You are not allowed to update this member"
2165
+ },
2166
+ status: 403
2167
+ });
2168
+ }
2169
+ const updatedMember = await adapter.updateMember(
2170
+ ctx.body.memberId,
2171
+ ctx.body.role
2172
+ );
2173
+ return ctx.json(updatedMember);
2174
+ }
2175
+ );
2176
+
2177
+ // src/plugins/organization/routes/crud-org.ts
2178
+ import { z as z10 } from "zod";
2179
+ var createOrganization = createAuthEndpoint(
2180
+ "/organization/create",
2181
+ {
2182
+ method: "POST",
2183
+ body: z10.object({
2184
+ name: z10.string(),
2185
+ slug: z10.string(),
2186
+ userId: z10.string().optional()
2187
+ }),
2188
+ use: [orgMiddleware, orgSessionMiddleware]
2189
+ },
2190
+ async (ctx) => {
2191
+ const user = ctx.context.session.user;
2192
+ if (!user) {
2193
+ return ctx.json(null, {
2194
+ status: 401
2195
+ });
2196
+ }
2197
+ const options = ctx.context.orgOptions;
2198
+ const canCreateOrg = typeof options?.allowUserToCreateOrganization === "function" ? await options.allowUserToCreateOrganization(user) : options?.allowUserToCreateOrganization === void 0 ? true : options.allowUserToCreateOrganization;
2199
+ if (!canCreateOrg) {
2200
+ return ctx.json(null, {
2201
+ status: 403,
2202
+ body: {
2203
+ message: "You are not allowed to create organizations"
2204
+ }
2205
+ });
2206
+ }
2207
+ const adapter = getOrgAdapter(ctx.context.adapter, options);
2208
+ const existingOrganization = await adapter.findOrganizationBySlug(
2209
+ ctx.body.slug
2210
+ );
2211
+ if (existingOrganization) {
2212
+ return ctx.json(null, {
2213
+ status: 400,
2214
+ body: {
2215
+ message: "Organization with this slug already exists"
2216
+ }
2217
+ });
2218
+ }
2219
+ const organization2 = await adapter.createOrganization({
2220
+ organization: {
2221
+ id: generateId(),
2222
+ slug: ctx.body.slug,
2223
+ name: ctx.body.name
2224
+ },
2225
+ user
2226
+ });
2227
+ return ctx.json(organization2);
2228
+ }
2229
+ );
2230
+ var updateOrganization = createAuthEndpoint(
2231
+ "/organization/update",
2232
+ {
2233
+ method: "POST",
2234
+ body: z10.object({
2235
+ name: z10.string().optional(),
2236
+ slug: z10.string().optional(),
2237
+ orgId: z10.string().optional()
2238
+ }),
2239
+ requireHeaders: true,
2240
+ use: [orgMiddleware]
2241
+ },
2242
+ async (ctx) => {
2243
+ const session = await ctx.context.getSession(ctx);
2244
+ if (!session) {
2245
+ return ctx.json(null, {
2246
+ status: 401
2247
+ });
2248
+ }
2249
+ const orgId = ctx.body.orgId || session.session.activeOrganizationId;
2250
+ if (!orgId) {
2251
+ return ctx.json(null, {
2252
+ status: 400,
2253
+ body: {
2254
+ message: "Organization id not found!"
2255
+ }
2256
+ });
2257
+ }
2258
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
2259
+ const member = await adapter.findMemberByOrgId({
2260
+ userId: session.user.id,
2261
+ organizationId: orgId
2262
+ });
2263
+ if (!member) {
2264
+ return ctx.json(null, {
2265
+ status: 400,
2266
+ body: {
2267
+ message: "User is not a member of this organization!"
2268
+ }
2269
+ });
2270
+ }
2271
+ const role2 = ctx.context.roles[member.role];
2272
+ if (!role2) {
2273
+ return ctx.json(null, {
2274
+ status: 400,
2275
+ body: {
2276
+ message: "Role not found!"
2277
+ }
2278
+ });
2279
+ }
2280
+ const canUpdateOrg = role2.authorize({
2281
+ organization: ["update"]
2282
+ });
2283
+ if (canUpdateOrg.error) {
2284
+ return ctx.json(null, {
2285
+ body: {
2286
+ message: "You are not allowed to update this organization"
2287
+ },
2288
+ status: 403
2289
+ });
2290
+ }
2291
+ const updatedOrg = await adapter.updateOrganization(orgId, ctx.body);
2292
+ return ctx.json(updatedOrg);
2293
+ }
2294
+ );
2295
+ var deleteOrganization = createAuthEndpoint(
2296
+ "/organization/delete",
2297
+ {
2298
+ method: "POST",
2299
+ body: z10.object({
2300
+ orgId: z10.string()
2301
+ }),
2302
+ requireHeaders: true,
2303
+ use: [orgMiddleware]
2304
+ },
2305
+ async (ctx) => {
2306
+ const session = await ctx.context.getSession(ctx);
2307
+ if (!session) {
2308
+ return ctx.json(null, {
2309
+ status: 401
2310
+ });
2311
+ }
2312
+ const orgId = ctx.body.orgId;
2313
+ if (!orgId) {
2314
+ return ctx.json(null, {
2315
+ status: 400,
2316
+ body: {
2317
+ message: "Organization id not found!"
2318
+ }
2319
+ });
2320
+ }
2321
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
2322
+ const member = await adapter.findMemberByOrgId({
2323
+ userId: session.user.id,
2324
+ organizationId: orgId
2325
+ });
2326
+ if (!member) {
2327
+ return ctx.json(null, {
2328
+ status: 400,
2329
+ body: {
2330
+ message: "User is not a member of this organization!"
2331
+ }
2332
+ });
2333
+ }
2334
+ const role2 = ctx.context.roles[member.role];
2335
+ if (!role2) {
2336
+ return ctx.json(null, {
2337
+ status: 400,
2338
+ body: {
2339
+ message: "Role not found!"
2340
+ }
2341
+ });
2342
+ }
2343
+ const canDeleteOrg = role2.authorize({
2344
+ organization: ["delete"]
2345
+ });
2346
+ if (canDeleteOrg.error) {
2347
+ return ctx.json(null, {
2348
+ body: {
2349
+ message: "You are not allowed to delete this organization"
2350
+ },
2351
+ status: 403
2352
+ });
2353
+ }
2354
+ if (orgId === session.session.activeOrganizationId) {
2355
+ await adapter.setActiveOrganization(session.session.id, null);
2356
+ }
2357
+ const deletedOrg = await adapter.deleteOrganization(orgId);
2358
+ return ctx.json(deletedOrg);
2359
+ }
2360
+ );
2361
+ var getFullOrganization = createAuthEndpoint(
2362
+ "/organization/full",
2363
+ {
2364
+ method: "GET",
2365
+ query: z10.object({
2366
+ orgId: z10.string().optional()
2367
+ }),
2368
+ requireHeaders: true,
2369
+ use: [orgMiddleware, orgSessionMiddleware]
2370
+ },
2371
+ async (ctx) => {
2372
+ const session = ctx.context.session;
2373
+ const orgId = ctx.query.orgId || session.session.activeOrganizationId;
2374
+ if (!orgId) {
2375
+ return ctx.json(null, {
2376
+ status: 400,
2377
+ body: {
2378
+ message: "Organization id not found!"
2379
+ }
2380
+ });
2381
+ }
2382
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
2383
+ const organization2 = await adapter.findFullOrganization(orgId);
2384
+ if (!organization2) {
2385
+ return ctx.json(null, {
2386
+ status: 404,
2387
+ body: {
2388
+ message: "Organization not found!"
2389
+ }
2390
+ });
2391
+ }
2392
+ return ctx.json(organization2);
2393
+ }
2394
+ );
2395
+ var setActiveOrganization = createAuthEndpoint(
2396
+ "/organization/set-active",
2397
+ {
2398
+ method: "POST",
2399
+ body: z10.object({
2400
+ orgId: z10.string()
2401
+ }),
2402
+ use: [sessionMiddleware, orgMiddleware]
2403
+ },
2404
+ async (ctx) => {
2405
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
2406
+ const session = ctx.context.session;
2407
+ const orgId = ctx.body.orgId;
2408
+ await adapter.setActiveOrganization(session.session.id, orgId);
2409
+ const organization2 = await adapter.findFullOrganization(orgId);
2410
+ return ctx.json(organization2);
2411
+ }
2412
+ );
2413
+ var listOrganization = createAuthEndpoint(
2414
+ "/organization/list",
2415
+ {
2416
+ method: "GET",
2417
+ use: [orgMiddleware, orgSessionMiddleware]
2418
+ },
2419
+ async (ctx) => {
2420
+ const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
2421
+ const organizations = await adapter.listOrganizations(
2422
+ ctx.context.session.user.id
2423
+ );
2424
+ return ctx.json(organizations);
2425
+ }
2426
+ );
2427
+
2428
+ // src/plugins/organization/organization.ts
2429
+ var organization = (options) => {
2430
+ const endpoints = {
2431
+ createOrganization,
2432
+ updateOrganization,
2433
+ setActiveOrganization,
2434
+ getFullOrganization,
2435
+ listOrganization,
2436
+ createInvitation,
2437
+ cancelInvitation,
2438
+ acceptInvitation,
2439
+ getActiveInvitation,
2440
+ rejectInvitation,
2441
+ removeMember,
2442
+ updateMember
2443
+ };
2444
+ const roles = {
2445
+ ...defaultRoles,
2446
+ ...options?.roles
2447
+ };
2448
+ const api = shimContext(endpoints, {
2449
+ orgOptions: options || {},
2450
+ roles,
2451
+ getSession: async (context) => {
2452
+ return await getSessionFromCtx(context);
2453
+ }
2454
+ });
2455
+ return {
2456
+ id: "organization",
2457
+ endpoints: {
2458
+ ...api,
2459
+ hasPermission: createAuthEndpoint(
2460
+ "/organization/has-permission",
2461
+ {
2462
+ method: "POST",
2463
+ requireHeaders: true,
2464
+ body: z11.object({
2465
+ permission: z11.record(z11.string(), z11.array(z11.string()))
2466
+ }),
2467
+ use: [orgSessionMiddleware]
2468
+ },
2469
+ async (ctx) => {
2470
+ if (!ctx.context.session.session.activeOrganizationId) {
2471
+ throw new APIError5("BAD_REQUEST", {
2472
+ message: "No active organization"
2473
+ });
2474
+ }
2475
+ const adapter = getOrgAdapter(ctx.context.adapter);
2476
+ const member = await adapter.findMemberByOrgId({
2477
+ userId: ctx.context.session.user.id,
2478
+ organizationId: ctx.context.session.session.activeOrganizationId || ""
2479
+ });
2480
+ if (!member) {
2481
+ throw new APIError5("UNAUTHORIZED", {
2482
+ message: "You are not a member of this organization"
2483
+ });
2484
+ }
2485
+ const role2 = roles[member.role];
2486
+ const result = role2.authorize(ctx.body.permission);
2487
+ if (result.error) {
2488
+ return ctx.json(
2489
+ {
2490
+ error: result.error,
2491
+ success: false
2492
+ },
2493
+ {
2494
+ status: 403
2495
+ }
2496
+ );
2497
+ }
2498
+ return ctx.json({
2499
+ error: null,
2500
+ success: true
2501
+ });
2502
+ }
2503
+ )
2504
+ },
2505
+ schema: {
2506
+ session: {
2507
+ fields: {
2508
+ activeOrganizationId: {
2509
+ type: "string",
2510
+ required: false
2511
+ }
2512
+ }
2513
+ },
2514
+ organization: {
2515
+ fields: {
2516
+ name: {
2517
+ type: "string"
2518
+ },
2519
+ slug: {
2520
+ type: "string",
2521
+ unique: true
2522
+ }
2523
+ }
2524
+ },
2525
+ member: {
2526
+ fields: {
2527
+ organizationId: {
2528
+ type: "string",
2529
+ required: true
2530
+ },
2531
+ userId: {
2532
+ type: "string",
2533
+ required: true
2534
+ },
2535
+ email: {
2536
+ type: "string",
2537
+ required: true
2538
+ },
2539
+ name: {
2540
+ type: "string"
2541
+ },
2542
+ role: {
2543
+ type: "string",
2544
+ required: true,
2545
+ defaultValue: "member"
2546
+ }
2547
+ }
2548
+ },
2549
+ invitation: {
2550
+ fields: {
2551
+ organizationId: {
2552
+ type: "string",
2553
+ required: true
2554
+ },
2555
+ email: {
2556
+ type: "string",
2557
+ required: true
2558
+ },
2559
+ role: {
2560
+ type: "string",
2561
+ required: false
2562
+ },
2563
+ status: {
2564
+ type: "string",
2565
+ required: true,
2566
+ defaultValue: "pending"
2567
+ },
2568
+ expiresAt: {
2569
+ type: "date",
2570
+ required: true
2571
+ },
2572
+ inviterId: {
2573
+ type: "string",
2574
+ references: {
2575
+ model: "user",
2576
+ field: "id"
2577
+ }
2578
+ }
2579
+ }
2580
+ }
2581
+ }
2582
+ };
2583
+ };
2584
+
2585
+ // src/plugins/two-factor/index.ts
2586
+ import { alphabet as alphabet3, generateRandomString as generateRandomString3 } from "oslo/crypto";
2587
+ import "zod";
2588
+
2589
+ // src/crypto/index.ts
2590
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
2591
+ import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/ciphers/utils";
2592
+ import { managedNonce } from "@noble/ciphers/webcrypto";
2593
+ import { sha256 } from "@noble/hashes/sha256";
2594
+ async function hs256(secretKey, message) {
2595
+ const enc = new TextEncoder();
2596
+ const algorithm = { name: "HMAC", hash: "SHA-256" };
2597
+ const key = await crypto.subtle.importKey(
2598
+ "raw",
2599
+ enc.encode(secretKey),
2600
+ algorithm,
2601
+ false,
2602
+ ["sign", "verify"]
2603
+ );
2604
+ const signature = await crypto.subtle.sign(
2605
+ algorithm.name,
2606
+ key,
2607
+ enc.encode(message)
2608
+ );
2609
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
2610
+ }
2611
+ var symmetricEncrypt = ({ key, data }) => {
2612
+ const keyAsBytes = sha256(key);
2613
+ const dataAsBytes = utf8ToBytes(data);
2614
+ const chacha = managedNonce(xchacha20poly1305)(keyAsBytes);
2615
+ return bytesToHex(chacha.encrypt(dataAsBytes));
2616
+ };
2617
+ var symmetricDecrypt = ({ key, data }) => {
2618
+ const keyAsBytes = sha256(key);
2619
+ const dataAsBytes = hexToBytes(data);
2620
+ const chacha = managedNonce(xchacha20poly1305)(keyAsBytes);
2621
+ return chacha.decrypt(dataAsBytes);
2622
+ };
2623
+
2624
+ // src/plugins/two-factor/backup-codes/index.ts
2625
+ import { alphabet as alphabet2, generateRandomString as generateRandomString2 } from "oslo/crypto";
2626
+ import { z as z12 } from "zod";
2627
+
2628
+ // src/plugins/two-factor/verify-middleware.ts
2629
+ import { APIError as APIError6 } from "better-call";
2630
+
2631
+ // src/plugins/two-factor/constant.ts
2632
+ var TWO_FACTOR_COOKIE_NAME = "better-auth.two-factor";
2633
+ var OTP_RANDOM_NUMBER_COOKIE_NAME = "otp.counter";
2634
+
2635
+ // src/plugins/two-factor/verify-middleware.ts
2636
+ var verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
2637
+ const cookie = await ctx.getSignedCookie(
2638
+ TWO_FACTOR_COOKIE_NAME,
2639
+ ctx.context.secret
2640
+ );
2641
+ if (!cookie) {
2642
+ throw new APIError6("UNAUTHORIZED", {
2643
+ message: "two factor isn't enabled"
2644
+ });
2645
+ }
2646
+ const [userId, hash] = cookie.split("!");
2647
+ if (!userId || !hash) {
2648
+ throw new APIError6("UNAUTHORIZED", {
2649
+ message: "invalid two factor cookie"
2650
+ });
2651
+ }
2652
+ const sessions = await ctx.context.adapter.findMany({
2653
+ model: "session",
2654
+ where: [
2655
+ {
2656
+ field: "userId",
2657
+ value: userId
2658
+ }
2659
+ ]
2660
+ });
2661
+ if (!sessions.length) {
2662
+ throw new APIError6("UNAUTHORIZED", {
2663
+ message: "invalid session"
2664
+ });
2665
+ }
2666
+ const activeSessions = sessions.filter(
2667
+ (session) => session.expiresAt > /* @__PURE__ */ new Date()
2668
+ );
2669
+ if (!activeSessions) {
2670
+ throw new APIError6("UNAUTHORIZED", {
2671
+ message: "invalid session"
2672
+ });
2673
+ }
2674
+ for (const session of activeSessions) {
2675
+ const hashToMatch = await hs256(ctx.context.secret, session.id);
2676
+ const user = await ctx.context.adapter.findOne({
2677
+ model: "user",
2678
+ where: [
2679
+ {
2680
+ field: "id",
2681
+ value: session.userId
2682
+ }
2683
+ ]
2684
+ });
2685
+ if (!user) {
2686
+ throw new APIError6("UNAUTHORIZED", {
2687
+ message: "invalid session"
2688
+ });
2689
+ }
2690
+ if (hashToMatch === hash) {
2691
+ return {
2692
+ valid: async () => {
2693
+ await ctx.setSignedCookie(
2694
+ ctx.context.authCookies.sessionToken.name,
2695
+ session.id,
2696
+ ctx.context.secret,
2697
+ ctx.context.authCookies.sessionToken.options
2698
+ );
2699
+ if (ctx.body.callbackURL) {
2700
+ return ctx.json({
2701
+ status: true,
2702
+ callbackURL: ctx.body.callbackURL,
2703
+ redirect: true
2704
+ });
2705
+ }
2706
+ return ctx.json({ status: true });
2707
+ },
2708
+ invalid: async () => {
2709
+ return ctx.json(
2710
+ { status: false },
2711
+ {
2712
+ status: 401,
2713
+ body: {
2714
+ message: "Invalid code"
2715
+ }
2716
+ }
2717
+ );
2718
+ },
2719
+ session: {
2720
+ id: session.id,
2721
+ userId: session.userId,
2722
+ expiresAt: session.expiresAt,
2723
+ user
2724
+ }
2725
+ };
2726
+ }
2727
+ }
2728
+ throw new APIError6("UNAUTHORIZED", {
2729
+ message: "invalid two factor authentication"
2730
+ });
2731
+ });
2732
+
2733
+ // src/plugins/two-factor/backup-codes/index.ts
2734
+ function generateBackupCodesFn(options) {
2735
+ return Array.from({ length: options?.amount ?? 10 }).fill(null).map(
2736
+ () => generateRandomString2(options?.length ?? 10, alphabet2("a-z", "0-9"))
2737
+ ).map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
2738
+ }
2739
+ async function generateBackupCodes(secret, options) {
2740
+ const key = secret;
2741
+ const backupCodes = options?.customBackupCodesGenerate ? options.customBackupCodesGenerate() : generateBackupCodesFn();
2742
+ const encCodes = symmetricEncrypt({
2743
+ data: JSON.stringify(backupCodes),
2744
+ key
2745
+ });
2746
+ return {
2747
+ backupCodes,
2748
+ encryptedBackupCodes: encCodes
2749
+ };
2750
+ }
2751
+ async function verifyBackupCode(data, key) {
2752
+ const codes = await getBackupCodes(data.user, key);
2753
+ if (!codes) {
2754
+ return false;
2755
+ }
2756
+ return codes.includes(data.code);
2757
+ }
2758
+ async function getBackupCodes(user, key) {
2759
+ const secret = Buffer.from(
2760
+ await symmetricDecrypt({ key, data: user.twoFactorBackupCodes })
2761
+ ).toString("utf-8");
2762
+ const data = JSON.parse(secret);
2763
+ const result = z12.array(z12.string()).safeParse(data);
2764
+ if (result.success) {
2765
+ return result.data;
2766
+ }
2767
+ return null;
2768
+ }
2769
+ var backupCode2fa = (options) => {
2770
+ return {
2771
+ id: "backup_code",
2772
+ endpoints: {
2773
+ verifyBackupCode: createAuthEndpoint(
2774
+ "/two-factor/verify-backup-code",
2775
+ {
2776
+ method: "POST",
2777
+ body: z12.object({
2778
+ code: z12.string()
2779
+ }),
2780
+ use: [verifyTwoFactorMiddleware]
2781
+ },
2782
+ async (ctx) => {
2783
+ const validate = verifyBackupCode(
2784
+ {
2785
+ user: ctx.context.session.user,
2786
+ code: ctx.body.code
2787
+ },
2788
+ ctx.context.secret
2789
+ );
2790
+ if (!validate) {
2791
+ return ctx.json(
2792
+ { status: false },
2793
+ {
2794
+ status: 401
2795
+ }
2796
+ );
2797
+ }
2798
+ return ctx.json({ status: true });
2799
+ }
2800
+ ),
2801
+ generateBackupCodes: createAuthEndpoint(
2802
+ "/two-factor/generate-backup-codes",
2803
+ {
2804
+ method: "POST",
2805
+ use: [sessionMiddleware]
2806
+ },
2807
+ async (ctx) => {
2808
+ const backupCodes = await generateBackupCodes(
2809
+ ctx.context.secret,
2810
+ options
2811
+ );
2812
+ await ctx.context.adapter.update({
2813
+ model: "user",
2814
+ update: {
2815
+ twoFactorEnabled: true,
2816
+ twoFactorBackupCodes: backupCodes.encryptedBackupCodes
2817
+ },
2818
+ where: [
2819
+ {
2820
+ field: "id",
2821
+ value: ctx.context.session.user.id
2822
+ }
2823
+ ]
2824
+ });
2825
+ return ctx.json({
2826
+ status: true,
2827
+ backupCodes: backupCodes.backupCodes
2828
+ });
2829
+ }
2830
+ ),
2831
+ viewBackupCodes: createAuthEndpoint(
2832
+ "/view/backup-codes",
2833
+ {
2834
+ method: "GET",
2835
+ use: [sessionMiddleware]
2836
+ },
2837
+ async (ctx) => {
2838
+ const user = ctx.context.session.user;
2839
+ const backupCodes = getBackupCodes(user, ctx.context.secret);
2840
+ return ctx.json({
2841
+ status: true,
2842
+ backupCodes
2843
+ });
2844
+ }
2845
+ )
2846
+ }
2847
+ };
2848
+ };
2849
+
2850
+ // src/plugins/two-factor/otp/index.ts
2851
+ import { APIError as APIError7 } from "better-call";
2852
+ import { generateRandomInteger } from "oslo/crypto";
2853
+ import { generateHOTP } from "oslo/otp";
2854
+ import { z as z13 } from "zod";
2855
+ var otp2fa = (options) => {
2856
+ const send2FaOTP = createAuthEndpoint(
2857
+ "/two-factor/send-otp",
2858
+ {
2859
+ method: "POST",
2860
+ use: [verifyTwoFactorMiddleware]
2861
+ },
2862
+ async (ctx) => {
2863
+ if (!options || !options.sendOTP) {
2864
+ ctx.context.logger.error(
2865
+ "otp isn't configured. please pass otp option on two factor plugin to enable otp"
2866
+ );
2867
+ throw new APIError7("BAD_REQUEST", {
2868
+ message: "otp isn't configured"
2869
+ });
2870
+ }
2871
+ const randomNumber = generateRandomInteger(1e5);
2872
+ const otp = await generateHOTP(
2873
+ Buffer.from(ctx.context.secret),
2874
+ randomNumber
2875
+ );
2876
+ await options.sendOTP(ctx.context.session.user, otp);
2877
+ const cookie = ctx.context.createAuthCookie(
2878
+ OTP_RANDOM_NUMBER_COOKIE_NAME,
2879
+ {
2880
+ maxAge: options.period
2881
+ }
2882
+ );
2883
+ await ctx.setSignedCookie(
2884
+ cookie.name,
2885
+ randomNumber.toString(),
2886
+ ctx.context.secret,
2887
+ cookie.options
2888
+ );
2889
+ return ctx.json({ status: true });
2890
+ }
2891
+ );
2892
+ const verifyOTP = createAuthEndpoint(
2893
+ "/two-factor/verify-otp",
2894
+ {
2895
+ method: "POST",
2896
+ body: z13.object({
2897
+ code: z13.string()
2898
+ }),
2899
+ use: [verifyTwoFactorMiddleware]
2900
+ },
2901
+ async (ctx) => {
2902
+ const user = ctx.context.session.user;
2903
+ if (!user.twoFactorEnabled) {
2904
+ throw new APIError7("BAD_REQUEST", {
2905
+ message: "two factor isn't enabled"
2906
+ });
2907
+ }
2908
+ const cookie = ctx.context.createAuthCookie(
2909
+ OTP_RANDOM_NUMBER_COOKIE_NAME
2910
+ );
2911
+ const randomNumber = await ctx.getSignedCookie(
2912
+ cookie.name,
2913
+ ctx.context.secret
2914
+ );
2915
+ if (!randomNumber) {
2916
+ throw new APIError7("UNAUTHORIZED", {
2917
+ message: "OTP is expired"
2918
+ });
2919
+ }
2920
+ const toCheckOtp = await generateHOTP(
2921
+ Buffer.from(ctx.context.secret),
2922
+ parseInt(randomNumber)
2923
+ );
2924
+ if (toCheckOtp === ctx.body.code) {
2925
+ ctx.setCookie(cookie.name, "", {
2926
+ path: "/",
2927
+ sameSite: "lax",
2928
+ httpOnly: true,
2929
+ secure: false,
2930
+ maxAge: 0
2931
+ });
2932
+ return ctx.context.valid();
2933
+ } else {
2934
+ return ctx.context.invalid();
2935
+ }
2936
+ }
2937
+ );
2938
+ return {
2939
+ id: "otp",
2940
+ endpoints: {
2941
+ send2FaOTP,
2942
+ verifyOTP
2943
+ }
2944
+ };
2945
+ };
2946
+
2947
+ // src/plugins/two-factor/totp/index.ts
2948
+ import { APIError as APIError8 } from "better-call";
2949
+ import { TimeSpan as TimeSpan3 } from "oslo";
2950
+ import { TOTPController, createTOTPKeyURI } from "oslo/otp";
2951
+ import { z as z14 } from "zod";
2952
+ var totp2fa = (options) => {
2953
+ const opts = {
2954
+ digits: 6,
2955
+ period: new TimeSpan3(options?.period || 30, "s")
2956
+ };
2957
+ const generateTOTP = createAuthEndpoint(
2958
+ "/totp/generate",
2959
+ {
2960
+ method: "POST",
2961
+ use: [sessionMiddleware]
2962
+ },
2963
+ async (ctx) => {
2964
+ if (!options) {
2965
+ ctx.context.logger.error(
2966
+ "totp isn't configured. please pass totp option on two factor plugin to enable totp"
2967
+ );
2968
+ throw new APIError8("BAD_REQUEST", {
2969
+ message: "totp isn't configured"
2970
+ });
2971
+ }
2972
+ const session = ctx.context.session.user;
2973
+ const totp = new TOTPController(opts);
2974
+ const code = await totp.generate(Buffer.from(session.twoFactorSecret));
2975
+ return { code };
2976
+ }
2977
+ );
2978
+ const getTOTPURI = createAuthEndpoint(
2979
+ "/two-factor/get-totp-uri",
2980
+ {
2981
+ method: "GET",
2982
+ use: [sessionMiddleware]
2983
+ },
2984
+ async (ctx) => {
2985
+ if (!options) {
2986
+ ctx.context.logger.error(
2987
+ "totp isn't configured. please pass totp option on two factor plugin to enable totp"
2988
+ );
2989
+ throw new APIError8("BAD_REQUEST", {
2990
+ message: "totp isn't configured"
2991
+ });
2992
+ }
2993
+ const user = ctx.context.session.user;
2994
+ return {
2995
+ totpURI: createTOTPKeyURI(
2996
+ options?.issuer || "BetterAuth",
2997
+ user.email,
2998
+ Buffer.from(user.twoFactorSecret),
2999
+ opts
3000
+ )
3001
+ };
3002
+ }
3003
+ );
3004
+ const verifyTOTP = createAuthEndpoint(
3005
+ "/two-factor/verify-totp",
3006
+ {
3007
+ method: "POST",
3008
+ body: z14.object({
3009
+ code: z14.string(),
3010
+ callbackURL: z14.string().optional()
3011
+ }),
3012
+ use: [verifyTwoFactorMiddleware]
3013
+ },
3014
+ async (ctx) => {
3015
+ if (!options) {
3016
+ ctx.context.logger.error(
3017
+ "totp isn't configured. please pass totp option on two factor plugin to enable totp"
3018
+ );
3019
+ throw new APIError8("BAD_REQUEST", {
3020
+ message: "totp isn't configured"
3021
+ });
3022
+ }
3023
+ const totp = new TOTPController(opts);
3024
+ const secret = Buffer.from(
3025
+ await symmetricDecrypt({
3026
+ key: ctx.context.secret,
3027
+ data: ctx.context.session.user.twoFactorSecret
3028
+ })
3029
+ );
3030
+ const status = await totp.verify(ctx.body.code, secret);
3031
+ if (!status) {
3032
+ return ctx.context.invalid();
3033
+ }
3034
+ return ctx.context.valid();
3035
+ }
3036
+ );
3037
+ return {
3038
+ id: "totp",
3039
+ endpoints: {
3040
+ generateTOTP,
3041
+ viewTOTPURI: getTOTPURI,
3042
+ verifyTOTP
3043
+ }
3044
+ };
3045
+ };
3046
+
3047
+ // src/client/create-client-plugin.ts
3048
+ var createClientPlugin = () => {
3049
+ return ($fn) => {
3050
+ return ($fetch) => {
3051
+ const data = $fn($fetch);
3052
+ return {
3053
+ ...data,
3054
+ integrations: data.integrations,
3055
+ plugin: {}
3056
+ };
3057
+ };
3058
+ };
3059
+ };
3060
+
3061
+ // src/plugins/two-factor/client.ts
3062
+ var twoFactorClient = (options = {
3063
+ redirect: true,
3064
+ twoFactorPage: "/"
3065
+ }) => {
3066
+ return createClientPlugin()(($fetch) => {
3067
+ return {
3068
+ id: "two-factor",
3069
+ authProxySignal: [
3070
+ {
3071
+ matcher: (path) => path === "/two-factor/enable" || path === "/two-factor/send-otp",
3072
+ atom: "$sessionSignal"
3073
+ }
3074
+ ],
3075
+ pathMethods: {
3076
+ "enable/totp": "POST",
3077
+ "/two-factor/disable": "POST",
3078
+ "/two-factor/enable": "POST",
3079
+ "/two-factor/send-otp": "POST"
3080
+ },
3081
+ fetchPlugins: [
3082
+ {
3083
+ id: "two-factor",
3084
+ name: "two-factor",
3085
+ hooks: {
3086
+ async onSuccess(context) {
3087
+ if (context.data?.twoFactorRedirect) {
3088
+ if (options.redirect) {
3089
+ window.location.href = options.twoFactorPage;
3090
+ }
3091
+ }
3092
+ }
3093
+ }
3094
+ }
3095
+ ]
3096
+ };
3097
+ });
3098
+ };
3099
+
3100
+ // src/plugins/two-factor/index.ts
3101
+ var twoFactor = (options) => {
3102
+ const totp = totp2fa({
3103
+ issuer: options.issuer,
3104
+ ...options.totpOptions
3105
+ });
3106
+ const backupCode = backupCode2fa(options.backupCodeOptions);
3107
+ const otp = otp2fa(options.otpOptions);
3108
+ const providers = [totp, backupCode, otp];
3109
+ return {
3110
+ id: "two-factor",
3111
+ endpoints: {
3112
+ ...totp.endpoints,
3113
+ ...otp.endpoints,
3114
+ ...backupCode.endpoints,
3115
+ enableTwoFactor: createAuthEndpoint(
3116
+ "/two-factor/enable",
3117
+ {
3118
+ method: "POST",
3119
+ use: [sessionMiddleware]
3120
+ },
3121
+ async (ctx) => {
3122
+ const user = ctx.context.session.user;
3123
+ const secret = generateRandomString3(16, alphabet3("a-z", "0-9", "-"));
3124
+ const encryptedSecret = await symmetricEncrypt({
3125
+ key: ctx.context.secret,
3126
+ data: secret
3127
+ });
3128
+ const backupCodes = await generateBackupCodes(
3129
+ ctx.context.secret,
3130
+ options.backupCodeOptions
3131
+ );
3132
+ await ctx.context.adapter.update({
3133
+ model: "user",
3134
+ update: {
3135
+ twoFactorSecret: encryptedSecret,
3136
+ twoFactorEnabled: true,
3137
+ twoFactorBackupCodes: backupCodes.encryptedBackupCodes
3138
+ },
3139
+ where: [
3140
+ {
3141
+ field: "id",
3142
+ value: user.id
3143
+ }
3144
+ ]
3145
+ });
3146
+ return ctx.json({ status: true });
3147
+ }
3148
+ ),
3149
+ disableTwoFactor: createAuthEndpoint(
3150
+ "/two-factor/disable",
3151
+ {
3152
+ method: "POST",
3153
+ use: [sessionMiddleware]
3154
+ },
3155
+ async (ctx) => {
3156
+ const user = ctx.context.session.user;
3157
+ await ctx.context.adapter.update({
3158
+ model: "user",
3159
+ update: {
3160
+ twoFactorEnabled: false
3161
+ },
3162
+ where: [
3163
+ {
3164
+ field: "id",
3165
+ value: user.id
3166
+ }
3167
+ ]
3168
+ });
3169
+ return ctx.json({ status: true });
3170
+ }
3171
+ )
3172
+ },
3173
+ options,
3174
+ hooks: {
3175
+ after: [
3176
+ {
3177
+ matcher(context) {
3178
+ return context.path === "/sign-in/email" || context.path === "/sign-in/username";
3179
+ },
3180
+ handler: createAuthMiddleware(async (ctx) => {
3181
+ const returned = await ctx.returned;
3182
+ if (returned?.status !== 200) {
3183
+ return;
3184
+ }
3185
+ const response = await returned.json();
3186
+ if (!response.user.twoFactorEnabled) {
3187
+ return;
3188
+ }
3189
+ ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
3190
+ path: "/",
3191
+ sameSite: "lax",
3192
+ httpOnly: true,
3193
+ secure: false,
3194
+ maxAge: 0
3195
+ });
3196
+ const hash = await hs256(ctx.context.secret, response.session.id);
3197
+ await ctx.setSignedCookie(
3198
+ "better-auth.two-factor",
3199
+ `${response.session.userId}!${hash}`,
3200
+ ctx.context.secret,
3201
+ ctx.context.authCookies.sessionToken.options
3202
+ );
3203
+ const res = new Response(
3204
+ JSON.stringify({
3205
+ twoFactorRedirect: true
3206
+ }),
3207
+ {
3208
+ headers: ctx.responseHeader
3209
+ }
3210
+ );
3211
+ return {
3212
+ response: res
3213
+ };
3214
+ })
3215
+ }
3216
+ ]
3217
+ },
3218
+ schema: {
3219
+ user: {
3220
+ fields: {
3221
+ twoFactorEnabled: {
3222
+ type: "boolean",
3223
+ required: false,
3224
+ defaultValue: false
3225
+ },
3226
+ twoFactorSecret: {
3227
+ type: "string",
3228
+ required: false
3229
+ },
3230
+ twoFactorBackupCodes: {
3231
+ type: "string",
3232
+ required: false,
3233
+ returned: false
3234
+ }
3235
+ }
3236
+ }
3237
+ }
3238
+ };
3239
+ };
3240
+
3241
+ // src/plugins/passkey/index.ts
3242
+ import {
3243
+ generateAuthenticationOptions,
3244
+ generateRegistrationOptions,
3245
+ verifyAuthenticationResponse,
3246
+ verifyRegistrationResponse
3247
+ } from "@simplewebauthn/server";
3248
+ import { APIError as APIError9 } from "better-call";
3249
+ import { alphabet as alphabet4, generateRandomString as generateRandomString4 } from "oslo/crypto";
3250
+ import { z as z16 } from "zod";
3251
+
3252
+ // src/plugins/passkey/client.ts
3253
+ import {
3254
+ WebAuthnError,
3255
+ startAuthentication,
3256
+ startRegistration
3257
+ } from "@simplewebauthn/browser";
3258
+ var getPasskeyActions = ($fetch) => {
3259
+ const signInPasskey = async (opts) => {
3260
+ const response = await $fetch(
3261
+ "/passkey/generate-authenticate-options",
3262
+ {
3263
+ method: "POST",
3264
+ body: {
3265
+ email: opts?.email
3266
+ }
3267
+ }
3268
+ );
3269
+ if (!response.data) {
3270
+ return response;
3271
+ }
3272
+ try {
3273
+ const res = await startAuthentication(
3274
+ response.data,
3275
+ opts?.autoFill || false
3276
+ );
3277
+ const verified = await $fetch("/passkey/verify-authentication", {
3278
+ body: {
3279
+ response: res,
3280
+ type: "authenticate"
3281
+ }
3282
+ });
3283
+ if (!verified.data) {
3284
+ return verified;
3285
+ }
3286
+ } catch (e) {
3287
+ console.log(e);
3288
+ }
3289
+ };
3290
+ const registerPasskey = async () => {
3291
+ const options = await $fetch(
3292
+ "/passkey/generate-register-options",
3293
+ {
3294
+ method: "GET"
3295
+ }
3296
+ );
3297
+ if (!options.data) {
3298
+ return options;
3299
+ }
3300
+ try {
3301
+ const res = await startRegistration(options.data);
3302
+ const verified = await $fetch("/passkey/verify-registration", {
3303
+ body: {
3304
+ response: res,
3305
+ type: "register"
3306
+ }
3307
+ });
3308
+ if (!verified.data) {
3309
+ return verified;
3310
+ }
3311
+ } catch (e) {
3312
+ if (e instanceof WebAuthnError) {
3313
+ if (e.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED") {
3314
+ return {
3315
+ data: null,
3316
+ error: {
3317
+ message: "previously registered",
3318
+ status: 400,
3319
+ statusText: "BAD_REQUEST"
3320
+ }
3321
+ };
3322
+ }
3323
+ }
3324
+ }
3325
+ };
3326
+ return {
3327
+ signInPasskey,
3328
+ registerPasskey
3329
+ };
3330
+ };
3331
+ var passkeyClient = createClientPlugin()(
3332
+ ($fetch) => {
3333
+ return {
3334
+ id: "passkey",
3335
+ actions: getPasskeyActions($fetch)
3336
+ };
3337
+ }
3338
+ );
3339
+
3340
+ // src/plugins/passkey/index.ts
3341
+ var passkey = (options) => {
3342
+ const opts = {
3343
+ origin: null,
3344
+ ...options,
3345
+ rpID: process.env.NODE_ENV === "development" ? "localhost" : options.rpID,
3346
+ advanced: {
3347
+ webAuthnChallengeCookie: "better-auth-passkey",
3348
+ ...options.advanced
3349
+ }
3350
+ };
3351
+ const webAuthnChallengeCookieExpiration = 60 * 60 * 24;
3352
+ return {
3353
+ id: "passkey",
3354
+ endpoints: {
3355
+ generatePasskeyRegistrationOptions: createAuthEndpoint(
3356
+ "/passkey/generate-register-options",
3357
+ {
3358
+ method: "GET",
3359
+ use: [sessionMiddleware],
3360
+ metadata: {
3361
+ client: false
3362
+ }
3363
+ },
3364
+ async (ctx) => {
3365
+ const session = ctx.context.session;
3366
+ const userPasskeys = await ctx.context.adapter.findMany({
3367
+ model: "passkey",
3368
+ where: [
3369
+ {
3370
+ field: "userId",
3371
+ value: session.user.id
3372
+ }
3373
+ ]
3374
+ });
3375
+ const userID = new Uint8Array(
3376
+ Buffer.from(generateRandomString4(32, alphabet4("a-z", "0-9")))
3377
+ );
3378
+ let options2;
3379
+ options2 = await generateRegistrationOptions({
3380
+ rpName: opts.rpName,
3381
+ rpID: opts.rpID,
3382
+ userID,
3383
+ userName: session.user.email || session.user.id,
3384
+ attestationType: "none",
3385
+ excludeCredentials: userPasskeys.map((passkey2) => ({
3386
+ id: passkey2.id,
3387
+ transports: passkey2.transports?.split(
3388
+ ","
3389
+ )
3390
+ })),
3391
+ authenticatorSelection: {
3392
+ residentKey: "preferred",
3393
+ userVerification: "preferred",
3394
+ authenticatorAttachment: "platform"
3395
+ }
3396
+ });
3397
+ const data = {
3398
+ expectedChallenge: options2.challenge,
3399
+ userData: {
3400
+ ...session.user,
3401
+ email: session.user.email || session.user.id
3402
+ }
3403
+ };
3404
+ await ctx.setSignedCookie(
3405
+ opts.advanced.webAuthnChallengeCookie,
3406
+ JSON.stringify(data),
3407
+ ctx.context.secret,
3408
+ {
3409
+ secure: true,
3410
+ httpOnly: true,
3411
+ sameSite: "lax",
3412
+ maxAge: webAuthnChallengeCookieExpiration
3413
+ }
3414
+ );
3415
+ return ctx.json(options2, {
3416
+ status: 200
3417
+ });
3418
+ }
3419
+ ),
3420
+ generatePasskeyAuthenticationOptions: createAuthEndpoint(
3421
+ "/passkey/generate-authenticate-options",
3422
+ {
3423
+ method: "POST",
3424
+ body: z16.object({
3425
+ email: z16.string().optional(),
3426
+ callbackURL: z16.string().optional()
3427
+ }).optional()
3428
+ },
3429
+ async (ctx) => {
3430
+ const session = await getSessionFromCtx(ctx);
3431
+ let userPasskeys = [];
3432
+ if (session) {
3433
+ userPasskeys = await ctx.context.adapter.findMany({
3434
+ model: "passkey",
3435
+ where: [
3436
+ {
3437
+ field: "userId",
3438
+ value: session.user.id
3439
+ }
3440
+ ]
3441
+ });
3442
+ }
3443
+ const options2 = await generateAuthenticationOptions({
3444
+ rpID: opts.rpID,
3445
+ userVerification: "preferred",
3446
+ ...userPasskeys.length ? {
3447
+ allowCredentials: userPasskeys.map((passkey2) => ({
3448
+ id: passkey2.id,
3449
+ transports: passkey2.transports?.split(
3450
+ ","
3451
+ )
3452
+ }))
3453
+ } : {}
3454
+ });
3455
+ const data = {
3456
+ expectedChallenge: options2.challenge,
3457
+ userData: {
3458
+ email: session?.user.email || session?.user.id || "",
3459
+ id: session?.user.id || ""
3460
+ },
3461
+ callbackURL: ctx.body?.callbackURL
3462
+ };
3463
+ await ctx.setSignedCookie(
3464
+ opts.advanced.webAuthnChallengeCookie,
3465
+ JSON.stringify(data),
3466
+ ctx.context.secret,
3467
+ {
3468
+ secure: true,
3469
+ httpOnly: true,
3470
+ sameSite: "lax",
3471
+ maxAge: webAuthnChallengeCookieExpiration
3472
+ }
3473
+ );
3474
+ return ctx.json(options2, {
3475
+ status: 200
3476
+ });
3477
+ }
3478
+ ),
3479
+ verifyPasskeyRegistration: createAuthEndpoint(
3480
+ "/passkey/verify-registration",
3481
+ {
3482
+ method: "POST",
3483
+ body: z16.object({
3484
+ response: z16.any()
3485
+ }),
3486
+ use: [sessionMiddleware]
3487
+ },
3488
+ async (ctx) => {
3489
+ const origin = options.origin || ctx.headers?.get("origin") || "";
3490
+ if (!origin) {
3491
+ return ctx.json(null, {
3492
+ status: 400
3493
+ });
3494
+ }
3495
+ const resp = ctx.body.response;
3496
+ const challengeString = await ctx.getSignedCookie(
3497
+ opts.advanced.webAuthnChallengeCookie,
3498
+ ctx.context.secret
3499
+ );
3500
+ if (!challengeString) {
3501
+ return ctx.json(null, {
3502
+ status: 400
3503
+ });
3504
+ }
3505
+ const { userData, expectedChallenge } = JSON.parse(
3506
+ challengeString
3507
+ );
3508
+ if (userData.id !== ctx.context.session.user.id) {
3509
+ throw new APIError9("UNAUTHORIZED", {
3510
+ message: "You are not authorized to register this passkey"
3511
+ });
3512
+ }
3513
+ try {
3514
+ const verification = await verifyRegistrationResponse({
3515
+ response: resp,
3516
+ expectedChallenge,
3517
+ expectedOrigin: origin,
3518
+ expectedRPID: options.rpID
3519
+ });
3520
+ const { verified, registrationInfo } = verification;
3521
+ if (!verified || !registrationInfo) {
3522
+ return ctx.json(null, {
3523
+ status: 400
3524
+ });
3525
+ }
3526
+ const {
3527
+ credentialID,
3528
+ credentialPublicKey,
3529
+ counter,
3530
+ credentialDeviceType,
3531
+ credentialBackedUp
3532
+ } = registrationInfo;
3533
+ const pubKey = Buffer.from(credentialPublicKey).toString("base64");
3534
+ const userID = generateRandomString4(32, alphabet4("a-z", "0-9"));
3535
+ const newPasskey = {
3536
+ userId: userData.id,
3537
+ webauthnUserID: userID,
3538
+ id: credentialID,
3539
+ publicKey: pubKey,
3540
+ counter,
3541
+ deviceType: credentialDeviceType,
3542
+ transports: resp.response.transports.join(","),
3543
+ backedUp: credentialBackedUp,
3544
+ createdAt: /* @__PURE__ */ new Date()
3545
+ };
3546
+ const newPasskeyRes = await ctx.context.adapter.create({
3547
+ model: "passkey",
3548
+ data: newPasskey
3549
+ });
3550
+ return ctx.json(newPasskeyRes, {
3551
+ status: 200
3552
+ });
3553
+ } catch (e) {
3554
+ console.log(e);
3555
+ return ctx.json(null, {
3556
+ status: 400,
3557
+ body: {
3558
+ message: "Registration failed"
3559
+ }
3560
+ });
3561
+ }
3562
+ }
3563
+ ),
3564
+ verifyPasskeyAuthentication: createAuthEndpoint(
3565
+ "/passkey/verify-authentication",
3566
+ {
3567
+ method: "POST",
3568
+ body: z16.object({
3569
+ response: z16.any()
3570
+ })
3571
+ },
3572
+ async (ctx) => {
3573
+ const origin = options.origin || ctx.headers?.get("origin") || "";
3574
+ if (!origin) {
3575
+ return ctx.json(null, {
3576
+ status: 400
3577
+ });
3578
+ }
3579
+ const resp = ctx.body.response;
3580
+ const challengeString = await ctx.getSignedCookie(
3581
+ opts.advanced.webAuthnChallengeCookie,
3582
+ ctx.context.secret
3583
+ );
3584
+ if (!challengeString) {
3585
+ return ctx.json(null, {
3586
+ status: 400
3587
+ });
3588
+ }
3589
+ const { expectedChallenge, callbackURL } = JSON.parse(
3590
+ challengeString
3591
+ );
3592
+ const passkey2 = await ctx.context.adapter.findOne({
3593
+ model: "passkey",
3594
+ where: [
3595
+ {
3596
+ field: "id",
3597
+ value: resp.id
3598
+ }
3599
+ ]
3600
+ });
3601
+ if (!passkey2) {
3602
+ return ctx.json(null, {
3603
+ status: 401,
3604
+ body: {
3605
+ message: "Passkey not found"
3606
+ }
3607
+ });
3608
+ }
3609
+ try {
3610
+ const verification = await verifyAuthenticationResponse({
3611
+ response: resp,
3612
+ expectedChallenge,
3613
+ expectedOrigin: origin,
3614
+ expectedRPID: opts.rpID,
3615
+ authenticator: {
3616
+ credentialID: passkey2.id,
3617
+ credentialPublicKey: new Uint8Array(
3618
+ Buffer.from(passkey2.publicKey, "base64")
3619
+ ),
3620
+ counter: passkey2.counter,
3621
+ transports: passkey2.transports?.split(
3622
+ ","
3623
+ )
3624
+ }
3625
+ });
3626
+ const { verified } = verification;
3627
+ if (!verified)
3628
+ return ctx.json(null, {
3629
+ status: 401,
3630
+ body: {
3631
+ message: "verification failed"
3632
+ }
3633
+ });
3634
+ await ctx.context.adapter.update({
3635
+ model: "passkey",
3636
+ where: [
3637
+ {
3638
+ field: "id",
3639
+ value: passkey2.id
3640
+ }
3641
+ ],
3642
+ update: {
3643
+ counter: verification.authenticationInfo.newCounter
3644
+ }
3645
+ });
3646
+ const s = await ctx.context.internalAdapter.createSession(
3647
+ passkey2.userId,
3648
+ ctx.request
3649
+ );
3650
+ await ctx.setSignedCookie(
3651
+ ctx.context.authCookies.sessionToken.name,
3652
+ s.id,
3653
+ ctx.context.secret,
3654
+ ctx.context.authCookies.sessionToken.options
3655
+ );
3656
+ if (callbackURL) {
3657
+ return ctx.json({
3658
+ url: callbackURL,
3659
+ redirect: true,
3660
+ session: s
3661
+ });
3662
+ }
3663
+ return ctx.json(
3664
+ {
3665
+ session: s
3666
+ },
3667
+ {
3668
+ status: 200
3669
+ }
3670
+ );
3671
+ } catch (e) {
3672
+ ctx.context.logger.error(e);
3673
+ return ctx.json(null, {
3674
+ status: 400,
3675
+ body: {
3676
+ message: "Authentication failed"
3677
+ }
3678
+ });
3679
+ }
3680
+ }
3681
+ )
3682
+ },
3683
+ schema: {
3684
+ passkey: {
3685
+ fields: {
3686
+ publicKey: {
3687
+ type: "string"
3688
+ },
3689
+ userId: {
3690
+ type: "string",
3691
+ references: {
3692
+ model: "user",
3693
+ field: "id"
3694
+ }
3695
+ },
3696
+ webauthnUserID: {
3697
+ type: "string"
3698
+ },
3699
+ counter: {
3700
+ type: "number"
3701
+ },
3702
+ deviceType: {
3703
+ type: "string"
3704
+ },
3705
+ backedUp: {
3706
+ type: "boolean"
3707
+ },
3708
+ transports: {
3709
+ type: "string",
3710
+ required: false
3711
+ },
3712
+ createdAt: {
3713
+ type: "date",
3714
+ defaultValue: /* @__PURE__ */ new Date(),
3715
+ required: false
3716
+ }
3717
+ }
3718
+ }
3719
+ }
3720
+ };
3721
+ };
3722
+
3723
+ // src/plugins/username/index.ts
3724
+ import { z as z18 } from "zod";
3725
+ import { Argon2id as Argon2id4 } from "oslo/password";
3726
+ import { APIError as APIError10 } from "better-call";
3727
+
3728
+ // src/api/routes/sign-up.ts
3729
+ import { alphabet as alphabet5, generateRandomString as generateRandomString5 } from "oslo/crypto";
3730
+ import { Argon2id as Argon2id3 } from "oslo/password";
3731
+ import { z as z17 } from "zod";
3732
+ var signUpEmail = createAuthEndpoint(
3733
+ "/sign-up/email",
3734
+ {
3735
+ method: "POST",
3736
+ body: z17.object({
3737
+ name: z17.string(),
3738
+ email: z17.string().email(),
3739
+ password: z17.string(),
3740
+ image: z17.string().optional(),
3741
+ callbackURL: z17.string().optional()
3742
+ })
3743
+ },
3744
+ async (ctx) => {
3745
+ if (!ctx.context.options.emailAndPassword?.enabled) {
3746
+ return ctx.json(null, {
3747
+ status: 400,
3748
+ body: {
3749
+ message: "Email and password is not enabled"
3750
+ }
3751
+ });
3752
+ }
3753
+ const { name, email, password, image } = ctx.body;
3754
+ const minPasswordLength = ctx.context.options?.emailAndPassword?.minPasswordLength || 8;
3755
+ if (password.length < minPasswordLength) {
3756
+ ctx.context.logger.error("Password is too short");
3757
+ return ctx.json(null, {
3758
+ status: 400,
3759
+ body: { message: "Password is too short" }
3760
+ });
3761
+ }
3762
+ const argon2id = new Argon2id3();
3763
+ const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
3764
+ const hash = await argon2id.hash(password);
3765
+ if (dbUser?.user) {
3766
+ return ctx.json(null, {
3767
+ status: 400,
3768
+ body: {
3769
+ message: "User already exists"
3770
+ }
3771
+ });
3772
+ }
3773
+ const createdUser = await ctx.context.internalAdapter.createUser({
3774
+ id: generateRandomString5(32, alphabet5("a-z", "0-9", "A-Z")),
3775
+ email: email.toLowerCase(),
3776
+ name,
3777
+ image,
3778
+ emailVerified: false,
3779
+ createdAt: /* @__PURE__ */ new Date(),
3780
+ updatedAt: /* @__PURE__ */ new Date()
3781
+ });
3782
+ await ctx.context.internalAdapter.linkAccount({
3783
+ id: generateRandomString5(32, alphabet5("a-z", "0-9", "A-Z")),
3784
+ userId: createdUser.id,
3785
+ providerId: "credential",
3786
+ accountId: createdUser.id,
3787
+ password: hash
3788
+ });
3789
+ const session = await ctx.context.internalAdapter.createSession(
3790
+ createdUser.id,
3791
+ ctx.request
3792
+ );
3793
+ await ctx.setSignedCookie(
3794
+ ctx.context.authCookies.sessionToken.name,
3795
+ session.id,
3796
+ ctx.context.secret,
3797
+ ctx.context.authCookies.sessionToken.options
3798
+ );
3799
+ return ctx.json(
3800
+ {
3801
+ user: createdUser,
3802
+ session
3803
+ },
3804
+ {
3805
+ body: ctx.body.callbackURL ? {
3806
+ url: ctx.body.callbackURL,
3807
+ redirect: true
3808
+ } : {
3809
+ user: createdUser,
3810
+ session
3811
+ }
3812
+ }
3813
+ );
3814
+ }
3815
+ );
3816
+
3817
+ // src/plugins/username/index.ts
3818
+ var username = () => {
3819
+ return {
3820
+ id: "username",
3821
+ endpoints: {
3822
+ signInUsername: createAuthEndpoint(
3823
+ "/sign-in/username",
3824
+ {
3825
+ method: "POST",
3826
+ body: z18.object({
3827
+ username: z18.string(),
3828
+ password: z18.string(),
3829
+ dontRememberMe: z18.boolean().optional(),
3830
+ callbackURL: z18.string().optional()
3831
+ })
3832
+ },
3833
+ async (ctx) => {
3834
+ const user = await ctx.context.adapter.findOne({
3835
+ model: "user",
3836
+ where: [
3837
+ {
3838
+ field: "username",
3839
+ value: ctx.body.username
3840
+ }
3841
+ ]
3842
+ });
3843
+ const argon2id = new Argon2id4();
3844
+ if (!user) {
3845
+ await argon2id.hash(ctx.body.password);
3846
+ ctx.context.logger.error("User not found", { username });
3847
+ throw new APIError10("UNAUTHORIZED", {
3848
+ message: "Invalid email or password"
3849
+ });
3850
+ }
3851
+ const account = await ctx.context.adapter.findOne({
3852
+ model: "account",
3853
+ where: [
3854
+ {
3855
+ field: "userId",
3856
+ value: user.id
3857
+ },
3858
+ {
3859
+ field: "providerId",
3860
+ value: "credential"
3861
+ }
3862
+ ]
3863
+ });
3864
+ if (!account) {
3865
+ throw new APIError10("UNAUTHORIZED", {
3866
+ message: "Invalid email or password"
3867
+ });
3868
+ }
3869
+ const currentPassword = account?.password;
3870
+ if (!currentPassword) {
3871
+ ctx.context.logger.error("Password not found", { username });
3872
+ throw new APIError10("UNAUTHORIZED", {
3873
+ message: "Unexpected error"
3874
+ });
3875
+ }
3876
+ const validPassword = await argon2id.verify(
3877
+ currentPassword,
3878
+ ctx.body.password
3879
+ );
3880
+ if (!validPassword) {
3881
+ ctx.context.logger.error("Invalid password");
3882
+ throw new APIError10("UNAUTHORIZED", {
3883
+ message: "Invalid email or password"
3884
+ });
3885
+ }
3886
+ const session = await ctx.context.internalAdapter.createSession(
3887
+ user.id,
3888
+ ctx.request
3889
+ );
3890
+ await ctx.setSignedCookie(
3891
+ ctx.context.authCookies.sessionToken.name,
3892
+ session.id,
3893
+ ctx.context.secret,
3894
+ ctx.body.dontRememberMe ? {
3895
+ ...ctx.context.authCookies.sessionToken.options,
3896
+ maxAge: void 0
3897
+ } : ctx.context.authCookies.sessionToken.options
3898
+ );
3899
+ return ctx.json({
3900
+ user,
3901
+ session,
3902
+ redirect: !!ctx.body.callbackURL,
3903
+ url: ctx.body.callbackURL
3904
+ });
3905
+ }
3906
+ ),
3907
+ signUpUsername: createAuthEndpoint(
3908
+ "/sign-up/username",
3909
+ {
3910
+ method: "POST",
3911
+ body: z18.object({
3912
+ username: z18.string().min(3).max(20),
3913
+ name: z18.string(),
3914
+ email: z18.string().email(),
3915
+ password: z18.string(),
3916
+ image: z18.string().optional(),
3917
+ callbackURL: z18.string().optional()
3918
+ })
3919
+ },
3920
+ async (ctx) => {
3921
+ const res = await signUpEmail({
3922
+ ...ctx,
3923
+ //@ts-expect-error
3924
+ _flag: void 0
3925
+ });
3926
+ if (!res) {
3927
+ return ctx.json(null, {
3928
+ status: 400,
3929
+ body: {
3930
+ message: "Sign up failed",
3931
+ status: 400
3932
+ }
3933
+ });
3934
+ }
3935
+ await ctx.context.internalAdapter.updateUserByEmail(res.user.email, {
3936
+ username: ctx.body.username
3937
+ });
3938
+ if (ctx.body.callbackURL) {
3939
+ return ctx.json(res, {
3940
+ body: {
3941
+ url: ctx.body.callbackURL,
3942
+ redirect: true,
3943
+ ...res
3944
+ }
3945
+ });
3946
+ }
3947
+ return ctx.json(res);
3948
+ }
3949
+ )
3950
+ },
3951
+ schema: {
3952
+ user: {
3953
+ fields: {
3954
+ username: {
3955
+ type: "string",
3956
+ required: false,
3957
+ unique: true,
3958
+ returned: true
3959
+ }
3960
+ }
3961
+ }
3962
+ }
3963
+ };
3964
+ };
3965
+
3966
+ // src/plugins/bearer/index.ts
3967
+ import { serializeSigned } from "better-call";
3968
+ var bearer = () => {
3969
+ return {
3970
+ id: "bearer",
3971
+ hooks: {
3972
+ before: [
3973
+ {
3974
+ matcher(context) {
3975
+ return context.request?.headers.get("authorization")?.startsWith("Bearer ") || false;
3976
+ },
3977
+ handler: async (ctx) => {
3978
+ const token = ctx.request?.headers.get("authorization")?.replace("Bearer ", "");
3979
+ if (!token) {
3980
+ throw new BetterAuthError("No token found");
3981
+ }
3982
+ const headers = ctx.headers || new Headers();
3983
+ const signedToken = await serializeSigned(
3984
+ "",
3985
+ token,
3986
+ ctx.context.secret
3987
+ );
3988
+ headers.set(
3989
+ "cookie",
3990
+ `${ctx.context.authCookies.sessionToken.name}=${signedToken.replace("=", "")}`
3991
+ );
3992
+ }
3993
+ }
3994
+ ]
301
3995
  }
302
- return res;
303
3996
  };
304
3997
  };
305
3998
  export {
306
- emailVerification,
307
- getPlugins,
308
- usePlugins,
309
- withPlugins
3999
+ access_exports as ac,
4000
+ bearer,
4001
+ getPasskeyActions,
4002
+ organization,
4003
+ passkey,
4004
+ passkeyClient,
4005
+ twoFactor,
4006
+ twoFactorClient,
4007
+ username
310
4008
  };
311
4009
  //# sourceMappingURL=plugins.js.map