@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 +168 -0
- package/package.json +27 -0
- package/src/index.ts +21 -0
- package/src/jwt/index.ts +24 -0
- package/src/jwt/sign.ts +129 -0
- package/src/jwt/types.ts +80 -0
- package/src/jwt/utils.ts +109 -0
- package/src/jwt/verify.ts +183 -0
- package/src/oauth/index.ts +7 -0
- package/src/oauth/state.ts +117 -0
- package/src/tokens/index.ts +20 -0
- package/src/tokens/magic.ts +57 -0
- package/src/tokens/refresh.ts +75 -0
- package/src/tokens/signed.ts +61 -0
- package/src/totp/index.ts +15 -0
- package/src/totp/recovery.ts +13 -0
- package/src/totp/totp.ts +171 -0
- package/src/totp/uri.ts +26 -0
- package/src/validation/index.ts +8 -0
- package/src/validation/password.ts +241 -0
- package/tsconfig.json +5 -0
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'
|
package/src/jwt/index.ts
ADDED
|
@@ -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'
|
package/src/jwt/sign.ts
ADDED
|
@@ -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
|
+
}
|
package/src/jwt/types.ts
ADDED
|
@@ -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
|
+
}
|
package/src/jwt/utils.ts
ADDED
|
@@ -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
|
+
}
|