# cursedbelt-server

> The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.

Latest version **4.30.1** (published 2026-09-24) · ISC license · 0 weekly downloads

## Install

```sh
npm install cursedbelt-server
pnpm add cursedbelt-server
yarn add cursedbelt-server
bun add cursedbelt-server
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 4.30.1 |
| Published | 2026-09-24 |
| First published | 2026-09-15 |
| Weekly downloads | 0 |
| License | ISC |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 5 |
| Unpacked size | 4.7 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | curtwphillips |

## Links

- npm: https://www.npmjs.com/package/cursedbelt-server
- npm.io page: https://npm.io/package/cursedbelt-server

## Dependencies (5)

- [cwip](https://npm.io/package/cwip.md) ^4.6.0
- [jose](https://npm.io/package/jose.md) ^6.2.3
- [argon2id](https://npm.io/package/argon2id.md) 1.0.1
- [cursedops](https://npm.io/package/cursedops.md) >=0.8.1 <0.10.0
- [cursedbelt-core](https://npm.io/package/cursedbelt-core.md) ^2.1.1

## Recent versions

- 4.30.1 (latest) — 2026-09-24
- 4.30.0 — 2026-09-24
- 4.29.0 — 2026-09-24
- 4.28.0 — 2026-09-24
- 4.27.0 — 2026-09-24
- 4.26.1 — 2026-09-24
- 4.26.0 — 2026-09-24
- 4.25.0 — 2026-09-24
- 4.24.1 — 2026-09-24
- 4.24.0 — 2026-09-24
- 4.23.0 — 2026-09-23
- 4.22.0 — 2026-09-23
- 4.21.0 — 2026-09-23
- 4.20.0 — 2026-09-23
- 4.19.2 — 2026-09-23
- … 32 more at https://npm.io/package/cursedbelt-server/versions

## README

# cursedbelt-server

The server tier of the cursedbelt split: Hono on Bun, the D1 seam a Worker crosses, the
binary-server client, the guard, and the fleet standards (retention, activity, notifications,
engagement). `cursedbelt-core` is below it, the React design system `cursedbelt` above it.

```sh
bun add cursedbelt-server
```

## 🔴 The root export is a BARREL — import the leaf

`import … from "cursedbelt-server"` re-exports everything, so it reaches every optional peer and
`bun:sqlite` at once (the table below). An app that wants one function pays for all of them, and
on a Worker `wrangler deploy` fails to bundle it. Import the subpath that owns the symbol —
`cursedbelt-server/login-throttle`, `cursedbelt-server/binary-store`, `cursedbelt-server/d1` —
never the root. `package.json` `exports` is the list of subpaths.

Why this stays prose: which symbol an app WANTS is decided in the app, and a repo's gate proves
that repo — so the barrel-importer grep belongs to the generation's tools, not to this package.

## What you install

Most subpaths need nothing beyond `hono` (and `zod` where they validate). These are the only
ones that statically reach an OPTIONAL peer, or a bun builtin a Worker does not have. The table
is checked: `src/readmeInstallTable.spec.ts` fails when it disagrees with `src/subpathReach.ts`,
and `src/barrelsReachNoOptionalPeer.spec.ts` fails when that disagrees with what a bundler
actually keeps.

| subpath | optional peers it imports | bun builtins it needs |
|---|---|---|
| `.` | `kysely`, `kysely-bun-sqlite`, `otplib`, `plainjob` | `bun:sqlite` |
| `./jobs` | `plainjob` | — |
| `./d1/backup-local` | — | `bun:sqlite` |
| `./guard` | — | `bun:sqlite` |
| `./guard/revocations` | — | `bun:sqlite` |
| `./sqlite` | — | `bun:sqlite` |
| `./engagement` | — | `bun:sqlite` |
| `./request-log` | — | `bun:sqlite` |

`sharp` and `@node-rs/argon2` are loaded lazily (`await import()`), so a missing one breaks only
the feature that asks for it, not the build.

## 🔴 `Bun.serve`'s `websocket` selects an OVERLOAD

It is not an optional field. An options object whose `websocket` may be `undefined` — a
conditional value or a conditional spread — matches no overload, and TypeScript reports an error
about the whole options object rather than the one field. Use `serveBun(app, { websocket })`,
which takes it as a genuinely optional field and makes the two concrete calls itself.
`src/server/serveBunOverload.spec.ts` runs `tsc` on the trap and goes red when bun's types change.

## 🔴 argon2id on a Worker: `cursedbelt-server/worker-argon2`

A Worker may not COMPILE WebAssembly, and pure-JS argon2id costs ~2.8 s of billed CPU per
derivation on workerd (collections' unlock was 8.5 s). `installBunPasswordShim()` first thing in
the Worker's `fetch` gives `Bun.password` to the auth code, backed by `argon2id`'s two `.wasm`
files as static imports that wrangler bundles from `node_modules`. No wrangler rule and no local
`.wasm` declaration are needed. `src/server/worker-argon2/argon2.ts` has the measurements.

## 🔴 A login throttle on a Worker: `cursedbelt-server/login-throttle/durable`

`createLoginThrottle()` remembers in memory, and a Worker's memory is one isolate — per colo,
several per colo, wiped by every deploy — so a Worker's sign-in keeps its ledger in the
`LoginThrottleObject` Durable Object and asks it through `createRemoteLoginThrottle(env.<BINDING>,
{ ledger, fallback })`, gating with `attempt` (check and charge in one step, so a concurrent burst
cannot share one count). `src/server/auth/loginThrottleDurable.ts` has the why and the wiring.

## 🔴 A master lock on a Worker: `attempts: createD1MasterLockAttempts(db)`

A Worker rebuilds `MasterLock` per request, so its guess throttle must be CHARGED where every
request can see it, before the argon2id verify — or a burst landing inside one verify is judged
against one stale count (task 2137). Pass `attempts: createD1MasterLockAttempts(db, { snapshot })`
(one conditional UPSERT … RETURNING on the `meta` row `master_lock:attempts`).
`src/server/master-lock/attempts.ts` has the why.

Its open unlocks go in rows of their own too: `sessions: createD1MasterLockSessions(db, writes,
{ rows })`, one `meta` row per token digest under `master_lock:session:` (read them in the same
snapshot query with `masterLockSessionRange()`). In the one-blob `state` two devices' overlapping
requests each wrote back the map they read, and the owner was asked for the password again right
after typing it (task 2138). `src/server/master-lock/sessions.ts` has the why.

## Standards

`docs/retention.md`, `docs/activity.md`, `docs/notifications.md`, `docs/engagement.md` — the
contracts every app that uses those modules is agreeing to. Cite them from an app with the
package prefix (`cursedbelt-server/docs/retention.md`).

## Verifying

```sh
cd "$FORGE/libs/cursedbelt-server" && bun run verify
```

---
_Source: https://npm.io/package/cursedbelt-server · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
