ideal-auth 0.1.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.
Files changed (41) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +533 -0
  3. package/dist/auth-instance.d.ts +17 -0
  4. package/dist/auth-instance.js +92 -0
  5. package/dist/auth.d.ts +2 -0
  6. package/dist/auth.js +25 -0
  7. package/dist/bin/ideal-auth.d.ts +2 -0
  8. package/dist/bin/ideal-auth.js +13 -0
  9. package/dist/crypto/encryption.d.ts +2 -0
  10. package/dist/crypto/encryption.js +59 -0
  11. package/dist/crypto/hmac.d.ts +2 -0
  12. package/dist/crypto/hmac.js +11 -0
  13. package/dist/crypto/timing-safe.d.ts +1 -0
  14. package/dist/crypto/timing-safe.js +16 -0
  15. package/dist/crypto/token.d.ts +1 -0
  16. package/dist/crypto/token.js +6 -0
  17. package/dist/hash/index.d.ts +2 -0
  18. package/dist/hash/index.js +23 -0
  19. package/dist/index.d.ts +12 -0
  20. package/dist/index.js +17 -0
  21. package/dist/rate-limit/index.d.ts +5 -0
  22. package/dist/rate-limit/index.js +15 -0
  23. package/dist/rate-limit/memory-store.d.ts +12 -0
  24. package/dist/rate-limit/memory-store.js +39 -0
  25. package/dist/rate-limit/types.d.ts +1 -0
  26. package/dist/rate-limit/types.js +1 -0
  27. package/dist/session/cookie.d.ts +2 -0
  28. package/dist/session/cookie.js +10 -0
  29. package/dist/session/seal.d.ts +3 -0
  30. package/dist/session/seal.js +19 -0
  31. package/dist/token-verifier/index.d.ts +2 -0
  32. package/dist/token-verifier/index.js +35 -0
  33. package/dist/totp/base32.d.ts +2 -0
  34. package/dist/totp/base32.js +36 -0
  35. package/dist/totp/index.d.ts +2 -0
  36. package/dist/totp/index.js +50 -0
  37. package/dist/totp/recovery.d.ts +6 -0
  38. package/dist/totp/recovery.js +19 -0
  39. package/dist/types.d.ts +106 -0
  40. package/dist/types.js +1 -0
  41. package/package.json +49 -0
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { randomBytes } from 'node:crypto';
3
+ const command = process.argv[2];
4
+ switch (command) {
5
+ case 'secret':
6
+ console.log(`IDEAL_AUTH_SECRET=${randomBytes(32).toString('base64url')}`);
7
+ break;
8
+ default:
9
+ console.log('Usage: ideal-auth <command>\n');
10
+ console.log('Commands:');
11
+ console.log(' secret Generate an IDEAL_AUTH_SECRET for your .env file');
12
+ break;
13
+ }
@@ -0,0 +1,2 @@
1
+ export declare function encrypt(plaintext: string, secret: string): Promise<string>;
2
+ export declare function decrypt(encoded: string, secret: string): Promise<string>;
@@ -0,0 +1,59 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scrypt, } from 'node:crypto';
2
+ const ALGORITHM = 'aes-256-gcm';
3
+ const IV_LENGTH = 12;
4
+ const AUTH_TAG_LENGTH = 16;
5
+ const SALT_LENGTH = 16;
6
+ const KEY_LENGTH = 32;
7
+ const MIN_CIPHERTEXT_LENGTH = SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH; // 44 bytes
8
+ // N=32768 (OWASP recommended), r=8, p=1. maxmem raised to 64MB because
9
+ // 128 * N * r = 32MB, which hits the default maxmem boundary in some runtimes.
10
+ const SCRYPT_OPTIONS = { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
11
+ function deriveKey(secret, salt) {
12
+ return new Promise((resolve, reject) => {
13
+ scrypt(secret, salt, KEY_LENGTH, SCRYPT_OPTIONS, (err, key) => {
14
+ if (err)
15
+ reject(err);
16
+ else
17
+ resolve(key);
18
+ });
19
+ });
20
+ }
21
+ export async function encrypt(plaintext, secret) {
22
+ if (!secret)
23
+ throw new Error('secret must not be empty');
24
+ const salt = randomBytes(SALT_LENGTH);
25
+ const key = await deriveKey(secret, salt);
26
+ const iv = randomBytes(IV_LENGTH);
27
+ const cipher = createCipheriv(ALGORITHM, key, iv, {
28
+ authTagLength: AUTH_TAG_LENGTH,
29
+ });
30
+ const encrypted = Buffer.concat([
31
+ cipher.update(plaintext, 'utf8'),
32
+ cipher.final(),
33
+ ]);
34
+ const authTag = cipher.getAuthTag();
35
+ // salt (16) + iv (12) + authTag (16) + ciphertext
36
+ const combined = Buffer.concat([salt, iv, authTag, encrypted]);
37
+ return combined.toString('base64url');
38
+ }
39
+ export async function decrypt(encoded, secret) {
40
+ if (!secret)
41
+ throw new Error('secret must not be empty');
42
+ const combined = Buffer.from(encoded, 'base64url');
43
+ if (combined.length < MIN_CIPHERTEXT_LENGTH) {
44
+ throw new Error('Invalid ciphertext: too short');
45
+ }
46
+ const salt = combined.subarray(0, SALT_LENGTH);
47
+ const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
48
+ const authTag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
49
+ const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
50
+ const key = await deriveKey(secret, salt);
51
+ const decipher = createDecipheriv(ALGORITHM, key, iv, {
52
+ authTagLength: AUTH_TAG_LENGTH,
53
+ });
54
+ decipher.setAuthTag(authTag);
55
+ return Buffer.concat([
56
+ decipher.update(encrypted),
57
+ decipher.final(),
58
+ ]).toString('utf8');
59
+ }
@@ -0,0 +1,2 @@
1
+ export declare function signData(data: string, secret: string): string;
2
+ export declare function verifySignature(data: string, signature: string, secret: string): boolean;
@@ -0,0 +1,11 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { timingSafeEqual } from './timing-safe';
3
+ export function signData(data, secret) {
4
+ if (!secret)
5
+ throw new Error('secret must not be empty');
6
+ return createHmac('sha256', secret).update(data).digest('hex');
7
+ }
8
+ export function verifySignature(data, signature, secret) {
9
+ const expected = signData(data, secret);
10
+ return timingSafeEqual(expected, signature);
11
+ }
@@ -0,0 +1 @@
1
+ export declare function timingSafeEqual(a: string, b: string): boolean;
@@ -0,0 +1,16 @@
1
+ import { timingSafeEqual as nodeTimingSafeEqual } from 'node:crypto';
2
+ export function timingSafeEqual(a, b) {
3
+ const bufA = Buffer.from(a, 'utf8');
4
+ const bufB = Buffer.from(b, 'utf8');
5
+ // Pad to equal length so nodeTimingSafeEqual always runs,
6
+ // even when inputs differ in length (avoids leaking length info).
7
+ const maxLen = Math.max(bufA.length, bufB.length);
8
+ const paddedA = Buffer.alloc(maxLen);
9
+ const paddedB = Buffer.alloc(maxLen);
10
+ bufA.copy(paddedA);
11
+ bufB.copy(paddedB);
12
+ // Run the constant-time comparison first, then check length.
13
+ // Both always execute — no short-circuit skipping the crypto comparison.
14
+ const contentsMatch = nodeTimingSafeEqual(paddedA, paddedB);
15
+ return contentsMatch && bufA.length === bufB.length;
16
+ }
@@ -0,0 +1 @@
1
+ export declare function generateToken(bytes?: number): string;
@@ -0,0 +1,6 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ export function generateToken(bytes = 32) {
3
+ if (bytes < 1)
4
+ throw new Error('bytes must be at least 1');
5
+ return randomBytes(bytes).toString('hex');
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { HashInstance, HashConfig } from '../types';
2
+ export declare function createHash(config?: HashConfig): HashInstance;
@@ -0,0 +1,23 @@
1
+ import { createHash as nodeCryptoHash } from 'node:crypto';
2
+ import bcrypt from 'bcryptjs';
3
+ const DEFAULT_ROUNDS = 12;
4
+ const BCRYPT_MAX_BYTES = 72;
5
+ function prehash(password) {
6
+ return nodeCryptoHash('sha256').update(password).digest('base64');
7
+ }
8
+ export function createHash(config) {
9
+ const rounds = config?.rounds ?? DEFAULT_ROUNDS;
10
+ return {
11
+ async make(password) {
12
+ if (!password)
13
+ throw new Error('password must not be empty');
14
+ const input = Buffer.byteLength(password, 'utf8') > BCRYPT_MAX_BYTES ? prehash(password) : password;
15
+ const salt = await bcrypt.genSalt(rounds);
16
+ return bcrypt.hash(input, salt);
17
+ },
18
+ async verify(password, hash) {
19
+ const input = Buffer.byteLength(password, 'utf8') > BCRYPT_MAX_BYTES ? prehash(password) : password;
20
+ return bcrypt.compare(input, hash);
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,12 @@
1
+ export { createAuth } from './auth';
2
+ export { createHash } from './hash';
3
+ export { generateToken } from './crypto/token';
4
+ export { signData, verifySignature } from './crypto/hmac';
5
+ export { encrypt, decrypt } from './crypto/encryption';
6
+ export { timingSafeEqual } from './crypto/timing-safe';
7
+ export { createTokenVerifier } from './token-verifier';
8
+ export { createRateLimiter } from './rate-limit';
9
+ export { MemoryRateLimitStore } from './rate-limit/memory-store';
10
+ export { createTOTP } from './totp';
11
+ export { generateRecoveryCodes, verifyRecoveryCode } from './totp/recovery';
12
+ export type { AnyUser, CookieBridge, ConfigurableCookieOptions, CookieOptions, SessionPayload, AuthConfig, HashConfig, LoginOptions, AuthInstance, HashInstance, TokenVerifierConfig, TokenVerifierInstance, RateLimitStore, RateLimiterConfig, RateLimitResult, TOTPConfig, TOTPInstance, RecoveryCodeResult, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ // Auth
2
+ export { createAuth } from './auth';
3
+ // Hash
4
+ export { createHash } from './hash';
5
+ // Crypto utilities
6
+ export { generateToken } from './crypto/token';
7
+ export { signData, verifySignature } from './crypto/hmac';
8
+ export { encrypt, decrypt } from './crypto/encryption';
9
+ export { timingSafeEqual } from './crypto/timing-safe';
10
+ // Token verification (password reset, email verification, etc.)
11
+ export { createTokenVerifier } from './token-verifier';
12
+ // Rate limiting
13
+ export { createRateLimiter } from './rate-limit';
14
+ export { MemoryRateLimitStore } from './rate-limit/memory-store';
15
+ // TOTP (Two-Factor Authentication)
16
+ export { createTOTP } from './totp';
17
+ export { generateRecoveryCodes, verifyRecoveryCode } from './totp/recovery';
@@ -0,0 +1,5 @@
1
+ import type { RateLimiterConfig, RateLimitResult } from '../types';
2
+ export declare function createRateLimiter(config: RateLimiterConfig): {
3
+ attempt(key: string): Promise<RateLimitResult>;
4
+ reset(key: string): Promise<void>;
5
+ };
@@ -0,0 +1,15 @@
1
+ import { MemoryRateLimitStore } from './memory-store';
2
+ export function createRateLimiter(config) {
3
+ const store = config.store ?? new MemoryRateLimitStore();
4
+ return {
5
+ async attempt(key) {
6
+ const { count, resetAt } = await store.increment(key, config.windowMs);
7
+ const allowed = count <= config.maxAttempts;
8
+ const remaining = Math.max(0, config.maxAttempts - count);
9
+ return { allowed, remaining, resetAt };
10
+ },
11
+ async reset(key) {
12
+ await store.reset(key);
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,12 @@
1
+ import type { RateLimitStore } from '../types';
2
+ export declare class MemoryRateLimitStore implements RateLimitStore {
3
+ private store;
4
+ private lastCleanup;
5
+ increment(key: string, windowMs: number): Promise<{
6
+ count: number;
7
+ resetAt: Date;
8
+ }>;
9
+ reset(key: string): Promise<void>;
10
+ private cleanup;
11
+ private evictExpired;
12
+ }
@@ -0,0 +1,39 @@
1
+ const CLEANUP_INTERVAL_MS = 60_000; // 1 minute
2
+ const MAX_ENTRIES = 10_000;
3
+ export class MemoryRateLimitStore {
4
+ store = new Map();
5
+ lastCleanup = Date.now();
6
+ async increment(key, windowMs) {
7
+ const now = Date.now();
8
+ this.cleanup(now);
9
+ const existing = this.store.get(key);
10
+ if (existing && existing.resetAt > now) {
11
+ existing.count++;
12
+ return { count: existing.count, resetAt: new Date(existing.resetAt) };
13
+ }
14
+ if (this.store.size >= MAX_ENTRIES) {
15
+ this.evictExpired(now);
16
+ }
17
+ if (this.store.size >= MAX_ENTRIES) {
18
+ return { count: MAX_ENTRIES, resetAt: new Date(now + windowMs) };
19
+ }
20
+ const entry = { count: 1, resetAt: now + windowMs };
21
+ this.store.set(key, entry);
22
+ return { count: 1, resetAt: new Date(entry.resetAt) };
23
+ }
24
+ async reset(key) {
25
+ this.store.delete(key);
26
+ }
27
+ cleanup(now) {
28
+ if (now - this.lastCleanup < CLEANUP_INTERVAL_MS)
29
+ return;
30
+ this.lastCleanup = now;
31
+ this.evictExpired(now);
32
+ }
33
+ evictExpired(now) {
34
+ for (const [key, entry] of this.store) {
35
+ if (entry.resetAt <= now)
36
+ this.store.delete(key);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1 @@
1
+ export type { RateLimitStore, RateLimiterConfig, RateLimitResult } from '../types';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { CookieOptions, ConfigurableCookieOptions } from '../types';
2
+ export declare function buildCookieOptions(maxAge: number | undefined, overrides?: Partial<ConfigurableCookieOptions>): CookieOptions;
@@ -0,0 +1,10 @@
1
+ export function buildCookieOptions(maxAge, overrides) {
2
+ return {
3
+ secure: process.env.NODE_ENV === 'production',
4
+ sameSite: 'lax',
5
+ path: '/',
6
+ ...(maxAge !== undefined && { maxAge }),
7
+ ...overrides,
8
+ httpOnly: true,
9
+ };
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { SessionPayload } from '../types';
2
+ export declare function seal(payload: SessionPayload, secret: string): Promise<string>;
3
+ export declare function unseal(sealed: string, secret: string): Promise<SessionPayload | null>;
@@ -0,0 +1,19 @@
1
+ import { sealData, unsealData } from 'iron-session';
2
+ export async function seal(payload, secret) {
3
+ return sealData(payload, { password: secret });
4
+ }
5
+ export async function unseal(sealed, secret) {
6
+ try {
7
+ const data = await unsealData(sealed, {
8
+ password: secret,
9
+ });
10
+ if (!data || !data.uid || !data.iat || !data.exp)
11
+ return null;
12
+ if (data.exp < Math.floor(Date.now() / 1000))
13
+ return null;
14
+ return data;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { TokenVerifierConfig, TokenVerifierInstance } from '../types';
2
+ export declare function createTokenVerifier(config: TokenVerifierConfig): TokenVerifierInstance;
@@ -0,0 +1,35 @@
1
+ import { generateToken } from '../crypto/token';
2
+ import { signData, verifySignature } from '../crypto/hmac';
3
+ const DEFAULT_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
4
+ export function createTokenVerifier(config) {
5
+ if (!config.secret || config.secret.length < 32) {
6
+ throw new Error('secret must be at least 32 characters');
7
+ }
8
+ const expiryMs = config.expiryMs ?? DEFAULT_EXPIRY_MS;
9
+ return {
10
+ createToken(userId) {
11
+ const encodedUserId = Buffer.from(userId, 'utf8').toString('base64url');
12
+ const id = generateToken(20);
13
+ const iat = Date.now();
14
+ const exp = iat + expiryMs;
15
+ const payload = `${encodedUserId}.${id}.${iat}.${exp}`;
16
+ const signature = signData(payload, config.secret);
17
+ return `${payload}.${signature}`;
18
+ },
19
+ verifyToken(token) {
20
+ const parts = token.split('.');
21
+ if (parts.length !== 5)
22
+ return null;
23
+ const [encodedUserId, id, iatStr, expStr, signature] = parts;
24
+ const payload = `${encodedUserId}.${id}.${iatStr}.${expStr}`;
25
+ if (!verifySignature(payload, signature, config.secret))
26
+ return null;
27
+ const exp = Number(expStr);
28
+ const iat = Number(iatStr);
29
+ if (Number.isNaN(exp) || Number.isNaN(iat) || Date.now() >= exp)
30
+ return null;
31
+ const userId = Buffer.from(encodedUserId, 'base64url').toString('utf8');
32
+ return { userId, iatMs: iat };
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,2 @@
1
+ export declare function base32Encode(buffer: Buffer): string;
2
+ export declare function base32Decode(encoded: string): Buffer;
@@ -0,0 +1,36 @@
1
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
2
+ export function base32Encode(buffer) {
3
+ let bits = 0;
4
+ let value = 0;
5
+ let result = '';
6
+ for (const byte of buffer) {
7
+ value = (value << 8) | byte;
8
+ bits += 8;
9
+ while (bits >= 5) {
10
+ bits -= 5;
11
+ result += ALPHABET[(value >>> bits) & 0x1f];
12
+ }
13
+ }
14
+ if (bits > 0) {
15
+ result += ALPHABET[(value << (5 - bits)) & 0x1f];
16
+ }
17
+ return result;
18
+ }
19
+ export function base32Decode(encoded) {
20
+ const cleaned = encoded.replace(/=+$/, '').toUpperCase();
21
+ let bits = 0;
22
+ let value = 0;
23
+ const bytes = [];
24
+ for (const char of cleaned) {
25
+ const idx = ALPHABET.indexOf(char);
26
+ if (idx === -1)
27
+ throw new Error(`Invalid base32 character: ${char}`);
28
+ value = (value << 5) | idx;
29
+ bits += 5;
30
+ if (bits >= 8) {
31
+ bits -= 8;
32
+ bytes.push((value >>> bits) & 0xff);
33
+ }
34
+ }
35
+ return Buffer.from(bytes);
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { TOTPConfig, TOTPInstance } from '../types';
2
+ export declare function createTOTP(config?: TOTPConfig): TOTPInstance;
@@ -0,0 +1,50 @@
1
+ import { createHmac, randomBytes } from 'node:crypto';
2
+ import { timingSafeEqual } from '../crypto/timing-safe';
3
+ import { base32Encode, base32Decode } from './base32';
4
+ function generate(secret, digits, period, counter) {
5
+ const time = counter ?? Math.floor(Date.now() / 1000 / period);
6
+ const buf = Buffer.alloc(8);
7
+ let tmp = time;
8
+ for (let i = 7; i >= 0; i--) {
9
+ buf[i] = tmp & 0xff;
10
+ tmp = Math.floor(tmp / 256);
11
+ }
12
+ const hmac = createHmac('sha1', base32Decode(secret)).update(buf).digest();
13
+ const offset = hmac[hmac.length - 1] & 0x0f;
14
+ const code = ((hmac[offset] & 0x7f) << 24) |
15
+ ((hmac[offset + 1] & 0xff) << 16) |
16
+ ((hmac[offset + 2] & 0xff) << 8) |
17
+ (hmac[offset + 3] & 0xff);
18
+ return String(code % 10 ** digits).padStart(digits, '0');
19
+ }
20
+ export function createTOTP(config) {
21
+ const digits = config?.digits ?? 6;
22
+ const period = config?.period ?? 30;
23
+ const window = config?.window ?? 1;
24
+ return {
25
+ generateSecret() {
26
+ return base32Encode(randomBytes(20));
27
+ },
28
+ generateQrUri(options) {
29
+ const { secret, issuer, account } = options;
30
+ const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(account)}`;
31
+ const params = new URLSearchParams({
32
+ secret,
33
+ issuer,
34
+ algorithm: 'SHA1',
35
+ digits: String(digits),
36
+ period: String(period),
37
+ });
38
+ return `otpauth://totp/${label}?${params.toString()}`;
39
+ },
40
+ verify(token, secret) {
41
+ const counter = Math.floor(Date.now() / 1000 / period);
42
+ for (let i = -window; i <= window; i++) {
43
+ const candidate = generate(secret, digits, period, counter + i);
44
+ if (timingSafeEqual(token, candidate))
45
+ return true;
46
+ }
47
+ return false;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,6 @@
1
+ import type { HashInstance, RecoveryCodeResult } from '../types';
2
+ export declare function generateRecoveryCodes(hashInstance: HashInstance, count?: number): Promise<{
3
+ codes: string[];
4
+ hashed: string[];
5
+ }>;
6
+ export declare function verifyRecoveryCode(code: string, hashedCodes: string[], hashInstance: HashInstance): Promise<RecoveryCodeResult>;
@@ -0,0 +1,19 @@
1
+ import { generateToken } from '../crypto/token';
2
+ export async function generateRecoveryCodes(hashInstance, count = 8) {
3
+ const codes = [];
4
+ for (let i = 0; i < count; i++) {
5
+ const raw = generateToken(8); // 16 hex chars
6
+ codes.push(`${raw.slice(0, 8)}-${raw.slice(8, 16)}`);
7
+ }
8
+ const hashed = await Promise.all(codes.map((code) => hashInstance.make(code)));
9
+ return { codes, hashed };
10
+ }
11
+ export async function verifyRecoveryCode(code, hashedCodes, hashInstance) {
12
+ for (let i = 0; i < hashedCodes.length; i++) {
13
+ if (await hashInstance.verify(code, hashedCodes[i])) {
14
+ const remaining = [...hashedCodes.slice(0, i), ...hashedCodes.slice(i + 1)];
15
+ return { valid: true, remaining };
16
+ }
17
+ }
18
+ return { valid: false, remaining: hashedCodes };
19
+ }
@@ -0,0 +1,106 @@
1
+ export type AnyUser = {
2
+ id: string | number;
3
+ [key: string]: any;
4
+ };
5
+ export interface CookieOptions {
6
+ httpOnly?: boolean;
7
+ secure?: boolean;
8
+ sameSite?: 'lax' | 'strict' | 'none';
9
+ path?: string;
10
+ maxAge?: number;
11
+ expires?: Date;
12
+ domain?: string;
13
+ }
14
+ /** Options exposed to consumers — `httpOnly` is always forced to `true` internally. */
15
+ export type ConfigurableCookieOptions = Omit<CookieOptions, 'httpOnly'>;
16
+ export interface CookieBridge {
17
+ get(name: string): Promise<string | undefined> | string | undefined;
18
+ set(name: string, value: string, options: CookieOptions): Promise<void> | void;
19
+ delete(name: string): Promise<void> | void;
20
+ }
21
+ export interface SessionPayload {
22
+ uid: string;
23
+ iat: number;
24
+ exp: number;
25
+ }
26
+ export interface LoginOptions {
27
+ remember?: boolean;
28
+ }
29
+ export interface AuthConfig<TUser extends AnyUser = AnyUser> {
30
+ secret: string;
31
+ cookie: CookieBridge;
32
+ session?: {
33
+ cookieName?: string;
34
+ maxAge?: number;
35
+ rememberMaxAge?: number;
36
+ cookie?: Partial<ConfigurableCookieOptions>;
37
+ };
38
+ resolveUser: (id: string) => Promise<TUser | null>;
39
+ hash?: HashInstance;
40
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
41
+ credentialKey?: string;
42
+ passwordField?: string;
43
+ attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null>;
44
+ }
45
+ export interface HashConfig {
46
+ rounds?: number;
47
+ }
48
+ export interface AuthInstance<TUser extends AnyUser = AnyUser> {
49
+ login(user: TUser, options?: LoginOptions): Promise<void>;
50
+ loginById(id: string, options?: LoginOptions): Promise<void>;
51
+ attempt(credentials: Record<string, any>, options?: LoginOptions): Promise<boolean>;
52
+ logout(): Promise<void>;
53
+ check(): Promise<boolean>;
54
+ user(): Promise<TUser | null>;
55
+ id(): Promise<string | null>;
56
+ }
57
+ export interface HashInstance {
58
+ make(password: string): Promise<string>;
59
+ verify(password: string, hash: string): Promise<boolean>;
60
+ }
61
+ export interface TokenVerifierConfig {
62
+ secret: string;
63
+ expiryMs?: number;
64
+ }
65
+ export interface TokenVerifierInstance {
66
+ createToken(userId: string): string;
67
+ verifyToken(token: string): {
68
+ userId: string;
69
+ iatMs: number;
70
+ } | null;
71
+ }
72
+ export interface RateLimitStore {
73
+ increment(key: string, windowMs: number): Promise<{
74
+ count: number;
75
+ resetAt: Date;
76
+ }>;
77
+ reset(key: string): Promise<void>;
78
+ }
79
+ export interface RateLimiterConfig {
80
+ maxAttempts: number;
81
+ windowMs: number;
82
+ store?: RateLimitStore;
83
+ }
84
+ export interface RateLimitResult {
85
+ allowed: boolean;
86
+ remaining: number;
87
+ resetAt: Date;
88
+ }
89
+ export interface TOTPConfig {
90
+ digits?: number;
91
+ period?: number;
92
+ window?: number;
93
+ }
94
+ export interface TOTPInstance {
95
+ generateSecret(): string;
96
+ generateQrUri(options: {
97
+ secret: string;
98
+ issuer: string;
99
+ account: string;
100
+ }): string;
101
+ verify(token: string, secret: string): boolean;
102
+ }
103
+ export interface RecoveryCodeResult {
104
+ valid: boolean;
105
+ remaining: string[];
106
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "ideal-auth",
3
+ "version": "0.1.0",
4
+ "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
+ "scripts": {
6
+ "build": "tsc",
7
+ "prepublishOnly": "bun run build",
8
+ "test": "bun test"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "bin": {
20
+ "ideal-auth": "./dist/bin/ideal-auth.js"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "dependencies": {
26
+ "iron-session": "^8.0.4",
27
+ "bcryptjs": "^3.0.3"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bcryptjs": "^3.0.0",
31
+ "@types/node": "^20"
32
+ },
33
+ "peerDependencies": {
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "typescript": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/ramonmalcolm10/ideal-auth.git"
47
+ },
48
+ "license": "MIT"
49
+ }