ideal-auth 0.3.0 → 0.4.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
@@ -171,7 +171,11 @@ const auth = createAuth({
171
171
 
172
172
  ### `createHash(config?)`
173
173
 
174
- Returns a `HashInstance` using bcrypt.
174
+ Returns a `HashInstance` using bcrypt. Requires `bcryptjs` (optional peer dependency):
175
+
176
+ ```bash
177
+ bun add bcryptjs
178
+ ```
175
179
 
176
180
  | Option | Type | Default |
177
181
  | --- | --- | --- |
@@ -186,6 +190,27 @@ const hashed = await hash.make('password');
186
190
  const valid = await hash.verify('password', hashed); // true
187
191
  ```
188
192
 
193
+ ### Custom hash (bring your own)
194
+
195
+ Skip `bcryptjs` entirely by providing your own `HashInstance`:
196
+
197
+ ```typescript
198
+ import { prehash } from 'ideal-auth';
199
+ import type { HashInstance } from 'ideal-auth';
200
+
201
+ // Bun native bcrypt (use prehash to prevent silent truncation at 72 bytes)
202
+ const hash: HashInstance = {
203
+ make: (password) => Bun.password.hash(prehash(password), { algorithm: 'bcrypt', cost: 12 }),
204
+ verify: (password, hash) => Bun.password.verify(prehash(password), hash),
205
+ };
206
+
207
+ // Bun argon2id (OWASP recommended — no prehash needed, no input length limit)
208
+ const hash: HashInstance = {
209
+ make: (password) => Bun.password.hash(password, { algorithm: 'argon2id' }),
210
+ verify: (password, hash) => Bun.password.verify(password, hash),
211
+ };
212
+ ```
213
+
189
214
  ---
190
215
 
191
216
  ### Crypto Utilities
@@ -517,10 +542,10 @@ cookie: {
517
542
 
518
543
  ## Dependencies
519
544
 
520
- | Package | Purpose |
521
- | --- | --- |
522
- | `iron-session` | Session sealing/unsealing (AES-256-CBC + HMAC) |
523
- | `bcryptjs` | Password hashing |
545
+ | Package | Purpose | Required |
546
+ | --- | --- | --- |
547
+ | `iron-session` | Session sealing/unsealing (AES-256-CBC + HMAC) | Yes |
548
+ | `bcryptjs` | Password hashing (used by `createHash()`) | Optional — not needed if you provide your own `HashInstance` |
524
549
 
525
550
  Zero framework imports. Works in Node, Bun, Deno, and edge runtimes.
526
551
 
@@ -1,2 +1,12 @@
1
1
  import type { HashInstance, HashConfig } from '../types';
2
+ /**
3
+ * SHA-256 prehash for bcrypt's 72-byte input limit.
4
+ * Passwords exceeding 72 UTF-8 bytes are hashed to a 44-char base64 string
5
+ * before being passed to bcrypt, preventing silent truncation.
6
+ *
7
+ * Only needed for bcrypt — argon2 has no input length limit.
8
+ * Applied automatically by `createHash()`. Use this when building
9
+ * a custom bcrypt `HashInstance` (e.g., with `Bun.password`).
10
+ */
11
+ export declare function prehash(password: string): string;
2
12
  export declare function createHash(config?: HashConfig): HashInstance;
@@ -1,23 +1,49 @@
1
1
  import { createHash as nodeCryptoHash } from 'node:crypto';
2
- import bcrypt from 'bcryptjs';
3
2
  const DEFAULT_ROUNDS = 12;
4
3
  const BCRYPT_MAX_BYTES = 72;
5
- function prehash(password) {
4
+ /**
5
+ * SHA-256 prehash for bcrypt's 72-byte input limit.
6
+ * Passwords exceeding 72 UTF-8 bytes are hashed to a 44-char base64 string
7
+ * before being passed to bcrypt, preventing silent truncation.
8
+ *
9
+ * Only needed for bcrypt — argon2 has no input length limit.
10
+ * Applied automatically by `createHash()`. Use this when building
11
+ * a custom bcrypt `HashInstance` (e.g., with `Bun.password`).
12
+ */
13
+ export function prehash(password) {
14
+ if (Buffer.byteLength(password, 'utf8') <= BCRYPT_MAX_BYTES)
15
+ return password;
6
16
  return nodeCryptoHash('sha256').update(password).digest('base64');
7
17
  }
18
+ async function loadBcrypt() {
19
+ try {
20
+ return await import('bcryptjs');
21
+ }
22
+ catch {
23
+ throw new Error('bcryptjs is required for createHash(). Install it as a dependency in your project.\n' +
24
+ 'Alternatively, provide your own HashInstance (e.g., using Bun.password or argon2).');
25
+ }
26
+ }
8
27
  export function createHash(config) {
9
28
  const rounds = config?.rounds ?? DEFAULT_ROUNDS;
29
+ let bcryptModule = null;
30
+ async function getBcrypt() {
31
+ if (!bcryptModule) {
32
+ bcryptModule = await loadBcrypt();
33
+ }
34
+ return bcryptModule;
35
+ }
10
36
  return {
11
37
  async make(password) {
12
38
  if (!password)
13
39
  throw new Error('password must not be empty');
14
- const input = Buffer.byteLength(password, 'utf8') > BCRYPT_MAX_BYTES ? prehash(password) : password;
40
+ const bcrypt = await getBcrypt();
15
41
  const salt = await bcrypt.genSalt(rounds);
16
- return bcrypt.hash(input, salt);
42
+ return bcrypt.hash(prehash(password), salt);
17
43
  },
18
44
  async verify(password, hash) {
19
- const input = Buffer.byteLength(password, 'utf8') > BCRYPT_MAX_BYTES ? prehash(password) : password;
20
- return bcrypt.compare(input, hash);
45
+ const bcrypt = await getBcrypt();
46
+ return bcrypt.compare(prehash(password), hash);
21
47
  },
22
48
  };
23
49
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createAuth } from './auth';
2
- export { createHash } from './hash';
2
+ export { createHash, prehash } from './hash';
3
3
  export { generateToken } from './crypto/token';
4
4
  export { signData, verifySignature } from './crypto/hmac';
5
5
  export { encrypt, decrypt } from './crypto/encryption';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Auth
2
2
  export { createAuth } from './auth';
3
3
  // Hash
4
- export { createHash } from './hash';
4
+ export { createHash, prehash } from './hash';
5
5
  // Crypto utilities
6
6
  export { generateToken } from './crypto/token';
7
7
  export { signData, verifySignature } from './crypto/hmac';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideal-auth",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
5
  "scripts": {
6
6
  "build": "tsc",
@@ -25,17 +25,20 @@
25
25
  "skills"
26
26
  ],
27
27
  "dependencies": {
28
- "iron-session": "^8.0.4",
29
- "bcryptjs": "^3.0.3"
28
+ "iron-session": "^8.0.4"
30
29
  },
31
30
  "devDependencies": {
32
31
  "@types/bcryptjs": "^3.0.0",
33
32
  "@types/node": "^20"
34
33
  },
35
34
  "peerDependencies": {
35
+ "bcryptjs": "^3.0.3",
36
36
  "typescript": "^5.0.0"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
+ "bcryptjs": {
40
+ "optional": true
41
+ },
39
42
  "typescript": {
40
43
  "optional": true
41
44
  }
@@ -194,27 +194,67 @@ const auth = createAuth({
194
194
 
195
195
  ---
196
196
 
197
- ### `createHash(config?): HashInstance`
197
+ ### Password Hashing
198
198
 
199
- bcrypt password hashing with automatic SHA-256 prehash for passwords > 72 bytes.
199
+ Password hashing is **only needed for the Laravel-style `attempt()` flow** (`hash` + `resolveUserByCredentials`). If you use `attemptUser`, `login(user)` directly, or `sessionFields` without credential verification, no `HashInstance` or `bcryptjs` is required.
200
200
 
201
- ```typescript
202
- type HashConfig = { rounds?: number }; // default: 12
201
+ ideal-auth accepts any `HashInstance`:
203
202
 
203
+ ```typescript
204
204
  type HashInstance = {
205
205
  make(password: string): Promise<string>;
206
206
  verify(password: string, hash: string): Promise<boolean>;
207
207
  };
208
208
  ```
209
209
 
210
+ **`createHash()` (bcryptjs)** — built-in convenience. Requires `bcryptjs` as an optional peer dependency (`bun add bcryptjs`). Throws a clear error if not installed.
211
+
210
212
  ```typescript
211
213
  import { createHash } from 'ideal-auth';
212
214
 
213
- const hash = createHash({ rounds: 12 });
215
+ const hash = createHash({ rounds: 12 }); // default: 12
214
216
  const hashed = await hash.make('password');
215
217
  const valid = await hash.verify('password', hashed); // true
216
218
  ```
217
219
 
220
+ **Custom hash (bring your own)** — use your runtime's native hashing or a different algorithm. No `bcryptjs` needed.
221
+
222
+ ```typescript
223
+ import { prehash } from 'ideal-auth';
224
+
225
+ // Bun native bcrypt (faster than bcryptjs — use prehash for 72-byte limit)
226
+ const hash: HashInstance = {
227
+ make: (password) => Bun.password.hash(prehash(password), { algorithm: 'bcrypt', cost: 12 }),
228
+ verify: (password, hash) => Bun.password.verify(prehash(password), hash),
229
+ };
230
+
231
+ // Bun argon2id (OWASP recommended — no prehash needed, no input length limit)
232
+ const hash: HashInstance = {
233
+ make: (password) => Bun.password.hash(password, { algorithm: 'argon2id', memoryCost: 65536, timeCost: 2 }),
234
+ verify: (password, hash) => Bun.password.verify(password, hash),
235
+ };
236
+
237
+ // Node argon2 (requires: bun add argon2 — no prehash needed)
238
+ import argon2 from 'argon2';
239
+ const hash: HashInstance = {
240
+ make: (password) => argon2.hash(password),
241
+ verify: (password, hash) => argon2.verify(hash, password),
242
+ };
243
+ ```
244
+
245
+ Pass either to `createAuth`:
246
+
247
+ ```typescript
248
+ const auth = createAuth({
249
+ secret: process.env.IDEAL_AUTH_SECRET!,
250
+ cookie: createCookieBridge(),
251
+ resolveUser: async (id) => db.user.findUnique({ where: { id } }),
252
+ hash, // createHash() or your custom HashInstance
253
+ resolveUserByCredentials: async (creds) =>
254
+ db.user.findUnique({ where: { email: creds.email } }),
255
+ });
256
+ ```
257
+
218
258
  ---
219
259
 
220
260
  ### `createTokenVerifier(config): TokenVerifierInstance`