BIJAY'S Toolkit

Security & Recovery — what protects the toolkit, and exactly what to do when you are locked out. Launcher v3.27.0

1. I am locked out — fix it now ADMIN

After 8 wrong passwords the API blocks that network. The block also rejects the correct password — deliberately, otherwise it would not slow an attacker down. Pick the first option that suits you.

Option A — Wait (easiest)

The login screen shows a live countdown. The first lockout is only 60 seconds. It grows only if you keep failing: 1 min → 5 min → 15 min → 60 min.

Option B — Redeploy on Vercel (instant, nothing to configure)

Lockout counters live in the serverless function's memory, so a fresh deployment starts clean.

  1. Open the Vercel dashboard and select this project.
  2. Go to Deployments.
  3. On the newest deployment click ⋯ → Redeploy, confirm.
  4. Wait for the build (usually under a minute), then sign in again.

Equivalent from a terminal — either works:

npx vercel --prod
# or simply push any commit
git commit --allow-empty -m "clear lockout" && git push
This clears the lock for everyone immediately, which is why it is the fastest fix when a whole office is affected.

Option C — Recovery key (no redeploy, no waiting)

Only works if you set ADMIN_RECOVERY_KEY beforehand — see section 3. Send it once and the lock is released:

curl -H "x-recovery-key: <your-recovery-key>" https://toolkit.bistab.com.np/api/tree

PowerShell:

Invoke-RestMethod -Uri "https://toolkit.bistab.com.np/api/tree" `
  -Headers @{ "x-recovery-key" = "<your-recovery-key>" }

Then sign in to /admin normally.

Option D — Different network

The lock is per public IP, so a phone hotspot or VPN gets you in. Useful as a last resort; prefer B or C.

2. I forgot the admin password ADMIN

There is no password reset email — the password is an environment variable. Change it:

  1. Vercel dashboard → your project → SettingsEnvironment Variables.
  2. Find ADMIN_PASSWORDEdit.
  3. Paste a new strong value (generate one below) → Save.
  4. Go to Deployments⋯ → Redeploy. Env changes only take effect on a new deployment.
  5. Sign in with the new password. The redeploy also cleared any lockout.

Generate a strong password:

node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"
Make sure you tick the same Environments (Production / Preview / Development) that the old value used, or production will keep the old password.

3. Break-glass recovery key ADMIN

Optional but recommended — it turns a lockout from "wait or redeploy" into a one-line fix.

  1. Generate a key (must be 32+ characters or it is ignored):
    node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
  2. Vercel → Settings → Environment VariablesAdd New.
  3. Name ADMIN_RECOVERY_KEY, paste the value, select all environments, Save.
  4. Redeploy so it takes effect.
  5. Store it somewhere you can reach when locked out — a password manager, not this repo.

Why this is not just a second, weaker password

Every use is logged to the Vercel function logs as [guard] recovery key used from <ip>. Check there if you see one you did not trigger.

4. How the lockout works

SettingValueWhy
Failures before lockout8Room for typos, far below what guessing needs
Lockout length1 → 5 → 15 → 60 minEscalates per repeat offence; caps at an hour
ResetOn any successful loginA fixed typo does not haunt you later
ScopePer IP and per credential typeA bad launcher key cannot lock the admin panel
Requests / minute600 authenticated · 120 anonymousA whole office behind one NAT is not throttled

Credential scopes are isolated

Lockouts are keyed by IP plus which credential was attempted. In practice:

Known limitation. Launcher clients that share one public IP (typical office NAT) do share a launcher-scope lockout. Letting a valid key bypass the lock would let an attacker keep testing guesses, so the trade-off stays on the safe side. The first lockout is only 60 seconds.
Also worth knowing: counters are held in each serverless instance's memory. That reliably stops a single-source password grinder, but a cold start clears them and a widely distributed attack could exceed the nominal limit. See the backlog for the durable version.

5. Security model & credentials

Nothing sensitive ever reaches a client. The browser and the launcher talk only to the Vercel API, which holds the GitHub token server-side and proxies the repo.

Launcher / Admin browser
        │   x-launcher-key  or  x-admin-key
        ▼
   Vercel API  (api/_guard.js gates every route)
        │   Authorization: Bearer GITHUB_TOKEN   ← never leaves the server
        ▼
   Private GitHub repo (data/ subtree only)
VariablePurposeRequired
GITHUB_TOKENReads/writes the private repo. Server-side only.Yes
GITHUB_REPOowner/repoYes
GITHUB_REFBranch (defaults to main)No
ADMIN_PASSWORDSign-in for /admin; required for all writesYes
LAUNCHER_KEYPassword for the PowerShell launcher. If unset, read endpoints are open.No
ADMIN_RECOVERY_KEYBreak-glass lockout release (32+ chars)No
If LAUNCHER_KEY is not set, anyone who knows the URL can read data/. That is intentional (the launcher bootstraps anonymously), but set it if your scripts are sensitive.

6. What is protected

AreaProtection
Brute forcePer-IP rate limiting + escalating lockout on every route
Timing attacksAll secrets compared with crypto.timingSafeEqual over SHA-256
Path traversalOne shared validator: rejects .., absolute paths, backslashes, NUL bytes, ://, and confines everything to data/
Privilege escalationWrites and deletes cannot touch anything outside data/ — including public/launcher.ps1
CSRFAuth uses a custom header, which cross-origin pages cannot send without a preflight the API refuses
ClickjackingX-Frame-Options: DENY + frame-ancestors 'none'
Stored XSSUploaded HTML/XML forced to download; API responses carry default-src 'none'; sandbox
Supply chainAll 8 CDN assets pinned with SHA-384 Subresource Integrity
Secret leakageToken never sent to clients; upstream errors logged server-side, not returned
TransportHSTS (2 years, includeSubDomains)

7. Audit findings & fixes

From the security review of the whole codebase. Kept here so the reasoning is not lost.

CRITICAL Admin writes were not confined to data/

write.js and delete.js only checked for path traversal, never for the data/ prefix that every read route enforced. A leaked or guessed admin password could therefore overwrite public/launcher.ps1 — the script every managed endpoint executes via irm | iex — turning admin access into remote code execution across the fleet.

FIXED Both routes now use the shared validator and are confined to data/.

HIGH No rate limiting anywhere

/api/tree doubles as the login oracle (the admin UI validates the password against it), so the single shared password could be guessed at full speed, unthrottled and unlogged.

FIXED Per-IP limiting plus escalating lockout on every route.

MEDIUM The rest

Verified good already: no secrets client-side or in logs, no wildcard CORS, and .env gitignored and untracked.

8. Launcher-side security

The same protections cover the PowerShell launcher, because it authenticates server-side — not with a local password check that could be edited out.

No footprint on client machines

9. Setting up secrets

  1. Vercel → project → Settings → Environment Variables.
  2. Add each variable from section 5, ticking every environment you use.
  3. Redeploy — env changes only apply to new deployments.
  4. Never commit secrets. .env and .env.* are gitignored; keep it that way.

Set MSP_KEY on a client PC so the launcher signs in without prompting:

[Environment]::SetEnvironmentVariable('MSP_KEY','<launcher key>','User')

10. Verifying it still works

The protections have an automated suite. Run it after touching anything under api/:

node test/security.test.js    # 30 checks — auth, escalation, traversal, headers
node test/lockout.test.js     # 15 checks — backoff, recovery key, both login surfaces
node test/startup.test.js     #  7 checks — admin loads its landing page
node test/relocate.test.js    # 13 checks — rename/move/copy never hangs

All four should end with ALL n CHECKS PASSED. They use a mocked GitHub API, so they are safe to run offline and touch nothing real.

11. Hardening backlog

Not code-fixable here — operational choices worth making:

  1. Rotate ADMIN_PASSWORD to a long random string. It is the only credential and is held in the browser's sessionStorage.
  2. Scope GITHUB_TOKEN to a fine-grained PAT with Contents read/write on this repo only, then rotate it.
  3. Move rate-limit counters to Vercel KV / Upstash for hard cross-instance limits. Unlocking would then mean deleting the KV key rather than redeploying.
  4. Enable Deployment Protection on preview deployments, so branch URLs do not expose the same API with the same env vars.
  5. Set LAUNCHER_KEY if the scripts in data/ are sensitive.