Breaking wire-format change: dual-read (V1+V2) / single-write (V2) so
existing user secrets, stored messages, and inter-tenant key keep
working. Old clients and relays lose cross-version chat until both
sides are upgraded.
Relay
- Switch clientEncrypt/masterEncrypt to AES-256-GCM with HKDF-derived
per-ciphertext salts; keep CBC-V1 fallback on decrypt only.
- On-bootstrap migration re-envelopes tenant secret_encrypted and the
inter-tenant key to GCM-V2 (idempotent).
- CORS: stop reflecting arbitrary Origin; allow own origin + optional
RELAY_CORS_ORIGINS env allowlist; drop Access-Control-Allow-Credentials.
- Generic error messages replace "no such user: <name>" enumeration.
- Exception messages no longer echoed back; logged server-side.
- Collab-lock max hold capped at 10min so a member can't grief-refresh
forever.
- Attachment filenames validated against hex.<ext> pattern.
- Passwords min length 10; /api/account/password rate-limited.
App / Electron client
- Shared hyprcrdRelayEncrypt/Decrypt helpers (GCM-V2 write, V1 read).
- Chat + collab-buckets plugins delegate to the shared helpers.
- Poll + push + attachments migrated to the dual-format path.
- /api/relay/settings GET returns has_key boolean; secret never echoed.
- Setup endpoint now CSRF-checked.
- SSRF: CURLOPT_PROTOCOLS restricted to HTTP(S) everywhere; bookmarks
favicon fetch also blocks private/loopback/link-local.
- Login / password-change rate limiter moved to DB-backed IP keying.
- Passwords min length 10 throughout.
- Zip import enforces per-file (25MB) and total (500MB) caps, extension
allowlist, no-overwrite.
- Verbose relay-push debug gated behind APP_DEBUG env.
Version bumped to 2.0.0 (protocol-breaking).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test-shield (#test-shield) was added in loader.js (always fresh
via plugin manager ?v=time()) but the removal lives in runner.js
hideProgress(). runner.js was still cached at ?v=3 in the browser,
so the shield stayed after the run. Bumping to ?v=4 in loader.js +
all 42 test files forces a fresh runner.js load.
chore: bump version to 0.9.15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* card-interactions.test.js: group delete tests used `.dialog-ok`
(class selector) but the button has `id="dialog-ok"`. querySelector
returned null, the click was skipped, the dialog stayed open and
blocked every downstream test. Fixed to getElementById.
* runner.js closeAll(): added #dialog, #bucket-super-popover (remove),
and #gitea-overlay to the cleanup list. A failed test that leaves any
of these open no longer poisons the rest of the suite.
* runner.js copy handler: when there are failures or flaky tests, the
"copy" button now builds the clipboard text from lastResults —
only failed + flaky lines (suite name, test name, timing, error,
original error for retried). When everything passed, copies the
full output as before. lastResults is set in renderResults().
* bucket-context.test.js: move-left test used `[data-bucket]` to find
user bucket tabs, which misses super-bucket tiles (they carry
`data-bucket-super`). Changed to `[data-bucket-drag]` which is on
both regular and super-bucket tiles but not on fixed filters.
* context-menu.test.js: make-ticket pre-fill assertion now polls up
to 2s for the title input to be populated (async openCreateForm
repo-fetch was racing against the immediate assertion).
* card-join.test.js: clickJoin sleep bumped from 400ms → 800ms.
joinSelectedCards does 3+ serial API calls (fetch bodies, PATCH
winner, N×DELETE losers, _reloadAll) which sometimes exceeded the
old 400ms window on slow runs.
* Runner cache: RUNNER_V bumped from ?v=2 → ?v=3 in loader.js + all
42 test files so the browser picks up the new runner.js content
(copy handler, closeAll, retry progress bar) without a hard reload.
super-buckets: remember last selection per group
* buckets.js: module-scope lastSuperSelection Map tracks the last
active member per super-bucket parent. Updated in renderBar()
whenever activeBucket matches a group member. tileFor() uses it
as a fallback when the user is on a different bucket — the tile
shows the last-picked child's name and color instead of reverting
to the parent. The dropdown popover also highlights the last
selection when no member is currently active.
chore: bump version to 0.9.11
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* runner.js: shows a 30px fixed-top progress strip during runs (label
"test x/total:", current suite/test name with ellipsis on the left,
fixed 50%-wide progress fill on the right that turns danger-red on
the first failure). The DOM node is created in loader.js so it appears
immediately on shortcut press; runner.js only updates it. The fill
width is fixed at flex:0 0 50% so it doesn't jiggle as the test name
changes length. Suites now live on window._testSuites as a defensive
fallback so two runner instances stay in sync.
* Cache invalidation: every test file's `from './runner.js'` is now
`from './runner.js?v=2'`, and loader.js imports the runner with the
same RUNNER_V suffix. Same URL → one module instance → suite
registration and runAll see the same array. Bumping RUNNER_V (in
loader.js + every test file in lock-step) is the way to force a
re-fetch when runner.js changes.
* loader.js: ensureProgressBar() draws the strip via setProperty(...,
'important') with z-index 2147483647 attached to documentElement so
nothing in user CSS, scanline overlay (z 9999) or modals can occlude
it. Auto-run path (?test=1) also pre-renders the strip.
chore: bump version to 0.9.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>