@absolutejs/auth 0.85.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,8 @@ export type OrganizationInvitationMessage = {
15
15
  };
16
16
  export type OrganizationsConfig<UserType> = {
17
17
  getUserId: (user: UserType) => string;
18
+ /** Return only an independently verified email. Missing evidence denies acceptance. */
19
+ getVerifiedEmail?: (user: UserType) => string | undefined | Promise<string | undefined>;
18
20
  organizationStore: OrganizationStore;
19
21
  canCreateOrganization?: (user: UserType) => boolean | Promise<boolean>;
20
22
  canManageMembers?: (context: {
@@ -1,10 +1,12 @@
1
1
  import type { OrganizationId } from '../tenancy';
2
- import type { Organization, OrganizationInvitation, OrganizationMembership, OrganizationStore } from './types';
3
- export declare const acceptInvitation: ({ organizationStore, token, userId }: {
2
+ import type { Organization, OrganizationInvitation, OrganizationStore } from './types';
3
+ export declare const acceptInvitation: ({ organizationStore, token, userId, verifiedEmail }: {
4
4
  organizationStore: OrganizationStore;
5
5
  token: string;
6
6
  userId: string;
7
- }) => Promise<OrganizationMembership | undefined>;
7
+ /** Provider-verified email, never an unverified profile field or request body. */
8
+ verifiedEmail?: string;
9
+ }) => Promise<import("./types").OrganizationMembership | undefined>;
8
10
  export declare const autoAssignOrgsByEmail: ({ email, getOrgsForDomain, organizationStore, roles, userId }: {
9
11
  email: string;
10
12
  getOrgsForDomain: (domain: string) => Promise<OrganizationId[]> | OrganizationId[];
@@ -34,6 +36,6 @@ export declare const listUserOrganizations: ({ organizationStore, userId }: {
34
36
  organizationStore: OrganizationStore;
35
37
  userId: string;
36
38
  }) => Promise<{
37
- membership: OrganizationMembership;
39
+ membership: import("./types").OrganizationMembership;
38
40
  organization: Organization | undefined;
39
41
  }[]>;
@@ -1,6 +1,6 @@
1
1
  import type { SessionRecord } from '../types';
2
2
  import { type OrganizationsRouteProps } from './config';
3
- export declare const organizationRoutes: <UserType>({ authSessionStore, canCreateOrganization, canManageMembers, emit, getUserId, invitationDurationMs, onMembershipAdded, onMembershipRemoved, onOrganizationCreated, onSendInvitation, organizationsRoute, organizationStore, ownerRoles }: OrganizationsRouteProps<UserType>) => import("elysia/types").AddRoute<"", "local", {
3
+ export declare const organizationRoutes: <UserType>({ authSessionStore, canCreateOrganization, canManageMembers, emit, getUserId, getVerifiedEmail, invitationDurationMs, onMembershipAdded, onMembershipRemoved, onOrganizationCreated, onSendInvitation, organizationsRoute, organizationStore, ownerRoles }: OrganizationsRouteProps<UserType>) => import("elysia/types").AddRoute<"", "local", {
4
4
  decorator: {};
5
5
  store: {
6
6
  session: SessionRecord<UserType>;
@@ -29,6 +29,14 @@ export type OrganizationInvitation = {
29
29
  tokenHash: string;
30
30
  };
31
31
  export type OrganizationStore = {
32
+ /** Atomically consume a live invitation and add membership. Custom stores must
33
+ * implement this capability; acceptance fails closed without it. */
34
+ acceptInvitation?: (input: {
35
+ tokenHash: string;
36
+ userId: string;
37
+ verifiedEmail: string;
38
+ now: number;
39
+ }) => Promise<OrganizationMembership | undefined>;
32
40
  deleteOrganization: (organizationId: OrganizationId) => Promise<void>;
33
41
  getInvitation: (invitationId: string) => Promise<OrganizationInvitation | undefined>;
34
42
  getInvitationByTokenHash: (tokenHash: string) => Promise<OrganizationInvitation | undefined>;
package/dist/server.js CHANGED
@@ -11239,29 +11239,17 @@ init_crypto();
11239
11239
  var acceptInvitation = async ({
11240
11240
  organizationStore,
11241
11241
  token,
11242
- userId
11242
+ userId,
11243
+ verifiedEmail
11243
11244
  }) => {
11244
- const invitation = await organizationStore.getInvitationByTokenHash(await hashToken(token));
11245
- if (!invitation || invitation.state !== "pending")
11245
+ if (!verifiedEmail?.trim() || !organizationStore.acceptInvitation)
11246
11246
  return;
11247
- if (invitation.expiresAt < Date.now())
11248
- return;
11249
- const now = Date.now();
11250
- await organizationStore.saveInvitation({
11251
- ...invitation,
11252
- acceptedAt: now,
11253
- state: "accepted"
11247
+ return organizationStore.acceptInvitation({
11248
+ now: Date.now(),
11249
+ tokenHash: await hashToken(token),
11250
+ userId,
11251
+ verifiedEmail: verifiedEmail.trim().toLowerCase()
11254
11252
  });
11255
- const membership = {
11256
- createdAt: now,
11257
- organizationId: invitation.organizationId,
11258
- roles: invitation.roles,
11259
- status: "active",
11260
- updatedAt: now,
11261
- userId
11262
- };
11263
- await organizationStore.saveMembership(membership);
11264
- return membership;
11265
11253
  };
11266
11254
  var createOrganization = async ({
11267
11255
  metadata,
@@ -11332,6 +11320,7 @@ var organizationRoutes = ({
11332
11320
  canManageMembers,
11333
11321
  emit,
11334
11322
  getUserId,
11323
+ getVerifiedEmail,
11335
11324
  invitationDurationMs,
11336
11325
  onMembershipAdded,
11337
11326
  onMembershipRemoved,
@@ -11520,7 +11509,8 @@ var organizationRoutes = ({
11520
11509
  const membership = await acceptInvitation({
11521
11510
  organizationStore,
11522
11511
  token,
11523
- userId: getUserId(user)
11512
+ userId: getUserId(user),
11513
+ verifiedEmail: await getVerifiedEmail?.(user)
11524
11514
  });
11525
11515
  if (!membership) {
11526
11516
  return status("Bad Request", "Invalid or expired invitation");
@@ -40417,6 +40407,27 @@ var toInvitation = (row) => ({
40417
40407
  });
40418
40408
  var createNeonOrganizationStore = (databaseUrl) => createPostgresOrganizationStore(createNeonDatabase(databaseUrl));
40419
40409
  var createPostgresOrganizationStore = (db) => ({
40410
+ acceptInvitation: async ({ now, tokenHash, userId, verifiedEmail }) => {
40411
+ const claimed = db.$with("claimed").as(db.update(organizationInvitationsTable).set({ accepted_at_ms: now, state: "accepted" }).where(and(eq(organizationInvitationsTable.token_hash, tokenHash), eq(organizationInvitationsTable.state, "pending"), gt(organizationInvitationsTable.expires_at_ms, now), sql`lower(trim(${organizationInvitationsTable.email})) = ${verifiedEmail}`, exists(db.select().from(organizationsTable).where(eq(organizationsTable.organization_id, organizationInvitationsTable.organization_id))), notExists(db.select().from(organizationMembershipsTable).where(and(eq(organizationMembershipsTable.organization_id, organizationInvitationsTable.organization_id), eq(organizationMembershipsTable.user_id, userId), eq(organizationMembershipsTable.status, "suspended")))))).returning({
40412
+ organizationId: organizationInvitationsTable.organization_id,
40413
+ roles: organizationInvitationsTable.roles
40414
+ }));
40415
+ const [row] = await db.with(claimed).insert(organizationMembershipsTable).select(db.select({
40416
+ created_at_ms: sql`${now}`.as("created_at_ms"),
40417
+ organization_id: claimed.organizationId,
40418
+ roles: claimed.roles,
40419
+ status: sql`'active'`.as("status"),
40420
+ updated_at_ms: sql`${now}`.as("updated_at_ms"),
40421
+ user_id: sql`${userId}`.as("user_id")
40422
+ }).from(claimed)).onConflictDoUpdate({
40423
+ set: { user_id: userId },
40424
+ target: [
40425
+ organizationMembershipsTable.organization_id,
40426
+ organizationMembershipsTable.user_id
40427
+ ]
40428
+ }).returning();
40429
+ return row && row.status === "active" ? toMembership(row) : undefined;
40430
+ },
40420
40431
  deleteOrganization: async (organizationId) => {
40421
40432
  await db.delete(organizationsTable).where(eq(organizationsTable.organization_id, organizationId));
40422
40433
  },
@@ -41467,6 +41478,30 @@ var createInMemoryOrganizationStore = () => {
41467
41478
  const memberships = new Map;
41468
41479
  const invitations = new Map;
41469
41480
  return {
41481
+ acceptInvitation: async ({ tokenHash, userId, verifiedEmail, now }) => {
41482
+ const invitation = [...invitations.values()].find((value) => value.tokenHash === tokenHash);
41483
+ if (!invitation || invitation.state !== "pending" || invitation.expiresAt <= now || invitation.email.trim().toLowerCase() !== verifiedEmail || !organizations.has(invitation.organizationId))
41484
+ return;
41485
+ const key = membershipKey(invitation.organizationId, userId);
41486
+ const existing = memberships.get(key);
41487
+ if (existing?.status === "suspended")
41488
+ return;
41489
+ const membership = existing ?? {
41490
+ createdAt: now,
41491
+ organizationId: invitation.organizationId,
41492
+ roles: [...invitation.roles],
41493
+ status: "active",
41494
+ updatedAt: now,
41495
+ userId
41496
+ };
41497
+ invitations.set(invitation.invitationId, {
41498
+ ...invitation,
41499
+ acceptedAt: now,
41500
+ state: "accepted"
41501
+ });
41502
+ memberships.set(key, membership);
41503
+ return cloneMembership(membership);
41504
+ },
41470
41505
  deleteOrganization: async (organizationId) => {
41471
41506
  organizations.delete(organizationId);
41472
41507
  },
@@ -42212,5 +42247,5 @@ export {
42212
42247
  userSessionIdTypebox
42213
42248
  };
42214
42249
 
42215
- //# debugId=111690DAA1672B5F64756E2164756E21
42250
+ //# debugId=0AD53C59A773867564756E2164756E21
42216
42251
  //# sourceMappingURL=server.js.map