(params: {
userAgent?: string;
ipAddress?: string;
})
| 27 | */ |
| 28 | export function generateUserCode(): string { |
| 29 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; |
| 30 | let code = ''; |
| 31 | for (let i = 0; i < CODE_LENGTH; i++) { |
| 32 | code += chars.charAt(randomInt(chars.length)); |
| 33 | } |
| 34 | // Format as XXXX-XXXX (8 characters total) |
| 35 | return `${code.slice(0, 4)}-${code.slice(4)}`; |
| 36 | } |
| 37 | |
| 38 | /** @deprecated — ponytail: remove after all shipped clients migrate to userCode/deviceCode split */ |
| 39 | export const generateDeviceCode = generateUserCode; |
| 40 | |
| 41 | /** |
| 42 | * Generate a high-entropy device secret for polling. |
| 43 | * 256-bit random value encoded as base64url. |
| 44 | */ |
| 45 | export function generateDeviceSecret(): string { |
| 46 | return randomBytes(32).toString('base64url'); |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Hash a device secret with SHA-256. |
| 51 | * The secret is already high-entropy, so a plain digest is sufficient. |
| 52 | */ |
| 53 | export function hashDeviceSecret(secret: string): string { |
| 54 | return createHash('sha256').update(secret).digest('hex'); |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Create a new device authorization request. |
| 59 | * Writes both the legacy code and new user_code/device_code_hash. |
| 60 | */ |
| 61 | export async function createDeviceAuthRequest(params: { |
| 62 | userAgent?: string; |
| 63 | ipAddress?: string; |
| 64 | }): Promise<{ code: string; userCode: string; deviceCode: string; expiresAt: Date }> { |
| 65 | const { userAgent, ipAddress } = params; |
| 66 | |
| 67 | // Validate IP address on Production |
| 68 | if (process.env['NODE_ENV'] === 'production' && !ipAddress) { |
| 69 | throw new Error('IP address is required in production'); |
| 70 | } |
| 71 | |
| 72 | // Rate limiting: check pending requests from this IP |
no test coverage detected