convex-affiliates 3.0.5 → 3.0.6

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.
@@ -2,8 +2,8 @@
2
2
  * Better Auth Server Plugin for Convex Affiliates
3
3
  *
4
4
  * This plugin automatically handles affiliate attribution when users sign up
5
- * through Better Auth. It hooks into the signup flow and calls the component's
6
- * attributeSignup mutation directly.
5
+ * through Better Auth. It uses databaseHooks to intercept user creation events,
6
+ * which fires for ALL auth methods (email, OAuth, magic-link, etc.).
7
7
  *
8
8
  * @example
9
9
  * ```typescript
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * @module
26
26
  */
27
- import type { BetterAuthPlugin, HookEndpointContext } from "better-auth";
27
+ import type { BetterAuthPlugin } from "better-auth";
28
28
  import type { GenericActionCtx, GenericDataModel } from "convex/server";
29
29
  import type { ComponentApi } from "../component/_generated/component.js";
30
30
  /**
@@ -73,9 +73,10 @@ export interface AffiliatePluginOptions {
73
73
  /**
74
74
  * Better Auth plugin for automatic affiliate attribution.
75
75
  *
76
- * This plugin intercepts signup requests and automatically attributes
77
- * new users to affiliates. It reads referral data from the request body
78
- * (injected by the client plugin) or from cookies.
76
+ * Uses `databaseHooks.user.create.after` to intercept user creation at the
77
+ * database level. This fires for ALL auth methods (email, OAuth, magic-link,
78
+ * passkey, etc.) and receives the actual user record directly — no need
79
+ * for endpoint path matching or response parsing.
79
80
  *
80
81
  * @param ctx - The Convex context (from createAuth function)
81
82
  * @param component - The affiliates component (components.affiliates)
@@ -108,11 +109,21 @@ export interface AffiliatePluginOptions {
108
109
  */
109
110
  export declare function affiliatePlugin(ctx: ConvexCtx, component: ComponentApi, options?: AffiliatePluginOptions): {
110
111
  id: "convex-affiliates";
111
- hooks: {
112
- after: {
113
- matcher: (context: HookEndpointContext) => boolean;
114
- handler: (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>;
115
- }[];
112
+ init(): {
113
+ options: {
114
+ databaseHooks: {
115
+ user: {
116
+ create: {
117
+ after: (user: {
118
+ id: string;
119
+ } & Record<string, unknown>, endpointCtx: {
120
+ body?: unknown;
121
+ headers?: Headers;
122
+ } | null) => Promise<void>;
123
+ };
124
+ };
125
+ };
126
+ };
116
127
  };
117
128
  };
118
129
  export type { BetterAuthPlugin };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/better-auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAMzE;;;;GAIG;AACH,KAAK,SAAS,GAAG,IAAI,CACnB,gBAAgB,CAAC,gBAAgB,CAAC,EAClC,UAAU,GAAG,aAAa,CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,CAAC,EAAE;QACX,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B;;OAEG;IACH,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpB;;OAEG;IACH,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,YAAY,EACvB,OAAO,GAAE,sBAA2B;;;;+BAkBT,mBAAmB;;;;EAyI/C;AA4BD,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/better-auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAMzE;;;;GAIG;AACH,KAAK,SAAS,GAAG,IAAI,CACnB,gBAAgB,CAAC,gBAAgB,CAAC,EAClC,UAAU,GAAG,aAAa,CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,CAAC,EAAE;QACX,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B;;OAEG;IACH,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpB;;OAEG;IACH,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,YAAY,EACvB,OAAO,GAAE,sBAA2B;;;;;;;sCAqBd;4BAAE,EAAE,EAAE,MAAM,CAAA;yBAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,eACjC;4BAAE,IAAI,CAAC,EAAE,OAAO,CAAC;4BAAC,OAAO,CAAC,EAAE,OAAO,CAAA;yBAAE,GAAG,IAAI;;;;;;EA8E1E;AA0BD,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
@@ -2,8 +2,8 @@
2
2
  * Better Auth Server Plugin for Convex Affiliates
3
3
  *
4
4
  * This plugin automatically handles affiliate attribution when users sign up
5
- * through Better Auth. It hooks into the signup flow and calls the component's
6
- * attributeSignup mutation directly.
5
+ * through Better Auth. It uses databaseHooks to intercept user creation events,
6
+ * which fires for ALL auth methods (email, OAuth, magic-link, etc.).
7
7
  *
8
8
  * @example
9
9
  * ```typescript
@@ -24,16 +24,16 @@
24
24
  *
25
25
  * @module
26
26
  */
27
- import { createAuthMiddleware } from "better-auth/plugins";
28
27
  // =============================================================================
29
28
  // Plugin Implementation
30
29
  // =============================================================================
31
30
  /**
32
31
  * Better Auth plugin for automatic affiliate attribution.
33
32
  *
34
- * This plugin intercepts signup requests and automatically attributes
35
- * new users to affiliates. It reads referral data from the request body
36
- * (injected by the client plugin) or from cookies.
33
+ * Uses `databaseHooks.user.create.after` to intercept user creation at the
34
+ * database level. This fires for ALL auth methods (email, OAuth, magic-link,
35
+ * passkey, etc.) and receives the actual user record directly — no need
36
+ * for endpoint path matching or response parsing.
37
37
  *
38
38
  * @param ctx - The Convex context (from createAuth function)
39
39
  * @param component - The affiliates component (components.affiliates)
@@ -75,110 +75,86 @@ export function affiliatePlugin(ctx, component, options = {}) {
75
75
  };
76
76
  return {
77
77
  id: "convex-affiliates",
78
- hooks: {
79
- after: [
80
- {
81
- // Match signup endpoints and OAuth callback (where social auth users are created)
82
- matcher: (context) => {
83
- const path = context.path ?? "";
84
- return (path === "/sign-up/email" ||
85
- path === "/sign-up/username" ||
86
- path.startsWith("/sign-up/") ||
87
- path.startsWith("/callback/"));
78
+ init() {
79
+ return {
80
+ options: {
81
+ databaseHooks: {
82
+ user: {
83
+ create: {
84
+ after: async (user, endpointCtx) => {
85
+ // Top-level try-catch: NEVER let attribution crash the auth flow
86
+ try {
87
+ if (!endpointCtx)
88
+ return;
89
+ const userId = user.id;
90
+ // Extract referral data from body (email signups)
91
+ const body = endpointCtx.body;
92
+ let referralId = body?.[config.referralIdField];
93
+ let referralCode = body?.[config.referralCodeField];
94
+ // Also check cookies (OAuth signups — cookies forwarded by Next.js proxy)
95
+ const cookieHeader = endpointCtx.headers?.get("cookie");
96
+ if (cookieHeader) {
97
+ const cookies = parseCookies(cookieHeader);
98
+ if (!referralId)
99
+ referralId = cookies[config.referralIdCookieName];
100
+ if (!referralCode)
101
+ referralCode = cookies[config.cookieName];
102
+ }
103
+ if (!referralId && !referralCode) {
104
+ try {
105
+ await config.onAttributionFailure?.({ userId, reason: "No referral data found" });
106
+ }
107
+ catch { /* swallow */ }
108
+ return;
109
+ }
110
+ // Attribution logic
111
+ let attributed = false;
112
+ let affiliateCode;
113
+ // Try by referral ID first (more accurate)
114
+ if (referralId) {
115
+ const referral = await ctx.runQuery(component.referrals.getByReferralId, { referralId });
116
+ if (referral?.status === "clicked") {
117
+ await ctx.runMutation(component.referrals.attributeSignup, {
118
+ referralId,
119
+ userId,
120
+ });
121
+ attributed = true;
122
+ const affiliate = await ctx.runQuery(component.affiliates.getById, { affiliateId: referral.affiliateId });
123
+ affiliateCode = affiliate?.code;
124
+ }
125
+ }
126
+ // Try by affiliate code if referral ID didn't work
127
+ if (!attributed && referralCode) {
128
+ const result = await ctx.runMutation(component.referrals.attributeSignupByCode, { userId, affiliateCode: referralCode });
129
+ attributed = result.success;
130
+ affiliateCode = referralCode;
131
+ }
132
+ if (attributed) {
133
+ try {
134
+ await config.onAttributionSuccess?.({ userId, affiliateCode });
135
+ }
136
+ catch { /* swallow */ }
137
+ }
138
+ else {
139
+ try {
140
+ await config.onAttributionFailure?.({ userId, reason: "Attribution failed - referral not found or invalid" });
141
+ }
142
+ catch { /* swallow */ }
143
+ }
144
+ }
145
+ catch (error) {
146
+ console.error("[convex-affiliates] Attribution error:", error);
147
+ try {
148
+ await config.onAttributionFailure?.({ userId: user.id, reason: error instanceof Error ? error.message : "Unknown error" });
149
+ }
150
+ catch { /* swallow */ }
151
+ }
152
+ },
153
+ },
154
+ },
88
155
  },
89
- handler: createAuthMiddleware(async (ctx_hook) => {
90
- const context = ctx_hook;
91
- // Check if signup was successful (user was created)
92
- const returned = context.context?.returned;
93
- if (!returned || typeof returned !== "object") {
94
- return;
95
- }
96
- // Extract user ID from response
97
- const responseObj = returned;
98
- const user = responseObj.user;
99
- if (!user?.id) {
100
- return;
101
- }
102
- const userId = user.id;
103
- // For OAuth sign-ins, check if this is actually a new user
104
- // by comparing createdAt to current time (within 10 seconds = new user)
105
- if (user.createdAt) {
106
- const createdAt = new Date(user.createdAt).getTime();
107
- const now = Date.now();
108
- const isNewUser = now - createdAt < 10000; // 10 seconds
109
- if (!isNewUser) {
110
- // Existing user signing in via OAuth, skip attribution
111
- return;
112
- }
113
- }
114
- // Extract referral data from request body
115
- const body = context.body ?? context.context?.body;
116
- let referralId = body?.[config.referralIdField];
117
- let referralCode = body?.[config.referralCodeField];
118
- // Also check cookies
119
- const cookieHeader = context.headers?.get?.("cookie") ??
120
- context.context?.request?.headers?.get?.("cookie");
121
- if (cookieHeader) {
122
- const cookies = parseCookies(cookieHeader);
123
- if (!referralId && config.referralIdCookieName) {
124
- referralId = cookies[config.referralIdCookieName];
125
- }
126
- if (!referralCode && config.cookieName) {
127
- referralCode = cookies[config.cookieName];
128
- }
129
- }
130
- // Skip if no referral data
131
- if (!referralId && !referralCode) {
132
- await config.onAttributionFailure?.({
133
- userId,
134
- reason: "No referral data found",
135
- });
136
- return;
137
- }
138
- // Attribute the signup
139
- try {
140
- let attributed = false;
141
- let affiliateCode;
142
- // Try by referral ID first (more accurate)
143
- if (referralId) {
144
- const referral = await ctx.runQuery(component.referrals.getByReferralId, { referralId });
145
- if (referral && referral.status === "clicked") {
146
- await ctx.runMutation(component.referrals.attributeSignup, {
147
- referralId: referralId,
148
- userId,
149
- });
150
- attributed = true;
151
- // Look up the affiliate to get their code
152
- const affiliate = await ctx.runQuery(component.affiliates.getById, { affiliateId: referral.affiliateId });
153
- affiliateCode = affiliate?.code;
154
- }
155
- }
156
- // Try by affiliate code if referral ID didn't work
157
- if (!attributed && referralCode) {
158
- const result = await ctx.runMutation(component.referrals.attributeSignupByCode, { userId, affiliateCode: referralCode });
159
- attributed = result.success;
160
- affiliateCode = referralCode;
161
- }
162
- if (attributed) {
163
- await config.onAttributionSuccess?.({ userId, affiliateCode });
164
- }
165
- else {
166
- await config.onAttributionFailure?.({
167
- userId,
168
- reason: "Attribution failed - referral not found or invalid",
169
- });
170
- }
171
- }
172
- catch (error) {
173
- console.error("[convex-affiliates] Attribution error:", error);
174
- await config.onAttributionFailure?.({
175
- userId,
176
- reason: error instanceof Error ? error.message : "Unknown error",
177
- });
178
- }
179
- }),
180
156
  },
181
- ],
157
+ };
182
158
  },
183
159
  };
184
160
  }
@@ -187,20 +163,18 @@ export function affiliatePlugin(ctx, component, options = {}) {
187
163
  // =============================================================================
188
164
  function parseCookies(cookieHeader) {
189
165
  const cookies = {};
190
- cookieHeader.split(";").forEach((cookie) => {
166
+ for (const cookie of cookieHeader.split(";")) {
191
167
  const [name, ...valueParts] = cookie.trim().split("=");
192
168
  if (name) {
193
169
  const value = valueParts.join("=");
194
- // Decode URI-encoded values (handles special characters in affiliate codes)
195
170
  try {
196
171
  cookies[name] = decodeURIComponent(value);
197
172
  }
198
173
  catch {
199
- // If decoding fails, use the raw value
200
174
  cookies[name] = value;
201
175
  }
202
176
  }
203
- });
177
+ }
204
178
  return cookies;
205
179
  }
206
180
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/better-auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AA4D3D,gFAAgF;AAChF,wBAAwB;AACxB,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAc,EACd,SAAuB,EACvB,UAAkC,EAAE;IAEpC,MAAM,MAAM,GAAG;QACb,eAAe,EAAE,OAAO,CAAC,UAAU,EAAE,UAAU,IAAI,YAAY;QAC/D,iBAAiB,EAAE,OAAO,CAAC,UAAU,EAAE,YAAY,IAAI,cAAc;QACrE,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,gBAAgB;QAClD,oBAAoB,EAAE,OAAO,CAAC,oBAAoB,IAAI,uBAAuB;QAC7E,oBAAoB,EAAE,OAAO,CAAC,oBAAoB;QAClD,oBAAoB,EAAE,OAAO,CAAC,oBAAoB;KACnD,CAAC;IAEF,OAAO;QACL,EAAE,EAAE,mBAAmB;QAEvB,KAAK,EAAE;YACL,KAAK,EAAE;gBACL;oBACE,kFAAkF;oBAClF,OAAO,EAAE,CAAC,OAA4B,EAAE,EAAE;wBACxC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;wBAChC,OAAO,CACL,IAAI,KAAK,gBAAgB;4BACzB,IAAI,KAAK,mBAAmB;4BAC5B,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;4BAC5B,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAC9B,CAAC;oBACJ,CAAC;oBAED,OAAO,EAAE,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;wBAC/C,MAAM,OAAO,GAAG,QAQf,CAAC;wBAEF,oDAAoD;wBACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC;wBAC3C,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;4BAC9C,OAAO;wBACT,CAAC;wBAED,gCAAgC;wBAChC,MAAM,WAAW,GAAG,QAAmC,CAAC;wBACxD,MAAM,IAAI,GAAG,WAAW,CAAC,IAGZ,CAAC;wBACd,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;4BACd,OAAO;wBACT,CAAC;wBAED,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;wBAEvB,2DAA2D;wBAC3D,wEAAwE;wBACxE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;4BACnB,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;4BACrD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;4BACvB,MAAM,SAAS,GAAG,GAAG,GAAG,SAAS,GAAG,KAAK,CAAC,CAAC,aAAa;4BACxD,IAAI,CAAC,SAAS,EAAE,CAAC;gCACf,uDAAuD;gCACvD,OAAO;4BACT,CAAC;wBACH,CAAC;wBAED,0CAA0C;wBAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;wBACnD,IAAI,UAAU,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,eAAe,CAAuB,CAAC;wBACtE,IAAI,YAAY,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAuB,CAAC;wBAE1E,qBAAqB;wBACrB,MAAM,YAAY,GAChB,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC;4BAChC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;wBAErD,IAAI,YAAY,EAAE,CAAC;4BACjB,MAAM,OAAO,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;4BAC3C,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,oBAAoB,EAAE,CAAC;gCAC/C,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;4BACpD,CAAC;4BACD,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;gCACvC,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;4BAC5C,CAAC;wBACH,CAAC;wBAED,2BAA2B;wBAC3B,IAAI,CAAC,UAAU,IAAI,CAAC,YAAY,EAAE,CAAC;4BACjC,MAAM,MAAM,CAAC,oBAAoB,EAAE,CAAC;gCAClC,MAAM;gCACN,MAAM,EAAE,wBAAwB;6BACjC,CAAC,CAAC;4BACH,OAAO;wBACT,CAAC;wBAED,uBAAuB;wBACvB,IAAI,CAAC;4BACH,IAAI,UAAU,GAAG,KAAK,CAAC;4BACvB,IAAI,aAAiC,CAAC;4BAEtC,2CAA2C;4BAC3C,IAAI,UAAU,EAAE,CAAC;gCACf,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,CACjC,SAAS,CAAC,SAAS,CAAC,eAAe,EACnC,EAAE,UAAU,EAAE,CACf,CAAC;gCAEF,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;oCAC9C,MAAM,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,SAAS,CAAC,eAAe,EAAE;wCACzD,UAAU,EAAE,UAAU;wCACtB,MAAM;qCACP,CAAC,CAAC;oCACH,UAAU,GAAG,IAAI,CAAC;oCAClB,0CAA0C;oCAC1C,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,QAAQ,CAClC,SAAS,CAAC,UAAU,CAAC,OAAO,EAC5B,EAAE,WAAW,EAAE,QAAQ,CAAC,WAAW,EAAE,CACtC,CAAC;oCACF,aAAa,GAAG,SAAS,EAAE,IAAI,CAAC;gCAClC,CAAC;4BACH,CAAC;4BAED,mDAAmD;4BACnD,IAAI,CAAC,UAAU,IAAI,YAAY,EAAE,CAAC;gCAChC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,WAAW,CAClC,SAAS,CAAC,SAAS,CAAC,qBAAqB,EACzC,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,CACxC,CAAC;gCACF,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC;gCAC5B,aAAa,GAAG,YAAY,CAAC;4BAC/B,CAAC;4BAED,IAAI,UAAU,EAAE,CAAC;gCACf,MAAM,MAAM,CAAC,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;4BACjE,CAAC;iCAAM,CAAC;gCACN,MAAM,MAAM,CAAC,oBAAoB,EAAE,CAAC;oCAClC,MAAM;oCACN,MAAM,EAAE,oDAAoD;iCAC7D,CAAC,CAAC;4BACL,CAAC;wBACH,CAAC;wBAAC,OAAO,KAAK,EAAE,CAAC;4BACf,OAAO,CAAC,KAAK,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAC;4BAC/D,MAAM,MAAM,CAAC,oBAAoB,EAAE,CAAC;gCAClC,MAAM;gCACN,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe;6BACjE,CAAC,CAAC;wBACL,CAAC;oBACH,CAAC,CAAC;iBACH;aACF;SACF;KACyB,CAAC;AAC/B,CAAC;AAED,gFAAgF;AAChF,mBAAmB;AACnB,gFAAgF;AAEhF,SAAS,YAAY,CAAC,YAAoB;IACxC,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;QACzC,MAAM,CAAC,IAAI,EAAE,GAAG,UAAU,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACvD,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,4EAA4E;YAC5E,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,uCAAuC;gBACvC,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/better-auth/index.ts"],"names":[],"mappings":"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"}
@@ -34,8 +34,8 @@ export declare const getByReferralId: import("convex/server").RegisteredQuery<"p
34
34
  status: "clicked" | "signed_up" | "converted" | "expired";
35
35
  referralId: string;
36
36
  landingPage: string;
37
- expiresAt: number;
38
37
  clickedAt: number;
38
+ expiresAt: number;
39
39
  } | null>>;
40
40
  /**
41
41
  * Get a referral by the user who was referred.
@@ -60,8 +60,8 @@ export declare const getByUserId: import("convex/server").RegisteredQuery<"publi
60
60
  status: "clicked" | "signed_up" | "converted" | "expired";
61
61
  referralId: string;
62
62
  landingPage: string;
63
- expiresAt: number;
64
63
  clickedAt: number;
64
+ expiresAt: number;
65
65
  } | null>>;
66
66
  /**
67
67
  * Get a referral by Stripe customer ID.
@@ -86,8 +86,8 @@ export declare const getByStripeCustomer: import("convex/server").RegisteredQuer
86
86
  status: "clicked" | "signed_up" | "converted" | "expired";
87
87
  referralId: string;
88
88
  landingPage: string;
89
- expiresAt: number;
90
89
  clickedAt: number;
90
+ expiresAt: number;
91
91
  } | null>>;
92
92
  /**
93
93
  * List referrals for an affiliate.
@@ -114,8 +114,8 @@ export declare const listByAffiliate: import("convex/server").RegisteredQuery<"p
114
114
  status: "clicked" | "signed_up" | "converted" | "expired";
115
115
  referralId: string;
116
116
  landingPage: string;
117
- expiresAt: number;
118
117
  clickedAt: number;
118
+ expiresAt: number;
119
119
  }[]>>;
120
120
  /**
121
121
  * Get the referee discount for a referral.
@@ -202,8 +202,8 @@ declare const _default: import("convex/server").SchemaDefinition<{
202
202
  status: "clicked" | "signed_up" | "converted" | "expired";
203
203
  referralId: string;
204
204
  landingPage: string;
205
- expiresAt: number;
206
205
  clickedAt: number;
206
+ expiresAt: number;
207
207
  }, {
208
208
  affiliateId: import("convex/values").VId<import("convex/values").GenericId<"affiliates">, "required">;
209
209
  referralId: import("convex/values").VString<string, "required">;
@@ -222,7 +222,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
222
222
  signedUpAt: import("convex/values").VFloat64<number | undefined, "optional">;
223
223
  convertedAt: import("convex/values").VFloat64<number | undefined, "optional">;
224
224
  expiresAt: import("convex/values").VFloat64<number, "required">;
225
- }, "required", "affiliateId" | "userId" | "status" | "referralId" | "stripeCustomerId" | "landingPage" | "ipAddress" | "subId" | "expiresAt" | "utmSource" | "utmMedium" | "utmCampaign" | "deviceType" | "country" | "clickedAt" | "signedUpAt" | "convertedAt">, {
225
+ }, "required", "affiliateId" | "userId" | "status" | "referralId" | "stripeCustomerId" | "landingPage" | "ipAddress" | "subId" | "utmSource" | "utmMedium" | "utmCampaign" | "deviceType" | "country" | "clickedAt" | "signedUpAt" | "convertedAt" | "expiresAt">, {
226
226
  by_referralId: ["referralId", "_creationTime"];
227
227
  by_affiliate: ["affiliateId", "_creationTime"];
228
228
  by_affiliate_status: ["affiliateId", "status", "_creationTime"];
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "bugs": {
7
7
  "url": "https://github.com/chiemerieokorie/convex-affiliates/issues"
8
8
  },
9
- "version": "3.0.5",
9
+ "version": "3.0.6",
10
10
  "license": "Apache-2.0",
11
11
  "keywords": [
12
12
  "convex",
@@ -2,8 +2,8 @@
2
2
  * Better Auth Server Plugin for Convex Affiliates
3
3
  *
4
4
  * This plugin automatically handles affiliate attribution when users sign up
5
- * through Better Auth. It hooks into the signup flow and calls the component's
6
- * attributeSignup mutation directly.
5
+ * through Better Auth. It uses databaseHooks to intercept user creation events,
6
+ * which fires for ALL auth methods (email, OAuth, magic-link, etc.).
7
7
  *
8
8
  * @example
9
9
  * ```typescript
@@ -25,8 +25,7 @@
25
25
  * @module
26
26
  */
27
27
 
28
- import type { BetterAuthPlugin, HookEndpointContext } from "better-auth";
29
- import { createAuthMiddleware } from "better-auth/plugins";
28
+ import type { BetterAuthPlugin } from "better-auth";
30
29
  import type { GenericActionCtx, GenericDataModel } from "convex/server";
31
30
  import type { ComponentApi } from "../component/_generated/component.js";
32
31
 
@@ -93,9 +92,10 @@ export interface AffiliatePluginOptions {
93
92
  /**
94
93
  * Better Auth plugin for automatic affiliate attribution.
95
94
  *
96
- * This plugin intercepts signup requests and automatically attributes
97
- * new users to affiliates. It reads referral data from the request body
98
- * (injected by the client plugin) or from cookies.
95
+ * Uses `databaseHooks.user.create.after` to intercept user creation at the
96
+ * database level. This fires for ALL auth methods (email, OAuth, magic-link,
97
+ * passkey, etc.) and receives the actual user record directly — no need
98
+ * for endpoint path matching or response parsing.
99
99
  *
100
100
  * @param ctx - The Convex context (from createAuth function)
101
101
  * @param component - The affiliates component (components.affiliates)
@@ -143,145 +143,90 @@ export function affiliatePlugin(
143
143
  return {
144
144
  id: "convex-affiliates",
145
145
 
146
- hooks: {
147
- after: [
148
- {
149
- // Match signup endpoints and OAuth callback (where social auth users are created)
150
- matcher: (context: HookEndpointContext) => {
151
- const path = context.path ?? "";
152
- return (
153
- path === "/sign-up/email" ||
154
- path === "/sign-up/username" ||
155
- path.startsWith("/sign-up/") ||
156
- path.startsWith("/callback/")
157
- );
158
- },
159
-
160
- handler: createAuthMiddleware(async (ctx_hook) => {
161
- const context = ctx_hook as unknown as {
162
- context: {
163
- returned?: unknown;
164
- body?: Record<string, unknown>;
165
- request?: Request;
166
- };
167
- body?: Record<string, unknown>;
168
- headers?: Headers;
169
- };
170
-
171
- // Check if signup was successful (user was created)
172
- const returned = context.context?.returned;
173
- if (!returned || typeof returned !== "object") {
174
- return;
175
- }
176
-
177
- // Extract user ID from response
178
- const responseObj = returned as Record<string, unknown>;
179
- const user = responseObj.user as {
180
- id?: string;
181
- createdAt?: Date | string | number;
182
- } | undefined;
183
- if (!user?.id) {
184
- return;
185
- }
146
+ init() {
147
+ return {
148
+ options: {
149
+ databaseHooks: {
150
+ user: {
151
+ create: {
152
+ after: async (
153
+ user: { id: string } & Record<string, unknown>,
154
+ endpointCtx: { body?: unknown; headers?: Headers } | null
155
+ ) => {
156
+ // Top-level try-catch: NEVER let attribution crash the auth flow
157
+ try {
158
+ if (!endpointCtx) return;
186
159
 
187
- const userId = user.id;
160
+ const userId = user.id;
188
161
 
189
- // For OAuth sign-ins, check if this is actually a new user
190
- // by comparing createdAt to current time (within 10 seconds = new user)
191
- if (user.createdAt) {
192
- const createdAt = new Date(user.createdAt).getTime();
193
- const now = Date.now();
194
- const isNewUser = now - createdAt < 10000; // 10 seconds
195
- if (!isNewUser) {
196
- // Existing user signing in via OAuth, skip attribution
197
- return;
198
- }
199
- }
162
+ // Extract referral data from body (email signups)
163
+ const body = endpointCtx.body as Record<string, unknown> | undefined;
164
+ let referralId = body?.[config.referralIdField] as string | undefined;
165
+ let referralCode = body?.[config.referralCodeField] as string | undefined;
200
166
 
201
- // Extract referral data from request body
202
- const body = context.body ?? context.context?.body;
203
- let referralId = body?.[config.referralIdField] as string | undefined;
204
- let referralCode = body?.[config.referralCodeField] as string | undefined;
167
+ // Also check cookies (OAuth signups — cookies forwarded by Next.js proxy)
168
+ const cookieHeader = endpointCtx.headers?.get("cookie");
169
+ if (cookieHeader) {
170
+ const cookies = parseCookies(cookieHeader);
171
+ if (!referralId) referralId = cookies[config.referralIdCookieName];
172
+ if (!referralCode) referralCode = cookies[config.cookieName];
173
+ }
205
174
 
206
- // Also check cookies
207
- const cookieHeader =
208
- context.headers?.get?.("cookie") ??
209
- context.context?.request?.headers?.get?.("cookie");
175
+ if (!referralId && !referralCode) {
176
+ try { await config.onAttributionFailure?.({ userId, reason: "No referral data found" }); } catch { /* swallow */ }
177
+ return;
178
+ }
210
179
 
211
- if (cookieHeader) {
212
- const cookies = parseCookies(cookieHeader);
213
- if (!referralId && config.referralIdCookieName) {
214
- referralId = cookies[config.referralIdCookieName];
215
- }
216
- if (!referralCode && config.cookieName) {
217
- referralCode = cookies[config.cookieName];
218
- }
219
- }
180
+ // Attribution logic
181
+ let attributed = false;
182
+ let affiliateCode: string | undefined;
220
183
 
221
- // Skip if no referral data
222
- if (!referralId && !referralCode) {
223
- await config.onAttributionFailure?.({
224
- userId,
225
- reason: "No referral data found",
226
- });
227
- return;
228
- }
184
+ // Try by referral ID first (more accurate)
185
+ if (referralId) {
186
+ const referral = await ctx.runQuery(
187
+ component.referrals.getByReferralId,
188
+ { referralId }
189
+ );
229
190
 
230
- // Attribute the signup
231
- try {
232
- let attributed = false;
233
- let affiliateCode: string | undefined;
191
+ if (referral?.status === "clicked") {
192
+ await ctx.runMutation(component.referrals.attributeSignup, {
193
+ referralId,
194
+ userId,
195
+ });
196
+ attributed = true;
197
+ const affiliate = await ctx.runQuery(
198
+ component.affiliates.getById,
199
+ { affiliateId: referral.affiliateId }
200
+ );
201
+ affiliateCode = affiliate?.code;
202
+ }
203
+ }
234
204
 
235
- // Try by referral ID first (more accurate)
236
- if (referralId) {
237
- const referral = await ctx.runQuery(
238
- component.referrals.getByReferralId,
239
- { referralId }
240
- );
205
+ // Try by affiliate code if referral ID didn't work
206
+ if (!attributed && referralCode) {
207
+ const result = await ctx.runMutation(
208
+ component.referrals.attributeSignupByCode,
209
+ { userId, affiliateCode: referralCode }
210
+ );
211
+ attributed = result.success;
212
+ affiliateCode = referralCode;
213
+ }
241
214
 
242
- if (referral && referral.status === "clicked") {
243
- await ctx.runMutation(component.referrals.attributeSignup, {
244
- referralId: referralId,
245
- userId,
246
- });
247
- attributed = true;
248
- // Look up the affiliate to get their code
249
- const affiliate = await ctx.runQuery(
250
- component.affiliates.getById,
251
- { affiliateId: referral.affiliateId }
252
- );
253
- affiliateCode = affiliate?.code;
254
- }
255
- }
256
-
257
- // Try by affiliate code if referral ID didn't work
258
- if (!attributed && referralCode) {
259
- const result = await ctx.runMutation(
260
- component.referrals.attributeSignupByCode,
261
- { userId, affiliateCode: referralCode }
262
- );
263
- attributed = result.success;
264
- affiliateCode = referralCode;
265
- }
266
-
267
- if (attributed) {
268
- await config.onAttributionSuccess?.({ userId, affiliateCode });
269
- } else {
270
- await config.onAttributionFailure?.({
271
- userId,
272
- reason: "Attribution failed - referral not found or invalid",
273
- });
274
- }
275
- } catch (error) {
276
- console.error("[convex-affiliates] Attribution error:", error);
277
- await config.onAttributionFailure?.({
278
- userId,
279
- reason: error instanceof Error ? error.message : "Unknown error",
280
- });
281
- }
282
- }),
215
+ if (attributed) {
216
+ try { await config.onAttributionSuccess?.({ userId, affiliateCode }); } catch { /* swallow */ }
217
+ } else {
218
+ try { await config.onAttributionFailure?.({ userId, reason: "Attribution failed - referral not found or invalid" }); } catch { /* swallow */ }
219
+ }
220
+ } catch (error) {
221
+ console.error("[convex-affiliates] Attribution error:", error);
222
+ try { await config.onAttributionFailure?.({ userId: user.id, reason: error instanceof Error ? error.message : "Unknown error" }); } catch { /* swallow */ }
223
+ }
224
+ },
225
+ },
226
+ },
227
+ },
283
228
  },
284
- ],
229
+ };
285
230
  },
286
231
  } satisfies BetterAuthPlugin;
287
232
  }
@@ -292,19 +237,17 @@ export function affiliatePlugin(
292
237
 
293
238
  function parseCookies(cookieHeader: string): Record<string, string> {
294
239
  const cookies: Record<string, string> = {};
295
- cookieHeader.split(";").forEach((cookie) => {
240
+ for (const cookie of cookieHeader.split(";")) {
296
241
  const [name, ...valueParts] = cookie.trim().split("=");
297
242
  if (name) {
298
243
  const value = valueParts.join("=");
299
- // Decode URI-encoded values (handles special characters in affiliate codes)
300
244
  try {
301
245
  cookies[name] = decodeURIComponent(value);
302
246
  } catch {
303
- // If decoding fails, use the raw value
304
247
  cookies[name] = value;
305
248
  }
306
249
  }
307
- });
250
+ }
308
251
  return cookies;
309
252
  }
310
253
 
@@ -312,4 +255,4 @@ function parseCookies(cookieHeader: string): Record<string, string> {
312
255
  // Exports
313
256
  // =============================================================================
314
257
 
315
- export type { BetterAuthPlugin };
258
+ export type { BetterAuthPlugin };