better-auth 1.1.18 → 1.1.19-beta.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.
@@ -15,7 +15,11 @@ var mongodb = require('mongodb');
15
15
 
16
16
  var createTransform = (options) => {
17
17
  const schema = chunkDVHD6IW3_cjs.getAuthTables(options);
18
+ const customIdGen = options.advanced?.generateId;
18
19
  function serializeID(field, value, model) {
20
+ if (customIdGen) {
21
+ return value;
22
+ }
19
23
  if (field === "id" || field === "_id" || schema[model].fields[field].references?.field === "id") {
20
24
  if (typeof value !== "string") {
21
25
  if (value instanceof mongodb.ObjectId) {
@@ -47,6 +51,9 @@ var createTransform = (options) => {
47
51
  return value;
48
52
  }
49
53
  function deserializeID(field, value, model) {
54
+ if (customIdGen) {
55
+ return value;
56
+ }
50
57
  if (field === "id" || schema[model].fields[field].references?.field === "id") {
51
58
  if (value instanceof mongodb.ObjectId) {
52
59
  return value.toHexString();
@@ -65,6 +72,9 @@ var createTransform = (options) => {
65
72
  }
66
73
  function getField(field, model) {
67
74
  if (field === "id") {
75
+ if (customIdGen) {
76
+ return "id";
77
+ }
68
78
  return "_id";
69
79
  }
70
80
  const f = schema[model].fields[field];
@@ -72,7 +82,9 @@ var createTransform = (options) => {
72
82
  }
73
83
  return {
74
84
  transformInput(data, model, action) {
75
- const transformedData = action === "update" ? {} : {
85
+ const transformedData = action === "update" ? {} : customIdGen ? {
86
+ id: customIdGen({ model })
87
+ } : {
76
88
  _id: new mongodb.ObjectId()
77
89
  };
78
90
  const fields = schema[model].fields;
@@ -179,17 +191,18 @@ var createTransform = (options) => {
179
191
  };
180
192
  var mongodbAdapter = (db) => (options) => {
181
193
  const transform = createTransform(options);
194
+ const hasCustomId = options.advanced?.generateId;
182
195
  return {
183
196
  id: "mongodb-adapter",
184
197
  async create(data) {
185
198
  const { model, data: values, select } = data;
186
199
  const transformedData = transform.transformInput(values, model, "create");
187
- if (transformedData.id) {
200
+ if (transformedData.id && !hasCustomId) {
188
201
  delete transformedData.id;
189
202
  }
190
203
  const res = await db.collection(transform.getModelName(model)).insertOne(transformedData);
191
204
  const id = res.insertedId;
192
- const insertedData = { ...transformedData, id: id.toString() };
205
+ const insertedData = { id: id.toString(), ...transformedData };
193
206
  const t = transform.transformOutput(insertedData, model, select);
194
207
  return t;
195
208
  },
@@ -13,7 +13,11 @@ import { ObjectId } from 'mongodb';
13
13
 
14
14
  var createTransform = (options) => {
15
15
  const schema = getAuthTables(options);
16
+ const customIdGen = options.advanced?.generateId;
16
17
  function serializeID(field, value, model) {
18
+ if (customIdGen) {
19
+ return value;
20
+ }
17
21
  if (field === "id" || field === "_id" || schema[model].fields[field].references?.field === "id") {
18
22
  if (typeof value !== "string") {
19
23
  if (value instanceof ObjectId) {
@@ -45,6 +49,9 @@ var createTransform = (options) => {
45
49
  return value;
46
50
  }
47
51
  function deserializeID(field, value, model) {
52
+ if (customIdGen) {
53
+ return value;
54
+ }
48
55
  if (field === "id" || schema[model].fields[field].references?.field === "id") {
49
56
  if (value instanceof ObjectId) {
50
57
  return value.toHexString();
@@ -63,6 +70,9 @@ var createTransform = (options) => {
63
70
  }
64
71
  function getField(field, model) {
65
72
  if (field === "id") {
73
+ if (customIdGen) {
74
+ return "id";
75
+ }
66
76
  return "_id";
67
77
  }
68
78
  const f = schema[model].fields[field];
@@ -70,7 +80,9 @@ var createTransform = (options) => {
70
80
  }
71
81
  return {
72
82
  transformInput(data, model, action) {
73
- const transformedData = action === "update" ? {} : {
83
+ const transformedData = action === "update" ? {} : customIdGen ? {
84
+ id: customIdGen({ model })
85
+ } : {
74
86
  _id: new ObjectId()
75
87
  };
76
88
  const fields = schema[model].fields;
@@ -177,17 +189,18 @@ var createTransform = (options) => {
177
189
  };
178
190
  var mongodbAdapter = (db) => (options) => {
179
191
  const transform = createTransform(options);
192
+ const hasCustomId = options.advanced?.generateId;
180
193
  return {
181
194
  id: "mongodb-adapter",
182
195
  async create(data) {
183
196
  const { model, data: values, select } = data;
184
197
  const transformedData = transform.transformInput(values, model, "create");
185
- if (transformedData.id) {
198
+ if (transformedData.id && !hasCustomId) {
186
199
  delete transformedData.id;
187
200
  }
188
201
  const res = await db.collection(transform.getModelName(model)).insertOne(transformedData);
189
202
  const id = res.insertedId;
190
- const insertedData = { ...transformedData, id: id.toString() };
203
+ const insertedData = { id: id.toString(), ...transformedData };
191
204
  const t = transform.transformOutput(insertedData, model, select);
192
205
  return t;
193
206
  },
@@ -332,7 +332,7 @@ var getMetadata = (ctx, options) => {
332
332
  issuer,
333
333
  authorization_endpoint: `${baseURL}/oauth2/authorize`,
334
334
  token_endpoint: `${baseURL}/oauth2/token`,
335
- userInfo_endpoint: `${baseURL}/oauth2/userinfo`,
335
+ userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
336
336
  jwks_uri: `${baseURL}/jwks`,
337
337
  registration_endpoint: `${baseURL}/oauth2/register`,
338
338
  scopes_supported: ["openid", "profile", "email", "offline_access"],
@@ -1,7 +1,7 @@
1
1
  import { createAuthEndpoint, APIError } from './chunk-KALC2G3Y.js';
2
2
  import { setSessionCookie } from './chunk-QQGZ3XGI.js';
3
3
  import { z } from 'zod';
4
- import { betterFetch } from '@better-fetch/fetch';
4
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
5
5
 
6
6
  // src/utils/boolean.ts
7
7
  function toBoolean(value) {
@@ -53,59 +53,80 @@ var oneTap = (options) => ({
53
53
  },
54
54
  async (ctx) => {
55
55
  const { idToken } = ctx.body;
56
- const { data, error } = await betterFetch("https://oauth2.googleapis.com/tokeninfo?id_token=" + idToken);
57
- if (error) {
58
- return ctx.json({
59
- error: "Invalid token"
56
+ let payload;
57
+ try {
58
+ const JWKS = createRemoteJWKSet(
59
+ new URL("https://www.googleapis.com/oauth2/v3/certs")
60
+ );
61
+ const { payload: verifiedPayload } = await jwtVerify(
62
+ idToken,
63
+ JWKS,
64
+ {
65
+ issuer: ["https://accounts.google.com", "accounts.google.com"],
66
+ audience: options?.clientId || ctx.context.options.socialProviders?.google?.clientId
67
+ }
68
+ );
69
+ payload = verifiedPayload;
70
+ } catch (error) {
71
+ throw new APIError("BAD_REQUEST", {
72
+ message: "invalid id token"
60
73
  });
61
74
  }
62
- const user = await ctx.context.internalAdapter.findUserByEmail(
63
- data.email
64
- );
75
+ const { email, email_verified, name, picture, sub } = payload;
76
+ if (!email) {
77
+ return ctx.json({ error: "Email not available in token" });
78
+ }
79
+ const user = await ctx.context.internalAdapter.findUserByEmail(email);
65
80
  if (!user) {
66
81
  if (options?.disableSignup) {
67
82
  throw new APIError("BAD_GATEWAY", {
68
83
  message: "User not found"
69
84
  });
70
85
  }
71
- const user2 = await ctx.context.internalAdapter.createOAuthUser(
86
+ const newUser = await ctx.context.internalAdapter.createOAuthUser(
72
87
  {
73
- email: data.email,
74
- emailVerified: toBoolean(data.email_verified),
75
- name: data.name,
76
- image: data.picture
88
+ email,
89
+ emailVerified: typeof email_verified === "boolean" ? email_verified : toBoolean(email_verified),
90
+ name,
91
+ image: picture
77
92
  },
78
93
  {
79
94
  providerId: "google",
80
- accountId: data.sub
95
+ accountId: sub
81
96
  }
82
97
  );
83
- if (!user2) {
98
+ if (!newUser) {
84
99
  throw new APIError("INTERNAL_SERVER_ERROR", {
85
100
  message: "Could not create user"
86
101
  });
87
102
  }
88
103
  const session2 = await ctx.context.internalAdapter.createSession(
89
- user2?.user.id,
104
+ newUser.user.id,
90
105
  ctx.request
91
106
  );
92
107
  await setSessionCookie(ctx, {
93
- user: user2.user,
108
+ user: newUser.user,
94
109
  session: session2
95
110
  });
96
111
  return ctx.json({
97
112
  token: session2.token,
98
113
  user: {
99
- id: user2.user.id,
100
- email: user2.user.email,
101
- emailVerified: user2.user.emailVerified,
102
- name: user2.user.name,
103
- image: user2.user.image,
104
- createdAt: user2.user.createdAt,
105
- updatedAt: user2.user.updatedAt
114
+ id: newUser.user.id,
115
+ email: newUser.user.email,
116
+ emailVerified: newUser.user.emailVerified,
117
+ name: newUser.user.name,
118
+ image: newUser.user.image,
119
+ createdAt: newUser.user.createdAt,
120
+ updatedAt: newUser.user.updatedAt
106
121
  }
107
122
  });
108
123
  }
124
+ const account = await ctx.context.internalAdapter.findAccount(sub);
125
+ if (!account) {
126
+ throw new APIError("UNAUTHORIZED", {
127
+ message: "Google sub doesn't match"
128
+ });
129
+ }
109
130
  const session = await ctx.context.internalAdapter.createSession(
110
131
  user.user.id,
111
132
  ctx.request
@@ -179,6 +179,9 @@ function getResponse(responses) {
179
179
  ...responses
180
180
  };
181
181
  }
182
+ function toOpenApiPath(path) {
183
+ return path.split("/").map((part) => part.startsWith(":") ? `{${part.slice(1)}}` : part).join("/");
184
+ }
182
185
  async function generator(ctx, options) {
183
186
  const baseEndpoints = chunkCQNPMJJ3_cjs.getEndpoints(ctx, {
184
187
  ...options,
@@ -209,8 +212,9 @@ async function generator(ctx, options) {
209
212
  Object.entries(baseEndpoints.api).forEach(([_, value]) => {
210
213
  const options2 = value.options;
211
214
  if (options2.metadata?.SERVER_ONLY) return;
215
+ const path = toOpenApiPath(value.path);
212
216
  if (options2.method === "GET") {
213
- paths[value.path] = {
217
+ paths[path] = {
214
218
  get: {
215
219
  tags: ["Default", ...options2.metadata?.openapi?.tags || []],
216
220
  description: options2.metadata?.openapi?.description,
@@ -227,7 +231,7 @@ async function generator(ctx, options) {
227
231
  }
228
232
  if (options2.method === "POST") {
229
233
  const body = getRequestBody(options2);
230
- paths[value.path] = {
234
+ paths[path] = {
231
235
  post: {
232
236
  tags: ["Default", ...options2.metadata?.openapi?.tags || []],
233
237
  description: options2.metadata?.openapi?.description,
@@ -273,8 +277,9 @@ async function generator(ctx, options) {
273
277
  Object.entries(api).forEach(([key, value]) => {
274
278
  const options2 = value.options;
275
279
  if (options2.metadata?.SERVER_ONLY) return;
280
+ const path = toOpenApiPath(value.path);
276
281
  if (options2.method === "GET") {
277
- paths[value.path] = {
282
+ paths[path] = {
278
283
  get: {
279
284
  tags: options2.metadata?.openapi?.tags || [
280
285
  plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)
@@ -292,7 +297,7 @@ async function generator(ctx, options) {
292
297
  };
293
298
  }
294
299
  if (options2.method === "POST") {
295
- paths[value.path] = {
300
+ paths[path] = {
296
301
  post: {
297
302
  tags: options2.metadata?.openapi?.tags || [
298
303
  plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)
@@ -3,7 +3,7 @@
3
3
  var chunkCQNPMJJ3_cjs = require('./chunk-CQNPMJJ3.cjs');
4
4
  var chunkV6HE2MP7_cjs = require('./chunk-V6HE2MP7.cjs');
5
5
  var zod = require('zod');
6
- var fetch = require('@better-fetch/fetch');
6
+ var jose = require('jose');
7
7
 
8
8
  // src/utils/boolean.ts
9
9
  function toBoolean(value) {
@@ -55,59 +55,80 @@ var oneTap = (options) => ({
55
55
  },
56
56
  async (ctx) => {
57
57
  const { idToken } = ctx.body;
58
- const { data, error } = await fetch.betterFetch("https://oauth2.googleapis.com/tokeninfo?id_token=" + idToken);
59
- if (error) {
60
- return ctx.json({
61
- error: "Invalid token"
58
+ let payload;
59
+ try {
60
+ const JWKS = jose.createRemoteJWKSet(
61
+ new URL("https://www.googleapis.com/oauth2/v3/certs")
62
+ );
63
+ const { payload: verifiedPayload } = await jose.jwtVerify(
64
+ idToken,
65
+ JWKS,
66
+ {
67
+ issuer: ["https://accounts.google.com", "accounts.google.com"],
68
+ audience: options?.clientId || ctx.context.options.socialProviders?.google?.clientId
69
+ }
70
+ );
71
+ payload = verifiedPayload;
72
+ } catch (error) {
73
+ throw new chunkCQNPMJJ3_cjs.APIError("BAD_REQUEST", {
74
+ message: "invalid id token"
62
75
  });
63
76
  }
64
- const user = await ctx.context.internalAdapter.findUserByEmail(
65
- data.email
66
- );
77
+ const { email, email_verified, name, picture, sub } = payload;
78
+ if (!email) {
79
+ return ctx.json({ error: "Email not available in token" });
80
+ }
81
+ const user = await ctx.context.internalAdapter.findUserByEmail(email);
67
82
  if (!user) {
68
83
  if (options?.disableSignup) {
69
84
  throw new chunkCQNPMJJ3_cjs.APIError("BAD_GATEWAY", {
70
85
  message: "User not found"
71
86
  });
72
87
  }
73
- const user2 = await ctx.context.internalAdapter.createOAuthUser(
88
+ const newUser = await ctx.context.internalAdapter.createOAuthUser(
74
89
  {
75
- email: data.email,
76
- emailVerified: toBoolean(data.email_verified),
77
- name: data.name,
78
- image: data.picture
90
+ email,
91
+ emailVerified: typeof email_verified === "boolean" ? email_verified : toBoolean(email_verified),
92
+ name,
93
+ image: picture
79
94
  },
80
95
  {
81
96
  providerId: "google",
82
- accountId: data.sub
97
+ accountId: sub
83
98
  }
84
99
  );
85
- if (!user2) {
100
+ if (!newUser) {
86
101
  throw new chunkCQNPMJJ3_cjs.APIError("INTERNAL_SERVER_ERROR", {
87
102
  message: "Could not create user"
88
103
  });
89
104
  }
90
105
  const session2 = await ctx.context.internalAdapter.createSession(
91
- user2?.user.id,
106
+ newUser.user.id,
92
107
  ctx.request
93
108
  );
94
109
  await chunkV6HE2MP7_cjs.setSessionCookie(ctx, {
95
- user: user2.user,
110
+ user: newUser.user,
96
111
  session: session2
97
112
  });
98
113
  return ctx.json({
99
114
  token: session2.token,
100
115
  user: {
101
- id: user2.user.id,
102
- email: user2.user.email,
103
- emailVerified: user2.user.emailVerified,
104
- name: user2.user.name,
105
- image: user2.user.image,
106
- createdAt: user2.user.createdAt,
107
- updatedAt: user2.user.updatedAt
116
+ id: newUser.user.id,
117
+ email: newUser.user.email,
118
+ emailVerified: newUser.user.emailVerified,
119
+ name: newUser.user.name,
120
+ image: newUser.user.image,
121
+ createdAt: newUser.user.createdAt,
122
+ updatedAt: newUser.user.updatedAt
108
123
  }
109
124
  });
110
125
  }
126
+ const account = await ctx.context.internalAdapter.findAccount(sub);
127
+ if (!account) {
128
+ throw new chunkCQNPMJJ3_cjs.APIError("UNAUTHORIZED", {
129
+ message: "Google sub doesn't match"
130
+ });
131
+ }
111
132
  const session = await ctx.context.internalAdapter.createSession(
112
133
  user.user.id,
113
134
  ctx.request
@@ -177,6 +177,9 @@ function getResponse(responses) {
177
177
  ...responses
178
178
  };
179
179
  }
180
+ function toOpenApiPath(path) {
181
+ return path.split("/").map((part) => part.startsWith(":") ? `{${part.slice(1)}}` : part).join("/");
182
+ }
180
183
  async function generator(ctx, options) {
181
184
  const baseEndpoints = getEndpoints(ctx, {
182
185
  ...options,
@@ -207,8 +210,9 @@ async function generator(ctx, options) {
207
210
  Object.entries(baseEndpoints.api).forEach(([_, value]) => {
208
211
  const options2 = value.options;
209
212
  if (options2.metadata?.SERVER_ONLY) return;
213
+ const path = toOpenApiPath(value.path);
210
214
  if (options2.method === "GET") {
211
- paths[value.path] = {
215
+ paths[path] = {
212
216
  get: {
213
217
  tags: ["Default", ...options2.metadata?.openapi?.tags || []],
214
218
  description: options2.metadata?.openapi?.description,
@@ -225,7 +229,7 @@ async function generator(ctx, options) {
225
229
  }
226
230
  if (options2.method === "POST") {
227
231
  const body = getRequestBody(options2);
228
- paths[value.path] = {
232
+ paths[path] = {
229
233
  post: {
230
234
  tags: ["Default", ...options2.metadata?.openapi?.tags || []],
231
235
  description: options2.metadata?.openapi?.description,
@@ -271,8 +275,9 @@ async function generator(ctx, options) {
271
275
  Object.entries(api).forEach(([key, value]) => {
272
276
  const options2 = value.options;
273
277
  if (options2.metadata?.SERVER_ONLY) return;
278
+ const path = toOpenApiPath(value.path);
274
279
  if (options2.method === "GET") {
275
- paths[value.path] = {
280
+ paths[path] = {
276
281
  get: {
277
282
  tags: options2.metadata?.openapi?.tags || [
278
283
  plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)
@@ -290,7 +295,7 @@ async function generator(ctx, options) {
290
295
  };
291
296
  }
292
297
  if (options2.method === "POST") {
293
- paths[value.path] = {
298
+ paths[path] = {
294
299
  post: {
295
300
  tags: options2.metadata?.openapi?.tags || [
296
301
  plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)
@@ -330,7 +330,7 @@ var getMetadata = (ctx, options) => {
330
330
  issuer,
331
331
  authorization_endpoint: `${baseURL}/oauth2/authorize`,
332
332
  token_endpoint: `${baseURL}/oauth2/token`,
333
- userInfo_endpoint: `${baseURL}/oauth2/userinfo`,
333
+ userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
334
334
  jwks_uri: `${baseURL}/jwks`,
335
335
  registration_endpoint: `${baseURL}/oauth2/register`,
336
336
  scopes_supported: ["openid", "profile", "email", "offline_access"],
@@ -8,6 +8,7 @@ var chunkXAK7GFTD_cjs = require('../chunk-XAK7GFTD.cjs');
8
8
  require('../chunk-U4I57HJ4.cjs');
9
9
  require('../chunk-S5UORXJH.cjs');
10
10
  require('../chunk-CCKQSGIR.cjs');
11
+ var chunkG2LZ73E2_cjs = require('../chunk-G2LZ73E2.cjs');
11
12
  require('../chunk-VXYIYABQ.cjs');
12
13
  var chunkPEZRSDZS_cjs = require('../chunk-PEZRSDZS.cjs');
13
14
  var nanostores = require('nanostores');
@@ -235,26 +236,58 @@ var oneTapClient = (options) => {
235
236
  const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
236
237
  const contextValue = context ?? options.context ?? "signin";
237
238
  await loadGoogleScript();
238
- await new Promise((resolve) => {
239
+ await new Promise((resolve, reject) => {
240
+ let isResolved = false;
241
+ const baseDelay = options.promptOptions?.baseDelay ?? 1e3;
242
+ const maxAttempts = options.promptOptions?.maxAttempts ?? 5;
239
243
  window.google?.accounts.id.initialize({
240
244
  client_id: options.clientId,
241
245
  callback: async (response) => {
242
- await $fetch("/one-tap/callback", {
243
- method: "POST",
244
- body: { idToken: response.credential },
245
- ...opts?.fetchOptions,
246
- ...fetchOptions
247
- });
248
- if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
249
- window.location.href = opts?.callbackURL ?? "/";
246
+ isResolved = true;
247
+ try {
248
+ await $fetch("/one-tap/callback", {
249
+ method: "POST",
250
+ body: { idToken: response.credential },
251
+ ...opts?.fetchOptions,
252
+ ...fetchOptions
253
+ });
254
+ if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
255
+ window.location.href = opts?.callbackURL ?? "/";
256
+ }
257
+ resolve();
258
+ } catch (error) {
259
+ console.error("Error during One Tap callback:", error);
260
+ reject(error);
250
261
  }
251
- resolve();
252
262
  },
253
263
  auto_select: autoSelect,
254
264
  cancel_on_tap_outside: cancelOnTapOutside,
255
- context: contextValue
265
+ context: contextValue,
266
+ nonce: chunkG2LZ73E2_cjs.generateRandomString(16),
267
+ ...options.additionalOptions
256
268
  });
257
- window.google?.accounts.id.prompt();
269
+ const handlePrompt = (attempt) => {
270
+ if (isResolved) return;
271
+ window.google?.accounts.id.prompt((notification) => {
272
+ if (isResolved) return;
273
+ if (notification.isDismissedMoment && notification.isDismissedMoment()) {
274
+ if (attempt < maxAttempts) {
275
+ const delay = Math.pow(2, attempt) * baseDelay;
276
+ setTimeout(() => handlePrompt(attempt + 1), delay);
277
+ } else {
278
+ opts?.onPromptNotification?.(notification);
279
+ }
280
+ } else if (notification.isSkippedMoment && notification.isSkippedMoment()) {
281
+ if (attempt < maxAttempts) {
282
+ const delay = Math.pow(2, attempt) * baseDelay;
283
+ setTimeout(() => handlePrompt(attempt + 1), delay);
284
+ } else {
285
+ opts?.onPromptNotification?.(notification);
286
+ }
287
+ }
288
+ });
289
+ };
290
+ handlePrompt(0);
258
291
  });
259
292
  } catch (error) {
260
293
  console.error("Error during Google One Tap flow:", error);
@@ -324,7 +324,7 @@ declare global {
324
324
  accounts: {
325
325
  id: {
326
326
  initialize: (config: any) => void;
327
- prompt: () => void;
327
+ prompt: (callback?: (notification: any) => void) => void;
328
328
  };
329
329
  };
330
330
  };
@@ -345,13 +345,51 @@ interface GoogleOneTapOptions {
345
345
  */
346
346
  cancelOnTapOutside?: boolean;
347
347
  /**
348
- * Context of the Google One Tap flow
348
+ * The mode to use for the Google One Tap flow
349
+ *
350
+ * popup: Use a popup window
351
+ * redirect: Redirect the user to the Google One Tap flow
352
+ *
353
+ * @default "popup"
349
354
  */
350
- context?: "signin" | "signup" | "use";
355
+ uxMode?: "popup" | "redirect";
356
+ /**
357
+ * The context to use for the Google One Tap flow
358
+ *
359
+ * @default "signin"
360
+ */
361
+ context?: string;
362
+ /**
363
+ * Additional configuration options to pass to the Google One Tap API.
364
+ */
365
+ additionalOptions?: Record<string, any>;
366
+ /**
367
+ * Configuration options for the prompt and exponential backoff behavior.
368
+ */
369
+ promptOptions?: {
370
+ /**
371
+ * Base delay (in milliseconds) for exponential backoff.
372
+ * @default 1000
373
+ */
374
+ baseDelay?: number;
375
+ /**
376
+ * Maximum number of prompt attempts before calling onPromptNotification.
377
+ * @default 5
378
+ */
379
+ maxAttempts?: number;
380
+ };
351
381
  }
352
- interface GoogleOneTapActionOptions extends Omit<GoogleOneTapOptions, "clientId"> {
382
+ interface GoogleOneTapActionOptions extends Omit<GoogleOneTapOptions, "clientId" | "promptOptions"> {
353
383
  fetchOptions?: BetterFetchOption;
384
+ /**
385
+ * Callback URL.
386
+ */
354
387
  callbackURL?: string;
388
+ /**
389
+ * Optional callback that receives the prompt notification if (or when) the prompt is dismissed or skipped.
390
+ * This lets you render an alternative UI (e.g. a Google Sign-In button) to restart the process.
391
+ */
392
+ onPromptNotification?: (notification: any) => void;
355
393
  }
356
394
  declare const oneTapClient: (options: GoogleOneTapOptions) => {
357
395
  id: "one-tap";
@@ -324,7 +324,7 @@ declare global {
324
324
  accounts: {
325
325
  id: {
326
326
  initialize: (config: any) => void;
327
- prompt: () => void;
327
+ prompt: (callback?: (notification: any) => void) => void;
328
328
  };
329
329
  };
330
330
  };
@@ -345,13 +345,51 @@ interface GoogleOneTapOptions {
345
345
  */
346
346
  cancelOnTapOutside?: boolean;
347
347
  /**
348
- * Context of the Google One Tap flow
348
+ * The mode to use for the Google One Tap flow
349
+ *
350
+ * popup: Use a popup window
351
+ * redirect: Redirect the user to the Google One Tap flow
352
+ *
353
+ * @default "popup"
349
354
  */
350
- context?: "signin" | "signup" | "use";
355
+ uxMode?: "popup" | "redirect";
356
+ /**
357
+ * The context to use for the Google One Tap flow
358
+ *
359
+ * @default "signin"
360
+ */
361
+ context?: string;
362
+ /**
363
+ * Additional configuration options to pass to the Google One Tap API.
364
+ */
365
+ additionalOptions?: Record<string, any>;
366
+ /**
367
+ * Configuration options for the prompt and exponential backoff behavior.
368
+ */
369
+ promptOptions?: {
370
+ /**
371
+ * Base delay (in milliseconds) for exponential backoff.
372
+ * @default 1000
373
+ */
374
+ baseDelay?: number;
375
+ /**
376
+ * Maximum number of prompt attempts before calling onPromptNotification.
377
+ * @default 5
378
+ */
379
+ maxAttempts?: number;
380
+ };
351
381
  }
352
- interface GoogleOneTapActionOptions extends Omit<GoogleOneTapOptions, "clientId"> {
382
+ interface GoogleOneTapActionOptions extends Omit<GoogleOneTapOptions, "clientId" | "promptOptions"> {
353
383
  fetchOptions?: BetterFetchOption;
384
+ /**
385
+ * Callback URL.
386
+ */
354
387
  callbackURL?: string;
388
+ /**
389
+ * Optional callback that receives the prompt notification if (or when) the prompt is dismissed or skipped.
390
+ * This lets you render an alternative UI (e.g. a Google Sign-In button) to restart the process.
391
+ */
392
+ onPromptNotification?: (notification: any) => void;
355
393
  }
356
394
  declare const oneTapClient: (options: GoogleOneTapOptions) => {
357
395
  id: "one-tap";
@@ -6,6 +6,7 @@ import { useAuthQuery } from '../chunk-PY47VMWF.js';
6
6
  import '../chunk-HVHN3Y2L.js';
7
7
  import '../chunk-XFCIANZX.js';
8
8
  import '../chunk-3XTQSPPA.js';
9
+ import { generateRandomString } from '../chunk-DBPOZRMS.js';
9
10
  import '../chunk-TQQSPPNA.js';
10
11
  import { BetterAuthError } from '../chunk-UNWCXKMP.js';
11
12
  import { atom } from 'nanostores';
@@ -233,26 +234,58 @@ var oneTapClient = (options) => {
233
234
  const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
234
235
  const contextValue = context ?? options.context ?? "signin";
235
236
  await loadGoogleScript();
236
- await new Promise((resolve) => {
237
+ await new Promise((resolve, reject) => {
238
+ let isResolved = false;
239
+ const baseDelay = options.promptOptions?.baseDelay ?? 1e3;
240
+ const maxAttempts = options.promptOptions?.maxAttempts ?? 5;
237
241
  window.google?.accounts.id.initialize({
238
242
  client_id: options.clientId,
239
243
  callback: async (response) => {
240
- await $fetch("/one-tap/callback", {
241
- method: "POST",
242
- body: { idToken: response.credential },
243
- ...opts?.fetchOptions,
244
- ...fetchOptions
245
- });
246
- if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
247
- window.location.href = opts?.callbackURL ?? "/";
244
+ isResolved = true;
245
+ try {
246
+ await $fetch("/one-tap/callback", {
247
+ method: "POST",
248
+ body: { idToken: response.credential },
249
+ ...opts?.fetchOptions,
250
+ ...fetchOptions
251
+ });
252
+ if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
253
+ window.location.href = opts?.callbackURL ?? "/";
254
+ }
255
+ resolve();
256
+ } catch (error) {
257
+ console.error("Error during One Tap callback:", error);
258
+ reject(error);
248
259
  }
249
- resolve();
250
260
  },
251
261
  auto_select: autoSelect,
252
262
  cancel_on_tap_outside: cancelOnTapOutside,
253
- context: contextValue
263
+ context: contextValue,
264
+ nonce: generateRandomString(16),
265
+ ...options.additionalOptions
254
266
  });
255
- window.google?.accounts.id.prompt();
267
+ const handlePrompt = (attempt) => {
268
+ if (isResolved) return;
269
+ window.google?.accounts.id.prompt((notification) => {
270
+ if (isResolved) return;
271
+ if (notification.isDismissedMoment && notification.isDismissedMoment()) {
272
+ if (attempt < maxAttempts) {
273
+ const delay = Math.pow(2, attempt) * baseDelay;
274
+ setTimeout(() => handlePrompt(attempt + 1), delay);
275
+ } else {
276
+ opts?.onPromptNotification?.(notification);
277
+ }
278
+ } else if (notification.isSkippedMoment && notification.isSkippedMoment()) {
279
+ if (attempt < maxAttempts) {
280
+ const delay = Math.pow(2, attempt) * baseDelay;
281
+ setTimeout(() => handlePrompt(attempt + 1), delay);
282
+ } else {
283
+ opts?.onPromptNotification?.(notification);
284
+ }
285
+ }
286
+ });
287
+ };
288
+ handlePrompt(0);
256
289
  });
257
290
  } catch (error) {
258
291
  console.error("Error during Google One Tap flow:", error);
package/dist/index.cjs CHANGED
@@ -198,7 +198,7 @@ var betterAuth = (options) => {
198
198
  }
199
199
  ctx.trustedOrigins = [
200
200
  ...options.trustedOrigins ? Array.isArray(options.trustedOrigins) ? options.trustedOrigins : options.trustedOrigins(request) : [],
201
- ctx.baseURL,
201
+ ctx.options.baseURL,
202
202
  url.origin
203
203
  ];
204
204
  const { handler } = chunkCQNPMJJ3_cjs.router(ctx, options);
package/dist/index.js CHANGED
@@ -198,7 +198,7 @@ var betterAuth = (options) => {
198
198
  }
199
199
  ctx.trustedOrigins = [
200
200
  ...options.trustedOrigins ? Array.isArray(options.trustedOrigins) ? options.trustedOrigins : options.trustedOrigins(request) : [],
201
- ctx.baseURL,
201
+ ctx.options.baseURL,
202
202
  url.origin
203
203
  ];
204
204
  const { handler } = router(ctx, options);
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkWVSCDHH5_cjs = require('../chunk-WVSCDHH5.cjs');
3
+ var chunkBTKNMP5Q_cjs = require('../chunk-BTKNMP5Q.cjs');
4
4
  require('../chunk-CQNPMJJ3.cjs');
5
5
  require('../chunk-NU4SXYJ5.cjs');
6
6
  require('../chunk-V6RGXSG4.cjs');
@@ -20,5 +20,5 @@ require('../chunk-PEZRSDZS.cjs');
20
20
 
21
21
  Object.defineProperty(exports, "oidcProvider", {
22
22
  enumerable: true,
23
- get: function () { return chunkWVSCDHH5_cjs.oidcProvider; }
23
+ get: function () { return chunkBTKNMP5Q_cjs.oidcProvider; }
24
24
  });
@@ -396,7 +396,7 @@ interface OIDCMetadata {
396
396
  *
397
397
  * @default `/oauth2/userinfo`
398
398
  */
399
- userInfo_endpoint: string;
399
+ userinfo_endpoint: string;
400
400
  /**
401
401
  * The URL of the jwks_uri endpoint.
402
402
  *
@@ -396,7 +396,7 @@ interface OIDCMetadata {
396
396
  *
397
397
  * @default `/oauth2/userinfo`
398
398
  */
399
- userInfo_endpoint: string;
399
+ userinfo_endpoint: string;
400
400
  /**
401
401
  * The URL of the jwks_uri endpoint.
402
402
  *
@@ -1,4 +1,4 @@
1
- export { oidcProvider } from '../chunk-IWKRGAYE.js';
1
+ export { oidcProvider } from '../chunk-XE4KK72J.js';
2
2
  import '../chunk-KALC2G3Y.js';
3
3
  import '../chunk-VXDYNCOX.js';
4
4
  import '../chunk-GTQM7JU7.js';
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkHVYF3QT2_cjs = require('../chunk-HVYF3QT2.cjs');
3
+ var chunkQE4APVC5_cjs = require('../chunk-QE4APVC5.cjs');
4
4
  require('../chunk-CQNPMJJ3.cjs');
5
5
  require('../chunk-NU4SXYJ5.cjs');
6
6
  require('../chunk-V6RGXSG4.cjs');
@@ -20,5 +20,5 @@ require('../chunk-PEZRSDZS.cjs');
20
20
 
21
21
  Object.defineProperty(exports, "oneTap", {
22
22
  enumerable: true,
23
- get: function () { return chunkHVYF3QT2_cjs.oneTap; }
23
+ get: function () { return chunkQE4APVC5_cjs.oneTap; }
24
24
  });
@@ -8,6 +8,13 @@ interface OneTapOptions {
8
8
  * @default false
9
9
  */
10
10
  disableSignup?: boolean;
11
+ /**
12
+ * Google Client ID
13
+ *
14
+ * If a client ID is provided in the social provider configuration,
15
+ * it will be used.
16
+ */
17
+ clientId?: string;
11
18
  }
12
19
  declare const oneTap: (options?: OneTapOptions) => {
13
20
  id: "one-tap";
@@ -8,6 +8,13 @@ interface OneTapOptions {
8
8
  * @default false
9
9
  */
10
10
  disableSignup?: boolean;
11
+ /**
12
+ * Google Client ID
13
+ *
14
+ * If a client ID is provided in the social provider configuration,
15
+ * it will be used.
16
+ */
17
+ clientId?: string;
11
18
  }
12
19
  declare const oneTap: (options?: OneTapOptions) => {
13
20
  id: "one-tap";
@@ -1,4 +1,4 @@
1
- export { oneTap } from '../chunk-7H62SU6D.js';
1
+ export { oneTap } from '../chunk-IM6ZPUVI.js';
2
2
  import '../chunk-KALC2G3Y.js';
3
3
  import '../chunk-VXDYNCOX.js';
4
4
  import '../chunk-GTQM7JU7.js';
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunk5N4RX2ID_cjs = require('../chunk-5N4RX2ID.cjs');
3
+ var chunkMJZAAOE7_cjs = require('../chunk-MJZAAOE7.cjs');
4
4
  require('../chunk-CQNPMJJ3.cjs');
5
5
  require('../chunk-NU4SXYJ5.cjs');
6
6
  require('../chunk-V6RGXSG4.cjs');
@@ -22,5 +22,5 @@ require('../chunk-PEZRSDZS.cjs');
22
22
 
23
23
  Object.defineProperty(exports, "openAPI", {
24
24
  enumerable: true,
25
- get: function () { return chunk5N4RX2ID_cjs.openAPI; }
25
+ get: function () { return chunkMJZAAOE7_cjs.openAPI; }
26
26
  });
@@ -1,4 +1,4 @@
1
- export { openAPI } from '../chunk-K7BZ36L5.js';
1
+ export { openAPI } from '../chunk-VMQM7XMT.js';
2
2
  import '../chunk-KALC2G3Y.js';
3
3
  import '../chunk-VXDYNCOX.js';
4
4
  import '../chunk-GTQM7JU7.js';
package/dist/plugins.cjs CHANGED
@@ -7,10 +7,10 @@ require('./chunk-ZBKCS3KP.cjs');
7
7
  var chunkSDEOOYUI_cjs = require('./chunk-SDEOOYUI.cjs');
8
8
  var chunkVR4QGIHG_cjs = require('./chunk-VR4QGIHG.cjs');
9
9
  var chunkXAF4TPVN_cjs = require('./chunk-XAF4TPVN.cjs');
10
- var chunkHVYF3QT2_cjs = require('./chunk-HVYF3QT2.cjs');
11
- var chunk5N4RX2ID_cjs = require('./chunk-5N4RX2ID.cjs');
10
+ var chunkQE4APVC5_cjs = require('./chunk-QE4APVC5.cjs');
11
+ var chunkMJZAAOE7_cjs = require('./chunk-MJZAAOE7.cjs');
12
12
  var chunk5JET74FS_cjs = require('./chunk-5JET74FS.cjs');
13
- var chunkWVSCDHH5_cjs = require('./chunk-WVSCDHH5.cjs');
13
+ var chunkBTKNMP5Q_cjs = require('./chunk-BTKNMP5Q.cjs');
14
14
  var chunkW2IU5LNT_cjs = require('./chunk-W2IU5LNT.cjs');
15
15
  var chunkUWA64YHN_cjs = require('./chunk-UWA64YHN.cjs');
16
16
  var chunkEIDQBPY6_cjs = require('./chunk-EIDQBPY6.cjs');
@@ -201,11 +201,11 @@ Object.defineProperty(exports, "multiSession", {
201
201
  });
202
202
  Object.defineProperty(exports, "oneTap", {
203
203
  enumerable: true,
204
- get: function () { return chunkHVYF3QT2_cjs.oneTap; }
204
+ get: function () { return chunkQE4APVC5_cjs.oneTap; }
205
205
  });
206
206
  Object.defineProperty(exports, "openAPI", {
207
207
  enumerable: true,
208
- get: function () { return chunk5N4RX2ID_cjs.openAPI; }
208
+ get: function () { return chunkMJZAAOE7_cjs.openAPI; }
209
209
  });
210
210
  Object.defineProperty(exports, "organization", {
211
211
  enumerable: true,
@@ -213,7 +213,7 @@ Object.defineProperty(exports, "organization", {
213
213
  });
214
214
  Object.defineProperty(exports, "oidcProvider", {
215
215
  enumerable: true,
216
- get: function () { return chunkWVSCDHH5_cjs.oidcProvider; }
216
+ get: function () { return chunkBTKNMP5Q_cjs.oidcProvider; }
217
217
  });
218
218
  Object.defineProperty(exports, "admin", {
219
219
  enumerable: true,
package/dist/plugins.js CHANGED
@@ -5,10 +5,10 @@ import './chunk-WMXBA6LX.js';
5
5
  export { jwt } from './chunk-5D5P7O3K.js';
6
6
  export { magicLink } from './chunk-QG36GJF4.js';
7
7
  export { multiSession } from './chunk-S7V7XM55.js';
8
- export { oneTap } from './chunk-7H62SU6D.js';
9
- export { openAPI } from './chunk-K7BZ36L5.js';
8
+ export { oneTap } from './chunk-IM6ZPUVI.js';
9
+ export { openAPI } from './chunk-VMQM7XMT.js';
10
10
  export { organization } from './chunk-KFSLYLGZ.js';
11
- export { oidcProvider } from './chunk-IWKRGAYE.js';
11
+ export { oidcProvider } from './chunk-XE4KK72J.js';
12
12
  export { admin } from './chunk-RPETDYVK.js';
13
13
  export { anonymous } from './chunk-OEZ6U6NM.js';
14
14
  export { bearer } from './chunk-DOEW523N.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.1.18",
3
+ "version": "1.1.19-beta.2",
4
4
  "description": "The most comprehensive authentication library for TypeScript.",
5
5
  "type": "module",
6
6
  "repository": {