CoreApiTester now seeds the execution context from the trilium-* request
headers, so Pattern 0 documents how to drive a hoisted request and warns that
a spec omitting trilium-hoisted-note-id exercises the unhoisted path. Anything
reading hoistedNoteService.getHoistedNoteId() — quick search, autocomplete,
SearchContext's implicit ancestorNoteId — sees "root" without it, so a passing
spec is not evidence that scoping works. A contributor's quick-search PR
asserted exactly that and proved nothing.
The Windows note covered only `pnpm --filter … exec vitest`; the auto-install
fires for a package's own test script too. It now names the symptom seen here:
EPERM / Access is denied on a node_modules directory VS Code holds open, which
rolls back cleanly but loses the run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bookmarking a note clones it under `_lbBookmarks`, so every descendant gains a
second note path running through `_hidden`. Searching creates an ad-hoc search
note under `_hidden/_search` and activates it. Result cards address their target
by bare note id and leave the path to the client, and `getSortedNotePathRecords`
compared the active note's path prefix before every other criterion. The real
path shares only `root` with the active search note while the bookmark clone
shares `root/_hidden`, so two segments beat one and the hidden check below never
ran. The card opened the note under Bookmarks, and in grid view the breadcrumb
said so too.
The tiebreaker comes from #7552, where it picks the nearest clone so that
opening a note keeps the user in the subtree they came from. Every candidate
there is an ordinary clone, so ranking it below the hoisted, archived, hidden
and search comparisons preserves that and only stops it from overruling them.
The server was never wrong here: `compareNotePathRecords` ranks hidden paths
last and takes no active path. The client threw that answer away and re-derived
the path from froca.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quick search widened its scope to the whole tree whenever the hoisted note
lived inside _hidden, so browsing the in-app help returned the user's own
notes instead of help pages. It also passed includeHiddenNotes, which made an
unhoisted quick search return notes that have no visible path at all.
Neither setting was chosen for quick search. Both arrived together in
8e227a6146, a commit about highlighting exact matches, which copied the
search context of searchNotesForAutocomplete verbatim into the quickSearch
route.
The widening belongs to autocomplete alone. It was added in 6a9ac6f90a for
the launcher work: while hoisted into the hidden configuration area, the
note-link autocomplete still has to reach normal notes so a launcher can
point at one. It first sat in the SearchContext constructor, where it applied
to every search that passed no explicit ancestor, and 88bc7402a2 deliberately
narrowed it to the one function that needs it, introducing includeHiddenNotes
and the IsHiddenExp filter at the same time. That commit's subject states the
intent this one restores: hidden notes should not appear in the global search
unless hoisted into it.
Quick search now scopes to the hoisted note with no special case, so a hoist
into the hidden subtree confines it there like any other hoist. The two
settings have to move together: getAncestorExp only applies the hidden filter
when the ancestor is root, so dropping includeHiddenNotes while keeping the
widening would leave a hoist into help searching everything except help.
Hoisting to _hidden itself now yields nothing, because getSubtree defaults to
includeHidden: false and stops at that node. Hidden descendants, help and the
launcher bar included, traverse normally. Letting AncestorExp traverse hidden
notes would fix that, but the expression also backs saved searches and ETAPI
queries, so it is left for a separate change.
CoreApiTester never seeded the execution context from request headers, so no
spec could describe a hoisted request; the route read "root" whatever the
spec did. It now seeds hoistedNoteId, componentId and localNowDateTime from
the trilium-* headers, matching route_api.ts on Express and browser_routes.ts
in standalone. The new specs create their hidden note under _lbBookmarks
because the API refuses children directly under _hidden and _help*.
The comment in SearchContext explaining the widening has documented a
condition that no longer exists since 88bc7402a2 and is removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit stopped painting the scrolling container with
`--code-background-color`, which leaves nothing in the tree that reads
the variable. Everything that produced it is therefore dead:
- `CodeEditor` wrote the custom property onto the closest
`.scrolling-container` from a `backgroundColor` state, fed by a
callback chained onto `setTheme`. That callback ran
`getComputedStyle` against the `.cm-editor` on every theme change, so
dropping it also drops a forced style read per switch.
- `noBackgroundChange` existed only to suppress that write for embedded
editors, and `SplitEditor` was its only caller. `CodeEditorProps` held
nothing else, so it goes with it; the `Omit<CodeEditorProps,
"onContentChanged">` on `EditableCodeProps` never omitted anything,
since that interface declares its own `onContentChanged`.
- `parentComponent` reached `CodeEditor` only to find the scrolling
container, so it comes off the props type and off the three call
sites in `ReadOnlyCode`, `EditableCode` and `BackendLog`.
`EditableCode` keeps its own, which it uses for `triggerCommand` and
`useKeyboardShortcuts`.
Also moves the corner-rounding rule below the region comments in
`code.css`. It targets both `.note-detail-code` and
`.note-detail-readonly-code`, so sitting inside `#region Editable code`
described only half of what it does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make-electron runs on the same win-signing runner, where `pnpm` invoked from
PowerShell under pnpm/action-setup's pnpm 12 bootstrap exits 0 without
installing anything. The job has not run since that bootstrap landed, the
last release being v0.105.0 on Aug 19, so the next tag push would have hit
what the nightly hit.
Unlike the nightly this job never rewrites the workspace versions, so it
never triggered the implicit re-install that was covering for the dead
install step, and setup-node already caches its store. The shell is the only
part it needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Under pnpm/action-setup 6.1.0's pnpm 12 bootstrap, `pnpm` invoked from
PowerShell on the win-signing runner writes nothing and exits 0 without
installing anything. The same command under cmd works, which is why the
build step's implicit re-install was the only install the Windows jobs had
been getting since that bump, and why "Update nightly version" has been
leaving the version field untouched there.
So the previous commit removed the install that was actually running, and
the build failed on `'tsx' is not recognized`. Take the job's default shell
from the matrix, so the two job-level pnpm steps use cmd on Windows and bash
elsewhere, matching what build-electron already does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two Windows nightly jobs share one persistent win-signing runner, and
both re-download the whole dependency tree from npm on every run, twice.
That download is what fails: a tarball fetch timeout on Sep 9, hundreds of
connection resets on Sep 10, and a store import error against a half-written
store on Sep 11, after which the arm64 job built against the incomplete tree
and share-theme's esbuild step reported 43 missing files.
pnpm derives its store from PNPM_HOME, which pnpm/action-setup points at
<dest>/node_modules/.bin, so the store sits inside the directory the action
reinstalls on every run and starts empty. The last green run reports
"reused 0, downloaded 2284"; the Sep 11 failure reports "reused 269".
Pin the store to C:\pnpm-store on Windows so it survives between jobs, and
copy packages out of it rather than hardlinking, because the better-sqlite3
rebuild and electron-forge's pruning both write into node_modules.
"Update nightly version" rewrites the version field of six workspace
manifests, so the next `pnpm run` re-installs the whole workspace before
running the script it was asked for. That second install shows up under
`chore:update-build-info` in every Windows job, the green one included. The
dependency graph is unchanged and `pnpm install --frozen-lockfile` has
already run, so turn the check off for the job.
The npm_config_package_import_method this replaces never applied: pnpm 12
reads PNPM_CONFIG_*, not npm_config_*.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A six-digit code can repeat between adjacent time steps. findTimeStep
returned the first step that matched, so when a fresh code for the
current step was also the code of the previous step, and that step had
already been used, verifyTOTP took the fresh code for a replay and the
user had to wait for the next one.
findTimeStep now starts after the last used step, so a code is only
matched against the steps it can still be used for. A replay is still
refused: the step a used code was accepted for is no longer tried, and
the code only passes if it is also the code of a later step, where it is
as valid as any other code for that step.
A stored value that is missing, empty or not a number counts as no step
used, as before.
findTimeStep only tried the server's current 30-second step, as
Totp.validate without a drift option did before it. A code generated one
step earlier or later is rejected with {"success":false} and nothing in
the log, so a phone clock a few seconds off, or a code typed in the last
seconds of its step, fails enrollment and login with no way to tell why.
findTimeStep now takes how many steps on either side of the current one
also match, and enrollment and login pass CLOCK_DRIFT_STEPS, which is 1.
RFC 6238 allows for this: section 6 recommends a limit on how many steps
a client can be "out of synch", which "can be set both forward and
backward from the calculated time step", and section 5.2 recommends
"that at most one time step is allowed as the network delay". Enrollment
and login share the window, so a user cannot enroll with a code that
login would then reject.
At login a code now matches for up to 90 seconds instead of 30. The
previous commit keeps that from widening replay: verifyTOTP refuses a
used code for as long as it still matches, and any code from an earlier
step once a later one has been used. The new assertions in the
verifyTOTP test cover both.
The setup wizard's check, validateTOTP, records nothing, so it stays on
the current step: a wider window there would keep a code usable for 90
seconds instead of 30.
Login sits behind the existing limiter of 10 failed attempts per IP per
15 minutes, so accepting three codes instead of one leaves a blind guess
at about 30 in a million per 15-minute window.
A TOTP code that had just been used to log in stayed valid for the rest
of its 30-second step, so the same code could be sent again and be
accepted a second time. RFC 6238, section 5.2, rules that out: "The
verifier MUST NOT accept the second attempt of the OTP after the
successful validation has been issued for the first OTP, which ensures
one-time only use of an OTP."
Totp.validate from time2fa only answers yes or no, so findTimeStep
checks the step with Hotp.validate and returns the step that matched.
The login check, verifyTOTP, records that step in totpLastUsedStep and
refuses any code from the same or an earlier step. Both callers of
verifyLoginCredentials use it: the login form, and the token handler
behind /api/login/token and /api/sender/login. The password is checked
first, so a step is only recorded once the password has matched, as
with recovery codes. setSecret clears the recorded step, so a newly
enrolled secret's first code is not refused over a step the previous
secret used.
totpLastUsedStep is local to the instance, like the TOTP secret, and
the options API neither returns nor accepts it: it is not in
ALLOWED_OPTIONS. Reading, comparing and writing it happen in one
synchronous call, so two requests carrying the same code cannot both
pass.
validateTOTP keeps checking without recording anything, for the setup
wizard. The wizard runs with becca unloaded, where options must not be
written (see SetupSecondFactor.verify), which is also why
matchesRecoveryCode leaves a recovery code unspent there. Enrollment
records nothing either: the secret is not persisted yet when
validateTOTPForSecret checks the code.