From 980e0fe6a434761fde8516c5bbfd5a3072c2ca16 Mon Sep 17 00:00:00 2001 From: Cem Hurturk Date: Sat, 26 Sep 2026 19:31:18 +0000 Subject: [PATCH 1/3] release: 0.1.0 --- CHANGELOG.md | 84 ++++++++++++++++++++++++++++++++++ CLAUDE.md | 8 +++- CONTRIBUTING.md | 4 +- README.md | 10 +++- SECURITY.md | 14 ++++-- package.json | 2 +- packages/admin/package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/version.ts | 2 +- packages/db/package.json | 2 +- packages/ipdata/package.json | 2 +- packages/redirect/package.json | 2 +- packages/ui/package.json | 2 +- packages/worker/package.json | 2 +- 15 files changed, 121 insertions(+), 19 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4f91816 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,84 @@ +# Changelog + +ClickMonk is versioned as one product: every package in the repository carries the same +version, and a release tags the whole repository. `clickmonk version` prints the version an +image was built from. + +**Releases are git tags, and there is no published image.** An install builds its image from +the checkout, as `install.sh` does; upgrading is checking out the next tag and building it — +see "Upgrading" in the README. + +## 0.1.0 — 2026-09-26 + +The first release. Everything below is new in it; the list of what it does not do is part of +the release, not a footnote to it. + +### Added + +- **The redirect.** Links on your own domains, answered from an in-memory copy of the + configuration: weighted rotation across targets, click caps, expiry, backup URLs, query + passthrough, destination tokens, per-device and returning-visitor destinations, and country + rules. Every click is written to a spool on local disk before the visitor is answered, and + links keep answering while Postgres, ClickHouse or the worker is down. +- **Traffic classification.** Every click is classed as human, bot, abuser, anonymous (a Tor + exit), datacenter or unknown, from its user-agent, the client's request rate and IP data + held on your server, with an action per class: flag, nothing, block, or send to a safe URL. +- **IP data**: country and network from DB-IP's Lite databases (CC BY 4.0), hosting networks + from bad-asn-list, Tor exits from the Tor Project, downloaded and updated by the worker. +- **Domain verification** by a TXT record, which the worker looks for every five minutes and + `clickmonk domain verify` looks for at once. Links on a domain answer 404 until it verifies. +- **TLS for every link domain**, obtained by Caddy on the first HTTPS request and renewed + without anyone touching a file, for the domains you have verified and for the admin host you + name — and for nothing else. +- **Password-protected links**, with attempts counted per client per link. +- **The admin API**: one account, a session for a browser and API keys for scripts, two-factor + authentication with recovery codes, and endpoints for domains, links and the install-wide + settings. +- **Reports**: a summary, a chart by hour or by day, and a breakdown by country, device, + operating system, browser, referring host, target, traffic class, action, outcome or link, + read from hourly rollups; the click log, newest first, and a streamed CSV of it with a + stated row cap. +- **The web interface**, on the admin host: an overview, links and their reports, the click + log with its export, domains, settings and the account. Every screen is built on the admin + API. It serves Adobe's own static WOFF2 builds of Source Sans 3, Source Serif 4 and Source + Code Pro, unmodified, under the SIL Open Font License, so a browser using it asks no third + party for a font. +- **Retention**: raw clicks kept 90 days and the address on them 30 by default, each a floor + rather than a deadline, checked hourly by the worker; the rollups are kept for ever. +- **The CLI**: `migrate`, `domain add|list|verify`, `link add`, `settings show|set`, + `ipdata status|update`, `admin create|passwd`, `admin totp disable`, + `apikey create|list|revoke` and `version`. +- **`install.sh`**, which writes `.env` with fresh secrets once, builds the image and starts + the stack. +- **`backup.sh` and `restore.sh`.** A backup copies ClickHouse, Postgres, the spool, Caddy's + certificates and `.env` while links keep answering; a restore checks the backup before + touching anything, replaces all of it, and brings the install up on the same release or a + newer one. + +### What 0.1.0 does not do + +- One admin account, and no roles or second person. Neither the API nor the interface can be + tried without a real host name for the admin service. +- No time zone setting for the install. The API works in UTC; the interface uses the + browser's zone. +- No bulk operations: one link, one domain, one setting at a time. Most link settings are + the API's and the interface's, not the CLI's, and returning-visitor routing needs HTTPS. +- No notifications: `GET /api/alerts` is what there is instead. A domain verified by hand is + listed only once a check has passed for it and a later one has failed. +- Reports: one breakdown dimension at a time, no CSV of a report from the API (the interface + makes that file in the browser), zeroes for any window before this install had its + rollups, and no click in any of it until the worker has shipped it. While ClickHouse is + down the reports, the click log and its export answer 503, and `GET /api/status` says so. +- An export that fails part way through ends the connection rather than finishing the file. +- Proxy and VPN detection covers Tor exits only; datacenter traffic is recognised by network + only; region and city are never filled. +- Backups are not encrypted, rotated or copied off the server by ClickMonk, and a restore is + of everything, with links down while it runs. +- Clicks ClickHouse rejects are set aside in the spool and nothing reports them. +- One redirect process per spool directory; a full spool keeps redirecting and drops clicks, + and the count is on the internal `/health` only. +- The containers on the stack's own network can reach the redirect's internal port and the + admin service. +- The IPv6 test on the published ports runs only on a host with an IPv6 address of its own, + which most CI runners lack. +- No published container image. diff --git a/CLAUDE.md b/CLAUDE.md index 3b2b3ba..4edb59a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ run it on their own infrastructure; their click data stays theirs. ## Current status -**Early, unreleased, not for production.** What exists: the redirect (in-memory snapshot, +**Released as 0.1.0; early.** What exists: the redirect (in-memory snapshot, spool before response, click caps, traffic classification and actions, country rules from an in-memory IP lookup, the password gate on a link), Caddy in front of it with on-demand TLS gated on a verified domain or the configured admin host, the worker (spool @@ -377,7 +377,11 @@ pushing a tag creates none. The whole sequence, in order: 1. Branch `chore/release-X.Y.Z`. 2. Bump the version in every manifest and every version constant compiled into shipped output, then **grep for the new version and count** rather than grepping for the old - one and assuming. The places are listed here once the stack exists. + one and assuming. + The places: `"version"` in the root `package.json` and in each of the eight + `packages/*/package.json`, and `VERSION` in `packages/core/src/version.ts` — ten in all. + `packages/core/src/version.test.ts` fails if any of them disagrees, and counts the + manifests, so a new package without a version fails it too. 3. Write the `CHANGELOG.md` entry: Added / Fixed / Changed, plus what the release still cannot do. Before writing it, read `git log --oneline --no-merges ..HEAD` against the changelog; merged work with no entry is how releases under-report. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2e5a4e..5f23eff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to ClickMonk -Thanks for your interest. ClickMonk is early: the redirect and the click pipeline run, but most of the product is not built yet (see the status section of the [README](README.md)). The most useful contributions right now are use cases, problems you hit with the link tracking you run today, and feedback via [issues](../../issues). +Thanks for your interest. ClickMonk is early: 0.1.0 is its first release, and the status section of the [README](README.md) says what it does and what it does not do yet. The most useful contributions right now are use cases, problems you hit with the link tracking you run today, and feedback via [issues](../../issues). **Found a security problem? Do not open an issue.** Use [private vulnerability reporting](../../security/advisories/new) (the "Report a vulnerability" button on the Security tab) or email `hello@clickmonk.co` with `security` in the subject. See [SECURITY.md](SECURITY.md) for what to include and what to expect. ClickMonk is self-hosted, so a public report is a set of instructions for attacking every install that has not upgraded yet. @@ -43,7 +43,7 @@ hours from UTC and observes daylight saving — so time logic that is only right here. It runs as part of the root `pnpm test` too. The browser suite drives a real browser against the shipped stack over HTTPS, so it goes -through the stack suites (see "The stack suites" above), not `pnpm --filter @clickmonk/ui +through the stack suites (see "The stack suites" in `CLAUDE.md`), not `pnpm --filter @clickmonk/ui test`. Set `CLICKMONK_SHOTS=1` to have that run also write a screenshot of every screen, in both themes and at two widths, to `test/stack/tmp/e2e/shots/`. diff --git a/README.md b/README.md index 37c4ab3..1651d94 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ on your own infrastructure, and your click data stays yours. ## Status -**Early, and not ready for production.** There is no release yet. What runs today: +**0.1.0, the first release.** It has not yet carried anyone's production traffic, and the list +below of what it does not do is part of the release. What it does: - **The redirect**, which answers links on your domains from an in-memory copy of the configuration: weighted rotation across destinations, click caps, expiry, backup URLs @@ -114,7 +115,8 @@ What does not work yet: only; a summary, a chart or a breakdown is already one small answer, and turning one of those into a file is the web interface's own doing, in the browser, not a route this API streams. -- **A time zone, on the API.** Every window and retention period it takes is UTC, and so is +- **A time zone, for the install or on the API.** The install has no zone setting of its own + ([#43](../../issues/43)). Every window and retention period it takes is UTC, and so is a chart's bucket size — `offset` only moves where a day bucket begins, in whole hours, and does nothing for an hourly one. A preset like "yesterday" is for whoever is asking to work out — the web interface does that from the browser's own zone; a script calling the API @@ -148,6 +150,8 @@ What does not work yet: through the admin API, or by hand in SQL without it. Returning-visitor routing also needs HTTPS to do anything: its cookie is marked `Secure`, so a browser drops it over plain HTTP. +- **Bulk operations.** One link, one domain, one setting at a time, in the web interface as + in the CLI ([#39](../../issues/39)). - **Proxy and VPN detection beyond Tor.** The anonymous class covers Tor exit relays only. The well-known lists of VPN and proxy ranges publish no licence, so they are not used. @@ -182,6 +186,8 @@ What does not work yet: meant to be run by hand, on a host that has one, before a release. A second, always-run test proves the address Caddy passes on is the IPv6 client's own, but only on the stack's unique-local subnet. +- **A published container image.** A release is a tag, and an install builds its image from + the checkout: see ["Upgrading"](#upgrading). How fast a redirect is, how to back an install up, how to upgrade it and what it exposes each have a section below. diff --git a/SECURITY.md b/SECURITY.md index 6459ee5..9cdb4db 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,7 +19,7 @@ instructions for attacking them. Include, as far as you have it: -- the version you are running; +- the version you are running (see "Supported versions" below for how to find it); - what you did, in enough detail to reproduce; - what happened, and what you expected; - what an attacker gets out of it. @@ -45,8 +45,16 @@ so in the first message rather than at the end of it. ## Supported versions -There is no release yet. Once there is, only the latest release is supported and -a security fix ships in the next release; this table will say so. +| Version | Supported | +| --- | --- | +| the latest release | yes | +| anything older | no | + +There are no backports: a security fix ships in the next release, and upgrading to it is the +remedy. See "Upgrading" in the README. The line `clickmonk version` prints is the +version to put in a report: + + docker compose exec -T worker node packages/cli/dist/index.js version ## Scope diff --git a/package.json b/package.json index 0451547..e13cd12 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "clickmonk", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "engines": { "node": ">=22 <23" }, "packageManager": "pnpm@9.15.9", diff --git a/packages/admin/package.json b/packages/admin/package.json index 7e5011d..08f3835 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/admin", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index 4f80737..1ee2c85 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/cli", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index 65ab7fe..f3252d7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/core", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 4aab571..ba26569 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -3,4 +3,4 @@ * same number, and version.test.ts holds them to it: a release bumps all of * them and this together. `clickmonk version` prints it. */ -export const VERSION = '0.0.0' +export const VERSION = '0.1.0' diff --git a/packages/db/package.json b/packages/db/package.json index 18b60c8..e1e8a6c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/db", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/ipdata/package.json b/packages/ipdata/package.json index 9713b5b..0eb2919 100644 --- a/packages/ipdata/package.json +++ b/packages/ipdata/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/ipdata", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/redirect/package.json b/packages/redirect/package.json index d83282b..4a0d3b0 100644 --- a/packages/redirect/package.json +++ b/packages/redirect/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/redirect", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/ui/package.json b/packages/ui/package.json index 7920130..e2d85a5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/ui", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "scripts": { diff --git a/packages/worker/package.json b/packages/worker/package.json index 7e34291..3654dd9 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,6 +1,6 @@ { "name": "@clickmonk/worker", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/index.js", From 182902858a89c3b59b9bfb4af5d54699918d6017 Mon Sep 17 00:00:00 2001 From: Cem Hurturk Date: Sat, 26 Sep 2026 19:38:20 +0000 Subject: [PATCH 2/3] docs: bulk operations are one link and one domain at a time, and install checks out the tag --- CHANGELOG.md | 2 +- README.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f91816..49240c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,7 @@ the release, not a footnote to it. tried without a real host name for the admin service. - No time zone setting for the install. The API works in UTC; the interface uses the browser's zone. -- No bulk operations: one link, one domain, one setting at a time. Most link settings are +- No bulk operations: one link and one domain at a time. Most link settings are the API's and the interface's, not the CLI's, and returning-visitor routing needs HTTPS. - No notifications: `GET /api/alerts` is what there is instead. A domain verified by hand is listed only once a check has passed for it and a later one has failed. diff --git a/README.md b/README.md index 1651d94..fa16dee 100644 --- a/README.md +++ b/README.md @@ -150,8 +150,8 @@ What does not work yet: through the admin API, or by hand in SQL without it. Returning-visitor routing also needs HTTPS to do anything: its cookie is marked `Secure`, so a browser drops it over plain HTTP. -- **Bulk operations.** One link, one domain, one setting at a time, in the web interface as - in the CLI ([#39](../../issues/39)). +- **Bulk operations.** One link and one domain at a time, in the web interface as in the + CLI ([#39](../../issues/39)). - **Proxy and VPN detection beyond Tor.** The anonymous class covers Tor exit relays only. The well-known lists of VPN and proxy ranges publish no licence, so they are not used. @@ -203,6 +203,7 @@ it), and ports 80 and 443 free. ```sh git clone https://github.com/ClickMonk/clickmonk.git cd clickmonk +git checkout v0.1.0 ./install.sh ``` @@ -531,7 +532,7 @@ admin API documented in ["The admin API"](#the-admin-api) and do, a `curl` command or the CLI can do too. **What it does not do yet:** there is no time zone setting for the install itself, only the -browser's own zone; no bulk operations — one link, one domain, one setting at a time, the same +browser's own zone; no bulk operations — one link and one domain at a time, the same as the CLI; and no local development mode, so trying it needs a real admin host set up, the same as the API does. See [#43](../../issues/43), [#39](../../issues/39) and [#23](../../issues/23). From 821a5b34d15fb07f26dec2d82adada2a8fca15d2 Mon Sep 17 00:00:00 2001 From: Cem Hurturk Date: Sat, 26 Sep 2026 19:38:20 +0000 Subject: [PATCH 3/3] release: the tag checkout is a counted release step, and the version test counts its packages --- CLAUDE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4edb59a..f80e9f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -381,7 +381,10 @@ pushing a tag creates none. The whole sequence, in order: The places: `"version"` in the root `package.json` and in each of the eight `packages/*/package.json`, and `VERSION` in `packages/core/src/version.ts` — ten in all. `packages/core/src/version.test.ts` fails if any of them disagrees, and counts the - manifests, so a new package without a version fails it too. + manifests, so a new package fails it until the count in the test is raised. + The README's "Installing" block checks out the release by its tag, and is bumped in the + same commit. The version test does not see it, so count it too: + `sed -n '/^## Installing/,/^## /p' README.md | grep -c 'git checkout vX.Y.Z'` prints 1. 3. Write the `CHANGELOG.md` entry: Added / Fixed / Changed, plus what the release still cannot do. Before writing it, read `git log --oneline --no-merges ..HEAD` against the changelog; merged work with no entry is how releases under-report.