Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.1.0 to 26.1.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 26.1.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Xbin
Xbin is a PrivateBin-style, end-to-end encrypted pastebin built on Cloudflare Workers. It includes a browser app for creating and reading encrypted pastes, a modern JSON API, and a legacy PrivateBin-compatible surface for migration and interoperability.
Everything is encrypted in the browser before upload. The Worker stores encrypted payloads in R2, lifecycle metadata in D1, and uses Durable Objects plus Queues to handle burn-after-reading claims and background cleanup.
Highlights
- Browser-side encryption with fragment-based share keys
- Optional passwords, expiration, burn-after-reading links, and per-paste delete tokens
- Plain text, syntax-highlighted code, and Markdown rendering
- Encrypted attachment upload with download and preview support
- Discussion threads for compatible pastes
- Modern REST API under
/api/v1/* - Legacy PrivateBin-compatible API support and filesystem import tooling
- Cloudflare-native runtime built from Workers, D1, R2, Durable Objects, Queues, and cron cleanup
Security model
- The browser generates a random secret key and encrypts the payload before upload.
- The Worker stores only encrypted envelopes in R2 and metadata in D1.
- Share links use the query string for the paste id and the URL fragment for the decryption key, for example
https://paste.example.com/?abcdef1234567890#secretKey. The fragment is never sent to the server. - Optional passwords are combined with the fragment key client-side with PBKDF2 before decryption.
- Anyone with the full share URL can decrypt the paste. Delete tokens are separate and are only returned when a paste is created or imported.
Architecture
| Component | Responsibility |
|---|---|
| Cloudflare Worker | HTTP API, asset delivery, SEO metadata rewriting, config endpoint, and import auth |
| Durable Object | Serializes burn-after-reading claim and consume operations |
| D1 | Paste and comment metadata, lifecycle state, hashed delete tokens, burn claims |
| R2 | Encrypted paste and comment payload blobs |
| Queue | Async purge of expired, deleted, and burned content |
| Cron trigger | Releases stale burn claims and finds expired pastes every minute |
assets/ SPA |
Encrypts, decrypts, renders, and shares pastes in the browser |
Getting started
Prerequisites
- Bun
- A Cloudflare account with access to Workers, D1, R2, Queues, and Durable Objects
- Wrangler authenticated locally if you plan to deploy from your machine
Install dependencies
bun install
Provision Cloudflare resources
Create your own resources before the first deploy, then replace the names and IDs in wrangler.jsonc.
bunx wrangler d1 create xbin
bunx wrangler r2 bucket create xbin-pastes
bunx wrangler queues create xbin-gc
Notes:
- This repository already contains concrete D1 and bucket identifiers in
wrangler.jsonc. If you are forking or deploying your own copy, replace them with values from your account. - You only need to provision D1, R2, and Queues manually. The Durable Object binding and SQLite-backed class are declared in
wrangler.jsoncand are created as part of deployment and migration. - This repository sets
workers_dev = false. Before deploying your own fork, either configure your own routes or custom domain inwrangler.jsonc, or setworkers_dev = trueso the app has a reachable hostname. - After changing bindings or environment variables, regenerate Worker types:
bun run cf-typegen
Configure local secrets
Copy .dev.vars.example to .dev.vars and fill in any optional secrets you plan to use:
Copy-Item .dev.vars.example .dev.vars
Available secrets:
TURNSTILE_SITE_KEYTURNSTILE_SECRET_KEYIMPORT_TOKEN
Most non-secret runtime defaults live in the vars section of wrangler.jsonc.
Run locally
bun run dev
Wrangler serves the SPA and API from the same Worker entrypoint.
Deploy with GitHub Actions
The default deployment path is GitHub Actions. For your own instance, start from a fork of xixu-me/xbin.
The release flow is:
- Add
CLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKENas Actions secrets in your fork. - Push to
mainin your fork. - CI runs formatting, type-checking, Wrangler type validation, tests with coverage, and a Wrangler deploy dry run.
- If CI succeeds for that
mainpush, Deploy publishes the exact revision that passed.
Pull requests still run CI, but they do not auto-deploy. You can also trigger the deploy workflow manually through workflow_dispatch in your fork.
The deploy workflow ultimately runs:
bunx wrangler deploy --keep-vars --message "GitHub Actions deploy for ${GITHUB_SHA}"
--keep-vars means existing remote Worker variables are preserved unless you intentionally change them in Cloudflare or in your deployment setup.
Deploy locally
If you need a manual deployment outside GitHub Actions:
bun run deploy
Useful commands
| Command | Purpose |
|---|---|
bun run dev |
Run the Worker locally with Wrangler |
bun run start |
Alias for local Wrangler development |
bun run check |
Type-check the TypeScript codebase |
bun run test |
Run the Worker integration test suite with Vitest |
bun run test:coverage |
Run tests with Istanbul coverage output |
bun run format |
Format the repository with Prettier |
bun run format:check |
Verify formatting without changing files |
bun run cf-typegen |
Refresh Worker binding types after config changes |
bun run deploy |
Publish the Worker |
Configuration
| Variable | Default | Purpose |
|---|---|---|
XBIN_APP_NAME |
Xbin |
Brand name shown in the UI and metadata |
XBIN_APP_VERSION |
1.0.0 |
Version surfaced by the app config endpoint and footer |
XBIN_PROJECT_PAGE_URL |
https://github.com/xixu-me/xbin |
Project link shown in the UI |
XBIN_BASE_PATH |
/ |
Mount path used when building share URLs, canonical URLs, and sitemap links |
XBIN_MAX_PASTE_BYTES |
10000000 |
Maximum encrypted paste payload size in bytes |
XBIN_DEFAULT_EXPIRATION |
1hour |
Default expiration key used by the UI and API |
XBIN_SUPPORTED_EXPIRATIONS |
5min,10min,30min,1hour,3hour,6hour,12hour,1day,3day,1week |
Comma-separated list of expiration keys exposed by the app |
XBIN_ENABLE_LEGACY_API |
true |
Enables the PrivateBin-compatible JSON API surface |
XBIN_REQUIRE_TURNSTILE |
false |
Requires turnstileToken when creating pastes and comments |
XBIN_BURN_CLAIM_TTL_SECONDS |
120 |
How long a burn-after-reading claim stays reserved before being released |
TURNSTILE_SITE_KEY |
unset | Site key exposed to the client when Turnstile is enabled |
TURNSTILE_SECRET_KEY |
unset | Secret used by the Worker to verify Turnstile tokens |
IMPORT_TOKEN |
unset | Enables and protects the PrivateBin import endpoint |
The config parser also understands 1month, 1year, and never expiration keys if you choose to expose them.
API overview
The write API accepts encrypted PrivateBin-style envelopes, not plaintext content. A minimal create request looks like this:
{
"v": 2,
"adata": [["iv", "salt", 100000, 256, 128, "aes", "gcm", "none"], "plaintext", 0, 0],
"ct": "ciphertext",
"meta": { "expire": "1day" }
}
Core endpoints:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/config |
Returns runtime UI and feature config |
POST |
/api/v1/pastes |
Creates a paste and returns { id, shareUrl, deleteToken } |
GET |
/api/v1/pastes/:id |
Returns the encrypted paste envelope and comments |
DELETE |
/api/v1/pastes/:id |
Deletes a paste when given { "deleteToken": "..." } |
POST |
/api/v1/pastes/:id/comments |
Creates a comment for a discussion-enabled paste |
POST |
/api/v1/pastes/:id/consume |
Finalizes a burn-after-reading read with { "claimToken": "..." } |
POST |
/api/v1/admin/import/privatebin |
Imports a PrivateBin filesystem bundle when authorized |
Notes:
GET /api/v1/pastes/:idreturns aclaimTokenfor burn-after-reading pastes. The client must call/consumeafter a successful decrypt.- When Turnstile is enabled, include
turnstileTokenin the create-paste and create-comment request bodies. - Comments are disabled for burn-after-reading pastes.
PrivateBin compatibility and import
Xbin supports two compatibility paths:
- Legacy JSON API calls are detected via
X-Requested-With: JSONHttpRequest. - Older browser share URLs such as
/api/v1/pastes?<pasteId>are served the SPA shell so the client can recover the share locally.
To import a filesystem export from PrivateBin, first set IMPORT_TOKEN for the Worker, then run:
bun run import:privatebin:fs -- --source /path/to/privatebin/data --base-url https://paste.example.com --token your-import-token --report ./import-report.json
The importer:
- Walks
*.phppaste files and sibling.discussion/directories - Preserves creation and expiration metadata when present
- Skips already expired pastes
- Returns a fresh
deleteTokenfor each imported paste because Xbin generates new deletion credentials during import
Repository layout
| Path | Purpose |
|---|---|
src/index.ts |
Worker entrypoint for HTTP routes, asset serving, cron cleanup, and queue processing |
src/lib/ |
Config parsing, validation, data access, schema, and shared types |
assets/ |
Browser app, HTML shell, CSS, and vendored client-side libraries |
scripts/ |
One-off tooling such as the PrivateBin filesystem importer |
migrations/ |
D1 schema migrations |
test/ |
Worker integration and repository tests |
Testing and quality
The repository uses Vitest with the Cloudflare Workers pool, TypeScript type-checking, Wrangler type validation, and Prettier formatting. CI also runs a Wrangler deploy dry run before the production workflow is allowed to publish. Codecov is configured with 95% project coverage and 90% patch coverage targets, so documentation aside, code changes should usually come with tests.
A local verification pass that matches the main CI quality gates looks like this:
bun run format:check
bun run check
bunx wrangler types --check
bun run test:coverage
Related docs
License
This project is licensed under the GNU Affero General Public License v3.0. See LICENSE for the full text.