TOTP / HOTP / Steam Guard codes, served from a Cloudflare Worker.
The serverless sibling of the cliotp bash script and
cliotp-server — same crypto,
same REST API, same cliotpc CLI. Instead of a VPS, the secrets live in a
Durable Object on Cloudflare's edge.
Runs on Workers + Durable Objects (SQLite-backed). Zero runtime dependencies
(WebCrypto only). The existing cliotpc terminal client talks to it unchanged,
and the same web UI is served at /.
$ export CLIOTP_SERVER=https://cliotp.yourname.workers.dev
$ export CLIOTP_TOKEN=<your API key>
$ cliotpc code github
022863
One-click deploy clones this repo to your account and builds it. Set the
API_TOKENandSECRETsecrets right after — until then the Worker returns a 500.
npm install # pulls in wrangler (dev dependency)
npx wrangler secret put API_TOKEN # paste your admin API key (openssl rand -hex 32)
npx wrangler secret put SECRET # encryption secret for secrets-at-rest
npx wrangler deployAPI_TOKEN— the admin/rescue API key. It's always accepted and is seeded as thedefaultkey on first use. Save it somewhere safe.SECRET— used to derive the AES-256-GCM key that encrypts every secret before it's written to storage. Do not change it after adding entries, or the stored secrets become undecryptable.
To use Sign in with Google instead of an API key:
- Create an OAuth 2.0 Web application client at https://console.cloud.google.com/apis/credentials
- Add the redirect URI
https://otp.serghini.me/auth/google/callback - Set
GOOGLE_CLIENT_IDandGOOGLE_ALLOWED_EMAILSinwrangler.jsoncvars(or the dashboard), then:npx wrangler secret put GOOGLE_CLIENT_SECRET npx wrangler secret put SESSION_SECRET npx wrangler deploy
A Google sign-in sets a signed 30-day session cookie with admin access. API
keys keep working for cliotpc. Without Google config, the UI shows the
API-key form.
Local dev: npm run dev (bindings are simulated locally by default).
Exactly the same cliotpc client from cliotp-server:
cliotpc list
cliotpc code alice
cliotpc code 1 -c
cliotpc add alice --secret JBSWY3DPEHPK3PXP --issuer GitHub
cliotpc add "otpauth://totp/GitHub:alice?secret=...&issuer=GitHub"
cliotpc edit alice --issuer Work
cliotpc rm alice
cliotpc exportSet CLIOTP_SERVER to your Worker URL and CLIOTP_TOKEN to an API key. The
client lives at ../cliotp-server/client.js.
Identical surface to cliotp-server:
| Method | Path | Description |
|---|---|---|
| GET | /healthz |
Liveness (no auth) |
| GET | /api/entries |
List entries (no secrets) |
| POST | /api/entries |
Add from fields or { "uri": "otpauth://…" } |
| GET | /api/entries/:id |
Entry details |
| GET | /api/entries/:id/code |
Current code (advances HOTP counter) |
| PATCH | /api/entries/:id |
Edit fields |
| DELETE | /api/entries/:id |
Remove |
| GET | /api/entries/:id/uri |
otpauth URI (admin) |
| GET | /api/codes |
All codes (peek; no HOTP advance) |
| GET | /api/export |
All otpauth URIs (admin) |
| GET | /api/keys |
List keys — id/name/scope/lastUsedAt (admin) |
| POST | /api/keys |
Create a key (scope: admin or readonly) |
| DELETE | /api/keys/:id |
Revoke a key (never the last) |
Every /api/* route requires Authorization: Bearer <key> (or
X-Api-Token). Key scopes: admin (default) or readonly (fetch codes
only). Duplicate labels are rejected with 409.
- A single Durable Object (
TotpStore, instanceglobal) holds all entries and API keys. Durable Objects serialize requests per instance, which makes HOTP counter advances atomic — no distributed race. - SQLite-backed storage (
new_sqlite_classesmigration — new KV-backed DO namespaces are no longer allowed on Cloudflare). - Secrets are stored AES-256-GCM encrypted (key = SHA-256(
SECRET)). API keys are stored as sha256 hashes only. - Crypto is WebCrypto (
crypto.subtle) + a small base32 implementation, so the samesrc/otp.jsruns in Workers and under Node's test runner unchanged.
- Rate limiting / IP allowlist — delegated to Cloudflare (WAF rules / rate limiting rules in the dashboard), since edge protections sit in front of the Worker.
/metrics— Cloudflare provides analytics in the dashboard.
npm test # node --test (RFC vectors, store lifecycle, scopes, crypto)The suite covers the RFC 4226/6238 vectors, Steam (cross-checked against the reference implementation), Google migration import, otpauth round-trip, AES-GCM at-rest encryption, and the full DO API lifecycle (auth, scopes, HOTP counter atomicity, duplicates, key revocation).
- An API key is the whole ballgame: anyone with it can read every code. Use strong keys and revoke anything you don't recognize.
- Put Cloudflare Access or a WAF rule in front if you want to restrict who can reach the Worker at all.
SECRETmust stay stable for the life of your stored secrets.
MIT. Go wild.