Repository navigation
Releases
Spernakit v3.47.6
Other changes
- v3.47.6 - security fixes and derived-runner fix (#52) (
7a3a0c6)
1 commit since v3.47.5.
Full Changelog: v3.47.5...v3.47.6
v3.47.5
Documentation
- release: v3.47.5 - runner-delivered gate, auth hardening, advisory fixes, site identity (#51) (
55bfd7e)
2 commits since v3.47.4 (1 omitted as build/chore/ci plumbing).
Full Changelog: v3.47.4...v3.47.5
v3.47.4
[3.47.4] - 2026-09-28
Patch release. The notification bell shows only notifications, sign-out audit rows name the user
who signed out, the command palette builds its settings and profile entries from the same tab lists
the pages use, and the launcher waits for a slow backend instead of calling it a failure. The leak
guard gains a per-repository list of private names a public site may publish.
Fixed
- The notification bell no longer fills with dashboard and file events. The WebSocket dispatcher
now hands each channel handler the full message, and the bell acts only on messages of type
notification. - Sign-out audit rows are attributed to the user who signed out. The session is resolved before its
tokens are revoked, where previously the row was written after revocation and carried no user. - The command palette builds its settings entries from
settingsTabsand its profile entries from
profileTabs, so a derived app that swaps a tab no longer gets a palette entry for a route it
removed and none for the one it added. The demo account buttons keep their labels on one line. bun run startwaits up to 30 seconds for each server, tells a process that exited apart from
one still starting, and reports the actual failure. A cold backend start that took longer than
the old 10-second limit was killed and reported as a failed start.- The fast
qcsubset that runs at commit time now says it ran 4 of the qc steps, so its success
line no longer reads like a fullsmoke:qcpass.
Changed
- The settings area's minimum role is one exported constant,
SETTINGS_AREA_MIN_ROLE, read by the
route guard and the command palette alike. - The commit-time leak guard reads an optional, tracked
.githooks/leak-guard-publisheslisting
private names this repository is cleared to publish. The list is read from the staged index, an
entry must match a private pattern in full, it never exempts the secret-format or home-path
checks, and each use is announced. The template lists no names, so its behavior is unchanged.
Full Changelog: v3.47.3...v3.47.4
v3.47.3
Patch release. The commit-time leak guard reads its private pattern file from the Windows
user-level setting when the environment does not carry it, and it now stops a commit when a
configured pattern file is missing or unreadable instead of passing with a warning. Dependencies
move to current versions and the formatting configuration converges with the fleet standard.
Fixed
- The leak guard resolves
LEAK_GUARD_PATTERNSfromHKCU\Environmentwhen the variable is not
set, before falling back to~/.config/leak-guard/patterns. A process started from a parent that
predates the user-level variable never inherits it, and such a session previously committed with
the private tier silently off. The seeder resolves the path the same way, so it cannot seed a
second file at the default location. - A configured private pattern file that is missing or unreadable now stops the commit. Readability
is judged by grep's exit status on the actual read, because a permission check can report a file
readable when Windows access control denies the open.LEAK_GUARD_ALLOW_NO_PRIVATE=1lets a
single commit through deliberately. A machine with no pattern file configured at all still warns
and runs the generic checks, so a freshly scaffolded project can make its first commit. - The hit message no longer suggests
git commit --no-verify; a confirmed false positive is the
user's call. - The scaffold copies of
leak-guard.shandleak-guard-setup.shmatch the root copies again.
Changed
- Production and development dependencies move to current versions:
drizzle-orm0.45.3,
drizzle-kit0.31.11,lru-cache11.5.3,@tanstack/react-query5.104.0,lucide-react
1.48.0,@types/node26.6.3,vite8.3.1,eslint10.11.0,eslint-plugin-jsdoc64.5.4,
eslint-plugin-perfectionist5.12.1,knip6.38.0,puppeteer25.12.0,typescript-eslint
8.70.1, andprettier3.9.9, with the lockfile and license notices updated to match.
eslint10.11.0 matches aidd again. - The GitHub Actions workflows use
actions/deploy-pages5.0.1 and the current
github/codeql-action/upload-sarif. - Line endings, indentation, width, and trailing commas agree across
.editorconfig,
.prettierrc, and.gitattributes. The*.ps1/*.psm1/*.psd1CRLF exception, which
contradicted the LF settings and was never in effect, is removed, five duplicated
.gitattributesrules are dropped, and*.svgis treated as text so it diffs. Formatting output
is unchanged.
Full Changelog: v3.47.2...v3.47.3
v3.47.2
Fixes
- release: v3.47.2 - MFA token transport, upload signatures, secret redaction, and crawler route sampling (#44) (
fb26cae)
1 commit since v3.47.1.
Full Changelog: v3.47.1...v3.47.2
v3.47.1
Fixes
- release: v3.47.1 - harden authentication, secrets, and quality gates (#42) (
6e962b1)
1 commit since v3.47.0.
Full Changelog: v3.47.0...v3.47.1
v3.47.0
[3.47.0] - 2026-09-11
Minor release. Derived applications now detect quality checks missing from an overridden smoke
runbook. The fleet testing fixes cover authorization ordering, workspace errors, form state,
keyboard focus, validation messages, metrics, and health reporting.
Added
check:smoke-stepscompares each application's smoke commands with its declared template
version, including overridden runbooks. It reports missing template steps and commands with no
package script. The check runs without caching so fetching a previously missing template tag
makes the comparison take effect immediately.
Fixed
- Every authorization guard runs before body validation, including workspace membership and role
checks. Requests that should be refused no longer receive schema details first. - Workspace listings return 404 for an unknown workspace, and adding a member returns 404 for an
unknown user. Unauthorized callers continue to receive the same refusal regardless of existence. - Editing a workspace preserves typed names. Dialog forms remain mounted while a submission is
pending, and closing a dialog opened from a table row returns focus to that row's menu button. - Rejected URL filters leave the page usable with an error message and are not retried.
- Username validation agrees between the browser and API. Password rechecks identify the current
password as the one refused, and union validation names the accepted values without echoing input. - Authentication settings display the pre-boot rate-limit kill-switch alongside the editable
setting. Tables display their actual page size, and onboarding links to the password form's page. - Dashboard history spans the requested interval. Memory health checks use the process's available
memory limit and configured thresholds. - The create-user dialog has a complete description. The stylesheet no longer opts into
cross-document view transitions. - The Vite proxy connects to the address family the backend binds instead of relying on localhost.
- Documentation checks reject links to gitignored files, which are unavailable in a fresh clone.
- Error-log verification waits for the logging transport to flush and the probe to finish within
its existing timeout, fixing failures on a cold dependency cache. - The current-password regression check provisions its own temporary MFA keys, so it exercises
password rejection in a fresh checkout as well as on a configured workstation. - Container license inventories record package names and licenses without pinning live Alpine
package versions. Each built image retains and verifies its exact installed-version inventory.
Changed
- Updated React and React DOM to 19.3.0, Vite to 8.3.0, lucide-react to 1.45.0, nodemailer to
10.0.6, the React type packages to 19.3.0, and eslint-plugin-jsdoc to 64.3.9. The lockfile and
generated license notices follow those versions. - Regenerated frontend size budgets from the React 19.3 production build. A controlled comparison
with the prior React runtime measured 29,324 additional JavaScript bytes, 7,161 additional
first-load Brotli bytes, and 7,899 additional first-load JavaScript gzip bytes. The critical-path
budget retains its 2 KB allowance; React preload and import-waterfall checks remain enforced.
Full Changelog: v3.46.0...v3.47.0
v3.46.0
Features
- release: v3.46.0 - verifiable release screenshot captures (#36) (
65246a1)
Fixes
- release: make a release capture verifiable end to end (#37) (
0f62393)
2 commits since v3.45.0.
Full Changelog: v3.45.0...v3.46.0
v3.45.0
Minor release. It carries the findings the v3.44.3 fleet sweep produced across all ten derived
applications, plus three new capabilities that came out of them. The largest change is structural:
authorization now runs before body validation on every route, and a new gate proves that every route
which does not refuse an anonymous caller is on a list that says why.
Added
- The audit log can be filtered by outcome.
GET /api/v1/audittakesoutcome=failedor
outcome=succeeded, and every row carries the status the writer recorded and the username the
request named. A rejected sign-in never authenticates, so those rows used to render asSystem
and were indistinguishable from background activity; the viewer now shows the account that was
attempted and the status as a badge beside the path. Both fields are reachable through field
selection, so an operator can alert on a burst of failures. - A dashboard share link can be revoked.
DELETE /api/v1/dashboards/:id/shareclears the token and
its expiry, andGETon the same path reports whether a link is live and when it stops working.
Revoke deliberately ignores thesharingEnabledsetting, because turning sharing off must not
strand links that were issued while it was on. The Share dialog shows the current state and offers
Revoke or Create new link. - A bug report can name the report that supersedes it.
PUT /api/v1/bugs/:id/superseded-bysets the
link andnullclears it, open to the reporter as well as to an administrator. Self-links, cycles
of any length, and successors naming no report are refused before anything is written, and the
link never touches either report's text or status. The default inbox leaves superseded reports out
of its rows and its total, and takesincludeSuperseded=trueto bring them back. test:public-route-surfacejoinssmoke:qc. It boots the application, enumerates every route
Elysia registered, and sends each one an anonymous request. Anything that answers has to appear in
scripts/lib/public-routes.tswith a written reason. Authorization travels as a route option, so a
route ships open by leavingrequireAuthoff and nothing looks wrong in the file; three route
modules hold guarded and unguarded handlers side by side, which is why this asks the application
rather than reading the source. The list is checked in both directions, so an entry naming a route
that no longer exists, or one whose route is guarded now, fails rather than sitting there as an
exemption nobody is using.
Security
- Authorization runs before body validation. Elysia validates a request body against the route schema
before it callsbeforeHandle, so guards living there ran too late: an anonymous caller who posted
a malformed body to a protected route was answered 400 with the field names and constraints of a
route they were never allowed to reach, and every rejected request had its body parsed and checked
before anyone asked who sent it. The guards now run at the transform stage. They travel as
requireAuth,requireRoleandrequireSelectedWorkspaceroute options carried by macros on the
plugins every route file already uses, so a route added later gets the ordering by writing the
option rather than by remembering a lifecycle rule. The shared-dashboard rate limiter moved with
them for the same reason. Converting the routes also fixed a latent bug: the old composite form
built a throwaway context object, so the rolerequireRoleFreshread back from the database never
reached the handler. - The workspace guards answer 404 for a workspace that is not there. A soft delete leaves the
membership rows behind and the membership queries do not join workspaces, so a member of a deleted
workspace passed every check: the member roster answered 200 with the full list, and adding and
removing members still succeeded. The existence check now runs last in each guard, so a caller who
was going to be refused hears 403 for a real workspace and for an absent one and the status cannot
be used to learn which ids exist. config/example.jsonno longer excludes localhost from the audit log. It shipped
audit.ipWhitelistas["127.0.0.1", "::1"], which is exactly the state the audit plugin's own
comment says must never happen, so a fresh clone started with no audit trail for anything reaching
it locally. The list is empty now, andcheck:configfails on any loopback entry, matching the
whole127.0.0.0/8range, the::ffff:mapped form and the namelocalhost. An address an
operator lists inconfig/<slug>.jsonis still honoured.
Changed
- The workspace header has one owner and one answer. A caller who left
X-Workspace-IDoff was told
different things by different routes, in two spellings of the header name, neither matching what
the CORS allow-list publishes or the client sends.backend/src/guards/workspaceHeader.tsnow owns
the header name, the missing-header message, a distinct message for a header that could not be
read, and the not-found answer. The scope rule is now plain: a listing follows the header whoever
sent it, and only a request naming no workspace gets the cross-workspace view. Previously the scope
was decided by re-reading the caller's role, so a SYSOP who named a workspace had that id discarded
and was answered with every workspace instead of the one they asked for. - A 404 is treated as an answer rather than a failure.
queryClient.tsdecides that in one predicate
that both the retry rule and the throw rule consult, so a page can render its own not-found state
instead of waiting through three retries and then handing an unnamed failure to the error boundary.
A page added later inherits this without opting in; everything else still throws. - The multipart transport ceiling now sits above
storage.maxFileSize. Both shipped at 10 MB, so a
file at exactly the file limit exceeded the transport limit once the multipart envelope was added,
and Bun answers that with an empty 413 rather than the message the route would have given.
Fixed
- An interrupted view transition no longer raises an unhandled rejection. The template opts every
navigation into a cross-document transition, and navigating again inside the 220ms animation skips
the running one, which rejects its promises with anAbortErrorthat nothing held. Clicking twice
on any page produced a console rejection, and a crawler navigating faster than the animation
produced one on every page.frontend/src/lib/viewTransitions.tsholds those promises and absorbs
only anAbortError, so a transition that failed for another reason is still reported. The CSS is
untouched: same rule, same timing, same reduced-motion path. - A bug report that is empty once trimmed is refused. The route checked
minLengthagainst the
description exactly as it arrived and the service trimmed it afterwards, so a description of
nothing but spaces was stored as a row titled(untitled)with an empty body, which could then
never be corrected or removed. The route trims in a transform hook now, so the value the schema is
asked about is the value that gets stored, and a padded email address is accepted rather than
refused for padding that would have been removed anyway. GET /api/v1/bugsanswers. The listing registered its path with a trailing slash beside the POST's
empty string, which puts two nodes in the router and resolved a GET of either URL to the node the
POST created, so both spellings returned 404.- Opening a dashboard that no longer exists shows the not-found state. It previously showed a
skeleton for about seven seconds and then a failure that named nothing. The same was true of a
shared dashboard whose link had expired. - The onboarding checklist reports the sysop password change instead of affirming it. Its first step
was hard-coded complete and signing in was what made it true, so the checklist claimed a password
change nobody had performed, on the one account whose default password is published in the
project's own documentation. Signing in and changing the password are now separate steps, and the
second reads the seeded SYSOP row so it closes only when the account's holder actually changes it. - Application errors reach
logs/<name>.error.log. That file is the spawned server's stderr, but the
logger sent every level to stdout, so it could only ever hold crashes and interpreter warnings, and
every tool that read it to judge a startup was reading a file that could not hold an application
error. Errors are copied to stderr rather than moved, so the main log still carries every level.
Transport targets are resolved to absolute paths, which fixes a logger loaded from outside
backend/dying with "unable to determine transport target for pino-pretty". - A binary upload is no longer rejected as a long text line. The line-length check ran over every
buffered upload before the switch that chose a text-format validator, so a binary body with no
newline in it was refused with "Line 1 exceeds maximum length of 10000 characters". It now runs
only for a MIME type that has a content validator. - A fractional
pageorlimitis refused. Every paginated route declared them ast.Numericwith
no integer constraint, so?page=1.5became a fractional SQLLIMIT/OFFSET: a driver error on
some backends and a silently misaligned 200 on others. Both parameters now come from
backend/src/schemas/pagination.tsand are declared ont.Integer. A value outside its range also
reported a validation failure naming a property that exists on no schema; the message names the
parameter the caller actually sent. Reported by nine of the ten derived applications during the
v3.44.3 sweep.
15 commits since v3.44.3, squashed into 71fc77b (#32).
Full Changelog: v3.44.3...v3.45.0
v3.44.3
Refactoring
- branding: drop the normalizer branch for a deleted test file (#30) (
f7cd68b)
2 commits since v3.44.2 (1 omitted as build/chore/ci plumbing).
Full Changelog: v3.44.2...v3.44.3