@cauth/core 0.1.2 → 0.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.
package/README.md CHANGED
@@ -1,18 +1,31 @@
1
1
  # @cauth/core
2
2
 
3
- Core authentication library for Node.js applications with TypeScript support.
3
+ [![NPM Version](https://img.shields.io/npm/v/@cauth/core.svg)](https://www.npmjs.com/package/@cauth/core)
4
+ [![License](https://img.shields.io/npm/l/@cauth/core.svg)](https://github.com/jonace-mpelule/cauth/blob/main/LICENSE)
4
5
 
5
- ## Features
6
+ **CAuth Core** is a robust, type-safe authentication library for Node.js, built with TypeScript and Zod. It provides a modular foundation for building secure authentication systems with pluggable database and route handlers.
6
7
 
7
- - **Type-Safe Authentication**: Built with TypeScript and Zod validation
8
- - **JWT-Based Authentication**: Access and refresh token management
9
- - **Role-Based Access Control**: Flexible role management system
10
- - **Multi-Factor Authentication**: OTP-based two-factor authentication
11
- - **Phone Number Support**: E.164 format validation using libphonenumber-js
12
- - **Error Handling**: Comprehensive error types and handling
13
- - **Modular Design**: Pluggable database and route contractors
8
+ > [!IMPORTANT]
9
+ > For more information and full documentation, visit **[cauth.dev](https://cauth.dev)**.
14
10
 
15
- ## Installation
11
+ ---
12
+
13
+ ## ✨ Features
14
+
15
+ - **🛡️ Type-Safe**: Comprehensive TypeScript support with Zod schema validation.
16
+ - **🔑 JWT-Based**: Industry-standard access and refresh token management.
17
+ - **🎭 Role-Based Access Control (RBAC)**: Flexible, type-safe role management.
18
+ - **📱 Multi-Factor Authentication**: Secure OTP generation for 2FA, password resets, and more.
19
+ - **📞 Phone & Email Support**: E.164 phone validation and email support out of the box.
20
+ - **🔒 Secure by Design**:
21
+ - **Argon2id**: State-of-the-art password hashing.
22
+ - **Hashed Refresh Tokens**: Protection against database leaks.
23
+ - **CSPRNG OTPs**: Cryptographically secure numeric codes.
24
+ - **🧩 Modular Architecture**: Decoupled core logic from database (Prisma) and framework (Express).
25
+
26
+ ---
27
+
28
+ ## 🚀 Installation
16
29
 
17
30
  ```bash
18
31
  npm install @cauth/core
@@ -22,120 +35,120 @@ yarn add @cauth/core
22
35
  pnpm add @cauth/core
23
36
  ```
24
37
 
25
- ## Quick Start
38
+ ---
39
+
40
+ ## 🏁 Quick Start
41
+
42
+ Initialize CAuth by providing your database and route contractors, along with configuration for JWTs and roles.
26
43
 
27
44
  ```typescript
28
45
  import { CAuth } from '@cauth/core';
29
- import { PrismaProvider } from '@cauth/prisma';
46
+ import { PrismaContractor } from '@cauth/prisma';
30
47
  import { ExpressContractor } from '@cauth/express';
48
+ import { prisma } from './db';
31
49
 
32
- // Initialize authentication system
33
50
  const auth = CAuth({
34
- roles: ['USER', 'ADMIN'],
35
- dbContractor: new PrismaProvider(prismaClient),
51
+ // Define your application roles
52
+ roles: ['USER', 'ADMIN', 'EDITOR'] as const,
53
+
54
+ // Pluggable contractors
55
+ dbContractor: new PrismaContractor(prisma),
36
56
  routeContractor: new ExpressContractor(),
57
+
37
58
  jwtConfig: {
38
59
  accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
39
60
  refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,
40
- accessTokenLifeSpan: '15m',
61
+ accessTokenLifeSpan: '15m', // ms, string (ms format), or number
41
62
  refreshTokenLifeSpan: '7d',
42
63
  },
64
+
43
65
  otpConfig: {
44
- expiresIn: 300000, // 5 minutes
45
- length: 6, // 6-digit OTP codes
66
+ expiresIn: 300000, // 5 minutes in ms
67
+ length: 6, // 6-digit codes
46
68
  },
47
69
  });
48
70
 
49
- // Use authentication functions
71
+ export default auth;
72
+ ```
73
+
74
+ ### Basic Login Example
75
+
76
+ ```typescript
50
77
  const result = await auth.FN.Login({
51
- email: 'user@example.com',
52
- //or phoneNumber: '+2659900000'
53
- password: 'securepassword123',
78
+ email: 'dev@example.com',
79
+ password: 'SecurePassword123!',
54
80
  });
55
81
 
56
82
  if (result.success) {
57
- console.log('Login successful:', result.value);
83
+ console.log('Tokens:', result.value); // { accessToken, refreshToken, user }
58
84
  } else {
59
- console.log('Login failed:', result.errors); // FNErrors[]
85
+ console.error('Errors:', result.errors); // Array of FNError objects
60
86
  }
61
87
  ```
62
88
 
63
- ## Core Components
64
-
65
- ### Authentication Functions
89
+ ---
66
90
 
67
- The `FN` namespace provides these authentication functions:
91
+ ## 📖 Core Concepts
68
92
 
69
- ```typescript
70
- auth.FN.Login({ email?: string, phoneNumber?: string, password: string })
71
- auth.FN.Register({ email?: string, phoneNumber?: string, password: string, role: string })
72
- auth.FN.Logout({ refreshToken: string })
73
- auth.FN.Refresh({ refreshToken: string })
74
- auth.FN.ChangePassword({ oldPassword: string, newPassword: string })
75
- auth.FN.RequestOTPCode({ email?: string, phoneNumber?: string, otpPurpose: OtpPurpose })
76
- auth.FN.LoginWithOTP({ email?: string, phoneNumber?: string, code: string })
77
- auth.FN.VerifyOTP({ id: string, code: string, otpPurpose: OtpPurpose })
78
- ```
93
+ ### 1. Functional Namespace (`FN`)
94
+ The `FN` namespace contains the core business logic functions. These are framework-agnostic and can be used in CLI tools, background jobs, or custom route handlers.
79
95
 
80
- ### Token Management
96
+ - `auth.FN.Register(data)`: Create new accounts.
97
+ - `auth.FN.Login(credentials)`: Authenticate and get tokens.
98
+ - `auth.FN.Logout({ refreshToken })`: Revoke a session.
99
+ - `auth.FN.Refresh({ refreshToken })`: Get a new access token.
100
+ - `auth.FN.ChangePassword(data)`: Update password with old password verification.
101
+ - `auth.FN.RequestOTPCode(data)`: Generate and send (via callback) an OTP.
102
+ - `auth.FN.LoginWithOTP(data)`: Passwordless login via code.
81
103
 
82
- The `Tokens` namespace provides these utilities:
104
+ ### 2. Routes Namespace (`Routes`)
105
+ The `Routes` namespace provides pre-built handlers for your chosen framework (e.g., Express). These wrap the `FN` logic and handle HTTP plumbing (status codes, body parsing).
83
106
 
84
107
  ```typescript
85
- auth.Tokens.GenerateAccessToken(payload: any): Promise<string>
86
- auth.Tokens.GenerateRefreshToken(payload: any): Promise<string>
87
- auth.Tokens.GenerateTokenPairs(payload: any): Promise<{ accessToken: string, refreshToken: string }>
88
- auth.Tokens.VerifyAccessToken<T>(token: string): Promise<T | null>
89
- auth.Tokens.VerifyRefreshToken<T>(token: string): Promise<T | null>
108
+ // Express example
109
+ app.post('/auth/register', auth.Routes.Register());
110
+ app.post('/auth/login', auth.Routes.Login());
90
111
  ```
91
112
 
92
- ## Configuration
93
-
94
- The `CAuthOptions` interface defines the configuration:
113
+ ### 3. Middleware (`Guard`)
114
+ Protect your routes with type-safe RBAC.
95
115
 
96
116
  ```typescript
97
- interface CAuthOptions {
98
- dbContractor: DatabaseContract;
99
- routeContractor: RoutesContract;
100
- roles: string[];
101
- jwtConfig: {
102
- refreshTokenSecret: string;
103
- accessTokenSecret: string;
104
- accessTokenLifeSpan?: string | number; // ms string or number
105
- refreshTokenLifeSpan?: string | number; // ms string or number
106
- };
107
- otpConfig?: {
108
- expiresIn?: number; // milliseconds, default: 300000 (5 minutes)
109
- length?: number; // 4-8 digits, default: 6
110
- };
111
- }
117
+ // Only Admins can access this
118
+ app.get('/admin/stats', auth.Guard(['ADMIN']), (req, res) => {
119
+ console.log('Admin ID:', req.cauth.id);
120
+ res.send('Secret data');
121
+ });
112
122
  ```
113
123
 
114
- ## Error Types
115
-
116
- The library provides these error types:
124
+ ---
117
125
 
118
- - `CredentialMismatchError`: Invalid login credentials
119
- - `InvalidDataError`: Validation failures
120
- - `AccountNotFoundError`: Account not found
121
- - `InvalidRoleError`: Invalid role assignment
122
- - `InvalidRefreshTokenError`: Invalid/expired refresh token
123
- - `DuplicateAccountError`: Account already exists
124
- - `InvalidOTPCode`: Invalid/expired OTP code
126
+ ## 🔒 Security Considerations
125
127
 
126
- ## Development
128
+ ### Password Hashing
129
+ CAuth uses **Argon2id**, the winner of the Password Hashing Competition. It provides excellent resistance against GPU/ASIC cracking and side-channel attacks.
127
130
 
128
- ### Prerequisites
131
+ ### Refresh Token Security
132
+ Refresh tokens are stored as **HMAC hashes** in your database. Even if your database is compromised, attackers cannot use the stored hashes to generate valid refresh tokens.
129
133
 
130
- - Node.js >= 18
131
- - TypeScript >= 5.9
134
+ ### OTP Generation
135
+ OTPs are generated using `node:crypto`'s `randomInt`, ensuring they are not predictable by attackers.
132
136
 
137
+ ---
133
138
 
139
+ ## 🛠️ API Reference
134
140
 
135
- ## License
141
+ ### `CAuthOptions`
142
+ | Property | Type | Description |
143
+ | :--- | :--- | :--- |
144
+ | `dbContractor` | `DatabaseContract` | Implementation of database logic (e.g., `PrismaContractor`). |
145
+ | `routeContractor` | `RoutesContract` | Implementation of framework logic (e.g., `ExpressContractor`). |
146
+ | `roles` | `string[]` | Array of valid role strings. |
147
+ | `jwtConfig` | `JWTConfig` | Secret keys and lifespans for tokens. |
148
+ | `otpConfig` | `OTPConfig` | (Optional) Expiry and length for OTP codes. |
136
149
 
137
- MIT License - see LICENSE file for details.
150
+ ---
138
151
 
139
- ## Support
152
+ ## 📄 License
140
153
 
141
- For issues and feature requests, please visit the [GitHub repository](https://github.com/jonace-mpelule/cauth).
154
+ MIT © [Jonace Mpelule](https://github.com/jonace-mpelule)
package/dist/index.d.ts CHANGED
@@ -22,10 +22,13 @@ declare const AuthModelSchema: z$1.ZodObject<{
22
22
  passwordHash: z$1.ZodOptional<z$1.ZodString>;
23
23
  role: z$1.ZodString;
24
24
  lastLogin: z$1.ZodDate;
25
- refreshTokens: z$1.ZodOptional<z$1.ZodArray<z$1.ZodString>>;
25
+ refreshTokens: z$1.ZodArray<z$1.ZodObject<{
26
+ token: z$1.ZodString;
27
+ exp: z$1.ZodCoercedNumber<unknown>;
28
+ }, z$1.core.$strip>>;
26
29
  createdAt: z$1.ZodDate;
27
30
  updatedAt: z$1.ZodDate;
28
- }, z$1.z.core.$strip>;
31
+ }, z$1.core.$strip>;
29
32
  type AuthModel = z$1.infer<typeof AuthModelSchema>;
30
33
  //#endregion
31
34
  //#region src/errors/errors.d.ts
@@ -141,6 +144,7 @@ interface DatabaseContract {
141
144
  id: string;
142
145
  refreshToken: string;
143
146
  select?: any;
147
+ config: CAuthOptions;
144
148
  }): Promise<T>;
145
149
  removeAndAddRefreshToken({
146
150
  ...args
@@ -227,12 +231,12 @@ declare const CAuthOptionsSchema: z$1.ZodObject<{
227
231
  accessTokenSecret: z$1.ZodString;
228
232
  accessTokenLifeSpan: z$1.ZodOptional<z$1.ZodCustom<ms.StringValue, ms.StringValue>>;
229
233
  refreshTokenLifeSpan: z$1.ZodOptional<z$1.ZodCustom<ms.StringValue, ms.StringValue>>;
230
- }, z$1.z.core.$strip>;
234
+ }, z$1.core.$strip>;
231
235
  otpConfig: z$1.ZodObject<{
232
236
  expiresIn: z$1.ZodOptional<z$1.ZodNumber>;
233
237
  length: z$1.ZodOptional<z$1.ZodNumber>;
234
- }, z$1.z.core.$strip>;
235
- }, z$1.z.core.$strip>;
238
+ }, z$1.core.$strip>;
239
+ }, z$1.core.$strip>;
236
240
  type CAuthOptions = z$1.infer<typeof CAuthOptionsSchema>;
237
241
  //#endregion
238
242
  //#region src/types/dto-schemas.t.d.ts
@@ -350,9 +354,10 @@ declare class _CAuth<T extends string[], TContractor extends RoutesContract<any>
350
354
  }: ChangePasswordSchemaType) => Promise<Result<unknown>>;
351
355
  RequestOTPCode: ({
352
356
  ...args
353
- }: RequestOTP) => Promise<Result<{
357
+ }: RequestOTP & {
358
+ onCode: (code: string) => any;
359
+ }) => Promise<Result<{
354
360
  id: string;
355
- code: string;
356
361
  }>>;
357
362
  LoginWithOTP: (args: OTPLogin) => Promise<Result<{
358
363
  account: Account;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import e,{z as t}from"zod";import{parsePhoneNumberFromString as n}from"libphonenumber-js";import r from"bcrypt";import i from"jsonwebtoken";var a=class{static LoginPurpose=`LOGIN`;static ResetPasswordPurpose=`RESET_PASSWORD`;static ActionPurpose=`ACTION`};const o=t.string().trim().refine(e=>{let t=n(e);return!!t&&t.isValid()},{message:`Invalid phone number`}).transform(e=>n(e)?.format(`E.164`)??e),s=t.enum([`LOGIN`,`RESET_PASSWORD`,`ACTION`]),c=t.object({email:t.email(),phoneNumber:t.never().optional(),password:t.string().min(6).optional()}),l=t.object({phoneNumber:o,email:t.never().optional(),password:t.string().min(6).optional()}),u=t.union([c,l]).superRefine((e,n)=>{e.email&&e.phoneNumber&&n.addIssue({code:t.ZodIssueCode.custom,message:`Provide either email or phoneNumber`,path:[`email`,`phoneNumber`]})}),d=t.object({phoneNumber:o,email:t.never().optional(),code:t.string().min(4).max(8)}),f=t.object({email:t.email(),phoneNumber:t.never().optional(),code:t.string().min(4).max(8)}),p=t.union([f,d]),m=t.object({otpPurpose:s,usePassword:t.boolean().default(!1),password:t.string().optional()}),h=m.extend({phoneNumber:o,email:t.never().optional()}),g=m.extend({phoneNumber:t.never().optional(),email:t.string().email()});t.union([h,g]).refine(e=>e.usePassword?!!e.password:!e.password,{message:`Password required only if usePassword is true`,path:[`password`]});const _=t.object({phoneNumber:o.optional(),email:t.email().optional(),role:t.string(),password:t.string().optional()}).superRefine((e,n)=>{!e.email&&!e.phoneNumber&&n.addIssue({code:t.ZodIssueCode.custom,message:`Provide either email or phoneNumber`,path:[`email`,`phoneNumber`]})}),v=t.object({refreshToken:t.string()}),y=t.object({refreshToken:t.string()}),b=t.object({accountId:t.string(),oldPassword:t.string(),newPassword:t.string()});var x=class{static ValidationError=`validation-error`;static CredentialError=`credential-error`;static UnKnownError=`unknown-error`;static InvalidDataError=`invalid-data-error`},S=class{static ServerError=`internal-server-error`;static ServerErrorMessage=`Internal server error. We are working to fix this, please try again later`;static InvalidToken=`invalid-token`;static InvalidTokenMessage=`Invalid Token`;static ForbiddenResource=`forbidden-resource`;static ForbiddenResourceMessage=`You don't have sufficient permission for this action`;static InvalidOtp=`invalid-otp`;static InvalidOtpMessage=`Invalid Otp. Please check and try again`;static CredentialMismatch=`credential-mismatch`;static CredentialMismatchMessage=`Credential mismatch. Please check your credentials and try again.`;static InvalidData=`invalid-data`;static InvalidDataMessage=e=>`Invalid Body: ${e}`;static AccountNotFound=`account-not-found`;static AccountNotFoundMessage=`Account not found`;static InvalidRole=`invalid-role`;static InvalidRoleMessage=e=>`Role is invalid, please use one of the following roles: ${e.join(`, `)}`;static InvalidRefreshToken=`invalid-refresh-token`;static InvalidRefreshTokenMessage=`Invalid refresh token`;static DuplicateAccount=`account-already-exists`;static DuplicateAccountMessage=`Account with this credentials already exists`;static SchemaValidationError=`schema-validation`;static SchemaValidationMessage=`Your database error is not is sync with CAuth Spec`};const C={CredentialMismatchError:{type:x.CredentialError,message:S.CredentialMismatchMessage,code:S.CredentialMismatch,name:`CredentialMismatchError`},InvalidDataError:e=>({type:x.ValidationError,message:S.InvalidDataMessage(e),code:S.InvalidData,name:`InvalidDataError`}),AccountNotFoundError:{type:x.InvalidDataError,message:S.AccountNotFoundMessage,code:S.AccountNotFound,name:`AccountNotFoundError`},InvalidRoleError:e=>({type:x.ValidationError,message:S.InvalidRoleMessage(e),code:S.InvalidRole,name:`InvalidRoleError`}),InvalidRefreshTokenError:{type:x.ValidationError,message:S.InvalidRefreshTokenMessage,code:S.InvalidRefreshToken,name:`InvalidRefreshTokenError`},DuplicateAccountError:{type:x.ValidationError,message:S.DuplicateAccountMessage,code:S.DuplicateAccount,name:`DuplicateAccountError`},InvalidOTPCode:{type:x.ValidationError,message:S.InvalidOtpMessage,code:S.InvalidOtp,name:`InvalidOTPCode`},SchemaInvalidError:{type:x.ValidationError,message:S.SchemaValidationMessage,code:S.SchemaValidationError,name:`SchemaInvalidError`}};function w(e,t){return typeof e==`object`&&!!e&&`name`in e&&e.name===t}function T(e,t){return w(e,t)}function E(e){return`${e?.error?.issues[0].path}: ${e?.error?.issues[0].message}`}function D(e){return{success:!0,value:e}}function O(...e){return{success:!1,errors:e}}async function k({config:e},t){let n=u.safeParse({email:t.email,phoneNumber:t.phoneNumber});if(!n.success)return O({error:C.InvalidDataError(E(n))});let i=await e.dbContractor.findAccountWithCredential({phoneNumber:t.phoneNumber,email:t.email});if(!i||t.usePassword&&!await r.compare(String(t.password),String(i?.passwordHash)))return O({error:C.CredentialMismatchError});let a=await e.dbContractor.createOTP({config:e},{id:i.id,purpose:t.otpPurpose});return D({id:i.id,code:a.code})}async function A({config:e,tokens:t},n){let r=p.safeParse({phoneNumber:n.phoneNumber,email:n.email,code:n.code});if(!r.success)return O({error:C.InvalidDataError(E(r))});let i=await e.dbContractor.findAccountWithCredential({email:n.email,phoneNumber:n.phoneNumber});if(!i)return O({error:C.CredentialMismatchError});if(!(await e.dbContractor.verifyOTP({id:i.id,code:n.code,purpose:a.LoginPurpose})).isValid)return O({error:C.InvalidOTPCode});let o=await t.GenerateTokenPairs({id:i.id,role:i.role});return await e.dbContractor.updateAccountLogin({id:i.id,refreshToken:o.refreshToken}),delete i.passwordHash,delete i.refreshTokens,D({account:i,tokens:o})}async function j({config:e},t){return D({isValid:(await e.dbContractor.verifyOTP({id:t.id,code:t.code,purpose:t.otpPurpose})).isValid})}async function M({config:e},{...t}){let n=b.safeParse(t);if(!n.success)return O({error:C.InvalidDataError(E(n))});let i=await e.dbContractor.findAccountById({id:t.accountId});if(!i||!r.compare(t.oldPassword,String(i.passwordHash)))return O({error:C.CredentialMismatchError});let a=await r.hash(t.newPassword,10);return await e.dbContractor.updateAccount({id:i.id,data:{passwordHash:a}}),D({})}async function N({config:e,tokens:t},{...n}){let i=u.safeParse(n);if(!i.success)return O({error:C.InvalidDataError(E(i))});let a=await e.dbContractor.findAccountWithCredential({email:n.email,phoneNumber:n.phoneNumber});if(!a||!await r.compare(String(n.password),String(a?.passwordHash)))return O({error:C.CredentialMismatchError});let o=await t.GenerateTokenPairs({id:a.id,role:a.role});return await e.dbContractor.updateAccountLogin({id:a.id,refreshToken:o.refreshToken}),delete a.passwordHash,delete a.refreshTokens,D({account:a,tokens:o})}async function P(e){try{return{data:await e,error:null}}catch(e){return{data:null,error:e}}}async function F({config:e,tokens:t},{...n}){let r=y.safeParse(n);if(!r.success)return O({error:C.InvalidDataError(E(r))});let i=await P(t.VerifyRefreshToken(n.refreshToken));return i.error||!i?O({error:C.InvalidRefreshTokenError}):(await e.dbContractor.removeAndAddRefreshToken({id:String(i.data?.id),refreshToken:n.refreshToken}),D({}))}async function I({config:e,tokens:t},{...n}){let r=v.safeParse(n);if(!r.success)return O({error:C.InvalidDataError(E(r))});let i=await P(t.VerifyRefreshToken(n.refreshToken));if(i.error)return O({error:C.InvalidRefreshTokenError});let a=await e.dbContractor.findAccountById({id:String(i.data?.id)});if(!a)return O({error:C.AccountNotFoundError});if(!a?.refreshTokens?.includes(n.refreshToken))return O({error:C.InvalidRefreshTokenError});let o=await t.GenerateTokenPairs({id:a.id,role:a.role});return await e.dbContractor.removeAndAddRefreshToken({id:a.id,refreshToken:n.refreshToken,newRefreshToken:o.refreshToken}),delete a.refreshTokens,delete a.passwordHash,D({account:a,tokens:o})}async function L({config:e,tokens:t},{...n}){let i=_.safeParse(n);if(!i.success)return O({error:C.InvalidDataError(E(i))});if(!e.roles?.includes(n.role))return O({error:C.InvalidRoleError(e.roles)});if(await e.dbContractor.findAccountWithCredential({email:n.email,phoneNumber:n.phoneNumber}))return O({error:C.DuplicateAccountError});let a=await r.hash(String(n.password),10),o=await e.dbContractor.createAccount({data:{email:n.email,phoneNumber:n.phoneNumber,passwordHash:a,role:n.role,lastLogin:new Date}}),s=await t.GenerateTokenPairs({id:o.id,role:o.role});return await e.dbContractor.updateAccountLogin({id:o.id,refreshToken:s.refreshToken}),D({account:o,tokens:s})}async function R({...e}){return i.sign(e.payload,e.config.jwtConfig.accessTokenSecret,{expiresIn:e.config.jwtConfig?.accessTokenLifeSpan??`15m`})}async function z({...e}){return i.sign(e.payload,e.config.jwtConfig.refreshTokenSecret,{expiresIn:e.config.jwtConfig?.refreshTokenLifeSpan??`30d`})}async function B({...e}){return{accessToken:i.sign(e.payload,e.config.jwtConfig.accessTokenSecret,{expiresIn:e.config.jwtConfig?.accessTokenLifeSpan??`15m`}),refreshToken:i.sign(e.payload,e.config.jwtConfig.refreshTokenSecret,{expiresIn:e.config.jwtConfig?.refreshTokenLifeSpan??`30d`})}}async function V({...e}){let t=i.verify(e.token,e.config.jwtConfig.refreshTokenSecret);return t instanceof String?null:t}async function H({...e}){let t=i.verify(e.token,e.config.jwtConfig.accessTokenSecret);return t instanceof String?null:t}const U=e.custom(()=>!0,{message:`Invalid dbContractor: must implement Database Contract interface`}),W=e.custom(()=>!0,{message:`Invalid routeContractor: must implement RoutesContract interface`}),G=e.custom(),K=e.object({dbContractor:U,routeContractor:W,roles:e.array(e.string()).min(1),jwtConfig:e.object({refreshTokenSecret:e.string(),accessTokenSecret:e.string(),accessTokenLifeSpan:G.optional(),refreshTokenLifeSpan:G.optional()}),otpConfig:e.object({expiresIn:e.number().optional(),length:e.number().min(4).max(8).optional()})});var q=class{#config;constructor(e){if(!K.safeParse(e).success)throw Error(`❌ Failed to initiate CAuth. You provided an invalid config!`);this.#config=e}get RoleType(){return null}Guard=e=>this.#config.routeContractor.Guard({config:this.#config,tokens:this.Tokens,roles:e});Routes={Register:()=>this.#config.routeContractor.Register({config:this.#config,tokens:this.Tokens}),Login:()=>this.#config.routeContractor.Login({config:this.#config,tokens:this.Tokens}),Logout:()=>this.#config.routeContractor.Logout({config:this.#config,tokens:this.Tokens}),Refresh:()=>this.#config.routeContractor.Refresh({config:this.#config,tokens:this.Tokens}),ChangePassword:e=>this.#config.routeContractor.ChangePassword({config:this.#config,tokens:this.Tokens,userId:e})};FN={Login:({...e})=>N({config:this.#config,tokens:this.Tokens},e),Register:({...e})=>L({config:this.#config,tokens:this.Tokens},e),Logout:({...e})=>F({config:this.#config,tokens:this.Tokens},e),Refresh:({...e})=>I({config:this.#config,tokens:this.Tokens},e),ChangePassword:({...e})=>M({config:this.#config,tokens:this.Tokens},e),RequestOTPCode:({...e})=>k({config:this.#config,tokens:this.Tokens},e),LoginWithOTP:e=>A({config:this.#config,tokens:this.Tokens},e),VerifyOTP:e=>j({config:this.#config,tokens:this.Tokens},e)};Tokens={GenerateRefreshToken:e=>z({payload:e,config:this.#config}),GenerateAccessToken:e=>R({payload:e,config:this.#config}),GenerateTokenPairs:e=>B({payload:e,config:this.#config}),VerifyRefreshToken:e=>V({token:e,config:this.#config}),VerifyAccessToken:e=>H({token:e,config:this.#config})}};function J(e){return new q(e)}export{J as CAuth,C as CAuthErrors,T as is,w as isCAuthError};
1
+ import e from"argon2";import t,{z as n}from"zod";import{parsePhoneNumberFromString as r}from"libphonenumber-js";import i from"node:crypto";import a from"jsonwebtoken";var o=class{static LoginPurpose=`LOGIN`;static ResetPasswordPurpose=`RESET_PASSWORD`;static ActionPurpose=`ACTION`};const s=n.string().trim().refine(e=>{let t=r(e);return!!t&&t.isValid()},{message:`Invalid phone number`}).transform(e=>r(e)?.format(`E.164`)??e),c=n.enum([`LOGIN`,`RESET_PASSWORD`,`ACTION`]),l=n.object({email:n.email(),phoneNumber:n.never().optional(),password:n.string().min(6).optional()}),u=n.object({phoneNumber:s,email:n.never().optional(),password:n.string().min(6).optional()}),d=n.union([l,u]).superRefine((e,t)=>{e.email&&e.phoneNumber&&t.addIssue({code:n.ZodIssueCode.custom,message:`Provide either email or phoneNumber`,path:[`email`,`phoneNumber`]})}),f=n.object({phoneNumber:s,email:n.never().optional(),code:n.string().min(4).max(8)}),p=n.object({email:n.email(),phoneNumber:n.never().optional(),code:n.string().min(4).max(8)}),m=n.union([p,f]),h=n.object({otpPurpose:c,usePassword:n.boolean().default(!1),password:n.string().optional()}),g=h.extend({phoneNumber:s,email:n.never().optional()}),_=h.extend({phoneNumber:n.never().optional(),email:n.string().email()});n.union([g,_]).refine(e=>e.usePassword?!!e.password:!e.password,{message:`Password required only if usePassword is true`,path:[`password`]});const v=n.object({phoneNumber:s.optional(),email:n.email().optional(),role:n.string(),password:n.string().optional()}).superRefine((e,t)=>{!e.email&&!e.phoneNumber&&t.addIssue({code:n.ZodIssueCode.custom,message:`Provide either email or phoneNumber`,path:[`email`,`phoneNumber`]})}),y=n.object({refreshToken:n.string()}),b=n.object({refreshToken:n.string()}),x=n.object({accountId:n.string(),oldPassword:n.string(),newPassword:n.string()});var S=class{static ValidationError=`validation-error`;static CredentialError=`credential-error`;static UnKnownError=`unknown-error`;static InvalidDataError=`invalid-data-error`},C=class{static ServerError=`internal-server-error`;static ServerErrorMessage=`Internal server error. We are working to fix this, please try again later`;static InvalidToken=`invalid-token`;static InvalidTokenMessage=`Invalid Token`;static ForbiddenResource=`forbidden-resource`;static ForbiddenResourceMessage=`You don't have sufficient permission for this action`;static InvalidOtp=`invalid-otp`;static InvalidOtpMessage=`Invalid Otp. Please check and try again`;static CredentialMismatch=`credential-mismatch`;static CredentialMismatchMessage=`Credential mismatch. Please check your credentials and try again.`;static InvalidData=`invalid-data`;static InvalidDataMessage=e=>`Invalid Body: ${e}`;static AccountNotFound=`account-not-found`;static AccountNotFoundMessage=`Account not found`;static InvalidRole=`invalid-role`;static InvalidRoleMessage=e=>`Role is invalid, please use one of the following roles: ${e.join(`, `)}`;static InvalidRefreshToken=`invalid-refresh-token`;static InvalidRefreshTokenMessage=`Invalid refresh token`;static DuplicateAccount=`account-already-exists`;static DuplicateAccountMessage=`Account with this credentials already exists`;static SchemaValidationError=`schema-validation`;static SchemaValidationMessage=`Your database error is not is sync with CAuth Spec`};const w={CredentialMismatchError:{type:S.CredentialError,message:C.CredentialMismatchMessage,code:C.CredentialMismatch,name:`CredentialMismatchError`},InvalidDataError:e=>({type:S.ValidationError,message:C.InvalidDataMessage(e),code:C.InvalidData,name:`InvalidDataError`}),AccountNotFoundError:{type:S.InvalidDataError,message:C.AccountNotFoundMessage,code:C.AccountNotFound,name:`AccountNotFoundError`},InvalidRoleError:e=>({type:S.ValidationError,message:C.InvalidRoleMessage(e),code:C.InvalidRole,name:`InvalidRoleError`}),InvalidRefreshTokenError:{type:S.ValidationError,message:C.InvalidRefreshTokenMessage,code:C.InvalidRefreshToken,name:`InvalidRefreshTokenError`},DuplicateAccountError:{type:S.ValidationError,message:C.DuplicateAccountMessage,code:C.DuplicateAccount,name:`DuplicateAccountError`},InvalidOTPCode:{type:S.ValidationError,message:C.InvalidOtpMessage,code:C.InvalidOtp,name:`InvalidOTPCode`},SchemaInvalidError:{type:S.ValidationError,message:C.SchemaValidationMessage,code:C.SchemaValidationError,name:`SchemaInvalidError`}};function T(e,t){return typeof e==`object`&&!!e&&`name`in e&&e.name===t}function E(e,t){return T(e,t)}function D(e){return{success:!0,value:e}}function O(...e){return{success:!1,errors:e}}function k(e){return`${e?.error?.issues[0].path}: ${e?.error?.issues[0].message}`}async function A({config:t},{...n}){let r=d.safeParse({email:n.email,phoneNumber:n.phoneNumber});if(!r.success)return O({error:w.InvalidDataError(k(r))});let i=await t.dbContractor.findAccountWithCredential({phoneNumber:n.phoneNumber,email:n.email});if(!i||n.usePassword&&!await e.verify(String(n.password),String(i?.passwordHash)))return O({error:w.CredentialMismatchError});let a=await t.dbContractor.createOTP({config:t},{id:i.id,purpose:n.otpPurpose});return n.onCode(a.code),D({id:i.id})}async function j({config:e,tokens:t},n){let r=m.safeParse({phoneNumber:n.phoneNumber,email:n.email,code:n.code});if(!r.success)return O({error:w.InvalidDataError(k(r))});let i=await e.dbContractor.findAccountWithCredential({email:n.email,phoneNumber:n.phoneNumber});if(!i)return O({error:w.CredentialMismatchError});if(!(await e.dbContractor.verifyOTP({id:i.id,code:n.code,purpose:o.LoginPurpose})).isValid)return O({error:w.InvalidOTPCode});let a=await t.GenerateTokenPairs({id:i.id,role:i.role});return await e.dbContractor.updateAccountLogin({id:i.id,refreshToken:a.refreshToken,config:e}),delete i.passwordHash,delete i.refreshTokens,D({account:i,tokens:a})}async function M({config:e},t){return D({isValid:(await e.dbContractor.verifyOTP({id:t.id,code:t.code,purpose:t.otpPurpose})).isValid})}async function N({config:t},{...n}){let r=x.safeParse(n);if(!r.success)return O({error:w.InvalidDataError(k(r))});let i=await t.dbContractor.findAccountById({id:n.accountId});if(!i||!await e.verify(String(n.oldPassword),String(i.passwordHash)))return O({error:w.CredentialMismatchError});let a=await e.hash(n.newPassword,{type:e.argon2id});return await t.dbContractor.updateAccount({id:i.id,data:{passwordHash:a}}),D({})}async function P({config:t,tokens:n},{...r}){let i=d.safeParse(r);if(!i.success)return O({error:w.InvalidDataError(k(i))});let a=await t.dbContractor.findAccountWithCredential({email:r.email,phoneNumber:r.phoneNumber});if(!a||!await e.verify(String(r.password),String(a?.passwordHash)))return O({error:w.CredentialMismatchError});let o=await n.GenerateTokenPairs({id:a.id,role:a.role});return await t.dbContractor.updateAccountLogin({id:a.id,refreshToken:o.refreshToken,config:t}),delete a.passwordHash,delete a.refreshTokens,D({account:a,tokens:o})}async function F(e){try{return{data:await e,error:null}}catch(e){return{data:null,error:e}}}async function I({config:e,tokens:t},{...n}){let r=b.safeParse(n);if(!r.success)return O({error:w.InvalidDataError(k(r))});let i=await F(t.VerifyRefreshToken(n.refreshToken));return i.error||!i?O({error:w.InvalidRefreshTokenError}):(await e.dbContractor.removeAndAddRefreshToken({id:String(i.data?.id),refreshToken:n.refreshToken}),D({}))}function L({token:e,refreshTokenSecret:t}){return i.createHmac(`sha256`,t).update(e).digest(`hex`)}function R({incomingToken:e,storedHash:t,refreshTokenSecret:n}){let r=L({token:e,refreshTokenSecret:n});return i.timingSafeEqual(Buffer.from(r),Buffer.from(t))}async function z({config:e,tokens:t},{...n}){let r=y.safeParse(n);if(!r.success)return O({error:w.InvalidDataError(k(r))});let i=await F(t.VerifyRefreshToken(n.refreshToken));if(i.error)return O({error:w.InvalidRefreshTokenError});let a=await e.dbContractor.findAccountById({id:String(i.data?.id)});if(!a)return O({error:w.AccountNotFoundError});if(!a?.refreshTokens?.some(t=>R({incomingToken:n.refreshToken,storedHash:t.token,refreshTokenSecret:e.jwtConfig.refreshTokenSecret})))return O({error:w.InvalidRefreshTokenError});let o=await t.GenerateTokenPairs({id:a.id,role:a.role});return await e.dbContractor.removeAndAddRefreshToken({id:a.id,refreshToken:n.refreshToken,newRefreshToken:o.refreshToken}),delete a.refreshTokens,delete a.passwordHash,D({account:a,tokens:o})}async function B({config:t,tokens:n},{...r}){let i=v.safeParse(r);if(!i.success)return O({error:w.InvalidDataError(k(i))});if(!t.roles?.includes(r.role))return O({error:w.InvalidRoleError(t.roles)});if(await t.dbContractor.findAccountWithCredential({email:r.email,phoneNumber:r.phoneNumber}))return O({error:w.DuplicateAccountError});let a=await e.hash(String(r.password),{type:e.argon2id}),o=await t.dbContractor.createAccount({data:{email:r.email,phoneNumber:r.phoneNumber,passwordHash:a,role:r.role,lastLogin:new Date}}),s=await n.GenerateTokenPairs({id:o.id,role:o.role});return await t.dbContractor.updateAccountLogin({id:o.id,refreshToken:s.refreshToken,config:t}),D({account:o,tokens:s})}async function V({...e}){return a.sign(e.payload,e.config.jwtConfig.accessTokenSecret,{expiresIn:e.config.jwtConfig?.accessTokenLifeSpan??`15m`})}async function H({...e}){return a.sign(e.payload,e.config.jwtConfig.refreshTokenSecret,{expiresIn:e.config.jwtConfig?.refreshTokenLifeSpan??`30d`})}async function U({...e}){return{accessToken:a.sign(e.payload,e.config.jwtConfig.accessTokenSecret,{expiresIn:e.config.jwtConfig?.accessTokenLifeSpan??`15m`}),refreshToken:a.sign(e.payload,e.config.jwtConfig.refreshTokenSecret,{expiresIn:e.config.jwtConfig?.refreshTokenLifeSpan??`30d`})}}async function W({...e}){let t=a.verify(e.token,e.config.jwtConfig.refreshTokenSecret);return t instanceof String?null:t}async function G({...e}){let t=a.verify(e.token,e.config.jwtConfig.accessTokenSecret);return t instanceof String?null:t}const K=t.custom(()=>!0,{message:`Invalid dbContractor: must implement Database Contract interface`}),q=t.custom(()=>!0,{message:`Invalid routeContractor: must implement RoutesContract interface`}),J=t.custom(),Y=t.object({dbContractor:K,routeContractor:q,roles:t.array(t.string()).min(1),jwtConfig:t.object({refreshTokenSecret:t.string(),accessTokenSecret:t.string(),accessTokenLifeSpan:J.optional(),refreshTokenLifeSpan:J.optional()}),otpConfig:t.object({expiresIn:t.number().optional(),length:t.number().min(4).max(8).optional()})});var X=class{#config;constructor(e){if(!Y.safeParse(e).success)throw Error(`❌ Failed to initiate CAuth. You provided an invalid config!`);this.#config=e}get RoleType(){return null}Guard=e=>this.#config.routeContractor.Guard({config:this.#config,tokens:this.Tokens,roles:e});Routes={Register:()=>this.#config.routeContractor.Register({config:this.#config,tokens:this.Tokens}),Login:()=>this.#config.routeContractor.Login({config:this.#config,tokens:this.Tokens}),Logout:()=>this.#config.routeContractor.Logout({config:this.#config,tokens:this.Tokens}),Refresh:()=>this.#config.routeContractor.Refresh({config:this.#config,tokens:this.Tokens}),ChangePassword:e=>this.#config.routeContractor.ChangePassword({config:this.#config,tokens:this.Tokens,userId:e})};FN={Login:({...e})=>P({config:this.#config,tokens:this.Tokens},e),Register:({...e})=>B({config:this.#config,tokens:this.Tokens},e),Logout:({...e})=>I({config:this.#config,tokens:this.Tokens},e),Refresh:({...e})=>z({config:this.#config,tokens:this.Tokens},e),ChangePassword:({...e})=>N({config:this.#config,tokens:this.Tokens},e),RequestOTPCode:({...e})=>A({config:this.#config,tokens:this.Tokens},e),LoginWithOTP:e=>j({config:this.#config,tokens:this.Tokens},e),VerifyOTP:e=>M({config:this.#config,tokens:this.Tokens},e)};Tokens={GenerateRefreshToken:e=>H({payload:e,config:this.#config}),GenerateAccessToken:e=>V({payload:e,config:this.#config}),GenerateTokenPairs:e=>U({payload:e,config:this.#config}),VerifyRefreshToken:e=>W({token:e,config:this.#config}),VerifyAccessToken:e=>G({token:e,config:this.#config})}};function Z(e){return new X(e)}export{Z as CAuth,w as CAuthErrors,E as is,T as isCAuthError};
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cauth/core",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "type": "module",
8
8
  "scripts": {
9
- "build": "tsdown",
9
+ "build": "bun build.ts",
10
10
  "patch": "npm version patch",
11
11
  "minor": "npm version minor",
12
12
  "major": "npm version major",
@@ -27,14 +27,13 @@
27
27
  "license": "MIT",
28
28
  "packageManager": "pnpm@10.13.1",
29
29
  "devDependencies": {
30
- "@types/bcrypt": "^6.0.0",
31
30
  "@types/jsonwebtoken": "^9.0.10",
32
31
  "@types/ms": "^2.1.0",
33
32
  "tsdown": "^0.15.6",
34
33
  "typescript": "^5.9.3"
35
34
  },
36
35
  "dependencies": {
37
- "bcrypt": "^6.0.0",
36
+ "argon2": "^0.44.0",
38
37
  "jsonwebtoken": "^9.0.2",
39
38
  "libphonenumber-js": "^1.12.23",
40
39
  "ms": "^2.1.3",