Sentou is pre-1.0 and ships from main. Only the latest commit on main is supported. Fixes land there; there are no backported release branches yet.
| Version | Supported |
|---|---|
main (latest) |
yes |
| older commits / tags | no |
Please report security issues privately, not in a public issue.
Use GitHub's Private Vulnerability Reporting on this repository. That opens a private advisory only the maintainers can see.
Include what you found, how to reproduce it, and the impact you expect. We aim to acknowledge a report within a few days and will coordinate a fix and disclosure with you.
Please do not run automated scanners against any hosted instance you do not own.
How hard the email gate locks depends on how you configure it:
- Email verification is available, off by default. Publish a link with
verifyEmail: trueand configure an email sender (SENTOU_RESEND_KEY+SENTOU_EMAIL_FROM), and a gated link emails a one-time code and only grants access once the recipient enters it. That makes the email a verified address rather than a typed claim. - Without verification, the email gate is access friction, not a record. A gated link that does not opt into verification asks for an email and enforces expiry and revocation, but does not confirm the address, and does not store it. Sentou only ever persists a verified email, so an unverified gate's typed email is a key for that session, not a record.
- The domain allowlist inherits this. When verification is on, the allowlist checks a verified address and is a real lock. When it is off, the allowlist rides on an unverified email and is not a hard lock.
- The unguessable link, expiry, and revoke are always real controls. They hold no matter what email someone enters, with or without verification. Treat the link itself as a secret.
- An access session lasts up to 7 days. The cookie that unlocks a gated link expires, so a link opened on a shared or borrowed machine does not grant access forever. For a hard cutoff regardless of who has opened it, set the link's
expiresAtor revoke it. - The link is "living": its content can change in place. Republishing updates what every existing recipient sees, with no version pin or change indicator on their end. That is the intended feature, but in a context where a recipient must be able to prove what they were shown, capture it at view time.
- Always set
SENTOU_SECRETto a strong random value (openssl rand -hex 32). It signs and encrypts the access cookie. There is no hard-coded default: in production the app refuses to serve any request that needs the secret (it throws on the first publish or access call) until you set it, and outside production it mints a random per-process key so cookies are never forgeable with a known key. - Owner endpoints are protected by identity-scoped authentication. The publish, republish, revoke, stats, forget, and keys endpoints require either a logged-in Better Auth session cookie or a per-user API key sent as
Authorization: Bearer <key>. The app fails closed on any production or internet-exposed instance when there is no authenticated actor: unauthenticated requests receive a 401. The endpoints are open only on a purely local instance (localhost base URL,NODE_ENVnotproduction). The first account to sign up becomes the owner; further accounts are invite-only. Mint an API key for automation or MCP use viaPOST /api/keys(the plaintext key is returned once and not stored). - To make the email gate a real boundary, set
SENTOU_RESEND_KEY+SENTOU_EMAIL_FROMand publish links withverifyEmail: true. Recipients then have to enter a one-time code emailed to their address before they get in. In production, publishing averifyEmaillink with no sender configured is rejected. In local dev the code is logged to the server console as a testing fallback, so the console sender must never run on an internet-exposed instance. - The database holds personal data. Verified viewer emails and tracking events live in the SQLite database at
SENTOU_DB, unencrypted at rest. Restrict its file permissions and use disk encryption on an exposed host.SENTOU_RETENTION_DAYSprunes old data and/api/forgeterases a link's data, or one viewer's, on request. - The artifact itself runs sandboxed (
Content-Security-Policy: sandbox allow-scripts, opaque origin, noallow-same-origin), and the access check sits at the route that serves the bytes, so editing the URL does not get past the gate. The sandbox isolates the artifact from your site; it does not stop the artifact's own JavaScript from making outbound network requests, so only publish artifacts you trust. - Per-IP rate limiting needs a trusted proxy. Sentou rate-limits by the client IP it reads from
X-Forwarded-For, which is only trustworthy when a reverse proxy in front of it strips any client-supplied value. Exposed directly, or behind a proxy that appends rather than replaces that header, a client can spoof it and bypass the per-IP caps. The controls that do not depend on IP still hold: the per-address cap on verification-code sends, and the per-code attempt budget gated on a valid sealed cookie. Put Sentou behind a proxy you control.