UNPKG

better-auth-waitlist

Version:

A lightweight Better Auth plugin for waitlist management with admin approval workflows, domain restrictions, and customizable validation.

799 lines (792 loc) 30.2 kB
import * as better_auth from 'better-auth'; import { InferOptionSchema, User } from 'better-auth'; import { z } from 'zod/v3'; import { FieldAttribute } from 'better-auth/db'; declare const WAITLIST_MODEL_NAME = "waitlist"; declare const WAITLIST_STATUS: { PENDING: string; APPROVED: string; REJECTED: string; }; declare const schema: { waitlist: { fields: { email: { type: "string"; required: true; unique: true; }; status: { type: "string"; required: true; input: false; defaultValue: string; }; requestedAt: { type: "date"; required: false; input: false; defaultValue: () => Date; }; processedAt: { type: "date"; required: false; input: false; }; processedBy: { type: "string"; references: { model: string; field: string; onDelete: "no action"; }; required: false; input: false; }; }; }; }; interface WaitlistOptions { /** * Allow users to join the waitlist * @default true */ enabled?: boolean; /** * Restrict waitlist to specific email domains * @example ["@company.com", "@organization.org"] */ allowedDomains?: string[]; /** * Maximum number of waitlist entries * @default undefined (no limit) */ maximumWaitlistParticipants?: number; /** * schema for the waitlist plugin. Use this to rename fields. */ schema?: InferOptionSchema<typeof schema>; /** * Extend the `waitlist` schema with additional fields. */ additionalFields?: { [key: string]: FieldAttribute; }; /** * Wether to disable sign in & sign ups while the waitlist is active. * * @default false */ disableSignInAndSignUp?: boolean; /** * Auto-approve waitlist entries based on criteria * @param email - The email of the user * @returns true if the entry should be auto-approved, false otherwise * @default false */ autoApprove?: boolean | ((email: string) => boolean); /** * Custom validation for waitlist entries * @param data - The data of the entry including email and additional fields * @returns true if the entry should be validated, false otherwise */ validateEntry?: (data: { email: string; [key: string]: any; }) => Promise<boolean> | boolean; /** * Webhook/callback when entry is processed * * @example * ```ts * onStatusChange: (entry) => { * console.log(entry); * } * ``` * @param entry - The entry that has been processed * @returns void */ onStatusChange?: (entry: { id: string; email: string; status: "pending" | "accepted" | "rejected"; }) => Promise<void> | void; /** * Webhook/callback when entry is created * * @example * ```ts * onJoinRequest: (params) => { * console.log(params); * } * ``` * @param params - The params that has been created * @returns void */ onJoinRequest?: (params: { request: WaitlistEntry & { [key: string]: any; }; }) => Promise<void> | void; /** * Custom email notification settings */ notifications?: { enabled: boolean; onJoin?: boolean; onAccept?: boolean; onReject?: boolean; }; /** * Rate limiting for waitlist joins */ rateLimit?: { maxAttempts: number; windowMs: number; }; /** * Custom access control function for admin endpoints (list, findOne, approve, reject) * @param user - The authenticated user object from session * @returns Promise<boolean> - true for access granted, false for access denied * * @example * ```ts * canManageWaitlist: async (user) => { * // Simple role check * return user.role === "admin" || user.role === "moderator"; * } * * // Or with Better Auth's permission system * canManageWaitlist: async (user) => { * try { * const hasPermission = await auth.api.userHasPermission({ * body: { * userId: user.id, * permissions: { waitlist: ["list", "read", "update"] } * } * }); * return hasPermission; * } catch { * return user.role === "admin"; * } * } * ``` */ canManageWaitlist?: (user: User) => Promise<boolean>; } interface WaitlistEntry { id: string; email: string; status: (typeof WAITLIST_STATUS)[keyof typeof WAITLIST_STATUS]; requestedAt: Date; processedAt: Date | null | undefined; processedBy: string | null | undefined; } interface WaitlistClientOptions { additionalFields?: { [key: string]: FieldAttribute; }; } declare const waitlistClient: <CO extends WaitlistClientOptions>(options?: CO) => { id: "waitlist"; $InferServerPlugin: ReturnType<typeof waitlist<{ additionalFields: CO["additionalFields"] | undefined; }>>; }; declare const HTTP_STATUS_CODES: { readonly OK: 200; readonly CREATED: 201; readonly UNAUTHORIZED: 401; readonly FORBIDDEN: 403; readonly NOT_FOUND: 404; readonly UNPROCESSABLE_ENTITY: 422; readonly TOO_MANY_REQUESTS: 429; }; declare const HTTP_STATUS_CODE_MESSAGES: { readonly 200: "OK"; readonly 201: "Created"; readonly 401: "Unauthorized"; readonly 403: "Forbidden"; readonly 404: "Not Found"; readonly 422: "Unprocessable Entity"; readonly 429: "Too Many Requests"; }; declare const WAITLIST_ERROR_CODES: { readonly EMAIL_ALREADY_IN_WAITLIST: "email_already_in_waitlist"; readonly DOMAIN_NOT_ALLOWED: "domain_not_allowed"; readonly INVALID_ENTRY: "invalid_entry"; readonly WAITLIST_FULL: "waitlist_full"; readonly RATE_LIMIT_EXCEEDED: "rate_limit_exceeded"; readonly WAITLIST_NOT_ENABLED: "waitlist_not_enabled"; readonly UNAUTHORIZED: "unauthorized"; readonly FORBIDDEN: "permission_denied"; readonly WAITLIST_ENTRY_NOT_FOUND: "waitlist_entry_not_found"; }; declare const WAITLIST_ERROR_MESSAGES: { readonly email_already_in_waitlist: "Email already in waitlist"; readonly domain_not_allowed: "Email domain not allowed"; readonly invalid_entry: "Invalid entry data"; readonly waitlist_full: "Waitlist is full"; readonly rate_limit_exceeded: "Too many requests, please try again later"; readonly waitlist_not_enabled: "Waitlist is not enabled"; readonly unauthorized: "You are not authorized to perform this action"; readonly permission_denied: "Not enough permissions to perform this action"; readonly waitlist_entry_not_found: "Waitlist entry not found"; }; type WaitlistErrorCode = keyof typeof WAITLIST_ERROR_CODES; declare const waitlist: <O extends WaitlistOptions>(options?: O) => { id: "waitlist"; schema: { waitlist: { fields: { email: { type: "string"; required: true; unique: true; }; status: { type: "string"; required: true; input: false; defaultValue: string; }; requestedAt: { type: "date"; required: false; input: false; defaultValue: () => Date; }; processedAt: { type: "date"; required: false; input: false; }; processedBy: { type: "string"; references: { model: string; field: string; onDelete: "no action"; }; required: false; input: false; }; }; }; }; $ERROR_CODES: { readonly EMAIL_ALREADY_IN_WAITLIST: "email_already_in_waitlist"; readonly DOMAIN_NOT_ALLOWED: "domain_not_allowed"; readonly INVALID_ENTRY: "invalid_entry"; readonly WAITLIST_FULL: "waitlist_full"; readonly RATE_LIMIT_EXCEEDED: "rate_limit_exceeded"; readonly WAITLIST_NOT_ENABLED: "waitlist_not_enabled"; readonly UNAUTHORIZED: "unauthorized"; readonly FORBIDDEN: "permission_denied"; readonly WAITLIST_ENTRY_NOT_FOUND: "waitlist_entry_not_found"; }; endpoints: { join: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: Omit<WaitlistEntry, "status" | "requestedAt" | "processedAt" | "processedBy" | "id">; } & { method?: "POST" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { id: string; email: string; requestedAt: Date; }; } : { id: string; email: string; requestedAt: Date; }>; options: { method: "POST"; body: z.ZodType<Omit<WaitlistEntry, "id" | "status" | "requestedAt" | "processedAt" | "processedBy" | "requestedAt">>; } & { use: any[]; }; path: "/waitlist/join"; }; list: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body?: undefined; } & { method?: "GET" | undefined; } & { query: { status?: string | undefined; page?: string | number | undefined; limit?: string | number | undefined; sortBy?: "status" | "requestedAt" | undefined; direction?: "asc" | "desc" | undefined; }; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { data: (WaitlistEntry & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); } & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; })[]; page: string | number; limit: number; total: number; }; } : { data: (WaitlistEntry & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); } & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; })[]; page: string | number; limit: number; total: number; }>; options: { method: "GET"; use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{ session: { session: Record<string, any> & { id: string; userId: string; expiresAt: Date; createdAt: Date; updatedAt: Date; token: string; ipAddress?: string | null | undefined; userAgent?: string | null | undefined; }; user: Record<string, any> & { id: string; email: string; emailVerified: boolean; name: string; createdAt: Date; updatedAt: Date; image?: string | null | undefined; }; }; }>)[]; query: z.ZodObject<{ page: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>; limit: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>; status: z.ZodOptional<z.ZodEnum<[string, string, string]>>; sortBy: z.ZodOptional<z.ZodEnum<["requestedAt", "status"]>>; direction: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>; }, "strip", z.ZodTypeAny, { status?: string | undefined; page?: string | number | undefined; limit?: string | number | undefined; sortBy?: "status" | "requestedAt" | undefined; direction?: "asc" | "desc" | undefined; }, { status?: string | undefined; page?: string | number | undefined; limit?: string | number | undefined; sortBy?: "status" | "requestedAt" | undefined; direction?: "asc" | "desc" | undefined; }>; metadata: { openapi: { responses: { 200: { description: string; content: { "application/json": { schema: { type: "object"; properties: { waitlist: { type: string; items: { $ref: string; }; }; total: { type: string; }; limit: { type: string[]; }; page: { type: string[]; }; }; }; }; }; }; }; }; }; } & { use: any[]; }; path: "/waitlist/list"; }; findOne: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body?: undefined; } & { method?: "GET" | undefined; } & { query: { id: string; }; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: WaitlistEntry & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); } & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; }; } : WaitlistEntry & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>); } & { [x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; [x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined; }>; options: { method: "GET"; query: z.ZodObject<{ id: z.ZodString; }, "strip", z.ZodTypeAny, { id: string; }, { id: string; }>; use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{ session: { session: Record<string, any> & { id: string; userId: string; expiresAt: Date; createdAt: Date; updatedAt: Date; token: string; ipAddress?: string | null | undefined; userAgent?: string | null | undefined; }; user: Record<string, any> & { id: string; email: string; emailVerified: boolean; name: string; createdAt: Date; updatedAt: Date; image?: string | null | undefined; }; }; }>)[]; metadata: { openapi: { responses: { 200: { description: string; }; 401: { description: string; }; 403: { description: string; }; 404: { description: string; }; }; }; }; } & { use: any[]; }; path: "/waitlist/request/find"; }; checkRequestStatus: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body?: undefined; } & { method?: "GET" | undefined; } & { query: { email: string; }; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { status: string; requestedAt: Date; }; } : { status: string; requestedAt: Date; }>; options: { method: "GET"; query: z.ZodObject<{ email: z.ZodString; }, "strip", z.ZodTypeAny, { email: string; }, { email: string; }>; metadata: { openapi: { responses: { 200: { description: string; }; 404: { description: string; }; }; }; }; } & { use: any[]; }; path: "/waitlist/request/check-status"; }; approveRequest: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: { id: string; }; } & { method?: "POST" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { message: string; }; } : { message: string; }>; options: { method: "POST"; body: z.ZodObject<{ id: z.ZodString; }, "strip", z.ZodTypeAny, { id: string; }, { id: string; }>; use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{ session: { session: Record<string, any> & { id: string; userId: string; expiresAt: Date; createdAt: Date; updatedAt: Date; token: string; ipAddress?: string | null | undefined; userAgent?: string | null | undefined; }; user: Record<string, any> & { id: string; email: string; emailVerified: boolean; name: string; createdAt: Date; updatedAt: Date; image?: string | null | undefined; }; }; }>)[]; metadata: { openapi: { responses: { 200: { description: string; }; 401: { description: string; }; 403: { description: string; }; 404: { description: string; }; }; }; }; } & { use: any[]; }; path: "/waitlist/request/approve"; }; rejectRequest: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: { id: string; }; } & { method?: "POST" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { message: string; }; } : { message: string; }>; options: { method: "POST"; body: z.ZodObject<{ id: z.ZodString; }, "strip", z.ZodTypeAny, { id: string; }, { id: string; }>; use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{ session: { session: Record<string, any> & { id: string; userId: string; expiresAt: Date; createdAt: Date; updatedAt: Date; token: string; ipAddress?: string | null | undefined; userAgent?: string | null | undefined; }; user: Record<string, any> & { id: string; email: string; emailVerified: boolean; name: string; createdAt: Date; updatedAt: Date; image?: string | null | undefined; }; }; }>)[]; metadata: { openapi: { responses: { 200: { description: string; }; 401: { description: string; }; 403: { description: string; }; 404: { description: string; }; }; }; }; } & { use: any[]; }; path: "/waitlist/request/reject"; }; }; }; export { HTTP_STATUS_CODES, HTTP_STATUS_CODE_MESSAGES, WAITLIST_ERROR_CODES, WAITLIST_ERROR_MESSAGES, WAITLIST_MODEL_NAME, WAITLIST_STATUS, type WaitlistEntry, type WaitlistErrorCode, type WaitlistOptions, schema, waitlist, waitlistClient };