mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
add a few CLI commands
This commit is contained in:
@@ -81,6 +81,12 @@ export interface Account {
|
||||
* DTO (the DTO builders pick only known fields), so it doesn't leak.
|
||||
*/
|
||||
passwordHash?: string
|
||||
/**
|
||||
* Whether this account holds the developer role (backs GET /role/developer/:id).
|
||||
* Not set by any player-facing flow — only an operator grants it, via
|
||||
* `runx admin grant-developer`. Absent/false means no developer role.
|
||||
*/
|
||||
isDeveloper?: boolean
|
||||
}
|
||||
|
||||
interface AccountRow {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { RoomInstanceType, Accessibility, Role } from './enums'
|
||||
export * from './accounts-db'
|
||||
export * from './password'
|
||||
export * from './rooms-db'
|
||||
export * from './room-instance-db'
|
||||
export * from './presence-db'
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Password hashing, shared by everything that reads or writes an account's
|
||||
* credential: the `auth` worker (/connect/token credential login and
|
||||
* /account/me/changepassword) and the `admin` CLI (`runx admin set-password`).
|
||||
* It lives here in @repo/domain — next to the account storage the hash is written
|
||||
* into — so there is exactly one definition of the on-disk format and a hash minted
|
||||
* by one caller always verifies in another.
|
||||
*
|
||||
* PBKDF2-SHA256 with a random per-password salt, stored as `salt:hash` (both
|
||||
* base64). The raw password is never persisted.
|
||||
*/
|
||||
const ITERATIONS = 100_000
|
||||
|
||||
const b64 = (bytes: Uint8Array): string => btoa(String.fromCharCode(...bytes))
|
||||
const fromB64 = (s: string): Uint8Array => Uint8Array.from(atob(s), (ch) => ch.charCodeAt(0))
|
||||
|
||||
async function deriveBits(password: string, salt: Uint8Array): Promise<Uint8Array> {
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits']
|
||||
)
|
||||
const bits = await crypto.subtle.deriveBits(
|
||||
{ name: 'PBKDF2', salt, iterations: ITERATIONS, hash: 'SHA-256' },
|
||||
keyMaterial,
|
||||
256
|
||||
)
|
||||
return new Uint8Array(bits)
|
||||
}
|
||||
|
||||
/** Hash a password into a `salt:hash` string (both base64). */
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16))
|
||||
return `${b64(salt)}:${b64(await deriveBits(password, salt))}`
|
||||
}
|
||||
|
||||
/** Verify a password against a stored `salt:hash`. */
|
||||
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
|
||||
const [saltB64, hashB64] = stored.split(':')
|
||||
if (!saltB64 || !hashB64) return false
|
||||
const actual = b64(await deriveBits(password, fromB64(saltB64)))
|
||||
return actual === hashB64
|
||||
}
|
||||
Reference in New Issue
Block a user