feat(devices): add registration, heartbeat, linking, and device tokens
This commit is contained in:
86
Backend/utils/device-token.ts
Normal file
86
Backend/utils/device-token.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
type DeviceRole = 'camera' | 'client';
|
||||
|
||||
export type DeviceTokenPayload = {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
role: DeviceRole;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
const secret = process.env.BETTER_AUTH_SECRET;
|
||||
|
||||
if (!secret) {
|
||||
throw new Error('BETTER_AUTH_SECRET is required for device token signing');
|
||||
}
|
||||
|
||||
const base64UrlEncode = (input: string): string => Buffer.from(input, 'utf8').toString('base64url');
|
||||
const base64UrlDecode = (input: string): string => Buffer.from(input, 'base64url').toString('utf8');
|
||||
|
||||
const sign = (data: string): string => createHmac('sha256', secret).update(data).digest('base64url');
|
||||
|
||||
export const createDeviceToken = (
|
||||
payload: Omit<DeviceTokenPayload, 'exp'>,
|
||||
ttlSeconds = 60 * 60 * 24 * 30,
|
||||
): string => {
|
||||
const body: DeviceTokenPayload = {
|
||||
...payload,
|
||||
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
|
||||
};
|
||||
|
||||
const encodedPayload = base64UrlEncode(JSON.stringify(body));
|
||||
const signature = sign(encodedPayload);
|
||||
|
||||
return `${encodedPayload}.${signature}`;
|
||||
};
|
||||
|
||||
export const verifyDeviceToken = (token: string): DeviceTokenPayload | null => {
|
||||
const [encodedPayload, providedSignature] = token.split('.');
|
||||
|
||||
if (!encodedPayload || !providedSignature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expectedSignature = sign(encodedPayload);
|
||||
|
||||
const providedBuffer = Buffer.from(providedSignature, 'utf8');
|
||||
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
|
||||
|
||||
if (providedBuffer.length !== expectedBuffer.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsedPayload: unknown;
|
||||
|
||||
try {
|
||||
parsedPayload = JSON.parse(base64UrlDecode(encodedPayload));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parsedPayload || typeof parsedPayload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = parsedPayload as Partial<DeviceTokenPayload>;
|
||||
|
||||
if (
|
||||
typeof payload.userId !== 'string' ||
|
||||
typeof payload.deviceId !== 'string' ||
|
||||
(payload.role !== 'camera' && payload.role !== 'client') ||
|
||||
typeof payload.exp !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.exp <= Math.floor(Date.now() / 1000)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload as DeviceTokenPayload;
|
||||
};
|
||||
Reference in New Issue
Block a user