Skip to content

v0.2.10

Choose a tag to compare

@github-actions github-actions released this 31 Aug 09:00
· 95 commits to main since this release

Give the response ceiling a code instead of a message to grep

The ceiling threw a plain Error with [E_RESPONSE_TOO_LARGE] spelled into its
message, so an application deciding what to do with it had to match on a
string. It is a ReamError now, with the code where every other framework code
lives, the byte count and the ceiling in context, and the way out in the hint.

ReamError's own doc comment offered ATLAS_QUERY_ERROR and CONTAINER_NOT_FOUND
as examples of a code, neither of which carries the E_ prefix every real one
does — an example is where a convention is learned.

Apply the response ceiling to every path that builds a body

maxResponseBytes was enforced in sendBuffer() alone, so json(), send() and
jsonp() walked past it — and those are the doors a response actually comes
through. The failure the ceiling exists to name (a process that grows until it
dies, with nothing said about why) came back unchanged.

Every textual assignment goes through one checked setter, measured in UTF-8
bytes rather than characters: accented or CJK text costs one and a half to
three times its length on the wire.

Make serialize() the dump upstream reports, and name the safe view

serialize() withheld the body and the cookies and redacted the credential
headers, which is right for a log line but is not what upstream returns. It
now carries the whole request — body, cookies, every header verbatim — under
the shape upstream documents: id, url, query, body, params, headers, method,
protocol, cookies, hostname, ip, subdomains.

The log-safe view keeps its behaviour under serializeSafe(), which upstream
has no counterpart for. Both are pinned side by side: the same secret shows
up in one and not in the other.

Load APP_KEY before wiring the services that read it

The signer was built in the constructor, from process.env, while .env was
only read at the top of start(). A scaffolded app keeps its key in .env and
nowhere else, so no signer was ever registered and every signed cookie and
signed URL was refused.

Also brings the lifecycle in line: providers start before the preload files
that reach for them, and the application is marked ready once the server
listens rather than at the end of boot, so a health check cannot green a
process with no socket.

NODE_ENV is normalised in one place and read through it everywhere. Nine
sites compared the raw value, including the Secure flag on the session
cookie and the switch behind development error pages; NODE_ENV=prod read as
'not production' at every one of them. inDev is now an exact match, so an
unconfigured or staging machine no longer starts the reload watcher.

cookie() signs the cookie's NAME along with its value and refuses to write
without a key instead of silently sending it plain; request.cookie() answers
nothing rather than handing back an unverified value. serialize() redacts
the headers that carry a credential. Domain routes match a host
case-insensitively. warmUp() assembles an application without running it,
and repl is an environment of its own.

Discover a scheduled task declared in a module

A @Service() carrying @Schedule in app/modules/ — the natural place —
was never registered, and nothing said so. The registry was read when
providers boot; app/modules/** is auto-loaded during the start phase, after
that. The application started normally and the task simply never fired: no
error, no warning, nothing in the log.

The registry is now walked at start as well, once the modules are in place,
and discovery is idempotent per task name so the two passes cannot register
anything twice. A service declared earlier — in a provider, in a preload — is
in the registry by then too, so nothing is lost by looking again.

Reported by a consumer who watched a minute-cron produce zero fires in
seventy seconds.

Refuse an upload that claims a type its bytes cannot carry

An allowlist that accepts a claimed type is not an allowlist. When magic-byte
detection finds nothing, both halves of what validate() checked came from the
client: the extension off the filename, the mime off the header. A shell script
uploaded as avatar.png with Content-Type: image/png therefore passed
extnames: ['png'].

Upstream does the same — computeFileTypeFromName(clientName, headers) — so
this is a deliberate deviation, and a narrow one. The strictness comes from the
allowlist itself rather than from a new flag: when every allowed extension is
one the detector would have recognised, finding nothing PROVES the file is not
among them, and it is refused. When the list includes a format that carries no
signature — csv, txt — finding nothing is the normal case and says nothing,
so nothing changes.

That closes the hole without breaking a single legitimate upload, and needs no
decision from the caller. typeSource says which of the two answers mime and
extname are giving, for an application that wants to decide for itself.

Mention LDAP in the package list

Mention SAML in the package list

Mention OpenID Connect in the package list

List Transit among the packages of the universe

Ship the scheduler lock a second replica needs

The scheduler locks nothing by default and shipped only a memory backend, so
a horizontally-scaled application ran every task on every instance: the daily
invoice run went out N times, the reminder email arrived N times, and nothing
in the logs said so. The header even directed Redis backends to user-land.

locks.redis({ connection: 'main' }) fills it, over a @c9up/quasar
connection resolved by name at the first fire — ream imports nothing, so
quasar stays optional — or over any client answering set and eval.

The lease is taken with SET NX PX (one atomic round trip, so every instance
gets the same answer) and released through a compare-and-delete script. A
plain DEL is the classic way to break this: a task that outlives its TTL loses
the lock, another instance acquires the name and starts running, and the first
then deletes THAT lease on its way out.

config/scheduler.ts is now read by ScheduleProvider, which had no config
path at all — lock takes a backend or a factory, because the file is loaded
before the connection it names exists.

Read the session store key the way it is written elsewhere

Both store and driver were accepted, but the type presented driver
first and called store the foreign spelling. It is the other way round:
store is the key a session config uses, and driver is ream's older name
for it. The docs already read that way; only the type did not.

No behaviour change — store already won at resolution.

Also moves the exported VERSION to 0.2.10 with the manifest. It had stayed
at 0.2.9, which the export-map test catches — anything reading the constant
to report a version would have named the release before this one.


Changes since v0.2.9.