Skip to content

Merge accounts (integration-local) into integration; reset links stop signing people in - #48

Merged
codeGlaze merged 174 commits into
integrationfrom
merge/accounts
Oct 5, 2026
Merged

codeGlaze merged 174 commits into
integrationfrom
merge/accounts

Conversation

@codeGlaze

@codeGlaze codeGlaze commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Brings the accounts batch (integration-local, formerly PR #34) into integration, with the reset-link change agreed on 2026-10-04. Supersedes #34.

What arrives

  • The September hotfix batch:
    • short share links and Stop sharing
    • the server heartbeat
    • items loading with the character
    • parties keeping shared homebrew
    • the first e2e harness
  • The garden harvest, the mobile layout and one duplicate-key fix.
  • The accounts redesign:
    • password rules by length and shape
    • breach screening, refusing at 1000+ appearances
    • reset keys stored as digests, expiring in 2 hours
    • sign-in notices and sign out everywhere

Reset links (new here, f8f270dd)

A reset link no longer signs anyone in. Opening it used to mint an hour-long session, so anyone holding the mail was signed in without changing the password, and the owner was never told. Now, per OWASP's Forgot Password cheat sheet, the link is a one-time permission to set a password:

  • The page sets no session, only a plain reset-username cookie for the meter, plus Referrer-Policy: no-referrer.
  • The form submits the key, and reset-password checks it on submit, so a mail scanner that opens links spends nothing.
  • The key is retired in the same transaction as the new password.
  • There is no automatic sign-in afterwards.

Tests: a-reset-link-is-a-one-time-permission-not-a-sign-in and an-expired-reset-link-sets-nothing. Both fail on the old code.

Conflicts and merge-time fixes

  • run.sh: integration's runner wins. Accounts' mail sink, E2E-READY wait and seeded character IDs move into server.sh and run.sh. A merged-in block that rejected any suite outside scripts/e2e was dropped.
  • :register-failure: keeps accounts' per-field server errors, and turns every other failure into a :general notice, so no failure is silent. That is the locale-safety fix, kept.
  • Locale: accounts' password rules, email-domain check and breach-check setting are pinned to Locale/ROOT. Without that, a Turkish server judged passwords differently from the browser.
  • Browser suites:
    • 12 accounts suites read internals and are now tagged dev-only.
    • The two share suites that share and unshare the same seeded character each get their own server.
    • run-all.sh takes a lock, so a second run fails at once.
  • Registration tests: stub the per-host sign-up throttle, which counted across the whole JVM.
  • Brought in Shared picks namespace (walker moved, remove-at/put-at); lint clean #47: six small conflicts.

History

37 commits authored as claudecode@eliminater.com were rewritten to github@codeglaze.com before this branch was ever pushed. Trees are identical and every commit is signed. As a result, the commits here have new hashes, compared with f1852203-accounts.

The changelog's 110 references were remapped to the new hashes. Five hashes in the accounts changelog never existed; they now name the real commits.

Four references in integration's own changelog were already dangling before this branch (26f8c602, 2a6fde93, 2e13d2b2, 429152e). This PR leaves them as they were.

Verified locally

Check Result
Lint 0 errors, 0 warnings
lein test 674/674
ClojureScript, in headless Chromium 557/557
Browser suite 32/32, including auth-flows 43/43, which covers the new reset flow

Locale and Windows runs are in CI.

Found by screenshotting it, which had not been done -- the effect was written
and shipped without anyone looking at it.

Two faults, both from hanging the sparkle off the corner at top:-4px right:-3px.
It landed in the gap between Save and the next header button, where it read as
belonging to neither; and since the buttons are siblings in a row, the next one
along painted over it, so most of it was simply not visible.

Inset into the button's own top-right corner now, and the button raised a layer
so its glow and sparkle draw above the sibling rather than under it.
clj-kondo flagged it. The value was always the literal, so the compiled CSS
does not change.
Watched in a real browser for the first time, the heal worked when a character
was opened and failed twice in ways the unit tests could not see.

A reload never healed. :initialize-db restores the builder's in-progress
character with a bare assoc straight into app-db, which never reaches
:set-character, so the reconcilers did not run: a character that healed when
opened from the list stayed broken after a refresh. Boot now goes through the
same set-character, so both paths are one code path.

The toast never painted. Every way a saved character reaches the builder
dispatches [:route ...] beside :set-character, and :route queues a
[:hide-message] that runs after anything :set-character queued, so the message
was set and cleared in the same frame. :set-character now only records the
heal, and :route announces it after its own clear. dispatch-n is FIFO, which
makes that the one place navigation cannot erase the toast it delivered.
Builder only, and once per heal.

test/browser/character_heal_e2e.js drives both paths against lein e2e-server
and asserts the stored key is rewritten, the option renders bound by name, the
toast is on screen and the save button carries its cue. It found both bugs,
passes 13/13 with the fix, and is registered with the probe runner so a later
change cannot quietly undo either. Saving needs a verified account the
in-memory server does not seed, so the save-clears-the-cue half stays in
events_test.
Restoring the builder's draft on startup repaired it in memory and stopped
there. The copy in browser storage stayed broken, so every refresh repaired the
same keys again and announced it again: three refreshes, three repairs, three
toasts, found by watching it in a browser.

The repaired draft is now written back to browser storage when startup repaired
something, and only then. That is the builder's own working copy, which it
already rewrites on every edit. The character saved on the server is not
touched; keeping the fix there still takes Save, and the save button still
says so.
Loading sets an invalid entry aside for repair, and it did save that set-aside
copy, but it left the entry in the library copy as well. So every load found
the same entry and set it aside again, until some unrelated homebrew save
happened to rewrite the library. Watched in a browser: set aside on load 1, set
aside again on load 2, gone only after an ordinary save.

The library copy is now rewritten on the load that sets the entry aside. The
order is the safety: it is rewritten only after the set-aside copy is written.
If storage is full and that write fails, the entry stays exactly where it was,
which costs a repeat on the next load instead of the entry.

The ordering lives in persist-set-aside!, which takes the storage writer as an
argument, so the test drives a refused write directly. Faking a full
localStorage by patching Storage.prototype did not take effect in the CLJS
test page, although the same patch worked from a plain page, which is how the
real behaviour was confirmed before this was written.
The save banner's "Export this source" link carried a copy of the source from
the moment of saving and handed it to the export check. Since export began
saving its cleanup back to the library, clicking that link after any later
change wrote the old copy over the library: a source turned off came back on,
an entry added since was gone, and the file lacked it too. This is the third
time this link has broken, each time by trusting what it was handed.

The export check now works from what the library holds whenever it holds the
source (export-source), and falls back to a copy handed in only when it does
not. The banners pass just the source name. The save always lands before the
banner can be clicked, so the older bug where the file came out missing the
item just saved cannot return by this route.

builder_card_export_e2e.js now changes the source after saving and clicks the
link again. The stored spell carries curly quotes so cleanup has something to
rewrite, which is the only condition under which the old link wrote back.
Checked both ways on lein e2e-server: 13/13 with this change; without it both
new checks fail, the source back on and the added spell missing from the
library and the file.
A content section stored as text or as a list still holds everything that was
in it, and nothing recovered it. Salvage kept any section that was not a map
exactly as it was, so it loaded silently, its source looked fine, and export
later refused the source with no fix in sight.

mend-section puts those back in shape: text read back, a list keyed by each
entry's own key or the key its name derives, an empty section dropped. It
refuses to guess -- two entries claiming one key, or an element that is not an
entry, leaves the section for a person. mend-plugin, mend-library and
mend-import-data run it over a source, a library, and freshly read import data,
including a whole file stored as text.

What nothing can read is now set aside instead of kept: a damaged section, and
a whole source that is not a map. Salvage used to drop the latter from both
sides, which a write-back of the library would have turned into an erasure.

merge-plugins lets the newer value win wherever either side is not a map.
Merging set-aside data called merge on text, which throws, and it runs while
the app starts. For sections that are maps the result is unchanged.
Import now mends right after reading the file, before any other step walks the
data. A section stored as text or as a list imports with everything in it and
exports. A file whose whole content was stored as text used to crash inside the
cleanup steps and import nothing, with no message. The notice adds "Repaired N
damaged sections", the import log lists each repair, and a section nothing could
read no longer counts as an imported item -- it said "Imported 1 items" for one.

Loading mends before salvage and writes the repaired library back through the
same ordering as the set-aside write-back, so each repair happens once.

A section nothing could read is set aside, and the import notice and the
needs-attention panel both count it. The panel names the damaged section and
offers Export raw and Discard, since there is no name to type for it. Both used
to count only entries, which would have read "0 entries couldn't load".

Checked on lein e2e-server with every shape imported through the real import
event: 28 of 28, including the startup repair written back on the first load
and nothing left to repair on the second.
An import that skipped items arrived in the green success card with a check
mark, so the one thing worth noticing looked like good news. It said
"Skipped 1 invalid items" and sent people to the browser console.

A partial import, or one with key clashes, now uses the warning card. Its lines
carry a green check for what came in, an orange triangle for what was skipped
and a wrench for what was repaired. The colour sits on the icon, not the text,
so the text stays readable on the tinted box. The skipped line points at the
import log, which lists each skipped item and why. Counts read "1 item" and
"2 items".

A clean import keeps the success card unchanged.
An item with no name showed as its bare key, ":no-name", which reads as code.
It now shows as the placeholder Export & Auto-Fix will give it -- "Unnamed
Spell", "Unnamed Monster" -- with the key beneath it in small type when there is
one. The header also said "1 item need attention"; it reads "needs" now.
…er open menus

The round import-log button sat in the bottom-right corner of every page, even
with nothing in the log. On a laptop-height window the My Content menu reaches
that corner, and the button, drawn at z-index 900, covered the menu's last item:
clicking Selection Builder opened the empty log instead.

The button now appears only when the log has something in it, or while its panel
is open so the panel can be closed from it. Its z-index is 150: under an open
header tab (200), still above the sticky button row (100). The panel it opens
stays at 950. Its styles move from inline to garden.

header_menus_e2e.js runs the shortest window a second time after a real partial
import puts the button on the page. With the old z-index that pass fails on
Selection Builder; with this change all 30 checks pass.
… is none

Twenty-six probes carried their own copy of findChrome, a dozen variants between
them. All of them looked only for chrome-linux/chrome, which Playwright 1.57 names
chrome-linux64, so none found an installed browser by path. Five of the copies
threw when /opt/pw-browsers did not exist. Eight more probes skipped the finder and
hardcoded /opt/pw-browsers/chromium or read one env var each: CHROME, CHROME_PATH
or PLAYWRIGHT_CHROMIUM. Outside the web sandbox those failed at launch unless all
four variables were set by hand.

test/browser/lib/find-chrome.js is now the one copy, used by every probe and by
the CLJS test runner. It takes a path from any of those env vars (or E2E_CHROMIUM,
which scripts/e2e/run.js reads), a binary or an install folder. Then it tries the
newest chromium-<n> under PLAYWRIGHT_BROWSERS_PATH or /opt/pw-browsers, in either
folder layout, and otherwise leaves it to Playwright's own browser. A variable
that points at nothing is named on stderr rather than silently used.

run-browser-probes.js resolves the browser once. With none, each probe is a SKIP
naming the fix, not thirty identical launch failures; STRICT still fails the run.

Checked with no browser variables set: 14 of 14 probes pass, and header_menus
runs 30 of 30.
… just once

A library that reads back as text of itself loaded nothing: the loader saw a
string, copied it to plugins:corrupt and moved on. mend-library now reads it back
when the text is a map keyed by source names, and the load writes the library
back, so the repair happens once.

A stored value that is still not a library was copied to plugins:corrupt on every
load, because the active slot was never cleared; the unreadable-text case beside
it always cleared its slot. It is now cleared as well, but only once the copy is
saved, so a full storage cannot turn the copy into a loss.
Save anyway with placeholders in the selection builder derived the key straight
from the name. A name like "9 Lives" gave :9-lives, which the save accepted and
the next load set aside, so the placeholder save undid itself on refresh. The
other builders run sanitize-item-names first; the selection builder now does too.
… kept

Resolving an import conflict can rename or disable the copy already in the
library. Those changes sit in the base library handed to the import store, and the
store only wrote the library when at least one incoming entry was kept. When every
incoming entry was set aside, the renames and disables went with it, in storage
and on screen.
… export

fixes_persist_e2e.js applies each repair through the real app, then reads
localStorage, reloads, confirms the reload has nothing left to repair, and exports
where there is a file. Paths: import cleanups, the loader's repairs and set-asides,
a library stored as text, a stored value that is not a library, conflict renames,
Auto-name & Restore, Export & Auto-Fix, Save anyway for a spell and a selection,
and a character repaired on refresh.

Against the build before the two load fixes and the selection fix it passes 25 of
33, failing exactly those paths; with them, 33 of 33.
An entry stored as text, or anything else that is not a map, reached the step that
fills missing fields. That step assoc'ed into it and threw "No protocol method
IAssociative.-assoc defined for type string", so the import stopped partway. It
now leaves such an entry alone; validation skips it and the import log lists it,
as with any entry the import cannot use.

The full-import tests used an entry with no source as their broken example, which
the next commit makes importable, so they use an entry that is text instead.
The July rule sends content with no source to Default Option Source, but only an
:option-pack that was present and blank or nil got filled. An entry that left the
field out was skipped on a single-source import, counted as imported and then set
aside on a multi-source one, and set aside when found in stored homebrew.

An entry with no usable :option-pack now takes the name of the source it is filed
under. A single-source file has no source key, so there it takes the one source
the other entries name. Only when there is no name to take does it get Default
Option Source, as before. Import and load share the rule (e5/source-for), and the
loader writes the filled library back with its other repairs, so it happens once.
single-plugin-source now asks e5/declared-source, the same reading of which source
a file's entries name.

The persistence probe checks the import and the load path (35 of 35). The load
test and the header-menu probe used a sourceless entry as their broken example and
now use an entry that is text. The import cleanup table in ORCBREW_FILE_VALIDATION
also said a nameless source became Default Option Source; it becomes Unnamed
Content, as decided in b5ba38d.
A stored race whose :key was text left every page on the loading spinner, with
only the rescue download bar. At startup core calls init-template-cache!, which
builds the character template. The races subscription keeps races in a set sorted
by key, and compare threw comparing :dwarf with "text". That throw happened outside
render, where no error boundary can catch it, and the rest of core never ran, so
the app was never mounted.

Two layers. Each startup step in core runs through boot-step, which logs a failure
and carries on, and the template cache catches its own failure; the save handler
already works without a cache. The sorts that see homebrew keys or names use
common/safe-compare, which orders mixed types by type and then by printed form
instead of throwing: races, spells, class options, subclass names, template
options and the spell list.

boot_rescue_e2e.js stores such a race and checks the app still draws. It failed on
the build before this change and passes on this one. A sweep of wrong types and
shapes through import, reload, the template and export went from 43 crashes to 31:
every key crash is gone, and what remains is traits and options lists, and a
source that is not a map.
…port crashing on them

A sweep of wrong types and shapes through import, reload and export found 31
crashes left, all in three places:

- A :traits list (classes, subclasses, races, subraces, backgrounds) or a
  selection's :options holding something that is not a card. [42] or ["text"]
  made the import throw with nothing saved and no message; a bare 42 imported and
  then made export throw.
- A source that is a number or unreadable text made the import throw.
- A :key that is not a keyword, which used to stop the app starting.

mend-plugin now repairs inside entries as well as sections, so import and load
share it and a load writes a repair back once. A :key that is not a letter-first
keyword takes the key the entry is filed under, which keeps characters that point
at it working. A card list keeps its cards, turns text into a card with that name,
drops anything else, wraps a lone card, and is removed when it is none of these.
:options is only treated as a card list on a selection. A healthy entry is not
touched and reports no repair.

The import steps that walk every source or card now step past ones of the wrong
shape, so a non-map source reaches the store, which already sets it aside. The
import notice counts damaged entries apart from damaged sections, and the import
log names each repair.
… still breaks

Startup built the character builder's whole template before anything was drawn, outside
every error boundary, only so autosave had a cached copy. A throw there stopped the app
from mounting. Startup now reads storage and builds nothing from it:

- The template cache starts on the first character save (ensure-template-cache!), from
  the sheet, list, parties or combat tracker. A save retries while it fills and says so
  if it never does, instead of skipping silently.
- If loading the library itself throws, core copies it to plugins:corrupt (clearing the
  slot only once the copy is saved), starts without it and shows a notice with a
  download. If startup fails without homebrew too, the library is put back.

For an entry no repair can mend, each conversion into a character option is guarded
(homebrew-guard): races, subraces, backgrounds, feats, classes, subclasses and spell
lists. A throw skips that entry and reports it; the report sets it aside in
plugins:rejected, which is saved, and a notice names it with a link to My Content.

Option builders return lazy sequences, so some damage only throws when a page draws the
option, outside the guard. Fully realizing every entry to catch that costs about 2 s
with a 12-source library against about 40 ms unrealized, so the everyday path does not.
A deep check (homebrew-check) realizes entry by entry, only: after an import, for the
sources it changed; after a Restore, for that source; and when a page has already
failed, over everything, after which the page tries again. The conversions it runs are
registered by the subscriptions, so the events namespace still does not load them.

The startup notice's download goes through save-orcbrew-blob! (new :raw? option) and is
declared as a hatch in export_paths_test, like the other unvalidated downloads.

The error boundary's retry never redrew the page: Reagent does not re-render on React
state alone, so it now forces the update. A spell whose :spell-lists is not a map is
repaired on import and load like the other entry damage.

homebrew_safety_net_e2e.js makes one race throw on conversion and another throw only
while drawn. With this change all 16 checks pass; on the build before, 1 of the first 13.
The Orcpub#669 auth change moved get-auth-token into event-utils and updated the calls
it knew about. :report-character-problem, the email support button on the
character-load recovery panel, arrived later and still called the local function,
which no longer exists. Clicking it threw, so the report was never sent and the
button stayed on sending. Lint reported it as an unresolved symbol.

The new events test dispatches the report and checks the token it sends; it
errored before this change.
Moving ::mi5e/custom-items onto reg-api-sub and switching off the remote-item
chain left seven referred vars unused: reg-sub-raw and dispatch, url-for-route,
auth-headers and handle-api-response, <! and go. Lint went from 10 warnings to 17
with them.
The comments kept the constraints: why the item loader does not redirect on a 401,
that the parties list is stored under ::char5e/parties, that a filtered list
written into db stops following its source, which pieces of the switched-off
remote-item chain must be restored together, and that the old guard read a key
that is always nil.

They lost what the code or git already holds: commit hashes and dates, the audit
narrative, a diagram of the chain, notes on removed code, instructions to write KB
entries, and product questions about item sharing, which belong in the KB.

The switched-off chain's comment now says plainly that GET /api/dnd/e5/items/:id
has no check-auth and returns any owned item to anyone, which matters before
anyone wires that chain up. The e2e loader script no longer names another branch.
One probe per link between homebrew items: does a key rename (the one
import conflict resolution and relocation use) carry the link along, and
does the consumer actually read it. The first run asserted the correct
outcome everywhere; eleven of thirteen rename probes failed, and those
are now pinned as GAP rows that turn red when a fix makes them follow.
Only subclass -> class and subrace -> race survive a rename today.

Also pins two facts a code-reading sweep got wrong: race :props
:language and race :spells are both live grants. And a stranded feat
race prerequisite does not ungate the feat; it locks it for every
character under a blank " Only" label.
A rename happens only because another item holds the key, so a stranded
link latches onto that other item rather than dangling. Run through the
consumers the builder reads, in the import case: an imported spell joins
the library's class list and its own class offers nothing; an imported
feat requires the library's race; an imported class's level choice offers
the library's selection with the library's options.
Three conflicts, all both-sides-kept. The selection save-anyway keeps this
branch's name sanitizing and takes the source-tagged key, minted from the
sanitized name so a name like "9 Lives" cannot derive a key the next load
sets aside. The two test files each gained different tests on both sides.
The source-tagged-keys merge kept this side's ns form, which lacks the
testing refer its tests use, and a local k in moved-through shadowed the
k helper that branch added.
…lsewhere

Merged beside feat/source-tagged-keys, its relaxed save-collision treated
an item as back in its own slot by key alone, so retargeting an item's
Option Source Name to a library that holds the key silently overwrote
that library's item (retargeting-the-source-must-not-destroy-the-item-
sitting-there failed). The duplicate save gating is being reworked on its
own branch, with the builder remembering which source an item was opened
from; it comes in with that.
Brings save-gate (#37: builder origin, move-or-refuse saves, the shared
library-links model that repoints every link on a key change) and
comment-debt (#39). Where both sides changed one place:

- homebrew save, collision and links: integration's, which supersedes the
  two relocate fixes and the gating revert made here
- accounts, auth and sharing: this branch's (share line, party share
  tokens, image-probe limits, reg-api-sub logging out on 401)
- the library loader keeps this branch's repair step (mend-library,
  persist-set-aside!, fail-soft startup), which integration never had;
  integration's additions beside it (relinks, revision, cross-tab watch,
  pre-fix copy, builder origin) are all kept
- combined: party add/remove answer with the party after the write;
  imports and restores still run the homebrew build check through
  commit-library; the selection save-anyway sanitizes names under the
  save gate; export reads the live library and no longer writes back;
  an import that keeps nothing still stores renames of existing items
- the heal toast stays in :route, where a route change cannot clear it
- dropped a doubled set-aside notice the auto-merge produced
Integration's comment check (#37) fails docstrings over 6 lines, comment
blocks over 4 and history phrasing. 77 hits here, all from code written
before the rule. Docstrings are spec now, with a GOTCHA where a reader
would otherwise write a bug; the rationale moved out, and each trimmed
comment names the KB page that holds it (account-flows.md,
homebrew-safety-net.md, share-links.md, homebrew-keys-design.md). Code
is unchanged, checked token by token against the merge commit.

Also marks the startup write that restores a set-aside library as part
of the library load, the one write outside the gate the gate test allows.
set-aside-broken-homebrew wrote the library directly, bypassing the
gate's revision count and cross-tab merge, which library_gate_test now
refuses. It writes the set-aside copy first as before, then commits the
smaller library through commit-library with :deleting?, since removing
the entry is deliberate. If storage refuses the set-aside copy, the
entry now stays in the library and the author is told; before, it was
removed for that session only. Also trims this file's comments to the
comment rule.
Integration's heal of a renamed pick waits for the template's offered
keys, and integration built the template at startup. This branch builds
nothing at startup, and only a save asked for the template, so merged, a
character with a renamed pick stayed unhealed until it was saved.

:set-character and the routes to the builder and to a character's page
now ask for the template; other pages still build nothing. A heal that
lands when the template arrives comes after the route change, so it
announces itself through ::e5/announce-heal, which shares one rule with
the :route announcement (heal-announcement).

Measured with scripts/e2e/template-on-open.js, three runs each: opening
a character heals in 3 ms (before: not within 15 s); login and the
character list build nothing; the builder's first load goes from about
1.05 s to 1.22 s, the template's cost moving from the first save to the
first builder visit. test/browser/character_heal_e2e.js passes 13/13.

The heal tests now follow integration's model, and the test runner
registers the template request as a no-op so the watcher it starts
cannot dispatch into later tests.
The template's 230-320 ms build ran just after the builder appeared, so
the page was briefly unresponsive right as it showed. On the builder and
character pages it is now built at startup, before the app mounts: the
spinner stays up about a quarter second longer and the page is
responsive when it appears. Measured over three loads: the template
lands before the spinner clears, and what follows is the usual 60-150 ms
of drawing. Other pages still build nothing at startup; the step is
guarded like the lazy build, so a failure leaves the cache empty.
The stale-bundle check kept its own list of source folders, and the list
missed web/cljs, so startup edits were tested against an old bundle with
no warning. It now reads :watch-dirs from the build config of whichever
bundle is on disk, the same list the compiler uses, and stops the run if
it cannot read it rather than skipping the check. Proved by making
web/cljs/orcpub/core.cljs newer than the bundle: the run names it.
The boot spinner was spiral.gif: a 60 KB separate download, stair-stepped
against the gradient. It is now drawn inline by the server, so it shows
with the first bytes of the page, picked at random per load for now so
all three get seen; ?spinner=spiral|d20|spell forces one, and anything
else falls back to random.

The shapes are computed at load (orcpub.loading-spinner): the spiral's
curve, the d20's twenty face placements, and a ring of made-up runes.
Numbers are formatted with Locale/ROOT so a server locale cannot write
100,5 into the SVG. Only transform and opacity animate, each moving piece
its own element, so all three keep moving while the app builds: measured
on the real page with the main thread blocked for two seconds, the spiral
and spell circle stay smooth and the d20 drops to about 10 frames/s.
f185220-accounts carries the September hotfix batch (share links, Stop sharing, heartbeat, items
load with the character, parties keep shared homebrew, the first e2e harness), the garden harvest,
the mobile layout, one duplicate-key fix and the accounts redesign (password rules, breach
screening, reset keys stored as digests, sign-in notices, sign out everywhere). PR #34.

Conflicts (9 files):
- run.sh: integration's runner. Accounts' mail sink, E2E-READY wait and seeded character IDs moved
  into server.sh and run.sh; its no-default-suite rule kept as a usage listing. A stray block that
  rejected any suite outside scripts/e2e (and --describe) was dropped.
- routes_test.clj, .clj-kondo/config.edn: both sides' additions.
- views.cljs: integration's supporter-slot; accounts' redesigned sign-up form.
- events.cljs :register-failure: accounts' per-field server errors, and every other failure as a
  :general notice, so none is silent (the locale-safety fix, kept).
- styles/core.clj: integration's (already holds accounts' lines plus later fixes).
- index.clj: orcpub.env plus the spinner require. ENVIRONMENT.md: both. Archived spike: ../lib.

Beyond the markers: accounts' password rules, email-domain check and breach-check setting pinned
to Locale/ROOT (a Turkish server judged passwords differently from the browser). The two
registration test files stub the per-host sign-up throttle, which counts across the whole JVM and
refused the 11th test sign-up. auth-flows.js gets its own server.
Opening the link minted an hour-long session for the account, so whoever held the mail was
signed in without changing the password, and the owner got no notice. Now:
- the reset page sets no session, only a plain reset-username cookie for the strength meter
  and Referrer-Policy: no-referrer, since the key is in its URL;
- the form submits the key, and reset-password checks it at submit (unknown, expired and used
  are one answer) instead of trusting a session; the route no longer needs one;
- the key is retracted in the same transaction as the new password;
- index-page-response keeps the caller's headers instead of replacing them.
No auto sign-in afterwards: the success page already sends people to log in.

Tests: a-reset-link-is-a-one-time-permission-not-a-sign-in, an-expired-reset-link-sets-nothing;
both fail on the previous code.
Its Highlights joins the release summary as a third sentence; 15 bullets written before the
house-style lint are trimmed to it; the reset-link change is recorded.
…hare pair, one run at a time

12 suites read re-frame or cljs.core and only ran on a development bundle; they now say so. The two
share suites that share and unshare the same seeded character get their own server: in parallel
one shared it while the other expected it unshared. fixes_persist checks that the bad value left
the active slot, which then holds the app's default empty library. run-all.sh takes a lock, so a
second run fails at once instead of quietly sharing the first's server.
…accounts

Six conflicts: config.clj and equipment_subs.cljs keep accounts' versions, which already carry
#47's renames; events.cljs takes #47's structure with accounts' two additions inside it (clear
:homebrew-reported, re-check builds after a restore); the lifecycle test takes #47's rename; the
CLJS runner lists both new namespaces; CHANGELOG.md keeps both folded blocks.
The author-email rewrite gave the accounts commits new hashes; 110 references follow them. Five
hashes in the accounts changelog never existed (a1b7e3f9, f2c8d4e1, 8e3f5a21, f4a2d1c8, e8f6b2a1)
and now name the commits they describe; two bullets had their references swapped. The reset page
reads the username from a cookie it sets, not the session token it no longer creates.
@greptile-apps

greptile-apps Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Critical risk] Rebuilds authentication and account recovery flows.

The PR appears safe to merge based on the changes since the previous review.

Summary

This PR combines the accounts redesign with sharing, character-loading, and E2E changes. Since the previous review, it adds transactional guards for competing reset submissions and late share uploads, corrects reset confirmation when the password is revealed, and prevents a late share load from applying to another character page.

Reviews (2) · Last reviewed commit: "run-all.sh: the lock is released when th..."

Comment thread src/clj/orcpub/routes.clj Resolved
Comment thread src/cljs/orcpub/dnd/e5/views.cljs Resolved
Comment thread src/cljs/orcpub/dnd/e5/events.cljs Resolved
Comment thread src/clj/orcpub/routes/share.clj Resolved
Comment thread src/clj/orcpub/routes/share.clj Resolved
- reset-password: the write is conditional (:db/cas) on the reset the request read, so two submits
  of one link set one password; the loser gets the spent-link answer.
- The reset page sends the password as its own confirmation while it is revealed; the server still
  compares the two and refused every revealed reset.
- A short link's homebrew applies only while the page is on the character and token it was
  fetched for (::e5/apply-shared-homebrew-if-current).
- put-share writes only while the share still has the token it checked (:db/cas), so an upload
  racing New link or Stop sharing cannot land on the deleted record.
odb/cas-failed? tells a lost race from a real failure. Both server races have a test that replays
them with an older database snapshot; each fails on the previous code.
The server JVM and the memory sampler inherited the lock's descriptor and held it a moment past the
run, so a run started right after was refused.
@codeGlaze

Copy link
Copy Markdown
Owner Author

@greptileai review

@codeGlaze
codeGlaze merged commit 98932cf into integration Oct 5, 2026
6 checks passed
@codeGlaze
codeGlaze deleted the merge/accounts branch October 5, 2026 01:49
codeGlaze added a commit that referenced this pull request Oct 5, 2026
… accounts, #49)

Brings the shared picks namespace this branch's step 1 builds on. No
conflicts; no duplicate definitions.
codeGlaze added a commit that referenced this pull request Oct 5, 2026
A second exit rule (from 57d0c7c, reached integration with #48) required
a 'Ran N tests' line, which the full-suite page never prints (it reports
'Totals'), so every full run exited 1, green or not. The exit now uses
the summary verdict computed just above, plus no FAIL/ERROR. Checked both
ways: 606 passing tests exit 0; one planted failure exits 1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant