feat(device): OAuth2 device-pairing flow for HA add-on (code/approve/token/tunnel + /activate)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { createHash, randomBytes, randomInt } from 'node:crypto';
|
||||
|
||||
/**
|
||||
* Server-only helpers for the OAuth2 device-authorization-grant style "device
|
||||
* pairing" flow (Home Assistant add-on). Dependency-free (node:crypto only).
|
||||
*
|
||||
* Opaque secrets (device_code, device token) are high-entropy random values,
|
||||
* so a fast SHA-256 hash is sufficient for at-rest storage — we never persist
|
||||
* or log the plaintext. Mirrors the hashing style in lib/auth/recovery.ts.
|
||||
*/
|
||||
|
||||
/** Stable hash used both when storing and when looking up an opaque secret. */
|
||||
export function hashToken(raw: string): string {
|
||||
return createHash('sha256').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
/** Opaque device_code (the secret the device polls with). */
|
||||
export function generateDeviceCode(): string {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
/** Opaque long-lived device bearer token. */
|
||||
export function generateDeviceToken(): string {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
// Unambiguous alphabet: no 0/O/1/I to avoid transcription errors.
|
||||
const USER_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
|
||||
/** Short human code, formatted `XXXX-XXXX` (uppercase, unbiased selection). */
|
||||
export function generateUserCode(): string {
|
||||
let out = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
out += USER_CODE_ALPHABET[randomInt(USER_CODE_ALPHABET.length)];
|
||||
}
|
||||
return `${out.slice(0, 4)}-${out.slice(4, 8)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a user-typed code: uppercase, strip non-alphanumerics, re-insert
|
||||
* the dash as `XXXX-XXXX`. Returns '' if the result is not exactly 8 alnum
|
||||
* characters.
|
||||
*/
|
||||
export function normalizeUserCode(raw: string): string {
|
||||
const cleaned = raw.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
if (cleaned.length !== 8) return '';
|
||||
return `${cleaned.slice(0, 4)}-${cleaned.slice(4, 8)}`;
|
||||
}
|
||||
|
||||
/** Device-code lifetime before it must be re-requested. */
|
||||
export const DEVICE_CODE_TTL_SECONDS = 600;
|
||||
|
||||
/** Recommended polling interval the device should respect. */
|
||||
export const DEVICE_POLL_INTERVAL_SECONDS = 5;
|
||||
|
||||
/** frp server the add-on connects back to. */
|
||||
export const FRP_SERVER_ADDR = 'linumiq.net';
|
||||
export const FRP_SERVER_PORT = 7000;
|
||||
Reference in New Issue
Block a user