Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/api-keys/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"id": "instatic.api-keys",
"name": "API Keys",
"version": "0.1.0",
"apiVersion": "1.0.0",
"apiVersion": 1,
"description": "Issue, manage, and authenticate with scoped API keys for machine-to-machine access.",
"permissions": [
"cms.migrations",
Expand Down
2 changes: 1 addition & 1 deletion plugins/public-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"id": "instatic.public-auth",
"name": "Public Authentication",
"version": "0.1.0",
"apiVersion": "1.0.0",
"apiVersion": 1,
"description": "End-user registration, login, password reset, and JWT-based sessions for Instatic sites. Exposes a viewerContext provider.",
"permissions": [
"cms.migrations",
Expand Down
18 changes: 14 additions & 4 deletions plugins/public-auth/src/totp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,22 @@ function timingSafeEqualHex(a: string, b: string): boolean {
* Generate a set of single-use recovery codes. Each is 10 chars
* (alphanumeric, easy to type), shown to the user ONCE at enable time.
*/
const RECOVERY_CODE_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'

export function generateRecoveryCodes(count: number = 8): string[] {
return Array.from({ length: count }, () => {
// 8 bytes → 11 base64url chars (no padding). Filter to A-Z0-9 and
// pad/truncate to exactly 10 chars.
const bytes = randomBytes(8)
return bytes.toString('base64url').toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 10)
// 10 random bytes → 10 indices into a 36-char alphabet. The previous
// implementation encoded 8 bytes as base64url (11 chars), stripped the
// `-` / `_` characters, and sliced to 10 — which silently produced
// 8-10 char codes depending on how many of the 11 base64url chars landed
// in the index-62/63 range. Generate exactly 10 alphanumeric chars
// here instead. Modulo bias (256 % 36 = 4) is acceptable: recovery codes
// are rate-limited and shown to the user once, not a bearer of long-lived
// access.
const bytes = randomBytes(10)
let code = ''
for (let i = 0; i < 10; i++) code += RECOVERY_CODE_ALPHABET[bytes[i]! % 36]
return code
})
}

Expand Down
Loading