Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 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.
- 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.
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -377,7 +377,14 @@ 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 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 <lasttag>..HEAD`
against the changelog; merged work with no entry is how releases under-report.
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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/`.

Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 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.
Expand Down Expand Up @@ -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.
Expand All @@ -197,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
```

Expand Down Expand Up @@ -525,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).
Expand Down
14 changes: 11 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/admin",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/cli",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/core",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/db",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/ipdata/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/ipdata",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/redirect/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/redirect",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/ui",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/worker/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickmonk/worker",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
Loading