@better-auth/infra 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -535,7 +535,7 @@ const initSessionEvents = (tracker) => {
535
535
  eventData: {
536
536
  userId: session.userId,
537
537
  userName: user.name,
538
- userEmail: user.email,
538
+ userEmail: user.email ?? "unknown",
539
539
  sessionId: session.id,
540
540
  triggeredBy: trigger.triggeredBy,
541
541
  triggerContext: trigger.triggerContext
@@ -621,10 +621,10 @@ const initUserEvents = (tracker) => {
621
621
  trackEvent({
622
622
  eventKey: user.id,
623
623
  eventType: EVENT_TYPES.USER_CREATED,
624
- eventDisplayName: `${user.name || user.email} signed up`,
624
+ eventDisplayName: `${user.name || user.email || "unknown"} signed up`,
625
625
  eventData: {
626
626
  userId: user.id,
627
- userEmail: user.email,
627
+ userEmail: user.email ?? "unknown",
628
628
  userName: user.name,
629
629
  triggeredBy: trigger.triggeredBy,
630
630
  triggerContext: trigger.triggerContext
@@ -642,7 +642,7 @@ const initUserEvents = (tracker) => {
642
642
  eventDisplayName: "User deleted",
643
643
  eventData: {
644
644
  userId: user.id,
645
- userEmail: user.email,
645
+ userEmail: user.email ?? "unknown",
646
646
  userName: user.name,
647
647
  triggeredBy: trigger.triggeredBy,
648
648
  triggerContext: trigger.triggerContext
@@ -660,7 +660,7 @@ const initUserEvents = (tracker) => {
660
660
  eventDisplayName: "Profile updated",
661
661
  eventData: {
662
662
  userId: user.id,
663
- userEmail: user.email,
663
+ userEmail: user.email ?? "unknown",
664
664
  userName: user.name,
665
665
  updatedFields: Object.keys(ctx?.body || {}),
666
666
  triggeredBy: trigger.triggeredBy,
@@ -679,7 +679,7 @@ const initUserEvents = (tracker) => {
679
679
  eventDisplayName: "Profile image updated",
680
680
  eventData: {
681
681
  userId: user.id,
682
- userEmail: user.email,
682
+ userEmail: user.email ?? "unknown",
683
683
  userName: user.name,
684
684
  triggeredBy: trigger.triggeredBy,
685
685
  triggerContext: trigger.triggerContext
@@ -699,7 +699,7 @@ const initUserEvents = (tracker) => {
699
699
  eventDisplayName: `User banned${reasonSuffix}${expiresSuffix}`,
700
700
  eventData: {
701
701
  userId: user.id,
702
- userEmail: user.email,
702
+ userEmail: user.email ?? "unknown",
703
703
  userName: user.name,
704
704
  banned: user.banned,
705
705
  banReason: user.banReason,
@@ -720,7 +720,7 @@ const initUserEvents = (tracker) => {
720
720
  eventDisplayName: "User unbanned",
721
721
  eventData: {
722
722
  userId: user.id,
723
- userEmail: user.email,
723
+ userEmail: user.email ?? "unknown",
724
724
  userName: user.name,
725
725
  banned: user.banned,
726
726
  triggeredBy: trigger.triggeredBy,
@@ -739,7 +739,7 @@ const initUserEvents = (tracker) => {
739
739
  eventDisplayName: "Email verified",
740
740
  eventData: {
741
741
  userId: user.id,
742
- userEmail: user.email,
742
+ userEmail: user.email ?? "unknown",
743
743
  userName: user.name,
744
744
  triggeredBy: trigger.triggeredBy,
745
745
  triggerContext: trigger.triggerContext
@@ -1210,7 +1210,7 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
1210
1210
  return "";
1211
1211
  }
1212
1212
  },
1213
- async checkImpossibleTravel(userId, currentLocation, visitorId) {
1213
+ async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null) {
1214
1214
  if (!options.impossibleTravel?.enabled || !currentLocation) return null;
1215
1215
  try {
1216
1216
  const data = await $api("/security/impossible-travel", {
@@ -1219,6 +1219,7 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
1219
1219
  userId,
1220
1220
  visitorId,
1221
1221
  location: currentLocation,
1222
+ ip,
1222
1223
  config: options
1223
1224
  }
1224
1225
  });
@@ -1249,14 +1250,15 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
1249
1250
  /**
1250
1251
  * Store user's last known location for impossible travel detection
1251
1252
  */
1252
- async storeLastLocation(userId, location) {
1253
- if (!location) return;
1253
+ async storeLastLocation(userId, location, ip = null) {
1254
+ if (!options.impossibleTravel?.enabled || !location) return;
1254
1255
  try {
1255
1256
  await $api("/security/store-last-login", {
1256
1257
  method: "POST",
1257
1258
  body: {
1258
1259
  userId,
1259
- location
1260
+ location,
1261
+ ip
1260
1262
  }
1261
1263
  });
1262
1264
  } catch (error) {
@@ -1497,7 +1499,9 @@ const all = new Set([
1497
1499
  "/send-verification-email",
1498
1500
  "/change-email",
1499
1501
  "/organization/invite-member",
1500
- "/dash/organization/invite-member"
1502
+ "/dash/organization/invite-member",
1503
+ "/dash/create-user",
1504
+ "/dash/update-user"
1501
1505
  ]);
1502
1506
  /**
1503
1507
  * Path is one of `[
@@ -1516,7 +1520,9 @@ const all = new Set([
1516
1520
  * '/send-verification-email',
1517
1521
  * '/change-email',
1518
1522
  * '/organization/invite-member',
1519
- * '/dash/organization/invite-member'
1523
+ * '/dash/organization/invite-member',
1524
+ * '/dash/create-user',
1525
+ * '/dash/update-user'
1520
1526
  * ]`.
1521
1527
  * @param context Request context
1522
1528
  * @param context.path Request path
@@ -2088,33 +2094,42 @@ const sentinel = (options) => {
2088
2094
  emailValidation: opts.security?.emailValidation,
2089
2095
  emailNormalization: opts.security?.emailNormalization,
2090
2096
  databaseHooks: {
2091
- user: { create: {
2092
- async before(user, ctx) {
2093
- if (!ctx) return;
2094
- const visitorId = ctx.context.visitorId;
2095
- if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
2096
- recordCheck(ctx, "free_trial_abuse");
2097
- const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
2098
- if (abuseCheck.isAbuse && abuseCheck.action === "block") {
2099
- setOutcome(ctx, "blocked", "free_trial_abuse", {
2100
- accountCount: abuseCheck.accountCount,
2101
- maxAccounts: abuseCheck.maxAccounts
2102
- });
2103
- emitEvaluation(ctx, trackEvent);
2104
- throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
2097
+ user: {
2098
+ create: {
2099
+ async before(user, ctx) {
2100
+ if (!ctx) return;
2101
+ const visitorId = ctx.context.visitorId;
2102
+ if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
2103
+ recordCheck(ctx, "free_trial_abuse");
2104
+ const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
2105
+ if (abuseCheck.isAbuse && abuseCheck.action === "block") {
2106
+ setOutcome(ctx, "blocked", "free_trial_abuse", {
2107
+ accountCount: abuseCheck.accountCount,
2108
+ maxAccounts: abuseCheck.maxAccounts
2109
+ });
2110
+ emitEvaluation(ctx, trackEvent);
2111
+ throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
2112
+ }
2105
2113
  }
2114
+ if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2115
+ ...user,
2116
+ email: normalizeEmail(user.email, ctx.context)
2117
+ } };
2118
+ },
2119
+ async after(user, ctx) {
2120
+ if (!ctx) return;
2121
+ const visitorId = ctx.context.visitorId;
2122
+ if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
2106
2123
  }
2124
+ },
2125
+ update: { async before(user, ctx) {
2126
+ if (!ctx) return;
2107
2127
  if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2108
2128
  ...user,
2109
2129
  email: normalizeEmail(user.email, ctx.context)
2110
2130
  } };
2111
- },
2112
- async after(user, ctx) {
2113
- if (!ctx) return;
2114
- const visitorId = ctx.context.visitorId;
2115
- if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
2116
- }
2117
- } },
2131
+ } }
2132
+ },
2118
2133
  session: { create: {
2119
2134
  async before(session, ctx) {
2120
2135
  if (!ctx) return;
@@ -2122,7 +2137,7 @@ const sentinel = (options) => {
2122
2137
  const identification = ctx.context.identification;
2123
2138
  if (session.userId && identification?.location && visitorId) {
2124
2139
  recordCheck(ctx, "impossible_travel");
2125
- const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
2140
+ const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip);
2126
2141
  if (travelCheck?.isImpossible) {
2127
2142
  if (travelCheck.action === "block") {
2128
2143
  setOutcome(ctx, "blocked", "impossible_travel", {
@@ -2201,7 +2216,7 @@ const sentinel = (options) => {
2201
2216
  }
2202
2217
  }
2203
2218
  }
2204
- if (identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location));
2219
+ if (opts.security?.impossibleTravel?.enabled && identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location, identification.ip));
2205
2220
  }
2206
2221
  } }
2207
2222
  }
@@ -2356,7 +2371,7 @@ const initInvitationEvents = (tracker) => {
2356
2371
  inviteeTeamId: invitation.teamId,
2357
2372
  inviterId: inviter.id,
2358
2373
  inviterName: inviter.name,
2359
- inviterEmail: inviter.email,
2374
+ inviterEmail: inviter.email ?? "unknown",
2360
2375
  triggeredBy: trigger.triggeredBy,
2361
2376
  triggerContext: trigger.triggerContext
2362
2377
  }
@@ -2376,7 +2391,7 @@ const initInvitationEvents = (tracker) => {
2376
2391
  inviteeRole: invitation.role,
2377
2392
  inviteeTeamId: invitation.teamId,
2378
2393
  acceptedById: acceptedBy.id,
2379
- acceptedByEmail: acceptedBy.email,
2394
+ acceptedByEmail: acceptedBy.email ?? "unknown",
2380
2395
  acceptedByName: acceptedBy.name,
2381
2396
  memberId: member.id,
2382
2397
  memberRole: member.role,
@@ -2399,7 +2414,7 @@ const initInvitationEvents = (tracker) => {
2399
2414
  inviteeRole: invitation.role,
2400
2415
  inviteeTeamId: invitation.teamId,
2401
2416
  rejectedById: rejectedBy.id,
2402
- rejectedByEmail: rejectedBy.email,
2417
+ rejectedByEmail: rejectedBy.email ?? "unknown",
2403
2418
  rejectedByName: rejectedBy.name,
2404
2419
  triggeredBy: trigger.triggeredBy,
2405
2420
  triggerContext: trigger.triggerContext
@@ -2421,7 +2436,7 @@ const initInvitationEvents = (tracker) => {
2421
2436
  inviteeTeamId: invitation.teamId,
2422
2437
  cancelledById: cancelledBy.id,
2423
2438
  cancelledByName: cancelledBy.name,
2424
- cancelledByEmail: cancelledBy.email,
2439
+ cancelledByEmail: cancelledBy.email ?? "unknown",
2425
2440
  triggeredBy: trigger.triggeredBy,
2426
2441
  triggerContext: trigger.triggerContext
2427
2442
  }
@@ -2451,7 +2466,7 @@ const initMemberEvents = (tracker) => {
2451
2466
  memberName: user.name,
2452
2467
  role: member.role,
2453
2468
  memberId: member.id,
2454
- memberEmail: user.email,
2469
+ memberEmail: user.email ?? "unknown",
2455
2470
  triggeredBy: trigger.triggeredBy,
2456
2471
  triggerContext: trigger.triggerContext
2457
2472
  }
@@ -2470,7 +2485,7 @@ const initMemberEvents = (tracker) => {
2470
2485
  memberName: user.name,
2471
2486
  role: member.role,
2472
2487
  memberId: member.id,
2473
- memberEmail: user.email,
2488
+ memberEmail: user.email ?? "unknown",
2474
2489
  triggeredBy: trigger.triggeredBy,
2475
2490
  triggerContext: trigger.triggerContext
2476
2491
  }
@@ -2490,7 +2505,7 @@ const initMemberEvents = (tracker) => {
2490
2505
  newRole: member.role,
2491
2506
  oldRole: previousRole,
2492
2507
  memberId: member.id,
2493
- memberEmail: user.email,
2508
+ memberEmail: user.email ?? "unknown",
2494
2509
  triggeredBy: trigger.triggeredBy,
2495
2510
  triggerContext: trigger.triggerContext
2496
2511
  }
@@ -2779,7 +2794,7 @@ const jwtValidateMiddleware = (options) => {
2779
2794
  };
2780
2795
  //#endregion
2781
2796
  //#region src/version.ts
2782
- const PLUGIN_VERSION = "0.2.7";
2797
+ const PLUGIN_VERSION = "0.2.9";
2783
2798
  //#endregion
2784
2799
  //#region src/routes/auth/config.ts
2785
2800
  const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
@@ -3794,13 +3809,13 @@ const completeInvitation = (options) => {
3794
3809
  * Used by the platform to verify before sending invitation
3795
3810
  * This is different from /dash/organization/check-user-by-email which also checks membership
3796
3811
  */
3797
- const checkUserExists = (_options) => {
3812
+ const checkUserExists = (options) => {
3798
3813
  return createAuthEndpoint("/dash/check-user-exists", {
3799
3814
  method: "POST",
3815
+ use: [jwtMiddleware(options)],
3800
3816
  body: z$1.object({ email: z$1.email() })
3801
3817
  }, async (ctx) => {
3802
3818
  const { email } = ctx.body;
3803
- if (!ctx.request?.headers.get("Authorization")) throw new APIError("UNAUTHORIZED", { message: "Authorization required" });
3804
3819
  const normalizedEmail = normalizeEmail(email, ctx.context);
3805
3820
  const existingUser = await ctx.context.internalAdapter.findUserByEmail(normalizedEmail).then((user) => user?.user);
3806
3821
  return {
@@ -3837,7 +3852,7 @@ const listOrganizationInvitations = (options) => {
3837
3852
  operator: "in"
3838
3853
  }]
3839
3854
  }) : [];
3840
- const userByEmail = new Map(users.map((u) => [normalizeEmail(u.email, ctx.context), u]));
3855
+ const userByEmail = new Map(users.filter((u) => u.email != null).map((u) => [normalizeEmail(u.email, ctx.context), u]));
3841
3856
  return invitations.map((invitation) => {
3842
3857
  const invitationEmail = normalizeEmail(invitation.email, ctx.context);
3843
3858
  const user = userByEmail.get(invitationEmail);
@@ -3846,7 +3861,7 @@ const listOrganizationInvitations = (options) => {
3846
3861
  user: user ? {
3847
3862
  id: user.id,
3848
3863
  name: user.name,
3849
- email: user.email,
3864
+ email: user.email ?? void 0,
3850
3865
  image: user.image || null
3851
3866
  } : null
3852
3867
  };
@@ -3932,7 +3947,7 @@ const checkUserByEmail = (options) => {
3932
3947
  user: {
3933
3948
  id: user.id,
3934
3949
  name: user.name,
3935
- email: user.email,
3950
+ email: user.email ?? void 0,
3936
3951
  image: user.image ?? null
3937
3952
  },
3938
3953
  isAlreadyMember: !!existingMember
@@ -4088,18 +4103,18 @@ const listOrganizationMembers = (options) => {
4088
4103
  for (const m of members) {
4089
4104
  const joinedUser = Array.isArray(m.user) ? m.user[0] : m.user;
4090
4105
  if (!joinedUser) continue;
4091
- const invitation = invitationByEmail.get(joinedUser.email.toLowerCase());
4106
+ const invitation = joinedUser.email ? invitationByEmail.get(joinedUser.email.toLowerCase()) : void 0;
4092
4107
  const inviter = invitation ? inviterById.get(invitation.inviterId) : void 0;
4093
4108
  const user = {
4094
4109
  id: joinedUser.id,
4095
- email: joinedUser.email,
4110
+ email: joinedUser.email ?? void 0,
4096
4111
  name: joinedUser.name,
4097
4112
  image: joinedUser.image ?? null
4098
4113
  };
4099
4114
  const invitedBy = inviter ? {
4100
4115
  id: inviter.id,
4101
4116
  name: inviter.name,
4102
- email: inviter.email,
4117
+ email: inviter.email ?? void 0,
4103
4118
  image: inviter.image ?? null
4104
4119
  } : null;
4105
4120
  result.push({
@@ -4411,6 +4426,121 @@ async function withConcurrency(items, fn, options) {
4411
4426
  return results;
4412
4427
  }
4413
4428
  //#endregion
4429
+ //#region src/routes/organizations/schemas.ts
4430
+ const DENIED_ORG_WRITE_KEYS = new Set([
4431
+ "id",
4432
+ "createdAt",
4433
+ "updatedAt"
4434
+ ]);
4435
+ const CREATE_NON_PERSISTED_KEYS = new Set(["defaultTeamName"]);
4436
+ const CREATE_REQUEST_ONLY_KEYS = new Set(["userId", "skipDefaultTeam"]);
4437
+ const CORE_CREATE_KEYS = [
4438
+ "name",
4439
+ "slug",
4440
+ "logo"
4441
+ ];
4442
+ const CORE_UPDATE_KEYS = [
4443
+ "name",
4444
+ "slug",
4445
+ "logo",
4446
+ "metadata"
4447
+ ];
4448
+ function getOrganizationAdditionalFields(orgOptions) {
4449
+ return orgOptions?.schema?.organization?.additionalFields ?? {};
4450
+ }
4451
+ function getWritableOrganizationAdditionalFieldNames(orgOptions) {
4452
+ const additional = getOrganizationAdditionalFields(orgOptions);
4453
+ return Object.entries(additional).filter(([, field]) => {
4454
+ return field.input !== false;
4455
+ }).map(([name]) => name);
4456
+ }
4457
+ function getWritableOrganizationFieldNames(orgOptions, mode) {
4458
+ const keys = new Set(mode === "create" ? CORE_CREATE_KEYS : CORE_UPDATE_KEYS);
4459
+ for (const name of getWritableOrganizationAdditionalFieldNames(orgOptions)) keys.add(name);
4460
+ return keys;
4461
+ }
4462
+ /**
4463
+ * Returns only organization fields that may be persisted on create/update.
4464
+ * Strips unknown keys, denied keys, and create-only request fields.
4465
+ */
4466
+ function pickWritableOrganizationFields(body, orgOptions, mode) {
4467
+ const allowed = getWritableOrganizationFieldNames(orgOptions, mode);
4468
+ const skip = new Set([
4469
+ ...DENIED_ORG_WRITE_KEYS,
4470
+ ...mode === "create" ? CREATE_NON_PERSISTED_KEYS : [],
4471
+ ...mode === "create" ? CREATE_REQUEST_ONLY_KEYS : []
4472
+ ]);
4473
+ const result = {};
4474
+ for (const [key, value] of Object.entries(body)) {
4475
+ if (skip.has(key) || !allowed.has(key) || value === void 0) continue;
4476
+ result[key] = value;
4477
+ }
4478
+ return result;
4479
+ }
4480
+ const BaseCreateOrgCoreBodySchema = z$1.object({
4481
+ name: z$1.string(),
4482
+ slug: z$1.string(),
4483
+ logo: z$1.string().optional(),
4484
+ defaultTeamName: z$1.string().optional()
4485
+ });
4486
+ const BaseUpdateOrgCoreBodySchema = z$1.object({
4487
+ logo: z$1.union([z$1.url(), z$1.literal("")]).optional(),
4488
+ name: z$1.string().optional(),
4489
+ slug: z$1.string().optional(),
4490
+ metadata: z$1.string().optional()
4491
+ });
4492
+ const CreateOrganizationBodySchema = BaseCreateOrgCoreBodySchema.catchall(z$1.unknown());
4493
+ const UpdateOrganizationBodySchema = BaseUpdateOrgCoreBodySchema.catchall(z$1.unknown());
4494
+ function createSchemaForDBField(field) {
4495
+ switch (field.type) {
4496
+ case "number": return field.required ? z$1.coerce.number() : z$1.coerce.number().optional();
4497
+ case "boolean": return field.required ? z$1.coerce.boolean() : z$1.coerce.boolean().optional();
4498
+ case "date": return field.required ? z$1.union([z$1.string().min(1), z$1.coerce.date()]) : z$1.union([z$1.string(), z$1.coerce.date()]).optional();
4499
+ default: return field.required ? z$1.string().min(1) : z$1.string().optional();
4500
+ }
4501
+ }
4502
+ /**
4503
+ * Validates required core + configured additional fields on create.
4504
+ * Call after pickWritableOrgFields if you need required additional field checks.
4505
+ */
4506
+ function validateWritableCreateOrganizationFields(data, options) {
4507
+ const shape = {
4508
+ name: z$1.string().min(1),
4509
+ slug: z$1.string().min(1),
4510
+ logo: z$1.string().optional()
4511
+ };
4512
+ const additional = getOrganizationAdditionalFields(options);
4513
+ for (const [name, field] of Object.entries(additional)) {
4514
+ if (field.input === false) continue;
4515
+ shape[name] = createSchemaForDBField(field);
4516
+ }
4517
+ const result = z$1.object(shape).strict().safeParse(data);
4518
+ if (!result.success) throw result.error;
4519
+ }
4520
+ /**
4521
+ * Ensures at least one field is present on update and validates additional field types.
4522
+ */
4523
+ function validateWritableOrganizationUpdateFields(data, orgOptions) {
4524
+ if (Object.keys(data).length === 0) throw new z$1.ZodError([{
4525
+ code: "custom",
4526
+ message: "No valid fields to update",
4527
+ path: []
4528
+ }]);
4529
+ const shape = {
4530
+ name: z$1.string().min(1).optional(),
4531
+ slug: z$1.string().min(1).optional(),
4532
+ logo: z$1.union([z$1.url(), z$1.literal("")]).optional(),
4533
+ metadata: z$1.string().optional()
4534
+ };
4535
+ const additional = getOrganizationAdditionalFields(orgOptions);
4536
+ for (const [name, field] of Object.entries(additional)) {
4537
+ if (field.input === false) continue;
4538
+ shape[name] = createSchemaForDBField(field);
4539
+ }
4540
+ const result = z$1.object(shape).partial().strict().safeParse(data);
4541
+ if (!result.success) throw result.error;
4542
+ }
4543
+ //#endregion
4414
4544
  //#region src/routes/organizations/organization.ts
4415
4545
  const listOrganizations = (options) => {
4416
4546
  return createAuthEndpoint("/dash/list-organizations", {
@@ -4542,8 +4672,8 @@ const listOrganizations = (options) => {
4542
4672
  const members = organization.member.map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
4543
4673
  id: u.id,
4544
4674
  name: u.name,
4545
- email: u.email,
4546
- image: u.image
4675
+ email: u.email ?? void 0,
4676
+ image: u.image ?? null
4547
4677
  }));
4548
4678
  const { member: _members, ...org } = organization;
4549
4679
  return {
@@ -4729,15 +4859,11 @@ const createOrganization = (options) => {
4729
4859
  userId: z$1.string(),
4730
4860
  skipDefaultTeam: z$1.boolean().optional().default(false)
4731
4861
  }))],
4732
- body: z$1.looseObject({
4733
- name: z$1.string(),
4734
- slug: z$1.string(),
4735
- logo: z$1.string().optional(),
4736
- defaultTeamName: z$1.string().optional()
4737
- })
4862
+ body: CreateOrganizationBodySchema
4738
4863
  }, async (ctx) => {
4739
4864
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
4740
4865
  const { userId } = ctx.context.payload;
4866
+ const defaultTeamName = ctx.body.defaultTeamName;
4741
4867
  const user = await ctx.context.adapter.findOne({
4742
4868
  model: "user",
4743
4869
  where: [{
@@ -4753,10 +4879,13 @@ const createOrganization = (options) => {
4753
4879
  value: ctx.body.slug
4754
4880
  }]
4755
4881
  }) > 0) throw ctx.error("BAD_REQUEST", { message: "Organization already exists" });
4756
- let orgData = {
4757
- ...ctx.body,
4758
- defaultTeamName: void 0
4759
- };
4882
+ let orgData = pickWritableOrganizationFields(ctx.body, orgOptions, "create");
4883
+ try {
4884
+ validateWritableCreateOrganizationFields(orgData, orgOptions);
4885
+ } catch (error) {
4886
+ if (error instanceof z$1.ZodError) throw ctx.error("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid organization data" });
4887
+ throw error;
4888
+ }
4760
4889
  if (orgOptions.organizationCreation?.beforeCreate) {
4761
4890
  const response = await orgOptions.organizationCreation.beforeCreate({
4762
4891
  organization: {
@@ -4765,20 +4894,20 @@ const createOrganization = (options) => {
4765
4894
  },
4766
4895
  user
4767
4896
  }, ctx.request);
4768
- if (response && typeof response === "object" && "data" in response) orgData = {
4897
+ if (response && typeof response === "object" && "data" in response) orgData = pickWritableOrganizationFields({
4769
4898
  ...orgData,
4770
4899
  ...response.data
4771
- };
4900
+ }, orgOptions, "create");
4772
4901
  }
4773
4902
  if (orgOptions?.organizationHooks?.beforeCreateOrganization) {
4774
4903
  const response = await orgOptions?.organizationHooks.beforeCreateOrganization({
4775
4904
  organization: orgData,
4776
4905
  user
4777
4906
  });
4778
- if (response && typeof response === "object" && "data" in response) orgData = {
4907
+ if (response && typeof response === "object" && "data" in response) orgData = pickWritableOrganizationFields({
4779
4908
  ...orgData,
4780
4909
  ...response.data
4781
- };
4910
+ }, orgOptions, "create");
4782
4911
  }
4783
4912
  const organization = await ctx.context.adapter.create({
4784
4913
  model: "organization",
@@ -4827,7 +4956,7 @@ const createOrganization = (options) => {
4827
4956
  if (orgOptions?.teams?.enabled && orgOptions.teams.defaultTeam?.enabled !== false && !ctx.context.payload.skipDefaultTeam) {
4828
4957
  let teamData = {
4829
4958
  organizationId: organization.id,
4830
- name: ctx.body.defaultTeamName || `${organization.name}`,
4959
+ name: defaultTeamName || `${organization.name}`,
4831
4960
  createdAt: /* @__PURE__ */ new Date()
4832
4961
  };
4833
4962
  if (orgOptions?.organizationHooks?.beforeCreateTeam) {
@@ -4891,12 +5020,7 @@ const updateOrganization = (options) => {
4891
5020
  return createAuthEndpoint("/dash/organization/update", {
4892
5021
  method: "POST",
4893
5022
  use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
4894
- body: z$1.looseObject({
4895
- logo: z$1.url().optional(),
4896
- name: z$1.string().optional(),
4897
- slug: z$1.string().optional(),
4898
- metadata: z$1.string().optional()
4899
- })
5023
+ body: UpdateOrganizationBodySchema
4900
5024
  }, async (ctx) => {
4901
5025
  const { organizationId } = ctx.context.payload;
4902
5026
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
@@ -4931,7 +5055,13 @@ const updateOrganization = (options) => {
4931
5055
  const owner = owners[0];
4932
5056
  const updatedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
4933
5057
  if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
4934
- let updateData = { ...ctx.body };
5058
+ let updateData = pickWritableOrganizationFields(ctx.body, orgOptions, "update");
5059
+ try {
5060
+ validateWritableOrganizationUpdateFields(updateData, orgOptions);
5061
+ } catch (error) {
5062
+ if (error instanceof z$1.ZodError) throw ctx.error("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid organization data" });
5063
+ throw error;
5064
+ }
4935
5065
  if (typeof updateData.metadata === "string") try {
4936
5066
  updateData.metadata = updateData.metadata === "" ? void 0 : JSON.parse(updateData.metadata);
4937
5067
  } catch (parseError) {
@@ -4944,10 +5074,10 @@ const updateOrganization = (options) => {
4944
5074
  user: updatedByUser,
4945
5075
  member: owner
4946
5076
  });
4947
- if (response && typeof response === "object" && "data" in response) updateData = {
5077
+ if (response && typeof response === "object" && "data" in response) updateData = pickWritableOrganizationFields({
4948
5078
  ...updateData,
4949
5079
  ...response.data
4950
- };
5080
+ }, orgOptions, "update");
4951
5081
  }
4952
5082
  const organization = await ctx.context.adapter.update({
4953
5083
  model: "organization",
@@ -5285,7 +5415,7 @@ const listTeamMembers = (options) => {
5285
5415
  user: user ? {
5286
5416
  id: user.id,
5287
5417
  name: user.name,
5288
- email: user.email,
5418
+ email: user.email ?? void 0,
5289
5419
  image: user.image ?? null
5290
5420
  } : null
5291
5421
  };
@@ -7365,6 +7495,7 @@ const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verifi
7365
7495
  const user = await ctx.context.internalAdapter.findUserById(userId);
7366
7496
  if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
7367
7497
  if (user.emailVerified) throw ctx.error("BAD_REQUEST", { message: "Email is already verified" });
7498
+ if (!user.email) throw ctx.error("BAD_REQUEST", { message: "User has no associated email address" });
7368
7499
  if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
7369
7500
  await sendVerificationEmailFn({
7370
7501
  ...ctx,
@@ -7401,6 +7532,10 @@ const sendManyVerificationEmails = (options) => {
7401
7532
  });
7402
7533
  if (chunk.length - users.length > 0) for (const id of chunk.filter((id) => !users.some((u) => u.id === id))) skippedEmailUserIds.add(id);
7403
7534
  for (const result of await Promise.allSettled(users.map(async (user) => {
7535
+ if (!user.email) return {
7536
+ success: false,
7537
+ id: user.id
7538
+ };
7404
7539
  try {
7405
7540
  await sendVerificationEmailFn({
7406
7541
  ...ctx,
@@ -7440,6 +7575,7 @@ const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset
7440
7575
  const { userId } = ctx.context.payload;
7441
7576
  const user = await ctx.context.internalAdapter.findUserById(userId);
7442
7577
  if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
7578
+ if (!user.email) throw ctx.error("BAD_REQUEST", { message: "User has no associated email address" });
7443
7579
  ctx.body.redirectTo = ctx.body.callbackUrl;
7444
7580
  ctx.body.email = user.email;
7445
7581
  return await requestPasswordReset(ctx);
package/dist/native.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DashGetAuditLogsInput, i as DashGetAllAuditLogsInput, n as DashAuditLogsResponse, o as dashClient, r as DashClientOptions, t as DashAuditLog } from "./dash-client-B6U89e1S.mjs";
1
+ import { a as DashGetAuditLogsInput, i as DashGetAllAuditLogsInput, n as DashAuditLogsResponse, o as dashClient, r as DashClientOptions, t as DashAuditLog } from "./dash-client-CZzJyRrV.mjs";
2
2
  import { BetterAuthClientPlugin } from "better-auth";
3
3
 
4
4
  //#region src/sentinel/native/client.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/infra",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Dashboard and analytics plugin for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -64,21 +64,21 @@
64
64
  "@better-auth/scim": "catalog:",
65
65
  "@better-auth/sso": "catalog:",
66
66
  "@infra/mocks": "workspace:*",
67
- "@types/bun": "latest",
67
+ "@types/bun": "catalog:",
68
68
  "@types/node": "catalog:",
69
69
  "better-auth": "catalog:",
70
70
  "expo-crypto": "^14.0.2",
71
71
  "happy-dom": "^20.9.0",
72
- "msw": "^2.14.3",
73
- "tsdown": "^0.21.10",
72
+ "msw": "catalog:",
73
+ "tsdown": "^0.22.0",
74
74
  "typescript": "catalog:",
75
75
  "zod": "catalog:"
76
76
  },
77
77
  "dependencies": {
78
- "@better-fetch/fetch": "^1.1.21",
79
- "better-call": "^1.3.3",
78
+ "@better-fetch/fetch": "catalog:",
79
+ "better-call": "catalog:",
80
80
  "jose": "^6.1.0",
81
- "libphonenumber-js": "^1.12.42"
81
+ "libphonenumber-js": "^1.13.1"
82
82
  },
83
83
  "peerDependencies": {
84
84
  "better-auth": ">=1.4.0",