@cauth/core 0.1.3 → 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 +94 -81
- package/dist/index.d.ts +12 -7
- package/dist/index.js +1 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
# @cauth/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@cauth/core)
|
|
4
|
+
[](https://github.com/jonace-mpelule/cauth/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
35
|
-
|
|
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,
|
|
66
|
+
expiresIn: 300000, // 5 minutes in ms
|
|
67
|
+
length: 6, // 6-digit codes
|
|
46
68
|
},
|
|
47
69
|
});
|
|
48
70
|
|
|
49
|
-
|
|
71
|
+
export default auth;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Basic Login Example
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
50
77
|
const result = await auth.FN.Login({
|
|
51
|
-
email: '
|
|
52
|
-
|
|
53
|
-
password: 'securepassword123',
|
|
78
|
+
email: 'dev@example.com',
|
|
79
|
+
password: 'SecurePassword123!',
|
|
54
80
|
});
|
|
55
81
|
|
|
56
82
|
if (result.success) {
|
|
57
|
-
console.log('
|
|
83
|
+
console.log('Tokens:', result.value); // { accessToken, refreshToken, user }
|
|
58
84
|
} else {
|
|
59
|
-
console.
|
|
85
|
+
console.error('Errors:', result.errors); // Array of FNError objects
|
|
60
86
|
}
|
|
61
87
|
```
|
|
62
88
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
### Authentication Functions
|
|
89
|
+
---
|
|
66
90
|
|
|
67
|
-
|
|
91
|
+
## 📖 Core Concepts
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
auth.
|
|
87
|
-
auth.
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
The library provides these error types:
|
|
124
|
+
---
|
|
117
125
|
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
+
---
|
|
138
151
|
|
|
139
|
-
##
|
|
152
|
+
## 📄 License
|
|
140
153
|
|
|
141
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
235
|
-
}, z$1.
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
36
|
+
"argon2": "^0.44.0",
|
|
38
37
|
"jsonwebtoken": "^9.0.2",
|
|
39
38
|
"libphonenumber-js": "^1.12.23",
|
|
40
39
|
"ms": "^2.1.3",
|