@absolutejs/auth 0.85.0 → 0.86.1

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>;
@@ -100,6 +100,7 @@ export declare const organizationRoutes: <UserType>({ authSessionStore, canCreat
100
100
  property?: string;
101
101
  expected?: string;
102
102
  };
103
+ 502: "Invitation delivery failed; create a new invitation to retry";
103
104
  };
104
105
  error: never;
105
106
  };
@@ -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")
11246
- return;
11247
- if (invitation.expiresAt < Date.now())
11245
+ if (!verifiedEmail?.trim() || !organizationStore.acceptInvitation)
11248
11246
  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,
@@ -11438,13 +11427,21 @@ var organizationRoutes = ({
11438
11427
  organizationStore,
11439
11428
  roles: roles ?? []
11440
11429
  });
11441
- await onSendInvitation?.({
11442
- email: invitation.email,
11443
- expiresAt: invitation.expiresAt,
11444
- inviterUserId: invitation.inviterUserId,
11445
- organizationId,
11446
- token
11447
- });
11430
+ try {
11431
+ await onSendInvitation?.({
11432
+ email: invitation.email,
11433
+ expiresAt: invitation.expiresAt,
11434
+ inviterUserId: invitation.inviterUserId,
11435
+ organizationId,
11436
+ token
11437
+ });
11438
+ } catch {
11439
+ await organizationStore.saveInvitation({
11440
+ ...invitation,
11441
+ state: "revoked"
11442
+ });
11443
+ return status("Bad Gateway", "Invitation delivery failed; create a new invitation to retry");
11444
+ }
11448
11445
  await emit?.({
11449
11446
  at: Date.now(),
11450
11447
  metadata: { email: invitation.email },
@@ -11520,7 +11517,8 @@ var organizationRoutes = ({
11520
11517
  const membership = await acceptInvitation({
11521
11518
  organizationStore,
11522
11519
  token,
11523
- userId: getUserId(user)
11520
+ userId: getUserId(user),
11521
+ verifiedEmail: await getVerifiedEmail?.(user)
11524
11522
  });
11525
11523
  if (!membership) {
11526
11524
  return status("Bad Request", "Invalid or expired invitation");
@@ -11551,7 +11549,7 @@ var organizationRoutes = ({
11551
11549
  return status("Unauthorized", "Authentication required");
11552
11550
  }
11553
11551
  const membership = await organizationStore.getMembership(organizationId, getUserId(user));
11554
- if (membership?.status !== "active") {
11552
+ if (membership?.status !== "active" && !await mayManage(user, organizationId)) {
11555
11553
  return status("Forbidden", "Not a member");
11556
11554
  }
11557
11555
  const members = await organizationStore.listMembershipsByOrganization(organizationId);
@@ -40417,6 +40415,27 @@ var toInvitation = (row) => ({
40417
40415
  });
40418
40416
  var createNeonOrganizationStore = (databaseUrl) => createPostgresOrganizationStore(createNeonDatabase(databaseUrl));
40419
40417
  var createPostgresOrganizationStore = (db) => ({
40418
+ acceptInvitation: async ({ now, tokenHash, userId, verifiedEmail }) => {
40419
+ 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({
40420
+ organizationId: organizationInvitationsTable.organization_id,
40421
+ roles: organizationInvitationsTable.roles
40422
+ }));
40423
+ const [row] = await db.with(claimed).insert(organizationMembershipsTable).select(db.select({
40424
+ created_at_ms: sql`${now}`.as("created_at_ms"),
40425
+ organization_id: claimed.organizationId,
40426
+ roles: claimed.roles,
40427
+ status: sql`'active'`.as("status"),
40428
+ updated_at_ms: sql`${now}`.as("updated_at_ms"),
40429
+ user_id: sql`${userId}`.as("user_id")
40430
+ }).from(claimed)).onConflictDoUpdate({
40431
+ set: { user_id: userId },
40432
+ target: [
40433
+ organizationMembershipsTable.organization_id,
40434
+ organizationMembershipsTable.user_id
40435
+ ]
40436
+ }).returning();
40437
+ return row && row.status === "active" ? toMembership(row) : undefined;
40438
+ },
40420
40439
  deleteOrganization: async (organizationId) => {
40421
40440
  await db.delete(organizationsTable).where(eq(organizationsTable.organization_id, organizationId));
40422
40441
  },
@@ -41467,6 +41486,30 @@ var createInMemoryOrganizationStore = () => {
41467
41486
  const memberships = new Map;
41468
41487
  const invitations = new Map;
41469
41488
  return {
41489
+ acceptInvitation: async ({ tokenHash, userId, verifiedEmail, now }) => {
41490
+ const invitation = [...invitations.values()].find((value) => value.tokenHash === tokenHash);
41491
+ if (!invitation || invitation.state !== "pending" || invitation.expiresAt <= now || invitation.email.trim().toLowerCase() !== verifiedEmail || !organizations.has(invitation.organizationId))
41492
+ return;
41493
+ const key = membershipKey(invitation.organizationId, userId);
41494
+ const existing = memberships.get(key);
41495
+ if (existing?.status === "suspended")
41496
+ return;
41497
+ const membership = existing ?? {
41498
+ createdAt: now,
41499
+ organizationId: invitation.organizationId,
41500
+ roles: [...invitation.roles],
41501
+ status: "active",
41502
+ updatedAt: now,
41503
+ userId
41504
+ };
41505
+ invitations.set(invitation.invitationId, {
41506
+ ...invitation,
41507
+ acceptedAt: now,
41508
+ state: "accepted"
41509
+ });
41510
+ memberships.set(key, membership);
41511
+ return cloneMembership(membership);
41512
+ },
41470
41513
  deleteOrganization: async (organizationId) => {
41471
41514
  organizations.delete(organizationId);
41472
41515
  },
@@ -42212,5 +42255,5 @@ export {
42212
42255
  userSessionIdTypebox
42213
42256
  };
42214
42257
 
42215
- //# debugId=111690DAA1672B5F64756E2164756E21
42258
+ //# debugId=E5C70909EAE2DA0864756E2164756E21
42216
42259
  //# sourceMappingURL=server.js.map