40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
// Clock skew window: ±5 minutes
|
|
const CLOCK_SKEW_MS = 5 * 60 * 1000
|
|
|
|
export const deviceGuard = () => ({
|
|
beforeHandle(context: unknown) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const { headers, set } = context as any
|
|
// Validate Content-Type
|
|
if (headers['content-type'] !== 'application/json') {
|
|
set.status = 400
|
|
throw new Error('invalid_request')
|
|
}
|
|
|
|
// Validate X-API-Key
|
|
const apiKey = headers['x-api-key']
|
|
if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') {
|
|
set.status = 401
|
|
throw new Error('invalid_api_key')
|
|
}
|
|
|
|
// Validate X-Timestamp
|
|
const timestampStr = headers['x-timestamp']
|
|
if (!timestampStr) {
|
|
set.status = 403
|
|
throw new Error('replay_attack_detected')
|
|
}
|
|
const timestamp = new Date(timestampStr)
|
|
if (isNaN(timestamp.getTime()) || timestampStr !== timestamp.toISOString()) {
|
|
set.status = 403
|
|
throw new Error('replay_attack_detected')
|
|
}
|
|
const now = new Date()
|
|
const diff = Math.abs(now.getTime() - timestamp.getTime())
|
|
if (diff > CLOCK_SKEW_MS) {
|
|
set.status = 403
|
|
throw new Error('replay_attack_detected')
|
|
}
|
|
},
|
|
})
|