better-auth-organization-member 1.0.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.
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # better-auth-organization-member
2
+
3
+ A Better Auth plugin that extends the organization plugin with additional member management capabilities.
4
+
5
+ ## Features
6
+
7
+ - **Update Member Endpoint**: `/organization/update-member` - Update member information including role AND additional fields (firstName, lastName, avatar, and any custom fields)
8
+ - **Automatic Hook Injection**: Automatically adds `afterAcceptInvitation` hook to the organization plugin to transfer invitation data from invitations to member records
9
+ - **Before/After Update Hooks**: Lifecycle hooks for member updates
10
+ - **Full Type Inference**: Automatically infers member additional fields from organization plugin schema
11
+
12
+ ## Requirements
13
+
14
+ - `better-auth` >= 1.4.9
15
+ - `organization` plugin must be enabled
16
+
17
+ ## Installation
18
+
19
+ This is a workspace package. It's already included in the monorepo.
20
+
21
+ ## Usage
22
+
23
+ ### Server Setup
24
+
25
+ Simply add the plugin after the organization plugin. **No manual hooks required!**
26
+
27
+ ```typescript
28
+ import { betterAuth } from 'better-auth';
29
+ import { organization } from 'better-auth/plugins';
30
+ import { organizationMember } from 'better-auth-organization-member';
31
+
32
+ export const auth = betterAuth({
33
+ // ... other config
34
+ plugins: [
35
+ organization({
36
+ // organization config
37
+ schema: {
38
+ member: {
39
+ additionalFields: {
40
+ firstName: { type: 'string', input: true, required: false },
41
+ lastName: { type: 'string', input: true, required: false },
42
+ avatar: { type: 'string', input: true, required: false },
43
+ },
44
+ },
45
+ invitation: {
46
+ additionalFields: {
47
+ firstName: { type: 'string', input: true, required: false },
48
+ lastName: { type: 'string', input: true, required: false },
49
+ avatar: { type: 'string', input: true, required: false },
50
+ },
51
+ },
52
+ },
53
+ }),
54
+ // This plugin automatically injects the afterAcceptInvitation hook
55
+ organizationMember({
56
+ organizationMemberHooks: {
57
+ // Optional: Hook before member update
58
+ async beforeUpdateMember(data) {
59
+ console.log('Updating member:', data.member.id);
60
+ // You can modify the updates here
61
+ return {
62
+ data: {
63
+ ...data.updates,
64
+ // Add custom logic
65
+ },
66
+ };
67
+ },
68
+ // Optional: Hook after member update
69
+ async afterUpdateMember(data) {
70
+ console.log('Member updated:', data.member.id);
71
+ },
72
+ },
73
+ }),
74
+ ],
75
+ });
76
+ ```
77
+
78
+ ### What Happens Automatically
79
+
80
+ When the `organizationMember` plugin is added:
81
+
82
+ 1. ✅ It automatically injects an `afterAcceptInvitation` hook into the organization plugin
83
+ 2. ✅ When an invitation is accepted, the hook transfers `firstName`, `lastName`, and `avatar` from the invitation to the member record
84
+ 3. ✅ No need to manually add hooks in your provider configuration!
85
+
86
+ ### Client Usage
87
+
88
+ ```typescript
89
+ import { createAuthClient } from 'better-auth/client';
90
+ import { organizationMemberClient } from 'better-auth-organization-member/client';
91
+
92
+ const client = createAuthClient({
93
+ plugins: [organizationMemberClient()],
94
+ });
95
+
96
+ // Update member information (role + additional fields)
97
+ await client.organization.updateMember({
98
+ memberId: 'member-id',
99
+ role: 'admin', // Can update role
100
+ firstName: 'John',
101
+ lastName: 'Doe',
102
+ avatar: 'https://example.com/avatar.jpg',
103
+ // any other custom fields defined in your member schema
104
+ });
105
+ ```
106
+
107
+ ## API
108
+
109
+ ### Endpoint: `/organization/update-member`
110
+
111
+ Updates member information in an organization. Follows the **exact same permission logic** as `/organization/update-member-role` but accepts additional fields.
112
+
113
+ **Method**: `POST`
114
+
115
+ **Body**:
116
+ ```typescript
117
+ {
118
+ memberId: string; // Required: ID of the member to update
119
+ role: string | string[]; // Required: Role(s) to assign
120
+ organizationId?: string; // Optional: defaults to active organization
121
+ teamId?: string; // Optional: team ID (if teams are enabled)
122
+ // ... any additional fields defined in member.additionalFields
123
+ // Examples: firstName, lastName, avatar, etc.
124
+ }
125
+ ```
126
+
127
+ **Response**:
128
+ ```typescript
129
+ {
130
+ id: string;
131
+ userId: string;
132
+ organizationId: string;
133
+ role: string;
134
+ user: {
135
+ id: string;
136
+ email: string;
137
+ name: string | null;
138
+ image: string | null;
139
+ };
140
+ // ... all additional fields
141
+ }
142
+ ```
143
+
144
+ **Permissions**: Same as `updateMemberRole` - requires `member:update` permission or owner/admin role.
145
+
146
+ **Role Logic** (same as `updateMemberRole`):
147
+ - ✅ Owners can update any member
148
+ - ✅ Admins with `member:update` permission can update members
149
+ - ❌ Non-creators cannot update creators
150
+ - ❌ Last owner cannot demote themselves
151
+ - ❌ Same permission checks as the built-in role update
152
+
153
+ ## Hooks
154
+
155
+ ### `beforeUpdateMember`
156
+
157
+ Called before a member's information is updated. Can modify the update data.
158
+
159
+ ```typescript
160
+ beforeUpdateMember?: (data: {
161
+ member: Member;
162
+ updates: Record<string, any>;
163
+ user: User;
164
+ organization: Organization;
165
+ }) => Promise<void | { data: Record<string, any> }>;
166
+ ```
167
+
168
+ ### `afterUpdateMember`
169
+
170
+ Called after a member's information is updated.
171
+
172
+ ```typescript
173
+ afterUpdateMember?: (data: {
174
+ member: Member;
175
+ previousData: Record<string, any>;
176
+ user: User;
177
+ organization: Organization;
178
+ }) => Promise<void>;
179
+ ```
180
+
181
+ ### Automatic `afterAcceptInvitation` Hook
182
+
183
+ This hook is **automatically injected** into the organization plugin when you add `organizationMember()`. It:
184
+
185
+ 1. Extracts **all additional fields** from `invitation.additionalFields`
186
+ 2. Automatically transfers them to the newly created member record
187
+ 3. Works with any custom fields you define in both invitation and member schemas
188
+ 4. Logs the update (or errors if they occur)
189
+ 5. Does not fail the invitation acceptance if the update fails
190
+
191
+ ## Comparison with `updateMemberRole`
192
+
193
+ ### Built-in `updateMemberRole` (organization plugin)
194
+
195
+ ```typescript
196
+ // Only updates the role field
197
+ await client.organization.updateMemberRole({
198
+ memberId: 'member-id',
199
+ role: 'admin',
200
+ });
201
+ ```
202
+
203
+ ### This Plugin's `updateMember`
204
+
205
+ ```typescript
206
+ // Updates role AND additional fields in one call
207
+ await client.organization.updateMember({
208
+ memberId: 'member-id',
209
+ role: 'admin', // ✅ Can update role
210
+ firstName: 'John', // ✅ Plus additional fields
211
+ lastName: 'Doe',
212
+ avatar: 'https://example.com/avatar.jpg',
213
+ customField: 'value', // ✅ Plus any custom fields
214
+ });
215
+ ```
216
+
217
+ **Key Features:**
218
+ - ✅ **Exact same permission logic** as `updateMemberRole` (identical role checks)
219
+ - ✅ **Same validation** for role updates (creator protection, owner checks, etc.)
220
+ - ✅ **Additional field support** - Update member additional fields in the same request
221
+ - ✅ **Full type inference** - TypeScript autocomplete for all additional fields
222
+ - ✅ **Automatic schema validation** - Uses `toZodSchema` to validate additional fields
223
+
224
+ ## Implementation Details
225
+
226
+ This plugin:
227
+
228
+ 1. **Extends `updateMemberRole`**: Uses the exact same permission logic, role validation, and error handling as `updateMemberRole`, but accepts additional fields via `toZodSchema`
229
+ 2. **Auto-injects hooks**: Uses the plugin's `init()` method to inject `afterAcceptInvitation` into the organization plugin's hooks
230
+ 3. **Type-safe**: Full TypeScript support with `InferAdditionalFieldsFromPluginOptions` for automatic type inference of all additional fields
231
+ 4. **Uses `getOrgAdapter`**: Leverages the organization plugin's adapter utilities for consistent behavior
232
+ 5. **Dynamic field transfer**: Automatically detects and transfers all fields defined in `invitation.additionalFields` to member records
233
+ 6. **Production-ready**: Follows Better Auth best practices and patterns
234
+
235
+ ### How It Works
236
+
237
+ 1. **At initialization**: The plugin reads the organization plugin's schema to get member additional fields
238
+ 2. **Schema generation**: Uses `toZodSchema` to generate validation schema from `member.additionalFields`
239
+ 3. **Endpoint creation**: Creates the `/organization/update-member` endpoint with merged schemas (base + additional fields)
240
+ 4. **Hook injection**: Injects `afterAcceptInvitation` to automatically transfer invitation fields to member
241
+ 5. **Type inference**: TypeScript automatically infers all additional fields for full autocomplete support
242
+
243
+ ## License
244
+
245
+ UNLICENSED
@@ -0,0 +1,26 @@
1
+ import { i as organizationMember } from "./server-uBIvbfMk.mjs";
2
+
3
+ //#region src/client.d.ts
4
+ interface OrganizationMemberClient {
5
+ updateMember: <T extends Record<string, any> = {}>(data: {
6
+ memberId: string;
7
+ organizationId?: string;
8
+ } & T) => Promise<{
9
+ data: any | null;
10
+ error: Error | null;
11
+ }>;
12
+ updateInvitation: <T extends Record<string, any> = {}>(data: {
13
+ invitationId: string;
14
+ organizationId?: string;
15
+ } & T) => Promise<{
16
+ data: any | null;
17
+ error: Error | null;
18
+ }>;
19
+ }
20
+ declare const organizationMemberClient: () => {
21
+ id: string;
22
+ $InferServerPlugin: ReturnType<typeof organizationMember>;
23
+ };
24
+ //#endregion
25
+ export { organizationMemberClient as n, OrganizationMemberClient as t };
26
+ //# sourceMappingURL=client-B1z87oFG.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-B1z87oFG.d.mts","names":[],"sources":["../src/client.ts"],"sourcesContent":[],"mappings":";;;UAAiB,wBAAA;2BACU;;IADV,cAAA,CAAA,EAAA,MAAwB;EACd,CAAA,GAGrB,CAHqB,EAAA,GAGf,OAHe,CAAA;IAGrB,IAAA,EAAA,GAAA,GAAA,IAAA;IAEK,KAAA,EAAA,KAAA,GAAA,IAAA;EAFC,CAAA,CAAA;EAImB,gBAAA,EAAA,CAAA,UAAA,MAAA,CAAA,MAAA,EAAA,GAAA,CAAA,GAAA,CAAA,CAAA,CAAA,CAAA,IAAA,EAAA;IAGzB,YAAA,EAAA,MAAA;IAEK,cAAA,CAAA,EAAA,MAAA;EAFC,CAAA,GAAN,CAAM,EAAA,GAAA,OAAA,CAAA;IAAO,IAAA,EAAA,GAAA,GAAA,IAAA;IAMN,KAAA,EAJF,KAIE,GAAA,IAAA;;;cAAA;;sBAGiB,kBAI7B"}
@@ -0,0 +1,11 @@
1
+ //#region src/client.ts
2
+ const organizationMemberClient = () => {
3
+ return {
4
+ id: "organization-member",
5
+ $InferServerPlugin: {}
6
+ };
7
+ };
8
+
9
+ //#endregion
10
+ export { organizationMemberClient as t };
11
+ //# sourceMappingURL=client-PBBSie1a.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-PBBSie1a.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["export interface OrganizationMemberClient {\n updateMember: <T extends Record<string, any> = {}>(data: {\n memberId: string;\n organizationId?: string;\n } & T) => Promise<{\n data: any | null;\n error: Error | null;\n }>;\n updateInvitation: <T extends Record<string, any> = {}>(data: {\n invitationId: string;\n organizationId?: string;\n } & T) => Promise<{\n data: any | null;\n error: Error | null;\n }>;\n}\n\nexport const organizationMemberClient = () => {\n return {\n id: \"organization-member\",\n $InferServerPlugin: {} as ReturnType<\n typeof import(\"./server\").organizationMember\n >,\n };\n};\n"],"mappings":";AAiBA,MAAa,iCAAiC;AAC5C,QAAO;EACL,IAAI;EACJ,oBAAoB,EAAE;EAGvB"}
@@ -0,0 +1,3 @@
1
+ import "./server-uBIvbfMk.mjs";
2
+ import { n as organizationMemberClient, t as OrganizationMemberClient } from "./client-B1z87oFG.mjs";
3
+ export { OrganizationMemberClient, organizationMemberClient };
@@ -0,0 +1,21 @@
1
+ export interface OrganizationMemberClient {
2
+ updateMember: <T extends Record<string, any> = {}>(data: {
3
+ memberId: string;
4
+ organizationId?: string;
5
+ } & T) => Promise<{
6
+ data: any | null;
7
+ error: Error | null;
8
+ }>;
9
+ updateInvitation: <T extends Record<string, any> = {}>(data: {
10
+ invitationId: string;
11
+ organizationId?: string;
12
+ } & T) => Promise<{
13
+ data: any | null;
14
+ error: Error | null;
15
+ }>;
16
+ }
17
+ export declare const organizationMemberClient: () => {
18
+ id: string;
19
+ $InferServerPlugin: ReturnType<typeof import("./server").organizationMember>;
20
+ };
21
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE;QACvD,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,GAAG,CAAC,KAAK,OAAO,CAAC;QAChB,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;QACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;KACrB,CAAC,CAAC;IACH,gBAAgB,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE;QAC3D,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,GAAG,CAAC,KAAK,OAAO,CAAC;QAChB,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;QACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;KACrB,CAAC,CAAC;CACJ;AAED,eAAO,MAAM,wBAAwB;;wBAGP,UAAU,CAClC,cAAc,UAAU,EAAE,kBAAkB,CAC7C;CAEJ,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,7 @@
1
+ export const organizationMemberClient = () => {
2
+ return {
3
+ id: "organization-member",
4
+ $InferServerPlugin: {},
5
+ };
6
+ };
7
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,EAAE;IAC3C,OAAO;QACL,EAAE,EAAE,qBAAqB;QACzB,kBAAkB,EAAE,EAEnB;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { t as organizationMemberClient } from "./client-PBBSie1a.mjs";
2
+
3
+ export { organizationMemberClient };
@@ -0,0 +1,3 @@
1
+ import { i as organizationMember, t as OrganizationMemberOptions } from "./server-uBIvbfMk.mjs";
2
+ import { n as organizationMemberClient, t as OrganizationMemberClient } from "./client-B1z87oFG.mjs";
3
+ export { type OrganizationMemberClient, type OrganizationMemberOptions, organizationMember, organizationMemberClient };
@@ -0,0 +1,5 @@
1
+ export { organizationMember } from "./server";
2
+ export type { OrganizationMemberOptions } from "./server";
3
+ export { organizationMemberClient } from "./client";
4
+ export type { OrganizationMemberClient } from "./client";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,YAAY,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AACpD,YAAY,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { organizationMember } from "./server";
2
+ export { organizationMemberClient } from "./client";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ import { r as organizationMember } from "./server-Big_p9zG.mjs";
2
+ import { t as organizationMemberClient } from "./client-PBBSie1a.mjs";
3
+
4
+ export { organizationMember, organizationMemberClient };