@ccatto/react-auth 1.0.0 → 1.2.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.
@@ -9,6 +9,28 @@ interface CattoAuthSocialProvider {
9
9
  clientSecret: string;
10
10
  scope?: string[];
11
11
  }
12
+ /** Phone-number + OTP auth config (Better Auth `phoneNumber` plugin). */
13
+ interface CattoAuthPhoneConfig {
14
+ /** Master switch — when false/undefined the plugin is not registered. */
15
+ enabled?: boolean;
16
+ /**
17
+ * Deliver the OTP code via SMS. The app owns the transport (Telnyx, Twilio, etc.).
18
+ * Throw/reject to surface a send failure to the caller.
19
+ */
20
+ sendOtp: (args: {
21
+ phoneNumber: string;
22
+ code: string;
23
+ }) => Promise<void>;
24
+ /**
25
+ * Domain used to mint a synthetic email for phone-only users on first verify
26
+ * (Better Auth requires an email). Default: 'phone.catto.local'.
27
+ */
28
+ tempEmailDomain?: string;
29
+ /** Require phone verification before the number is trusted. Default: true. */
30
+ requireVerification?: boolean;
31
+ /** OTP lifetime in seconds. Default: Better Auth's default (300). */
32
+ otpExpiresInSeconds?: number;
33
+ }
12
34
  /** Minimal database client interface — compatible with PrismaClient without version coupling */
13
35
  interface CattoAuthDatabaseClient {
14
36
  user: {
@@ -36,7 +58,14 @@ interface CattoAuthServerConfig {
36
58
  google?: CattoAuthSocialProvider;
37
59
  facebook?: CattoAuthSocialProvider;
38
60
  github?: CattoAuthSocialProvider;
61
+ apple?: CattoAuthSocialProvider;
39
62
  };
63
+ /**
64
+ * Phone-number + OTP authentication (Better Auth `phoneNumber` plugin).
65
+ * Optional — only enabled when `enabled` is true and a `sendOtp` transport
66
+ * is provided. The app owns the SMS transport (Telnyx, Twilio, etc.).
67
+ */
68
+ phoneAuth?: CattoAuthPhoneConfig;
40
69
  /** Session configuration */
41
70
  session?: {
42
71
  /** Session expiry in seconds (default: 69 days = 5961600) */
@@ -85,4 +114,4 @@ interface CattoSessionProviderConfig {
85
114
  enrichedSessionEndpoint?: string;
86
115
  }
87
116
 
88
- export type { CattoAuthClientConfig as C, CattoAuthDatabaseClient as a, CattoAuthServerConfig as b, CattoAuthSocialProvider as c, CattoSessionProviderConfig as d };
117
+ export type { CattoAuthClientConfig as C, CattoAuthDatabaseClient as a, CattoAuthPhoneConfig as b, CattoAuthServerConfig as c, CattoAuthSocialProvider as d, CattoSessionProviderConfig as e };
@@ -9,6 +9,28 @@ interface CattoAuthSocialProvider {
9
9
  clientSecret: string;
10
10
  scope?: string[];
11
11
  }
12
+ /** Phone-number + OTP auth config (Better Auth `phoneNumber` plugin). */
13
+ interface CattoAuthPhoneConfig {
14
+ /** Master switch — when false/undefined the plugin is not registered. */
15
+ enabled?: boolean;
16
+ /**
17
+ * Deliver the OTP code via SMS. The app owns the transport (Telnyx, Twilio, etc.).
18
+ * Throw/reject to surface a send failure to the caller.
19
+ */
20
+ sendOtp: (args: {
21
+ phoneNumber: string;
22
+ code: string;
23
+ }) => Promise<void>;
24
+ /**
25
+ * Domain used to mint a synthetic email for phone-only users on first verify
26
+ * (Better Auth requires an email). Default: 'phone.catto.local'.
27
+ */
28
+ tempEmailDomain?: string;
29
+ /** Require phone verification before the number is trusted. Default: true. */
30
+ requireVerification?: boolean;
31
+ /** OTP lifetime in seconds. Default: Better Auth's default (300). */
32
+ otpExpiresInSeconds?: number;
33
+ }
12
34
  /** Minimal database client interface — compatible with PrismaClient without version coupling */
13
35
  interface CattoAuthDatabaseClient {
14
36
  user: {
@@ -36,7 +58,14 @@ interface CattoAuthServerConfig {
36
58
  google?: CattoAuthSocialProvider;
37
59
  facebook?: CattoAuthSocialProvider;
38
60
  github?: CattoAuthSocialProvider;
61
+ apple?: CattoAuthSocialProvider;
39
62
  };
63
+ /**
64
+ * Phone-number + OTP authentication (Better Auth `phoneNumber` plugin).
65
+ * Optional — only enabled when `enabled` is true and a `sendOtp` transport
66
+ * is provided. The app owns the SMS transport (Telnyx, Twilio, etc.).
67
+ */
68
+ phoneAuth?: CattoAuthPhoneConfig;
40
69
  /** Session configuration */
41
70
  session?: {
42
71
  /** Session expiry in seconds (default: 69 days = 5961600) */
@@ -85,4 +114,4 @@ interface CattoSessionProviderConfig {
85
114
  enrichedSessionEndpoint?: string;
86
115
  }
87
116
 
88
- export type { CattoAuthClientConfig as C, CattoAuthDatabaseClient as a, CattoAuthServerConfig as b, CattoAuthSocialProvider as c, CattoSessionProviderConfig as d };
117
+ export type { CattoAuthClientConfig as C, CattoAuthDatabaseClient as a, CattoAuthPhoneConfig as b, CattoAuthServerConfig as c, CattoAuthSocialProvider as d, CattoSessionProviderConfig as e };
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as CattoAuthClientConfig, a as CattoAuthDatabaseClient, b as CattoAuthServerConfig, c as CattoAuthSocialProvider, d as CattoSessionProviderConfig } from './config-CvzbPvtw.cjs';
1
+ export { C as CattoAuthClientConfig, a as CattoAuthDatabaseClient, b as CattoAuthPhoneConfig, c as CattoAuthServerConfig, d as CattoAuthSocialProvider, e as CattoSessionProviderConfig } from './config-MuDc5UVc.cjs';
2
2
 
3
3
  /**
4
4
  * @ccatto/react-auth - Session Types
@@ -13,8 +13,13 @@ interface EnrichedUser {
13
13
  /** Custom fields added by enrichSession hook */
14
14
  playerID?: number;
15
15
  role: string;
16
- organizationId: string | null;
17
- organizations: Array<{
16
+ /**
17
+ * Optional multi-tenant fields. Apps that don't model organizations can omit
18
+ * them entirely; legacy callers that always set them (incl. to null / [])
19
+ * still typecheck.
20
+ */
21
+ organizationId?: string | null;
22
+ organizations?: Array<{
18
23
  id: string;
19
24
  name: string;
20
25
  slug: string;
@@ -42,8 +47,9 @@ interface CompatSessionUser {
42
47
  image: string | null;
43
48
  playerID?: number;
44
49
  role: string;
45
- organizationId: string | null;
46
- organizations: Array<{
50
+ /** See `EnrichedUser.organizationId` optional multi-tenant field. */
51
+ organizationId?: string | null;
52
+ organizations?: Array<{
47
53
  id: string;
48
54
  name: string;
49
55
  slug: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as CattoAuthClientConfig, a as CattoAuthDatabaseClient, b as CattoAuthServerConfig, c as CattoAuthSocialProvider, d as CattoSessionProviderConfig } from './config-CvzbPvtw.js';
1
+ export { C as CattoAuthClientConfig, a as CattoAuthDatabaseClient, b as CattoAuthPhoneConfig, c as CattoAuthServerConfig, d as CattoAuthSocialProvider, e as CattoSessionProviderConfig } from './config-MuDc5UVc.js';
2
2
 
3
3
  /**
4
4
  * @ccatto/react-auth - Session Types
@@ -13,8 +13,13 @@ interface EnrichedUser {
13
13
  /** Custom fields added by enrichSession hook */
14
14
  playerID?: number;
15
15
  role: string;
16
- organizationId: string | null;
17
- organizations: Array<{
16
+ /**
17
+ * Optional multi-tenant fields. Apps that don't model organizations can omit
18
+ * them entirely; legacy callers that always set them (incl. to null / [])
19
+ * still typecheck.
20
+ */
21
+ organizationId?: string | null;
22
+ organizations?: Array<{
18
23
  id: string;
19
24
  name: string;
20
25
  slug: string;
@@ -42,8 +47,9 @@ interface CompatSessionUser {
42
47
  image: string | null;
43
48
  playerID?: number;
44
49
  role: string;
45
- organizationId: string | null;
46
- organizations: Array<{
50
+ /** See `EnrichedUser.organizationId` optional multi-tenant field. */
51
+ organizationId?: string | null;
52
+ organizations?: Array<{
47
53
  id: string;
48
54
  name: string;
49
55
  slug: string;
package/dist/server.cjs CHANGED
@@ -4,6 +4,7 @@ var betterAuth = require('better-auth');
4
4
  var prisma = require('better-auth/adapters/prisma');
5
5
  var api = require('better-auth/api');
6
6
  var nextJs = require('better-auth/next-js');
7
+ var plugins = require('better-auth/plugins');
7
8
 
8
9
  // src/server/create-auth.ts
9
10
  var DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;
@@ -16,6 +17,9 @@ function createCattoAuth(config) {
16
17
  if (config.socialProviders?.github) {
17
18
  socialProviders.github = config.socialProviders.github;
18
19
  }
20
+ if (config.socialProviders?.apple) {
21
+ socialProviders.apple = config.socialProviders.apple;
22
+ }
19
23
  if (config.socialProviders?.facebook) {
20
24
  socialProviders.facebook = {
21
25
  ...config.socialProviders.facebook,
@@ -25,6 +29,27 @@ function createCattoAuth(config) {
25
29
  ]
26
30
  };
27
31
  }
32
+ const plugins$1 = [];
33
+ const phoneCfg = config.phoneAuth;
34
+ if (phoneCfg?.enabled && phoneCfg.sendOtp) {
35
+ const tempEmailDomain = phoneCfg.tempEmailDomain ?? "phone.catto.local";
36
+ plugins$1.push(
37
+ plugins.phoneNumber({
38
+ sendOTP: async ({ phoneNumber: toNumber, code }) => {
39
+ await phoneCfg.sendOtp({ phoneNumber: toNumber, code });
40
+ },
41
+ requireVerification: phoneCfg.requireVerification ?? true,
42
+ ...phoneCfg.otpExpiresInSeconds ? { expiresIn: phoneCfg.otpExpiresInSeconds } : {},
43
+ signUpOnVerification: {
44
+ getTempEmail: (toNumber) => `${toNumber}@${tempEmailDomain}`,
45
+ // Use the phone number as the initial display name; the app can
46
+ // prompt for a real name post-verify.
47
+ getTempName: (toNumber) => toNumber
48
+ }
49
+ })
50
+ );
51
+ }
52
+ plugins$1.push(nextJs.nextCookies());
28
53
  const auth = betterAuth.betterAuth({
29
54
  // Database
30
55
  // Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability
@@ -76,8 +101,8 @@ function createCattoAuth(config) {
76
101
  enabled: false
77
102
  }
78
103
  },
79
- // Plugins
80
- plugins: [nextJs.nextCookies()],
104
+ // Plugins (phoneNumber when configured, then nextCookies last)
105
+ plugins: plugins$1,
81
106
  // Hooks
82
107
  hooks: {
83
108
  // Before hooks: email normalization
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/create-auth.ts"],"names":["betterAuth","prismaAdapter","nextCookies","createAuthMiddleware"],"mappings":";;;;;;;;AAoCA,IAAM,4BAAA,GAA+B,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,EAAA;AAa7C,SAAS,gBACd,MAAA,EACuB;AACvB,EAAA,MAAM,SAAA,GACJ,MAAA,CAAO,OAAA,EAAS,gBAAA,IAAoB,4BAAA;AAGtC,EAAA,MAAM,kBAGF,EAAC;AACL,EAAA,IAAI,MAAA,CAAO,iBAAiB,MAAA,EAAQ;AAClC,IAAA,eAAA,CAAgB,MAAA,GAAS,OAAO,eAAA,CAAgB,MAAA;AAAA,EAClD;AACA,EAAA,IAAI,MAAA,CAAO,iBAAiB,MAAA,EAAQ;AAClC,IAAA,eAAA,CAAgB,MAAA,GAAS,OAAO,eAAA,CAAgB,MAAA;AAAA,EAClD;AACA,EAAA,IAAI,MAAA,CAAO,iBAAiB,QAAA,EAAU;AACpC,IAAA,eAAA,CAAgB,QAAA,GAAW;AAAA,MACzB,GAAG,OAAO,eAAA,CAAgB,QAAA;AAAA,MAC1B,KAAA,EAAO,MAAA,CAAO,eAAA,CAAgB,QAAA,CAAS,KAAA,IAAS;AAAA,QAC9C,OAAA;AAAA,QACA;AAAA;AACF,KACF;AAAA,EACF;AAEA,EAAA,MAAM,OAAOA,qBAAA,CAAW;AAAA;AAAA;AAAA,IAGtB,QAAA,EAAUC,oBAAA;AAAA,MACR,MAAA,CAAO,QAAA;AAAA,MACP;AAAA,QACE,UAAU,MAAA,CAAO;AAAA;AACnB,KACF;AAAA;AAAA,IAGA,QAAQ,MAAA,CAAO,MAAA;AAAA,IACf,SAAS,MAAA,CAAO,OAAA;AAAA;AAAA,IAGhB,gBAAA,EAAkB;AAAA,MAChB,OAAA,EAAS,MAAA,CAAO,gBAAA,EAAkB,OAAA,IAAW,IAAA;AAAA,MAC7C,wBAAA,EACE,MAAA,CAAO,gBAAA,EAAkB,wBAAA,IAA4B;AAAA,KACzD;AAAA;AAAA,IAGA,eAAA;AAAA;AAAA,IAGA,OAAA,EAAS;AAAA,MACP,SAAA;AAAA,MACA,WAAA,EAAa;AAAA,QACX,OAAA,EAAS,IAAA;AAAA,QACT,MAAA,EAAQ;AAAA;AACV,KACF;AAAA;AAAA,IAGA,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB;AAAA,QACd,OAAA,EAAS,IAAA;AAAA,QACT,gBAAA,EAAkB,MAAA,CAAO,IAAA,CAAK,eAAe;AAAA;AAC/C,KACF;AAAA;AAAA,IAGA,IAAA,EAAM;AAAA,MACJ,gBAAA,EAAkB;AAAA,QAChB,IAAA,EAAM;AAAA,UACJ,IAAA,EAAM,QAAA;AAAA,UACN,QAAA,EAAU,KAAA;AAAA,UACV,YAAA,EAAc;AAAA;AAChB;AACF,KACF;AAAA;AAAA,IAGA,QAAA,EAAU;AAAA,MACR,kBACE,MAAA,CAAO,QAAA,EAAU,gBAAA,IACjB,OAAA,CAAQ,IAAI,QAAA,KAAa,YAAA;AAAA,MAC3B,qBAAA,EAAuB;AAAA,QACrB,OAAA,EAAS;AAAA;AACX,KACF;AAAA;AAAA,IAGA,OAAA,EAAS,CAACC,kBAAA,EAAa,CAAA;AAAA;AAAA,IAGvB,KAAA,EAAO;AAAA;AAAA,MAEL,MAAA,EAAQC,wBAAA,CAAqB,OAAO,GAAA,KAAQ;AAC1C,QAAA,IAAI,GAAA,CAAI,SAAS,gBAAA,EAAkB;AACjC,UAAA,MAAM,OAAO,GAAA,CAAI,IAAA;AACjB,UAAA,IAAI,IAAA,EAAM,KAAA,IAAS,OAAO,IAAA,CAAK,UAAU,QAAA,EAAU;AACjD,YAAC,GAAA,CAAI,IAAA,CAA2B,KAAA,GAAQ,IAAA,CAAK,MAAM,WAAA,EAAY;AAAA,UACjE;AAAA,QACF;AAAA,MACF,CAAC,CAAA;AAAA;AAAA,MAGD,KAAA,EAAOA,wBAAA,CAAqB,OAAO,GAAA,KAAQ;AACzC,QAAA,IACE,GAAA,CAAI,KAAK,UAAA,CAAW,UAAU,KAC9B,GAAA,CAAI,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA,EAC9B;AACA,UAAA,MAAM,UAAA,GAAa,IAAI,OAAA,CAAQ,UAAA;AAC/B,UAAA,IAAI,YAAY,IAAA,EAAM;AACpB,YAAA,MAAM,OAAO,UAAA,CAAW,IAAA;AAGxB,YAAA,MAAM,eAAA,GAAkB,IAAA,CAAK,KAAA,EAAO,WAAA,EAAY;AAChD,YAAA,IAAI,eAAA,IAAmB,IAAA,CAAK,KAAA,KAAU,eAAA,EAAiB;AACrD,cAAA,IAAI;AACF,gBAAA,MAAM,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,MAAA,CAAO;AAAA,kBAChC,KAAA,EAAO,EAAE,EAAA,EAAI,IAAA,CAAK,EAAA,EAAG;AAAA,kBACrB,IAAA,EAAM,EAAE,KAAA,EAAO,eAAA;AAAgB,iBAChC,CAAA;AAAA,cACH,SAAS,SAAA,EAAW;AAClB,gBAAA,MAAA,CAAO,MAAA,EAAQ,IAAA;AAAA,kBACb,wCAAA;AAAA,kBACA;AAAA,oBACE,QAAQ,IAAA,CAAK,EAAA;AAAA,oBACb,OACE,SAAA,YAAqB,KAAA,GACjB,SAAA,CAAU,OAAA,GACV,OAAO,SAAS;AAAA;AACxB,iBACF;AAAA,cACF;AAAA,YACF;AAGA,YAAA,IAAI,MAAA,CAAO,OAAO,aAAA,EAAe;AAC/B,cAAA,IAAI;AACF,gBAAA,MAAM,OAAO,KAAA,CAAM,aAAA;AAAA,kBACjB;AAAA,oBACE,IAAI,IAAA,CAAK,EAAA;AAAA,oBACT,KAAA,EAAO,mBAAmB,IAAA,CAAK,KAAA;AAAA,oBAC/B,MAAM,IAAA,CAAK;AAAA,mBACb;AAAA,kBACA,MAAA,CAAO;AAAA,iBACT;AAAA,cACF,SAAS,SAAA,EAAW;AAClB,gBAAA,MAAA,CAAO,MAAA,EAAQ,OAAO,uCAAA,EAAyC;AAAA,kBAC7D,QAAQ,IAAA,CAAK,EAAA;AAAA,kBACb,OACE,SAAA,YAAqB,KAAA,GACjB,SAAA,CAAU,OAAA,GACV,OAAO,SAAS;AAAA,iBACvB,CAAA;AAAA,cACH;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAC;AAAA;AACH,GACD,CAAA;AAGD,EAAA,eAAe,mBAAmB,OAAA,EAAuC;AACvE,IAAA,IAAI;AACF,MAAA,MAAM,cAAc,MAAM,IAAA,CAAK,IAAI,UAAA,CAAW,EAAE,SAAS,CAAA;AACzD,MAAA,IAAI,CAAC,WAAA,EAAa,IAAA,EAAM,OAAO,IAAA;AAE/B,MAAA,MAAM,MAAA,GAAS,YAAY,IAAA,CAAK,EAAA;AAGhC,MAAA,MAAM,UAAA,GAAa,MAAA,CAAO,KAAA,EAAO,aAAA,GAC7B,MAAM,MAAA,CAAO,KAAA,CAAM,aAAA,CAAc,MAAA,EAAQ,MAAA,CAAO,QAAQ,CAAA,GACxD,EAAC;AAEL,MAAA,OAAO;AAAA,QACL,IAAA,EAAM;AAAA,UACJ,EAAA,EAAI,YAAY,IAAA,CAAK,EAAA;AAAA,UACrB,KAAA,EAAO,YAAY,IAAA,CAAK,KAAA;AAAA,UACxB,IAAA,EAAM,YAAY,IAAA,CAAK,IAAA;AAAA,UACvB,KAAA,EAAO,YAAY,IAAA,CAAK,KAAA;AAAA,UACxB,GAAG;AAAA,SACL;AAAA,QACA,SAAS,WAAA,CAAY;AAAA,OACvB;AAAA,IACF,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,MAAM,kBAAA,EAAmB;AACpC","file":"server.cjs","sourcesContent":["/**\n * @ccatto/react-auth/server - createCattoAuth Factory\n *\n * Creates a configured Better Auth instance with sensible defaults,\n * automatic email normalization, and pluggable hooks for\n * session enrichment and user lifecycle events.\n *\n * @example\n * ```typescript\n * import { createCattoAuth } from '@ccatto/react-auth/server';\n * import { prisma } from '@myapp/database';\n *\n * export const { auth, getEnrichedSession } = createCattoAuth({\n * database: prisma,\n * databaseProvider: 'postgresql',\n * secret: process.env.BETTER_AUTH_SECRET!,\n * baseURL: process.env.BETTER_AUTH_URL!,\n * hooks: {\n * enrichSession: async (userId, db) => {\n * // Return custom fields to merge into session.user\n * return { role: 'admin', customField: 'value' };\n * },\n * },\n * });\n * ```\n */\nimport { betterAuth } from 'better-auth';\nimport { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { createAuthMiddleware } from 'better-auth/api';\nimport { nextCookies } from 'better-auth/next-js';\nimport type {\n CattoAuthServerConfig,\n CattoAuthSocialProvider,\n} from '../types/config';\n\n// Default: 69 days (matches common long-lived session configs)\nconst DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;\n\nexport interface CreateCattoAuthResult {\n /** The Better Auth instance (use for API routes) */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth: any;\n /**\n * Get enriched session with custom fields from hooks.enrichSession.\n * Call this instead of auth.api.getSession for full session data.\n */\n getEnrichedSession: (headers: Headers) => Promise<any | null>;\n}\n\nexport function createCattoAuth(\n config: CattoAuthServerConfig,\n): CreateCattoAuthResult {\n const expiresIn =\n config.session?.expiresInSeconds ?? DEFAULT_TOKEN_EXPIRY_SECONDS;\n\n // Build social providers config\n const socialProviders: Record<\n string,\n CattoAuthSocialProvider & { scope?: string[] }\n > = {};\n if (config.socialProviders?.google) {\n socialProviders.google = config.socialProviders.google;\n }\n if (config.socialProviders?.github) {\n socialProviders.github = config.socialProviders.github;\n }\n if (config.socialProviders?.facebook) {\n socialProviders.facebook = {\n ...config.socialProviders.facebook,\n scope: config.socialProviders.facebook.scope ?? [\n 'email',\n 'public_profile',\n ],\n };\n }\n\n const auth = betterAuth({\n // Database\n // Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability\n database: prismaAdapter(\n config.database as Parameters<typeof prismaAdapter>[0],\n {\n provider: config.databaseProvider,\n },\n ),\n\n // Secret & URL\n secret: config.secret,\n baseURL: config.baseURL,\n\n // Email/password\n emailAndPassword: {\n enabled: config.emailAndPassword?.enabled ?? true,\n requireEmailVerification:\n config.emailAndPassword?.requireEmailVerification ?? false,\n },\n\n // Social providers\n socialProviders,\n\n // Session\n session: {\n expiresIn,\n cookieCache: {\n enabled: true,\n maxAge: expiresIn,\n },\n },\n\n // Account linking\n account: {\n accountLinking: {\n enabled: true,\n trustedProviders: Object.keys(socialProviders),\n },\n },\n\n // User fields\n user: {\n additionalFields: {\n role: {\n type: 'string' as const,\n required: false,\n defaultValue: 'user',\n },\n },\n },\n\n // Advanced\n advanced: {\n useSecureCookies:\n config.advanced?.useSecureCookies ??\n process.env.NODE_ENV === 'production',\n crossSubDomainCookies: {\n enabled: false,\n },\n },\n\n // Plugins\n plugins: [nextCookies()],\n\n // Hooks\n hooks: {\n // Before hooks: email normalization\n before: createAuthMiddleware(async (ctx) => {\n if (ctx.path === '/sign-up/email') {\n const body = ctx.body as { email?: string } | undefined;\n if (body?.email && typeof body.email === 'string') {\n (ctx.body as { email: string }).email = body.email.toLowerCase();\n }\n }\n }),\n\n // After hooks: user lifecycle events\n after: createAuthMiddleware(async (ctx) => {\n if (\n ctx.path.startsWith('/sign-in') ||\n ctx.path.startsWith('/sign-up')\n ) {\n const newSession = ctx.context.newSession;\n if (newSession?.user) {\n const user = newSession.user;\n\n // Normalize email in database if needed\n const normalizedEmail = user.email?.toLowerCase();\n if (normalizedEmail && user.email !== normalizedEmail) {\n try {\n await config.database.user.update({\n where: { id: user.id },\n data: { email: normalizedEmail },\n });\n } catch (hookError) {\n config.logger?.warn?.(\n '[CattoAuth] Email normalization failed',\n {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n },\n );\n }\n }\n\n // Call onUserCreated hook\n if (config.hooks?.onUserCreated) {\n try {\n await config.hooks.onUserCreated(\n {\n id: user.id,\n email: normalizedEmail || user.email,\n name: user.name,\n },\n config.database,\n );\n } catch (hookError) {\n config.logger?.warn?.('[CattoAuth] onUserCreated hook failed', {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n });\n }\n }\n }\n }\n }),\n },\n });\n\n // Enriched session factory\n async function getEnrichedSession(headers: Headers): Promise<any | null> {\n try {\n const baseSession = await auth.api.getSession({ headers });\n if (!baseSession?.user) return null;\n\n const userId = baseSession.user.id;\n\n // Call enrichSession hook if provided\n const enrichment = config.hooks?.enrichSession\n ? await config.hooks.enrichSession(userId, config.database)\n : {};\n\n return {\n user: {\n id: baseSession.user.id,\n email: baseSession.user.email,\n name: baseSession.user.name,\n image: baseSession.user.image,\n ...enrichment,\n },\n session: baseSession.session,\n };\n } catch {\n return null;\n }\n }\n\n return { auth, getEnrichedSession };\n}\n"]}
1
+ {"version":3,"sources":["../src/server/create-auth.ts"],"names":["plugins","phoneNumber","nextCookies","betterAuth","prismaAdapter","createAuthMiddleware"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\n * @ccatto/react-auth/server - createCattoAuth Factory\n *\n * Creates a configured Better Auth instance with sensible defaults,\n * automatic email normalization, and pluggable hooks for\n * session enrichment and user lifecycle events.\n *\n * @example\n * ```typescript\n * import { createCattoAuth } from '@ccatto/react-auth/server';\n * import { prisma } from '@myapp/database';\n *\n * export const { auth, getEnrichedSession } = createCattoAuth({\n * database: prisma,\n * databaseProvider: 'postgresql',\n * secret: process.env.BETTER_AUTH_SECRET!,\n * baseURL: process.env.BETTER_AUTH_URL!,\n * hooks: {\n * enrichSession: async (userId, db) => {\n * // Return custom fields to merge into session.user\n * return { role: 'admin', customField: 'value' };\n * },\n * },\n * });\n * ```\n */\nimport { betterAuth } from 'better-auth';\nimport { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { createAuthMiddleware } from 'better-auth/api';\nimport { nextCookies } from 'better-auth/next-js';\nimport { phoneNumber } from 'better-auth/plugins';\nimport type {\n CattoAuthServerConfig,\n CattoAuthSocialProvider,\n} from '../types/config';\n\n// Default: 69 days (matches common long-lived session configs)\nconst DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;\n\nexport interface CreateCattoAuthResult {\n /** The Better Auth instance (use for API routes) */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth: any;\n /**\n * Get enriched session with custom fields from hooks.enrichSession.\n * Call this instead of auth.api.getSession for full session data.\n */\n getEnrichedSession: (headers: Headers) => Promise<any | null>;\n}\n\nexport function createCattoAuth(\n config: CattoAuthServerConfig,\n): CreateCattoAuthResult {\n const expiresIn =\n config.session?.expiresInSeconds ?? DEFAULT_TOKEN_EXPIRY_SECONDS;\n\n // Build social providers config\n const socialProviders: Record<\n string,\n CattoAuthSocialProvider & { scope?: string[] }\n > = {};\n if (config.socialProviders?.google) {\n socialProviders.google = config.socialProviders.google;\n }\n if (config.socialProviders?.github) {\n socialProviders.github = config.socialProviders.github;\n }\n if (config.socialProviders?.apple) {\n socialProviders.apple = config.socialProviders.apple;\n }\n if (config.socialProviders?.facebook) {\n socialProviders.facebook = {\n ...config.socialProviders.facebook,\n scope: config.socialProviders.facebook.scope ?? [\n 'email',\n 'public_profile',\n ],\n };\n }\n\n // Build plugins list. nextCookies() must stay last so Set-Cookie headers\n // from any preceding plugin/route are forwarded in Next.js server actions.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const plugins: any[] = [];\n const phoneCfg = config.phoneAuth;\n if (phoneCfg?.enabled && phoneCfg.sendOtp) {\n const tempEmailDomain = phoneCfg.tempEmailDomain ?? 'phone.catto.local';\n plugins.push(\n phoneNumber({\n sendOTP: async ({ phoneNumber: toNumber, code }) => {\n await phoneCfg.sendOtp({ phoneNumber: toNumber, code });\n },\n requireVerification: phoneCfg.requireVerification ?? true,\n ...(phoneCfg.otpExpiresInSeconds\n ? { expiresIn: phoneCfg.otpExpiresInSeconds }\n : {}),\n signUpOnVerification: {\n getTempEmail: (toNumber: string) => `${toNumber}@${tempEmailDomain}`,\n // Use the phone number as the initial display name; the app can\n // prompt for a real name post-verify.\n getTempName: (toNumber: string) => toNumber,\n },\n }),\n );\n }\n plugins.push(nextCookies());\n\n const auth = betterAuth({\n // Database\n // Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability\n database: prismaAdapter(\n config.database as Parameters<typeof prismaAdapter>[0],\n {\n provider: config.databaseProvider,\n },\n ),\n\n // Secret & URL\n secret: config.secret,\n baseURL: config.baseURL,\n\n // Email/password\n emailAndPassword: {\n enabled: config.emailAndPassword?.enabled ?? true,\n requireEmailVerification:\n config.emailAndPassword?.requireEmailVerification ?? false,\n },\n\n // Social providers\n socialProviders,\n\n // Session\n session: {\n expiresIn,\n cookieCache: {\n enabled: true,\n maxAge: expiresIn,\n },\n },\n\n // Account linking\n account: {\n accountLinking: {\n enabled: true,\n trustedProviders: Object.keys(socialProviders),\n },\n },\n\n // User fields\n user: {\n additionalFields: {\n role: {\n type: 'string' as const,\n required: false,\n defaultValue: 'user',\n },\n },\n },\n\n // Advanced\n advanced: {\n useSecureCookies:\n config.advanced?.useSecureCookies ??\n process.env.NODE_ENV === 'production',\n crossSubDomainCookies: {\n enabled: false,\n },\n },\n\n // Plugins (phoneNumber when configured, then nextCookies last)\n plugins,\n\n // Hooks\n hooks: {\n // Before hooks: email normalization\n before: createAuthMiddleware(async (ctx) => {\n if (ctx.path === '/sign-up/email') {\n const body = ctx.body as { email?: string } | undefined;\n if (body?.email && typeof body.email === 'string') {\n (ctx.body as { email: string }).email = body.email.toLowerCase();\n }\n }\n }),\n\n // After hooks: user lifecycle events\n after: createAuthMiddleware(async (ctx) => {\n if (\n ctx.path.startsWith('/sign-in') ||\n ctx.path.startsWith('/sign-up')\n ) {\n const newSession = ctx.context.newSession;\n if (newSession?.user) {\n const user = newSession.user;\n\n // Normalize email in database if needed\n const normalizedEmail = user.email?.toLowerCase();\n if (normalizedEmail && user.email !== normalizedEmail) {\n try {\n await config.database.user.update({\n where: { id: user.id },\n data: { email: normalizedEmail },\n });\n } catch (hookError) {\n config.logger?.warn?.(\n '[CattoAuth] Email normalization failed',\n {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n },\n );\n }\n }\n\n // Call onUserCreated hook\n if (config.hooks?.onUserCreated) {\n try {\n await config.hooks.onUserCreated(\n {\n id: user.id,\n email: normalizedEmail || user.email,\n name: user.name,\n },\n config.database,\n );\n } catch (hookError) {\n config.logger?.warn?.('[CattoAuth] onUserCreated hook failed', {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n });\n }\n }\n }\n }\n }),\n },\n });\n\n // Enriched session factory\n async function getEnrichedSession(headers: Headers): Promise<any | null> {\n try {\n const baseSession = await auth.api.getSession({ headers });\n if (!baseSession?.user) return null;\n\n const userId = baseSession.user.id;\n\n // Call enrichSession hook if provided\n const enrichment = config.hooks?.enrichSession\n ? await config.hooks.enrichSession(userId, config.database)\n : {};\n\n return {\n user: {\n id: baseSession.user.id,\n email: baseSession.user.email,\n name: baseSession.user.name,\n image: baseSession.user.image,\n ...enrichment,\n },\n session: baseSession.session,\n };\n } catch {\n return null;\n }\n }\n\n return { auth, getEnrichedSession };\n}\n"]}
package/dist/server.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { b as CattoAuthServerConfig } from './config-CvzbPvtw.cjs';
2
- export { c as CattoAuthSocialProvider } from './config-CvzbPvtw.cjs';
1
+ import { c as CattoAuthServerConfig } from './config-MuDc5UVc.cjs';
2
+ export { b as CattoAuthPhoneConfig, d as CattoAuthSocialProvider } from './config-MuDc5UVc.cjs';
3
3
 
4
4
  interface CreateCattoAuthResult {
5
5
  /** The Better Auth instance (use for API routes) */
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { b as CattoAuthServerConfig } from './config-CvzbPvtw.js';
2
- export { c as CattoAuthSocialProvider } from './config-CvzbPvtw.js';
1
+ import { c as CattoAuthServerConfig } from './config-MuDc5UVc.js';
2
+ export { b as CattoAuthPhoneConfig, d as CattoAuthSocialProvider } from './config-MuDc5UVc.js';
3
3
 
4
4
  interface CreateCattoAuthResult {
5
5
  /** The Better Auth instance (use for API routes) */
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@ import { betterAuth } from 'better-auth';
2
2
  import { prismaAdapter } from 'better-auth/adapters/prisma';
3
3
  import { createAuthMiddleware } from 'better-auth/api';
4
4
  import { nextCookies } from 'better-auth/next-js';
5
+ import { phoneNumber } from 'better-auth/plugins';
5
6
 
6
7
  // src/server/create-auth.ts
7
8
  var DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;
@@ -14,6 +15,9 @@ function createCattoAuth(config) {
14
15
  if (config.socialProviders?.github) {
15
16
  socialProviders.github = config.socialProviders.github;
16
17
  }
18
+ if (config.socialProviders?.apple) {
19
+ socialProviders.apple = config.socialProviders.apple;
20
+ }
17
21
  if (config.socialProviders?.facebook) {
18
22
  socialProviders.facebook = {
19
23
  ...config.socialProviders.facebook,
@@ -23,6 +27,27 @@ function createCattoAuth(config) {
23
27
  ]
24
28
  };
25
29
  }
30
+ const plugins = [];
31
+ const phoneCfg = config.phoneAuth;
32
+ if (phoneCfg?.enabled && phoneCfg.sendOtp) {
33
+ const tempEmailDomain = phoneCfg.tempEmailDomain ?? "phone.catto.local";
34
+ plugins.push(
35
+ phoneNumber({
36
+ sendOTP: async ({ phoneNumber: toNumber, code }) => {
37
+ await phoneCfg.sendOtp({ phoneNumber: toNumber, code });
38
+ },
39
+ requireVerification: phoneCfg.requireVerification ?? true,
40
+ ...phoneCfg.otpExpiresInSeconds ? { expiresIn: phoneCfg.otpExpiresInSeconds } : {},
41
+ signUpOnVerification: {
42
+ getTempEmail: (toNumber) => `${toNumber}@${tempEmailDomain}`,
43
+ // Use the phone number as the initial display name; the app can
44
+ // prompt for a real name post-verify.
45
+ getTempName: (toNumber) => toNumber
46
+ }
47
+ })
48
+ );
49
+ }
50
+ plugins.push(nextCookies());
26
51
  const auth = betterAuth({
27
52
  // Database
28
53
  // Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability
@@ -74,8 +99,8 @@ function createCattoAuth(config) {
74
99
  enabled: false
75
100
  }
76
101
  },
77
- // Plugins
78
- plugins: [nextCookies()],
102
+ // Plugins (phoneNumber when configured, then nextCookies last)
103
+ plugins,
79
104
  // Hooks
80
105
  hooks: {
81
106
  // Before hooks: email normalization
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/create-auth.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\n * @ccatto/react-auth/server - createCattoAuth Factory\n *\n * Creates a configured Better Auth instance with sensible defaults,\n * automatic email normalization, and pluggable hooks for\n * session enrichment and user lifecycle events.\n *\n * @example\n * ```typescript\n * import { createCattoAuth } from '@ccatto/react-auth/server';\n * import { prisma } from '@myapp/database';\n *\n * export const { auth, getEnrichedSession } = createCattoAuth({\n * database: prisma,\n * databaseProvider: 'postgresql',\n * secret: process.env.BETTER_AUTH_SECRET!,\n * baseURL: process.env.BETTER_AUTH_URL!,\n * hooks: {\n * enrichSession: async (userId, db) => {\n * // Return custom fields to merge into session.user\n * return { role: 'admin', customField: 'value' };\n * },\n * },\n * });\n * ```\n */\nimport { betterAuth } from 'better-auth';\nimport { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { createAuthMiddleware } from 'better-auth/api';\nimport { nextCookies } from 'better-auth/next-js';\nimport type {\n CattoAuthServerConfig,\n CattoAuthSocialProvider,\n} from '../types/config';\n\n// Default: 69 days (matches common long-lived session configs)\nconst DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;\n\nexport interface CreateCattoAuthResult {\n /** The Better Auth instance (use for API routes) */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth: any;\n /**\n * Get enriched session with custom fields from hooks.enrichSession.\n * Call this instead of auth.api.getSession for full session data.\n */\n getEnrichedSession: (headers: Headers) => Promise<any | null>;\n}\n\nexport function createCattoAuth(\n config: CattoAuthServerConfig,\n): CreateCattoAuthResult {\n const expiresIn =\n config.session?.expiresInSeconds ?? DEFAULT_TOKEN_EXPIRY_SECONDS;\n\n // Build social providers config\n const socialProviders: Record<\n string,\n CattoAuthSocialProvider & { scope?: string[] }\n > = {};\n if (config.socialProviders?.google) {\n socialProviders.google = config.socialProviders.google;\n }\n if (config.socialProviders?.github) {\n socialProviders.github = config.socialProviders.github;\n }\n if (config.socialProviders?.facebook) {\n socialProviders.facebook = {\n ...config.socialProviders.facebook,\n scope: config.socialProviders.facebook.scope ?? [\n 'email',\n 'public_profile',\n ],\n };\n }\n\n const auth = betterAuth({\n // Database\n // Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability\n database: prismaAdapter(\n config.database as Parameters<typeof prismaAdapter>[0],\n {\n provider: config.databaseProvider,\n },\n ),\n\n // Secret & URL\n secret: config.secret,\n baseURL: config.baseURL,\n\n // Email/password\n emailAndPassword: {\n enabled: config.emailAndPassword?.enabled ?? true,\n requireEmailVerification:\n config.emailAndPassword?.requireEmailVerification ?? false,\n },\n\n // Social providers\n socialProviders,\n\n // Session\n session: {\n expiresIn,\n cookieCache: {\n enabled: true,\n maxAge: expiresIn,\n },\n },\n\n // Account linking\n account: {\n accountLinking: {\n enabled: true,\n trustedProviders: Object.keys(socialProviders),\n },\n },\n\n // User fields\n user: {\n additionalFields: {\n role: {\n type: 'string' as const,\n required: false,\n defaultValue: 'user',\n },\n },\n },\n\n // Advanced\n advanced: {\n useSecureCookies:\n config.advanced?.useSecureCookies ??\n process.env.NODE_ENV === 'production',\n crossSubDomainCookies: {\n enabled: false,\n },\n },\n\n // Plugins\n plugins: [nextCookies()],\n\n // Hooks\n hooks: {\n // Before hooks: email normalization\n before: createAuthMiddleware(async (ctx) => {\n if (ctx.path === '/sign-up/email') {\n const body = ctx.body as { email?: string } | undefined;\n if (body?.email && typeof body.email === 'string') {\n (ctx.body as { email: string }).email = body.email.toLowerCase();\n }\n }\n }),\n\n // After hooks: user lifecycle events\n after: createAuthMiddleware(async (ctx) => {\n if (\n ctx.path.startsWith('/sign-in') ||\n ctx.path.startsWith('/sign-up')\n ) {\n const newSession = ctx.context.newSession;\n if (newSession?.user) {\n const user = newSession.user;\n\n // Normalize email in database if needed\n const normalizedEmail = user.email?.toLowerCase();\n if (normalizedEmail && user.email !== normalizedEmail) {\n try {\n await config.database.user.update({\n where: { id: user.id },\n data: { email: normalizedEmail },\n });\n } catch (hookError) {\n config.logger?.warn?.(\n '[CattoAuth] Email normalization failed',\n {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n },\n );\n }\n }\n\n // Call onUserCreated hook\n if (config.hooks?.onUserCreated) {\n try {\n await config.hooks.onUserCreated(\n {\n id: user.id,\n email: normalizedEmail || user.email,\n name: user.name,\n },\n config.database,\n );\n } catch (hookError) {\n config.logger?.warn?.('[CattoAuth] onUserCreated hook failed', {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n });\n }\n }\n }\n }\n }),\n },\n });\n\n // Enriched session factory\n async function getEnrichedSession(headers: Headers): Promise<any | null> {\n try {\n const baseSession = await auth.api.getSession({ headers });\n if (!baseSession?.user) return null;\n\n const userId = baseSession.user.id;\n\n // Call enrichSession hook if provided\n const enrichment = config.hooks?.enrichSession\n ? await config.hooks.enrichSession(userId, config.database)\n : {};\n\n return {\n user: {\n id: baseSession.user.id,\n email: baseSession.user.email,\n name: baseSession.user.name,\n image: baseSession.user.image,\n ...enrichment,\n },\n session: baseSession.session,\n };\n } catch {\n return null;\n }\n }\n\n return { auth, getEnrichedSession };\n}\n"]}
1
+ {"version":3,"sources":["../src/server/create-auth.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\n * @ccatto/react-auth/server - createCattoAuth Factory\n *\n * Creates a configured Better Auth instance with sensible defaults,\n * automatic email normalization, and pluggable hooks for\n * session enrichment and user lifecycle events.\n *\n * @example\n * ```typescript\n * import { createCattoAuth } from '@ccatto/react-auth/server';\n * import { prisma } from '@myapp/database';\n *\n * export const { auth, getEnrichedSession } = createCattoAuth({\n * database: prisma,\n * databaseProvider: 'postgresql',\n * secret: process.env.BETTER_AUTH_SECRET!,\n * baseURL: process.env.BETTER_AUTH_URL!,\n * hooks: {\n * enrichSession: async (userId, db) => {\n * // Return custom fields to merge into session.user\n * return { role: 'admin', customField: 'value' };\n * },\n * },\n * });\n * ```\n */\nimport { betterAuth } from 'better-auth';\nimport { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { createAuthMiddleware } from 'better-auth/api';\nimport { nextCookies } from 'better-auth/next-js';\nimport { phoneNumber } from 'better-auth/plugins';\nimport type {\n CattoAuthServerConfig,\n CattoAuthSocialProvider,\n} from '../types/config';\n\n// Default: 69 days (matches common long-lived session configs)\nconst DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;\n\nexport interface CreateCattoAuthResult {\n /** The Better Auth instance (use for API routes) */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth: any;\n /**\n * Get enriched session with custom fields from hooks.enrichSession.\n * Call this instead of auth.api.getSession for full session data.\n */\n getEnrichedSession: (headers: Headers) => Promise<any | null>;\n}\n\nexport function createCattoAuth(\n config: CattoAuthServerConfig,\n): CreateCattoAuthResult {\n const expiresIn =\n config.session?.expiresInSeconds ?? DEFAULT_TOKEN_EXPIRY_SECONDS;\n\n // Build social providers config\n const socialProviders: Record<\n string,\n CattoAuthSocialProvider & { scope?: string[] }\n > = {};\n if (config.socialProviders?.google) {\n socialProviders.google = config.socialProviders.google;\n }\n if (config.socialProviders?.github) {\n socialProviders.github = config.socialProviders.github;\n }\n if (config.socialProviders?.apple) {\n socialProviders.apple = config.socialProviders.apple;\n }\n if (config.socialProviders?.facebook) {\n socialProviders.facebook = {\n ...config.socialProviders.facebook,\n scope: config.socialProviders.facebook.scope ?? [\n 'email',\n 'public_profile',\n ],\n };\n }\n\n // Build plugins list. nextCookies() must stay last so Set-Cookie headers\n // from any preceding plugin/route are forwarded in Next.js server actions.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const plugins: any[] = [];\n const phoneCfg = config.phoneAuth;\n if (phoneCfg?.enabled && phoneCfg.sendOtp) {\n const tempEmailDomain = phoneCfg.tempEmailDomain ?? 'phone.catto.local';\n plugins.push(\n phoneNumber({\n sendOTP: async ({ phoneNumber: toNumber, code }) => {\n await phoneCfg.sendOtp({ phoneNumber: toNumber, code });\n },\n requireVerification: phoneCfg.requireVerification ?? true,\n ...(phoneCfg.otpExpiresInSeconds\n ? { expiresIn: phoneCfg.otpExpiresInSeconds }\n : {}),\n signUpOnVerification: {\n getTempEmail: (toNumber: string) => `${toNumber}@${tempEmailDomain}`,\n // Use the phone number as the initial display name; the app can\n // prompt for a real name post-verify.\n getTempName: (toNumber: string) => toNumber,\n },\n }),\n );\n }\n plugins.push(nextCookies());\n\n const auth = betterAuth({\n // Database\n // Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability\n database: prismaAdapter(\n config.database as Parameters<typeof prismaAdapter>[0],\n {\n provider: config.databaseProvider,\n },\n ),\n\n // Secret & URL\n secret: config.secret,\n baseURL: config.baseURL,\n\n // Email/password\n emailAndPassword: {\n enabled: config.emailAndPassword?.enabled ?? true,\n requireEmailVerification:\n config.emailAndPassword?.requireEmailVerification ?? false,\n },\n\n // Social providers\n socialProviders,\n\n // Session\n session: {\n expiresIn,\n cookieCache: {\n enabled: true,\n maxAge: expiresIn,\n },\n },\n\n // Account linking\n account: {\n accountLinking: {\n enabled: true,\n trustedProviders: Object.keys(socialProviders),\n },\n },\n\n // User fields\n user: {\n additionalFields: {\n role: {\n type: 'string' as const,\n required: false,\n defaultValue: 'user',\n },\n },\n },\n\n // Advanced\n advanced: {\n useSecureCookies:\n config.advanced?.useSecureCookies ??\n process.env.NODE_ENV === 'production',\n crossSubDomainCookies: {\n enabled: false,\n },\n },\n\n // Plugins (phoneNumber when configured, then nextCookies last)\n plugins,\n\n // Hooks\n hooks: {\n // Before hooks: email normalization\n before: createAuthMiddleware(async (ctx) => {\n if (ctx.path === '/sign-up/email') {\n const body = ctx.body as { email?: string } | undefined;\n if (body?.email && typeof body.email === 'string') {\n (ctx.body as { email: string }).email = body.email.toLowerCase();\n }\n }\n }),\n\n // After hooks: user lifecycle events\n after: createAuthMiddleware(async (ctx) => {\n if (\n ctx.path.startsWith('/sign-in') ||\n ctx.path.startsWith('/sign-up')\n ) {\n const newSession = ctx.context.newSession;\n if (newSession?.user) {\n const user = newSession.user;\n\n // Normalize email in database if needed\n const normalizedEmail = user.email?.toLowerCase();\n if (normalizedEmail && user.email !== normalizedEmail) {\n try {\n await config.database.user.update({\n where: { id: user.id },\n data: { email: normalizedEmail },\n });\n } catch (hookError) {\n config.logger?.warn?.(\n '[CattoAuth] Email normalization failed',\n {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n },\n );\n }\n }\n\n // Call onUserCreated hook\n if (config.hooks?.onUserCreated) {\n try {\n await config.hooks.onUserCreated(\n {\n id: user.id,\n email: normalizedEmail || user.email,\n name: user.name,\n },\n config.database,\n );\n } catch (hookError) {\n config.logger?.warn?.('[CattoAuth] onUserCreated hook failed', {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n });\n }\n }\n }\n }\n }),\n },\n });\n\n // Enriched session factory\n async function getEnrichedSession(headers: Headers): Promise<any | null> {\n try {\n const baseSession = await auth.api.getSession({ headers });\n if (!baseSession?.user) return null;\n\n const userId = baseSession.user.id;\n\n // Call enrichSession hook if provided\n const enrichment = config.hooks?.enrichSession\n ? await config.hooks.enrichSession(userId, config.database)\n : {};\n\n return {\n user: {\n id: baseSession.user.id,\n email: baseSession.user.email,\n name: baseSession.user.name,\n image: baseSession.user.image,\n ...enrichment,\n },\n session: baseSession.session,\n };\n } catch {\n return null;\n }\n }\n\n return { auth, getEnrichedSession };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccatto/react-auth",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Catto Auth - React/Next.js authentication with Better Auth, JWT, and mobile support",
5
5
  "license": "MIT",
6
6
  "author": "Chris Catto",