@strav/auth 0.2.13

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 ADDED
@@ -0,0 +1,168 @@
1
+ # @strav/auth
2
+
3
+ Authentication primitives for the [Strav](https://strav.dev) framework. Provides unopinionated, composable utilities for building secure authentication systems.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @strav/auth
9
+ ```
10
+
11
+ Requires `@strav/kernel` as a peer dependency.
12
+
13
+ ## Features
14
+
15
+ - **JWT Management** - Sign and verify JWTs using the jose library
16
+ - **Token Utilities** - Signed opaque tokens, magic links, refresh tokens
17
+ - **TOTP/2FA** - Time-based one-time passwords and recovery codes
18
+ - **Password Validation** - Strength checking and policy enforcement
19
+ - **OAuth Helpers** - State management for OAuth flows
20
+
21
+ ## Usage
22
+
23
+ ### JWT Operations
24
+
25
+ ```ts
26
+ import { signJWT, verifyJWT, createAccessToken, verifyAccessToken } from '@strav/auth'
27
+
28
+ // Sign a JWT
29
+ const token = await signJWT(
30
+ { userId: 123, role: 'admin' },
31
+ 'your-secret-key',
32
+ { expiresIn: '1h', issuer: 'my-app' }
33
+ )
34
+
35
+ // Verify a JWT
36
+ const payload = await verifyJWT(token, 'your-secret-key', {
37
+ issuer: 'my-app'
38
+ })
39
+
40
+ // Create access/refresh token pairs
41
+ const accessToken = await createAccessToken(userId, secret)
42
+ const userId = await verifyAccessToken(accessToken, secret)
43
+ ```
44
+
45
+ ### TOTP / Two-Factor Authentication
46
+
47
+ ```ts
48
+ import { generateSecret, verifyTotp, totpUri, generateRecoveryCodes } from '@strav/auth'
49
+
50
+ // Generate a secret for a user
51
+ const { raw, base32 } = generateSecret()
52
+
53
+ // Create QR code URI for authenticator apps
54
+ const uri = totpUri({
55
+ secret: base32,
56
+ issuer: 'MyApp',
57
+ account: 'user@example.com'
58
+ })
59
+
60
+ // Verify a TOTP code
61
+ const valid = await verifyTotp(raw, '123456')
62
+
63
+ // Generate recovery codes
64
+ const codes = generateRecoveryCodes(8)
65
+ ```
66
+
67
+ ### Password Validation
68
+
69
+ ```ts
70
+ import { validatePassword, calculatePasswordStrength, generatePassword } from '@strav/auth'
71
+
72
+ // Validate against a policy
73
+ const result = validatePassword(password, {
74
+ minLength: 12,
75
+ requireUppercase: true,
76
+ requireNumbers: true,
77
+ requireSpecialChars: true
78
+ })
79
+
80
+ if (!result.valid) {
81
+ console.log(result.errors)
82
+ }
83
+
84
+ // Calculate password strength
85
+ const strength = calculatePasswordStrength(password)
86
+ console.log(strength.score, strength.label) // 0-4, "Very Weak" to "Very Strong"
87
+
88
+ // Generate a secure password
89
+ const password = generatePassword(16)
90
+ ```
91
+
92
+ ### Signed Opaque Tokens
93
+
94
+ ```ts
95
+ import { createSignedToken, verifySignedToken } from '@strav/auth'
96
+
97
+ // Create an encrypted, tamper-proof token
98
+ const token = createSignedToken(
99
+ { sub: userId, typ: 'password-reset' },
100
+ 60 // expires in 60 minutes
101
+ )
102
+
103
+ // Verify and decode
104
+ const payload = verifySignedToken(token)
105
+ ```
106
+
107
+ ### Magic Links
108
+
109
+ ```ts
110
+ import { createMagicLinkToken, verifyMagicLinkToken } from '@strav/auth'
111
+
112
+ // Create a magic link token
113
+ const token = createMagicLinkToken(userId, {
114
+ email: 'user@example.com',
115
+ redirect: '/dashboard',
116
+ expiresInMinutes: 15
117
+ })
118
+
119
+ // Verify the token
120
+ const payload = verifyMagicLinkToken(token)
121
+ ```
122
+
123
+ ### OAuth State Management
124
+
125
+ ```ts
126
+ import { createOAuthStateStore } from '@strav/auth'
127
+
128
+ // Create a state store (implement storage backend)
129
+ const stateStore = createOAuthStateStore({
130
+ async store(state) { /* save to Redis/DB */ },
131
+ async retrieve(value) { /* fetch from storage */ },
132
+ async delete(value) { /* remove from storage */ },
133
+ ttl: 600 // 10 minutes
134
+ })
135
+
136
+ // Generate state for OAuth flow
137
+ const stateValue = await stateStore.generate({
138
+ redirect: '/dashboard',
139
+ data: { provider: 'github' }
140
+ })
141
+
142
+ // Verify state after OAuth callback
143
+ const state = await stateStore.verify(stateValue)
144
+ ```
145
+
146
+ ## Architecture
147
+
148
+ This package provides low-level authentication primitives without imposing any specific authentication flow or pattern. It's designed to be:
149
+
150
+ - **Unopinionated** - Build any authentication pattern you need
151
+ - **Composable** - Mix and match utilities as required
152
+ - **Secure** - Uses modern standards and best practices
153
+ - **Framework-agnostic** - Works with any HTTP framework
154
+
155
+ ## Migrating from @strav/jina
156
+
157
+ If you're using the deprecated `@strav/jina` package, you can migrate by:
158
+
159
+ 1. Installing `@strav/auth`
160
+ 2. Replacing jina's TOTP/token utilities with auth equivalents
161
+ 3. Building your own authentication handlers using these primitives
162
+ 4. Removing the jina dependency
163
+
164
+ The main difference is that `@strav/auth` doesn't provide pre-built routes or handlers - you implement those yourself using the utilities provided.
165
+
166
+ ## License
167
+
168
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@strav/auth",
3
+ "version": "0.2.13",
4
+ "type": "module",
5
+ "description": "Authentication primitives for the Strav framework",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./jwt": "./src/jwt/index.ts",
10
+ "./tokens": "./src/tokens/index.ts",
11
+ "./totp": "./src/totp/index.ts",
12
+ "./oauth": "./src/oauth/index.ts",
13
+ "./validation": "./src/validation/index.ts"
14
+ },
15
+ "files": [
16
+ "src/",
17
+ "package.json",
18
+ "tsconfig.json"
19
+ ],
20
+ "peerDependencies": {
21
+ "@strav/kernel": "0.2.13"
22
+ },
23
+ "scripts": {
24
+ "test": "bun test tests/",
25
+ "typecheck": "tsc --noEmit"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @strav/auth - Authentication primitives for the Strav framework
3
+ *
4
+ * A collection of unopinionated, composable authentication utilities
5
+ * for building secure authentication systems.
6
+ */
7
+
8
+ // JWT utilities
9
+ export * from './jwt/index.ts'
10
+
11
+ // Token management
12
+ export * from './tokens/index.ts'
13
+
14
+ // TOTP / Two-factor authentication
15
+ export * from './totp/index.ts'
16
+
17
+ // OAuth utilities
18
+ export * from './oauth/index.ts'
19
+
20
+ // Password validation
21
+ export * from './validation/index.ts'
@@ -0,0 +1,24 @@
1
+ // JWT signing
2
+ export {
3
+ signJWT,
4
+ createAccessToken,
5
+ createRefreshToken
6
+ } from './sign.ts'
7
+
8
+ // JWT verification
9
+ export {
10
+ verifyJWT,
11
+ verifyAccessToken,
12
+ verifyRefreshToken,
13
+ decodeJWT
14
+ } from './verify.ts'
15
+
16
+ // Types
17
+ export type {
18
+ JWTPayload,
19
+ JWTHeader,
20
+ JWTAlgorithm,
21
+ JWTSignOptions,
22
+ JWTVerifyOptions,
23
+ JWTKeyPair
24
+ } from './types.ts'
@@ -0,0 +1,129 @@
1
+ import type { JWTPayload, JWTHeader, JWTSignOptions, JWTAlgorithm } from './types.ts'
2
+ import {
3
+ base64urlEncode,
4
+ createHmacSignature,
5
+ parseExpirationTime,
6
+ getUnixTimestamp,
7
+ getHashAlgorithm,
8
+ } from './utils.ts'
9
+
10
+ /**
11
+ * Sign a JWT using built-in crypto with sensible defaults.
12
+ * Zero external dependencies.
13
+ *
14
+ * @param payload - The JWT payload
15
+ * @param secret - The secret key for HMAC
16
+ * @param options - Additional JWT options
17
+ * @returns Signed JWT string
18
+ */
19
+ export async function signJWT(
20
+ payload: JWTPayload,
21
+ secret: Uint8Array | string,
22
+ options: JWTSignOptions = {}
23
+ ): Promise<string> {
24
+ const {
25
+ algorithm = 'HS256',
26
+ expiresIn,
27
+ notBefore,
28
+ issuer,
29
+ audience,
30
+ subject,
31
+ jwtId,
32
+ } = options
33
+
34
+ // Build the payload with standard claims
35
+ const now = getUnixTimestamp()
36
+ const jwtPayload: JWTPayload = {
37
+ ...payload,
38
+ iat: now,
39
+ }
40
+
41
+ // Set standard claims if provided
42
+ if (expiresIn) {
43
+ const expSeconds = parseExpirationTime(expiresIn)
44
+ jwtPayload.exp = now + expSeconds
45
+ }
46
+ if (notBefore) {
47
+ const nbfSeconds = typeof notBefore === 'number'
48
+ ? notBefore
49
+ : parseExpirationTime(notBefore)
50
+ jwtPayload.nbf = now + nbfSeconds
51
+ }
52
+ if (issuer) jwtPayload.iss = issuer
53
+ if (audience) jwtPayload.aud = audience
54
+ if (subject) jwtPayload.sub = subject
55
+ if (jwtId) jwtPayload.jti = jwtId
56
+
57
+ // Build the header
58
+ const header: JWTHeader = {
59
+ alg: algorithm,
60
+ typ: 'JWT',
61
+ }
62
+
63
+ // Encode header and payload
64
+ const encodedHeader = base64urlEncode(JSON.stringify(header))
65
+ const encodedPayload = base64urlEncode(JSON.stringify(jwtPayload))
66
+
67
+ // Create the message to sign
68
+ const message = `${encodedHeader}.${encodedPayload}`
69
+
70
+ // Sign the message
71
+ const hashAlg = getHashAlgorithm(algorithm)
72
+ const signature = createHmacSignature(message, secret, hashAlg)
73
+
74
+ // Return the complete JWT
75
+ return `${message}.${signature}`
76
+ }
77
+
78
+ /**
79
+ * Create an access token with user claims.
80
+ *
81
+ * @param userId - User identifier
82
+ * @param secret - Secret key
83
+ * @param claims - Additional claims to include
84
+ * @param options - JWT options
85
+ */
86
+ export async function createAccessToken(
87
+ userId: string | number,
88
+ secret: Uint8Array | string,
89
+ claims: Record<string, unknown> = {},
90
+ options: JWTSignOptions = {}
91
+ ): Promise<string> {
92
+ return signJWT(
93
+ {
94
+ sub: String(userId),
95
+ type: 'access',
96
+ ...claims,
97
+ },
98
+ secret,
99
+ {
100
+ expiresIn: '15m',
101
+ ...options,
102
+ }
103
+ )
104
+ }
105
+
106
+ /**
107
+ * Create a longer-lived refresh token.
108
+ *
109
+ * @param userId - User identifier
110
+ * @param secret - Secret key
111
+ * @param options - JWT options
112
+ */
113
+ export async function createRefreshToken(
114
+ userId: string | number,
115
+ secret: Uint8Array | string,
116
+ options: JWTSignOptions = {}
117
+ ): Promise<string> {
118
+ return signJWT(
119
+ {
120
+ sub: String(userId),
121
+ type: 'refresh',
122
+ },
123
+ secret,
124
+ {
125
+ expiresIn: '30d',
126
+ ...options,
127
+ }
128
+ )
129
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * JWT type definitions for the auth package.
3
+ * Zero external dependencies.
4
+ */
5
+
6
+ /**
7
+ * Standard JWT payload claims.
8
+ */
9
+ export interface JWTPayload {
10
+ /** Issuer */
11
+ iss?: string
12
+ /** Subject */
13
+ sub?: string
14
+ /** Audience */
15
+ aud?: string | string[]
16
+ /** Expiration time (seconds since Unix epoch) */
17
+ exp?: number
18
+ /** Not before time (seconds since Unix epoch) */
19
+ nbf?: number
20
+ /** Issued at (seconds since Unix epoch) */
21
+ iat?: number
22
+ /** JWT ID */
23
+ jti?: string
24
+ /** Additional claims */
25
+ [key: string]: unknown
26
+ }
27
+
28
+ /**
29
+ * JWT header.
30
+ */
31
+ export interface JWTHeader {
32
+ /** Algorithm */
33
+ alg: string
34
+ /** Type (usually "JWT") */
35
+ typ?: string
36
+ /** Additional header params */
37
+ [key: string]: unknown
38
+ }
39
+
40
+ export type JWTAlgorithm =
41
+ | 'HS256' | 'HS384' | 'HS512' // HMAC (supported)
42
+
43
+ export interface JWTSignOptions {
44
+ /** Algorithm to use for signing (default: HS256) */
45
+ algorithm?: JWTAlgorithm
46
+ /** Expiration time (e.g., '15m', '1h', '7d') */
47
+ expiresIn?: string | number
48
+ /** Not before time */
49
+ notBefore?: string | number
50
+ /** Token issuer */
51
+ issuer?: string
52
+ /** Token audience */
53
+ audience?: string | string[]
54
+ /** Token subject */
55
+ subject?: string
56
+ /** JWT ID */
57
+ jwtId?: string
58
+ }
59
+
60
+ export interface JWTVerifyOptions {
61
+ /** Allowed algorithms */
62
+ algorithms?: JWTAlgorithm[]
63
+ /** Expected issuer */
64
+ issuer?: string | string[]
65
+ /** Expected audience */
66
+ audience?: string | string[]
67
+ /** Expected subject */
68
+ subject?: string
69
+ /** Required claims that must be present */
70
+ requiredClaims?: string[]
71
+ }
72
+
73
+ export interface JWTKeyPair {
74
+ /** Private key for signing */
75
+ privateKey: Uint8Array | string
76
+ /** Public key for verification */
77
+ publicKey: Uint8Array | string
78
+ /** Key algorithm */
79
+ algorithm: JWTAlgorithm
80
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * JWT utility functions - zero external dependencies.
3
+ * Uses only Node.js/Bun built-in crypto APIs.
4
+ */
5
+
6
+ import { createHmac, timingSafeEqual } from 'node:crypto'
7
+
8
+ /**
9
+ * Base64url encode a string or buffer.
10
+ */
11
+ export function base64urlEncode(data: string | Buffer): string {
12
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data
13
+ return buffer.toString('base64url')
14
+ }
15
+
16
+ /**
17
+ * Base64url decode a string.
18
+ */
19
+ export function base64urlDecode(data: string): string {
20
+ return Buffer.from(data, 'base64url').toString('utf8')
21
+ }
22
+
23
+ /**
24
+ * Create HMAC signature for JWT.
25
+ */
26
+ export function createHmacSignature(
27
+ message: string,
28
+ secret: string | Uint8Array,
29
+ algorithm: 'sha256' | 'sha384' | 'sha512'
30
+ ): string {
31
+ const key = typeof secret === 'string' ? secret : Buffer.from(secret)
32
+ return createHmac(algorithm, key)
33
+ .update(message)
34
+ .digest('base64url')
35
+ }
36
+
37
+ /**
38
+ * Verify HMAC signature with timing-safe comparison.
39
+ */
40
+ export function verifyHmacSignature(
41
+ message: string,
42
+ signature: string,
43
+ secret: string | Uint8Array,
44
+ algorithm: 'sha256' | 'sha384' | 'sha512'
45
+ ): boolean {
46
+ const expected = createHmacSignature(message, secret, algorithm)
47
+ const expectedBuffer = Buffer.from(expected)
48
+ const signatureBuffer = Buffer.from(signature)
49
+
50
+ // Must be same length for timingSafeEqual
51
+ if (expectedBuffer.length !== signatureBuffer.length) {
52
+ return false
53
+ }
54
+
55
+ return timingSafeEqual(expectedBuffer, signatureBuffer)
56
+ }
57
+
58
+ /**
59
+ * Parse expiration time strings like "15m", "1h", "30d" to seconds.
60
+ */
61
+ export function parseExpirationTime(exp: string | number): number {
62
+ if (typeof exp === 'number') {
63
+ return exp
64
+ }
65
+
66
+ const match = exp.match(/^(\d+)([smhd])$/)
67
+ if (!match) {
68
+ throw new Error(`Invalid expiration time format: ${exp}`)
69
+ }
70
+
71
+ const value = parseInt(match[1]!, 10)
72
+ const unit = match[2]!
73
+
74
+ switch (unit) {
75
+ case 's':
76
+ return value
77
+ case 'm':
78
+ return value * 60
79
+ case 'h':
80
+ return value * 3600
81
+ case 'd':
82
+ return value * 86400
83
+ default:
84
+ throw new Error(`Invalid time unit: ${unit}`)
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get Unix timestamp in seconds.
90
+ */
91
+ export function getUnixTimestamp(): number {
92
+ return Math.floor(Date.now() / 1000)
93
+ }
94
+
95
+ /**
96
+ * Map algorithm string to hash algorithm.
97
+ */
98
+ export function getHashAlgorithm(alg: string): 'sha256' | 'sha384' | 'sha512' {
99
+ switch (alg) {
100
+ case 'HS256':
101
+ return 'sha256'
102
+ case 'HS384':
103
+ return 'sha384'
104
+ case 'HS512':
105
+ return 'sha512'
106
+ default:
107
+ throw new Error(`Unsupported algorithm: ${alg}`)
108
+ }
109
+ }