Skip to content

Releases: runlightsh/runlight

Runlight 0.1.2

Choose a tag to compare

@github-actions github-actions released this 12 Oct 15:34

Security

  • Requests to a service someone set up (the AI assistant's API, every HTTP mail service, and a mail webhook) follow redirects by hand, at most five, and check each one against link-local and cloud metadata addresses, so a redirect can no longer lead to a metadata service. A refused address answers address_refused. The public refuseMetadata helper is gone from the assistant in PHP, Python, Ruby, Go, Java, .NET, and Rust, since the fetch checks every hop itself.
  • A fetch pinned to the address Runlight checked never goes out unpinned: TypeScript gives Node's https its own agent, so an environment proxy cannot look the name up again, and PHP's CurlFetcher and the other languages' fetchers refuse a pin they cannot apply, a pin for another host, or a redirect to an unpinned host, and ignore environment proxies for pinned requests. The WordPress, Drupal, and Craft fetchers refuse a transport or proxy that would skip the pin, and follow redirects by hand.
  • The goal picker no longer gives the tracked site's tab a handle on the dashboard, which let any script on the site send the dashboard tab somewhere else. The dashboard opens the site cut off from itself, and the picker sends its choice to a relay page at pick-return under the dashboard's own address, in the fragment so it never reaches a server. The picker's ticket names that page and a nonce, signed, and the page hands the choice to the dashboard tab that made the nonce over a channel only that origin's pages share. A hub picking on a site whose own install is older asks for that install to be updated first.

Fixed

  • Connect works against a Runlight whose one site lists no domains, which counts any host: its consent page offers that site instead of asking to add one.
  • Waiting for another process creating or upgrading the tables has a limit. After 30 seconds a request answers 503 with the code tables_busy and Retry-After: 30, and a tracker hit waits at most 10 seconds for the tables and is then dropped, so requests no longer pile up behind a long index build. On Postgres, an index left unusable is rebuilt under its own name when the table prefix itself starts rl_.
  • A new SQLite file is readable and writable by its owner and group (0660), and a folder Runlight makes for it by its owner and group (0770), in every language and in the WordPress plugin, so a cron job or CLI command running as another user of the same group can still open it. PHP and the WordPress plugin made them 0600 and 0700, and the other languages left them to the umask.
  • Java's app mailer answers an empty via rather than null, Rust refuses mail and app_mailer set together as Go does, and an app mailer in Rust that returns runlight::Error::Mail has its error passed on as it is.
  • Running inside Craft with trustedHosts left at its default, which believes forwarded headers from anyone, the plugin works out each visitor's address itself, as the WordPress plugin does. A site that sets trustedHosts gets Craft's own answer.
  • The dashboard has words for mail_url, mail_port, busy, and tables_busy in every language, and the mail_service refusal says the service is not offered here.
  • The dashboard has words for embed_token, embed_origin, embed_host, and token_embed_only in every language, so a refused dashboard key says what to do in the dashboard's language.
  • The prompt to copy for a custom link domain in Settings, Custom domains covers every language and framework Runlight runs in, from Next.js and Express to Laravel, Django, Rails, Go, Spring Boot, ASP.NET Core, Phoenix, and axum, and says no code is needed for a standalone server.
  • The dashboard's Powered by Runlight link opens in a new tab everywhere, so it works inside a CMS's admin too.
  • Removing a saved mail service in Settings, Email service says why when it fails, and an app's mailer is shown with its sender's name.
  • A broken percent escape in a path, such as %E0, answers as an unknown goal, share, site, link domain, or short link (a 404) instead of an internal error.
  • On a link domain, /go/<name> is a 404 like any other path with no link of that domain's, in the standalone server of every language that has one too, which redirected it to any site's link of that name.
  • In Laravel and Symfony, the /go/{slug} route through HttpFoundation::handle raises the framework's own not-found for a short link nobody has, so the app's 404 page answers it, as it does behind ShortLinks.
  • .NET: MapRunlight() inside UseEndpoints on a Startup-style host answers link domains, which it left to the app before. On an endpoint builder that reaches no middleware at all it refuses to start and says to use UseRunlight().
  • Elixir sorts names the way TypeScript does with punctuation in them, such as a-b and a_b, and reads a whole number past 2^53 in a location database as its decimal text, as the other languages do.

Added

  • The WordPress plugin runs Runlight inside WordPress: activate it, choose that on its setup screen, and the site is counted, with the dashboard at /runlight on the site for anyone who can manage options, AI agents recorded on the server, and the hourly check on WordPress's cron. The numbers go in WordPress's own MySQL 8.0.20 or MariaDB 11.4 database, through $wpdb, in tables named after WordPress's table prefix such as wp_rl_events, unless tables named that way are already there and are not Runlight's, or else in a SQLite file in uploads, which each site in a network always uses. Deleting the plugin drops those tables by name, and on a network removes what it kept for every site. Countries can come from DB-IP's free data, downloaded once a month when turned on. Sending to a Runlight elsewhere is the other choice in its settings. The plugin carries the PHP package at vendor/runlight/runlight, as Composer lays it out, leaving out the parts WordPress never runs, so it passes the plugin directory's checks, and running it this way needs PHP 8.2 with the mbstring and openssl extensions and, without MySQL, sqlite3.
  • Running inside WordPress, the plugin works out each visitor's address itself and hands it to Runlight, with trustProxy off: the connection's address, unless that is this server or a private network, where the nearest public address in X-Forwarded-For counts, and Cloudflare's CF-Connecting-IP only from Cloudflare's own addresses. The runlight_client_ip filter can change it. It hands Runlight the request without the slashes WordPress adds to $_SERVER. It offers no SMTP server in Settings, Email service, since an SMTP plugin covers it and Runlight sends through that plugin. It keeps rate counts and the site's icon in WordPress's object cache, or else in transients, where a minute's counts and the day's key take one each, so nothing piles up in wp_options even with WordPress's cron off. It downloads, unpacks, and reads the location data through WordPress's HTTP and filesystem APIs. With plain permalinks it answers at /index.php/runlight, which works on every server, and its settings page says so, and says when the hourly check has not run for a day. When wp-admin is served from another address than the site, its Runlight page opens the dashboard in a new tab, since only the site's own address may frame it.
  • The Drupal module and the Craft plugin run Runlight inside the CMS too: choose that on their setup screen, and the site is counted, with the dashboard at /runlight on the site and under Reports, Runlight in Drupal or the Runlight item in Craft's Control Panel, and short links at /go/<name>. The numbers go in the CMS's own Postgres, MySQL 8.0.20, or MariaDB 11.4 database, in tables named after its table prefix such as craft_rl_events and marked as that site's, unless tables named that way are already there and are not its own, or else in a SQLite file in Drupal's private files or Craft's storage, never in public files. Uninstalling drops those tables by name, and only that site's, even when Craft's storage folder is gone. Drupal opens Runlight's connection through its own database driver, so TLS options and init_commands apply, and Craft reads the choice from the database, so it holds on every server, and derives Runlight's secret from its security key. Drupal's Administer Runlight permission owns Runlight and a new View Runlight permission reads it; in Craft, admins and a new Manage Runlight permission own it and See Runlight's numbers reads it. Changes to Runlight are taken only from the site's own pages. Each visitor is counted by the address the CMS works out (Drupal's reverse_proxy settings, Craft's trustedHosts and ipHeaders), with trustProxy off, rate counts go in the CMS's cache, notes go to its log, and no environment variable is read. Drupal's cron, or Craft's queue or php craft runlight/check, runs the hourly check, and the settings say when it has not run for a day. Both need PHP 8.2, and Composer installs the PHP package with them.
  • Drupal keeps where Runlight runs, its address, and both keys in state rather than exported configuration, with settings.php overrides still winning, and Craft's Connect writes the keys into .env and saves the settings as $RUNLIGHT_AGENTS_KEY and $RUNLIGHT_DASHBOARD_KEY, so no key reaches a repository. Connect in every plugin shows why a Runlight could not be reached, and treats an address with its default port, such as https://stats.example.com:443, as the same address.
  • The WordPress, Drupal, and Craft plugins connect to a Runlight with a Connect button: enter its address, allow the connection on its consent page, and the plugin saves the site's id, agents key, and dashboard key itself. This works through a new cms OAuth scope, whose token is the site's dashboard key and whose answer also carries the site's agents key. When no site counts the plugin's domain, the consent page offers to add one, where sites are managed in the dashboard.
  • A cms opti...
Read more

Runlight 0.1.1

Choose a tag to compare

@github-actions github-actions released this 10 Oct 23:52

This release fixes the findings of a full security and code review of every implementation, plugin, and release workflow.

Security

  • Sign-in no longer sends people to another site when the next path resolves to one starting //, such as /..//example.com.
  • Sign-in limits and two-factor code limits hold under parallel requests in the PHP, Python, Ruby, Java, and .NET versions, which kept their counts in the database and could be raced past. One-time sign-in links, recovery codes, and two-factor codes are now single use across workers on SQLite too.
  • Sign-in counts tries per address across accounts before checking a password, checks only a few passwords at once, and answers busy with a 503 past that, so a flood of sign-ins cannot exhaust memory or CPU.
  • Counts kept per address no longer hold anything an IP address could be worked out from after a day: the key they are hashed with changes daily and old keys are deleted.
  • Tries on a decoy two-factor step no longer count against the real account, so nobody can keep someone else locked out.
  • Invites are deleted when the person who sent them is removed or demoted, and accepting one checks that its sender may still invite.
  • OAuth codes and connect requests are taken atomically, so a code works once.
  • The Elixir, Ruby, and .NET versions turn off MySQL's NO_BACKSLASH_ESCAPES mode on every connection, as the TypeScript library does. On a server with that mode on, a tracking hit could otherwise have injected SQL.
  • Deeply nested JSON can no longer crash the .NET version, and a JSON object with many keys can no longer tie up the Rust version.
  • The Rails engine checks a form posted with _method as the POST it is, so it cannot get past the JSON-only rule for changes.
  • The dashboard no longer opens to anyone because MIX_ENV is unset (Elixir) or the token is None (Python, which now uses runlight.OPEN for an open dashboard). Ruby asks for a shared secret when accounts are on outside development.
  • Imported short links pass the same domain and destination checks as new ones, so an import cannot take over the site's own address.
  • Visitor ids from a CSV import are keyed with a secret made for that import and never stored, so a visitor column holding IP addresses or emails cannot be recovered.
  • Share links and embedded dashboards keep the number of people here now but no longer get the feed of single visits.
  • The assistant's address and the SMTP server refuse link-local and cloud metadata addresses. Embedded dashboards need an https admin origin on its own port, except on localhost. Report links never use an address sent by the person setting them up.
  • Outgoing requests are capped while they are read, drop credentials when a redirect changes origin, refuse https to http, and ignore proxy settings that would get around the check on where they connect.
  • SQLite databases turn on secure_delete, so deleted daily salts do not linger in the file.
  • The PHP drop-in refuses to start when its data folder could be downloaded, and its README shows how to deny it in nginx and Caddy. Files Runlight makes are readable by the server's user alone.

Fixed

  • A session cookie that is not valid percent-encoding reads as signed out instead of failing with a 500.
  • MCP batches hold at most 20 messages.
  • The Go server can no longer crash when work arrives while it waits for background work, and it carries its own time zone data, as the .NET version now does.
  • Python closes its database connections, and Java caps its connection pool at ten. A cancelled request in Rust no longer leaves a transaction or lock open in the pool.
  • Ruby no longer freezes waiting for a database lock under Rails' query cache, and a Runlight failure no longer breaks the app's own pages.
  • The assistant's limits are checked and counted together in the Go, Java, .NET, and Rust versions, and Rust counts them per person.
  • The WordPress plugin reports AI agents after the page has been sent, with the right address on sites in a subfolder. Its settings page, and Drupal's, check the Runlight address after they load.
  • The Next.js instructions in the dashboard show middleware.ts for Next.js 15 and earlier.
  • Releases check that the branch matches GitHub before tagging and push the branch and tag together. The Docker image installs exactly what the lockfile names and is started once before it is published.

Runlight 0.1.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 16:44

This is Runlight's first release.

Added

  • @runlight/sdk is privacy friendly web analytics that runs inside an app you already have. It counts visitors without cookies and without storing anyone's IP address, keeps the numbers in your own SQLite, Postgres, MySQL, MariaDB, Turso, or Cloudflare D1 database, and serves its dashboard at /runlight on your own domain. It runs on Node 22 or later, Bun, Deno, and Cloudflare Workers.
  • runlight.sh is the same code as a server of its own, with one dashboard for any number of sites and sign-in for the people you invite. It starts with npx runlight.sh, and the Docker image is ghcr.io/runlightsh/runlight.
  • runlight/runlight is the PHP version of the library, for Laravel, Symfony, or plain PHP on PHP 8.2 or later. It uses the same tables as the TypeScript library, so either one can read the other's database.
  • Runlight counts goals and the revenue they bring, funnels, and the paths visits take through a site. It makes short links on your own domains and counts their clicks.
  • Runlight emails weekly or monthly reports and shares a read-only dashboard by link.
  • An assistant in the dashboard, and an MCP server for AI apps, answer questions about your numbers.
  • runlight on PyPI is the Python version, for Django, Flask, FastAPI, or any WSGI or ASGI app on Python 3.11 or later, with a standalone server of its own. It answers exactly as the TypeScript library does and shares its tables.
  • The runlight gem is the Ruby version, a Rails engine with an install generator for Rails 7.2, 8.0, and 8.1, or a Rack app or middleware anywhere else, on Ruby 3.2 or later. It also answers exactly as the TypeScript library does and shares its tables.
  • The runlight.sh/go module is the Go version, for net/http, chi, Echo, or any router that takes an http.Handler on Go 1.25 or later, with a standalone server and a runlight command of its own. It answers exactly as the TypeScript library does and shares its tables.
  • runlight on Hex is the Elixir version, for Phoenix or any Plug app on Elixir 1.18 or later and Erlang/OTP 27 or later, with its tables in your own Ecto repo. It answers exactly as the TypeScript library does and reads and writes the same tables.
  • sh.runlight:runlight on Maven Central is the Java version, for the JDK's own HTTP server, Tomcat, Jetty, or any Jakarta Servlet 6 container, or Spring Boot 3.5 or 4, on Java 21 or later, with a standalone server of its own. It answers exactly as the TypeScript library does and shares its tables.
  • Runlight on NuGet is the .NET version, for ASP.NET Core or any other .NET app on .NET 10 or later, with a standalone server that installs as the runlight tool. It answers exactly as the TypeScript library does and shares its tables.
  • The runlight crate is the Rust version, for axum, hyper, or any tokio server that takes a tower service, on Rust 1.88 or later, with its tables in your own database through runlight-sqlx, which needs Rust 1.94 or later. It answers exactly as the TypeScript library does and shares its tables.
  • Plugins for WordPress, Drupal, and Craft CMS add the script to a site and report the AI agents that read its pages to a Runlight you run elsewhere. With a dashboard key, each one shows the site's read-only dashboard inside the CMS's admin pages.