Skip to content

Releases

Quillo v0.8.0

Choose a tag to compare

@github-actions github-actions released this 07 Nov 15:08

Upgrade from 0.7.0: see Security below. One visitor's data could reach another in 0.7.0,
in twelve ways, one of them a worker reading a finished request after it was freed. A key of
idempotent now runs at most once, whoever asks. Not in it, moved on: FEEDBACK43's database driver interface (Q-494 to Q-497), the
apps planned for 0.8 (the live dashboard and the others), and the studies FEEDBACK52, 54 and
56 to 59.

The fixes of FEEDBACK55.md's sweep, all of it: what a program writes into markup, the
files files() serves, HTTP's answers and cookies, highlighting and Markdown, the three
databases, components' fields, the server and the browser giving one answer, values that
are exact or an error, what runs at the same time, a program's files and Rust modules,
plurals, dates, numbers and images, and the command line: Q-700 to Q-852, and Q-875, found
while fixing (a map's order in the browser).

Security

  • One viewer's live answer could reach another, for up to two seconds, in 0.7.0 and
    before: upgrade.
    A live run is shared between viewers unless its code read the request.
    session() gave its kept copy without saying so, so the second loader of a page, or a
    loader after a route that read the session, went to the next viewer: their page showed the
    first viewer's data. t() and locale() did the same with the language. Both mark the run
    as the viewer's now; a loader reading nothing of the viewer's stays shared (Q-876).
  • quillo check warns of a redirect or respond nothing takes. In
    if no_user { redirect("/login") } with more after it, the redirect is made and left, and
    the route goes on: the visitor is shown the page. Write reply 303 redirect("/login").
    Look for this in your code: two of the sample apps had it (Q-877).
  • Eleven more ways one visitor's data reached another are closed (an audit after
    Q-876; in 0.7.0 and most in 0.6.0):
    • a view's cache kept a service's answer until restart, so one visitor saw another's
      Authorization, and one tenant another's data (Q-878);
    • a tenant told by a function, or named with with_tenant, was not in a run's key, so a
      tenant saw another's rows (Q-879);
    • request in a parameter, pattern or lambda default went unseen, and the run was
      shared (Q-880);
    • visitors were told apart by five fields, so X-Api-Key, ?token= or X-Tenant
      reached the next visitor from the same address (Q-881);
    • mounted programs shared runs (Q-882);
    • a worker kept a finished request, memory-unsafely: a job or a channel handler read
      the last visitor's query, and a stream could go to the wrong visitor (Q-883);
    • kept values answered to a hash anyone could work out, so a visitor could guess another's
      short secret offline (Q-884);
    • a kept run lost its marks, so a page lost Cache-Control: private (Q-885);
    • a page showing the session with a status of 400 or more, one read through
      get(request, ...) or a pattern, and a translated page had no private or Vary
      (Q-886);
    • an idempotency key was shared by every session without .user (Q-887);
    • a run with crypto.token, random or a write was shared, so two visitors got one
      token and a second write was skipped (Q-888).

What to change in your code

  • A run that read the visitor is shared with nobody, not even their other tabs, and one
    that did anything but read is not shared either. A loader for everyone is one run, as
    before, now per tenant too. Loaders calling a service, a site or a plugin stop sharing.
    Builtins are classified: one not named is neither kept nor shared (Q-881, Q-888).
  • An idempotency key runs at most once, whoever asks, the user's decision: a double
    charge is the worst outcome. With nothing in common the answer is 409, nothing run or
    shown, also after signing out or losing a cookie. Two visitors with one key used to each
    run; the second now gets 409
    : pass { user } (a str or an int; a record is refused) or
    use unique keys (Q-887).
  • Pages reading request.headers, request.cookies or request.ip, or passing request
    on, are private
    , whatever their status, and so are those under middleware that reads the
    visitor or cannot be read (Q-886).

Databases (each driver hides its database's quirks, so a value means the same on all three;
the user's decision, Q-736):

  • NaN and the infinities are refused as values on every database, naming them (value 1 is NaN, which cannot be stored): SQLite stored NaN as null, PostgreSQL kept them. -0.0
    is stored and read as 0.0
    everywhere (Q-735).
  • last_id is the one inserted row's id, else null: after an insert of several rows
    (SQLite gave the last, MySQL the first), an upsert that updated, an update (SQLite gave an
    old one) or a read. It is null on PostgreSQL as before: returning id there (Q-753).
  • changes is the rows matched on MySQL too, as on SQLite and PostgreSQL: an update
    that sets the value a row has counts it, and an upsert's or a replace's row counts once
    (?client_found_rows=false gives MySQL's own count). changes is 0 for a read and a
    schema statement, and a script's last statement's (Q-739, Q-753).
  • SQL with no statement ("", spaces, a comment) is an error value, from db.exec,
    db.query and db.one (Q-753).
  • An error value given as a parameter is given back, not stored as its JSON text, and a
    function is refused (Q-727). Placeholders given no values are refused on SQLite, which
    stored nulls (Q-729). A map given to SQL with no named placeholder is no value, and a
    map for a lone ? is that value, on all three (Q-747, Q-748).
  • On MySQL, @name with a map holding name is refused: it is the session's variable
    (null); write :name. SQLite's @name, $name and #name take the map's plain name
    (Q-752).
  • MySQL and MariaDB: a statement that commits by itself (truncate, create table,
    alter, ...) is refused inside db.transaction and db.exec_many
    , and one that could
    not be seen beforehand ends the transaction with an error naming it, where everything
    before it was committed (Q-728, the user's decision: MySQL only). Migrations take them as
    before.
  • MySQL and MariaDB: db.open of a database that is not there is an error, as on
    PostgreSQL; it made an empty one. A tenant's is still made, by the driver (Q-736).
  • MySQL and MariaDB values: BIT(n) reads as its digits ("00000101") and takes them
    back (Q-741); ids and unsigned values past 2^63 are decimals, not negative or floats
    (Q-740, Q-756); tinyint(1) is a bool only for 0 and 1 (Q-756); FLOAT reads as the
    shortest float that prints the same, and a value it would keep as 0 is refused (Q-744,
    Q-734); a statement past max_allowed_packet is refused saying so (Q-751); || joins
    text on MariaDB, in migrations and scripts too, where it was or.
  • PostgreSQL values: whole numbers in JSON past an int are decimals, and a JSON value that
    cannot be read is an error in its own cell, not a failed query (Q-732); a str written as a
    json/jsonb column's value is the JSON it holds, so json.stringify(m) stores an object
    (Q-733); a float past real's or double precision's range is refused (Q-734); real
    reads as the shortest float (0.1) (Q-744); BC dates read as ISO 8601 (-0043-03-15) and
    go back in (Q-743); a type with no reader is hex in every row, and timetz, pg_lsn,
    xid, xid8 read as PostgreSQL writes them (Q-742); bit(n) keeps its length (Q-730); an
    array not starting at 1 comes back as its text, keeping its bounds (Q-756); a value with no
    type in the SQL is declared int4, int8 or float8, so select ? as v with 5 is 5,
    not "5", and greatest(10, 9) is 10 (Q-731).
  • PostgreSQL: set_config(..., false) is refused outside a transaction in every form
    (with values, inside values (...), in an update, any case or quoting), and so are
    pg_temp tables; do blocks and calls of the database's own functions run on a connection
    of their own, reset after; every transaction's connection is reset as it ends (Q-708).
  • db.transaction refuses an option it does not have, at run time and in quillo check
    (has no option `isolaton` (did you mean `isolation`?)); a misspelt key ran with none
    (Q-738).
  • The guide's column-list update, set (${sql_name(cols)}) = (${sql_param(cols)}),
    fails on MySQL and on PostgreSQL with one column
    : write the pairs, a = :a, b = :b,
    built from the columns (the guide shows how) (Q-754).

Files and HTTP:

  • files() and quillo serve serve no SQLite database, whatever its name (by its
    header, -wal, -shm or -journal): the program's, a tenant's, another's. file("x.db")
    serves one on purpose, with ranges (Q-702, the user's decision).
  • A link inside files()' folder is followed only through build-output and package-store
    dot folders
    (.pnpm, .vitepress/dist, .output/public, .next/static,
    .svelte-kit/output/client, .nuxt/dist/client, .vercel/output/static, .well-known,
    and node_modules' .pnpm, .deno, .bun, .store); a file is judged by where it leads,
    and the program's own files (.env, --env-file, --tls-key, loaded sources, open
    databases) by device and inode too, so a hard link to one is a miss (Q-701).
  • write_file, save_upload, delete_file and image.convert refuse the program's own
    files in any case and Unicode form, and as Windows reads names
    , on every system (.ENV
    was written, then read as the settings) (Q-704); delete_file refuses a folder that holds
    an open database
    (Q-705); check_program reads only .qwe files (and .qwer/.rs
    signatures), no dot paths, and its messages show no file's text (Q-703).
  • A header's null in respond, stream and events is no such header, Quillo's own
    default included (`{ "cache-control": null }...
Read more

Quillo v0.7.0

Choose a tag to compare

@github-actions github-actions released this 04 Nov 17:41

Quillo 0.7.0 is the release where programs grow up: files have modules, money is a type, a server can live on shared hosting, and services authenticate one another. It also carries the fixes from the review reports since 0.6.0, FEEDBACK27 to FEEDBACK53, among them a full security review (FEEDBACK51, 37 issues). The test suite is 2,689 tests; the tests run 98.69% of the Rust lines and 99.86% of the browser runtime's.

Read the full changelog for every change, and Upgrading from 0.6.0 below before you update a running program.

Highlights

Modules. A file sees its own names, the builtins, and what it takes: use lib/forum takes a file's public names, import lib/forum use latest, Feed only those. quillo fix main.qwe writes the imports a program needs. For now a name a file did not take still works, with a warning, so a program can move one file at a time.

Money. money is a type, held to money.of's rules wherever a value is read. a + b on two amounts adds them, and two currencies give an error value. money.parse(text, currency, locale) reads "1.234,56 €" as a German visitor writes it, and money.format writes what Intl.NumberFormat writes, for 122 locales, the same on the server and in the browser.

Shared hosting. quillo run --listen unix:PATH and quillo fcgi run behind Apache and cPanel. A policy file limits what a program may do. The Linux binaries are now linked against glibc 2.17, so they start on CentOS 7, CloudLinux 7 and 8, AlmaLinux 8 and anything newer.

Services. A service can be another program, possibly on another machine.

  • A caller proves who it is with a key of its own, a bearer or OAuth token, a JWT or a client certificate.
  • server fn f() for ["shop"] takes calls from those callers only.
  • An app can call a service whose source it does not hold.

Pages and components.

  • value={state} is two-way, and onclick={...} is a handler, as in React.
  • Markup is read as JSX and HTML write it.
  • defer(...) streams a slow part of a page after the rest.
  • use Chart from "https://..." brings a browser library in by name.
  • Prefetching is on for links.
  • Live data holds a thousand open pages.
  • <form idempotent> runs a form sent twice once.

Also new.

  • Phone apps, in preview: a program inside an iOS or Android app.
  • db.exec_many.
  • Query timings at /_quillo/health/queries.
  • --max-connections, --connections-per-client and --allowed-hosts.
  • Editors: the VS Code extension, and a tree-sitter grammar for Zed, Neovim, Helix, Sublime and Emacs, in quillolang/editors. quillo lsp offers a file's names as an import line is typed.

Security

A full review of Quillo found 37 issues (FEEDBACK51). Every one is fixed, a few of them in part, as the changelog says. The ones a running program is most likely to notice:

  • Only a site declaration makes a site. A map with a $site key no longer turns a method call into a request.
  • Channels and media streams are each tenant's own. A stream is watched and sent only as its media.on rules say.
  • navigate and redirect refuse javascript: URLs.
  • <style> and <script> values cannot make markup inside <svg> or <math>. CSS values in css templates and style maps cannot add rules.
  • http.* follows each redirect itself. The program's headers stay with their origin, and public_only is checked at every hop.
  • Middleware fails closed (below), and an accept_calls check that gives null refuses the caller.
  • run_script gets Quillo's own builtins and gives back no function. JSON has a budget of values, and comparing, printing or writing a deeply nested value no longer overflows the stack.

Upgrading from 0.6.0

These change what an existing program does. The changelog lists them all.

  • Middleware gives back the request to let a route go on. null now means the route does not match, so the next route is tried, and with none it is a 404. false answers 403. Any other value, such as true, text or a map like { error: "..." }, is a 500 that names the middleware. A middleware that only logs ends with r.
  • Channels and media are per tenant. Pages on different tenants no longer share a room. Use { tenants: "shared" } for one room for every tenant. Add media.on(name, { watch }) for every stream pages watch, an RTP camera's included, and publish for every stream pages send.
  • db.exec("begin") and the like are refused. Write db.transaction(() => ...).
  • Typed path, query and form values take one spelling. 05, +5 and NaN no longer match int or float.
  • Markup follows JSX and HTML.
    • An attribute's "text" is plain.
    • onChange runs on each keystroke.
    • {true} shows nothing.
    • A number in a style map is in pixels.
  • A Rust file says its effects. It needs //! effects: ... to be called inside a with function.
  • /_quillo/ready shows each check only with the health token (QUILLO_HEALTH_TOKEN). Without it, it says only whether the server is ready.
  • A static file from a visitor's path goes through files("public", path). quillo check now warns about file("public/" + path).
  • Under --watch, the server answers only localhost, IP addresses and --allowed-hosts.

Downloads

System File
Linux, x86-64 (glibc 2.17 and newer) quillo-v0.7.0-x86_64-unknown-linux-gnu.tar.gz
Linux, ARM (glibc 2.17 and newer) quillo-v0.7.0-aarch64-unknown-linux-gnu.tar.gz
macOS, Apple silicon quillo-v0.7.0-aarch64-apple-darwin.tar.gz
macOS, Intel quillo-v0.7.0-x86_64-apple-darwin.tar.gz
Windows quillo-v0.7.0-x86_64-pc-windows-msvc.zip

Each has a .sha256 beside it: shasum -a 256 -c quillo-v0.7.0-aarch64-apple-darwin.tar.gz.sha256.

The macOS Apple silicon build was made on an Apple silicon Mac from this tag's source (git archive v0.7.0, cargo build --release --locked), as GitHub's macOS runners gave its job none three times; the other four are the release workflow's.

To start a new program, use the starter template. Its get-quillo.sh fetches this release and checks it.

Documentation

Not in this release

These move to 0.8:

  • Database drivers as plugins (FEEDBACK43, Q-494 to Q-497).
  • The apps planned for 0.7, starting with the live dashboard.

Two security items stay partly open, as the changelog says:

  • Islands are not authenticated (Q-667). Visitor HTML must not keep data-qi and the other island attributes.
  • Very deep values (Q-661). Dropping a value nested about a million levels deep can still overflow the stack.

Quillo v0.6.0

Choose a tag to compare

@github-actions github-actions released this 29 Oct 11:02