This fork follows ZoeyVid/NPMplus while maintaining the installer, CrowdSec and
Anubis reporting, recovery tooling, and documented security corrections. Upstream
updates enter through reviewed merge proposals into mangyan1/NPMplus:develop.
The installed server then updates from this fork's tested image and installer.
Small, isolated additions reduce merge conflicts. They cannot guarantee that future upstream changes will preserve behavior, even when Git merges cleanly. Keep existing paths where they already isolate a feature; moving every file into a new directory would add merge work without improving the boundary.
| Area | Fork-owned implementation | Shared integration points to review |
|---|---|---|
| Installation and recovery | setup-npmplus.sh, doctors, tests/installer-recovery.py |
Compose mounts, rootfs startup, health checks, image channels |
| Security dashboard | frontend/src/pages/Crowdsec/, CrowdSec API/hooks and manual-ban modal |
Router, admin menu/permissions, translations, proxy-host controls |
| Security API and retained observations | CrowdSec routes/internal helpers, Anubis reporting, security telemetry and their tests | backend/routes/main.js, startup in backend/index.js, database migrations |
| Browser deployment recovery | frontend/src/fork/deployment-recovery.ts, ErrorBoundary.tsx |
Small startup call in main.jsx, route boundary in Router.jsx, nginx no-store policy |
| nginx enforcement observations | npmplus_telemetry.lua, private telemetry listener, bouncer instrumentation script |
Dockerfile hook, proxy template host ID, rootfs configuration |
| Fork publishing and maintenance | Fork release/security/smoke workflows, .github/scripts/upstream-sync.sh, this guide |
Upstream dependency changes, image builds, final-image scans |
| Security corrections in shared code | Auth, OIDC/MFA, validation, nginx privilege handling, certificate compatibility tests | Review each overlapping upstream change against the relevant regression test |
Prefer one small import or startup call in a shared entry point over embedding a new subsystem there. Add implementation beside the existing feature modules. Avoid unrelated formatting, bulk renaming, dependency churn, or moving historical migrations. Never remove a required security correction just to shrink the diff.
Certificate behavior has an additional owner restriction: do not restructure
backend/internal/certificate.js. Preserve DNS plugin installation and deliberately
update backend/test/certificate-dns.test.js before any approved behavior change.
The daily/manual upstream sync workflow runs only in mangyan1/NPMplus. Its
script refreshes ZoeyVid's develop tracking ref and prepares a merge on
automation/upstream-sync from the current origin/develop.
- Every
ghAPI command names the fork explicitly. GitHub CLI can otherwise default to a fork's parent. - If upstream is already included, nothing is published.
- A clean merge refreshes the disposable proposal branch and opens a PR if one does not already exist. Only that branch may be replaced, with an explicit expected-tip lease that rejects concurrent updates.
- A conflict stops the run and lists the files in its summary. The existing
proposal,
develop, and release tags remain untouched. Other merge/GitHub failures are reported as failures, rather than being mistaken for no changes. - The workflow does not auto-merge, deploy, create a release, or post issues to either repository. Check the job summary when a scheduled run is red.
GitHub may put PR checks created with GITHUB_TOKEN into an approval-required
state. Select Approve workflows to run on the PR and wait for required checks.
See GitHub's token event rules.
If PR creation is denied, verify the target repository and its Actions setting
allowing Actions to create pull requests; do not broaden permissions upstream.
The script is intended for an Actions checkout. Use the local procedure below when reviewing a merge manually. Its regression tests use local bare Git remotes and a fake GitHub CLI; no real branch or PR is created by the tests.
From a clean checkout, fetch and inspect before changing the working branch:
git fetch origin develop
git fetch upstream develop
git log --oneline origin/develop..upstream/develop
git merge-tree --write-tree --name-only origin/develop upstream/developmerge-tree creates Git objects without changing the index or working tree. A
zero exit code establishes only that the text merge succeeds. For an actual
review, create a separate branch from origin/develop, merge upstream there,
and resolve overlaps file by file. Never reset develop to upstream, rebase away
published fork commits, or blindly select one side of every conflict.
When upstream replaces one of our fixes, compare behavior and regression tests. Keep the upstream implementation if it covers the same requirements, then remove only the redundant fork implementation in a normal reviewable commit. Preserve attribution and upstream history with merge commits.
Two mistake classes occurred in one day, and both are now guarded:
-
Never cache anything that embeds per-request identity. The Ajv permission cache (PR #19) was only safe because the four
users-*permissions embedded the calling user's id enum through the per-requestobjectsschema and were therefore excluded from validator caching. That cache was removed with upstream's plaincan()/canUser()permission model - identity is now per-request by construction, and Rule 4 oftests/security-invariants.mjsrecords the logout-revocation contract that replaced it. If per-request caching ever returns, the exclusion must return with it. The regression pin iscached permission checks never leak one user's id into another's validationinbackend/test/api.test.js- it fails the suite if a cached validator is ever applied to ausers-*permission, and it was verified to fail against a deliberately sabotaged guard before being trusted. Run it whenever touching access control, and keep the negative-verification habit: break the guard on purpose, prove the test turns red, restore, prove green. -
Verify the mechanism, not just the outcome, before writing it down. The
mmap_sizeexclusion (PR #20) was initially recorded with the wrong reason - "compiled out" - when the real mechanism was better-sqlite3's pragma-API whitelist silently rejecting it. Both the exclusion decision and its recorded justification were corrected in85d9c244. When a claim about a driver, dependency, or runtime enters the CHANGELOG, it must be backed by a direct probe of that exact build (e.g.PRAGMA compile_options), not inference from a symptom like an empty readback.
The two rule classes above are also machine-enforced so future sessions cannot drift past them, even under pressure to "just make it work":
.github/CODE_GUIDELINES.mdis the always-loaded constraint summary for every coding session (VS Code reads it automatically): the nine security invariants, the change discipline, and the verification ladder. It points here and to the tests for rationale.tests/security-invariants.mjsruns inlint-and-formatand fails the build on drift in eight load-bearing properties: subprocess output never reaching API responses (app.js shape, error-object schema, CommandError visibility), the jwtdecode 401-for-rejected-session contract (comment-aware so a commented-out 401 cannot satisfy it), the no-store Cache-Control on the admin index and SPA fallback location blocks, per-request logout revocation at the access layer, the bounded-fetch timeout/byte-cap contracts, the merged permission model failing closed on malformed inputs, fork-only package.json lines surviving upstream merges, and the crowdsec data dir being dereferenced before the hub refresh.- Every rule in the checker was negative-verified before being trusted: each guarded file was sabotaged with its real drift pattern, the checker had to catch it, and the tree was restored. A check that has never been seen to fail proves nothing - keep that discipline when adding rules.
- The behavioral layer is the regression pins in
backend/test/api.test.js, notablycached permission checks never leak one user's id into another's validation, which was itself proven red against a sabotaged cache guard. - When a new incident reveals a drift class worth guarding, add a rule to the checker AND a sabotage case to the negative-verification habit, never a rule alone. Never edit the checker to make a change pass; redesign the invariant with owner approval instead, and record it here.
Before merging the proposal:
- Review the shared integration points above, dependency/lockfile changes, and any upstream rewrite. Confirm admin authorization, 401/403 semantics, DNS-01 support, CrowdSec/AppSec/Anubis enforcement, and no-store HTML behavior survive.
- Run
lint-and-format: both application linters/tests, schema validation, installer recovery tests, sync contracts, frontend build, translation sorting, and the clean-diff gate. Runpython3 tests/upstream-sync.pydirectly on Linux when changing sync behavior; it needs Bash and Git. - Exercise affected browser/API smoke flows. Deployment changes also need the installer variants, Debian restore, boot-resilience, and relevant image scans. Wait for required checks; an absence of checks is not a pass.
- Merge the reviewed proposal into this fork and let its image build finish. Update installed servers with this fork's safe updater. A direct switch to ZoeyVid's image is a separate migration, not this sync process.
Update this tracked guide when an integration boundary or sync rule changes.
Record user-visible behavior in CHANGELOG and the relevant docs/ guide. The
local AGENTS.md handoff is intentionally ignored and must not be force-added;
durable maintenance rules therefore also live here.
Internal audit reports belong in ignored local storage, outside the published
docs/ tree. Preserve the fork's .gitignore and .dockerignore exclusions and
intentional report deletions during upstream merges. Git ignore rules do not
prevent a merge from adding tracked files: review incoming documentation before
publishing a proposal. If upstream edits a report deleted by the fork, stop for
review rather than restoring it automatically. Keep security implementation,
regression tests, reporting policies, and scanner exception files maintained.
Local handoffs and private reports are not available in a fresh clone.
Release notes are user-facing and stay short: one line per change, no commit
hashes or file paths, with the full record kept in CHANGELOG. .github/release-notes/TEMPLATE.md
carries the skeleton and the rules; release.yml refuses a tag whose notes file
is missing or empty and appends the image-digest sections itself.
The 2026-09-08 tidy extracts the sync workflow into a tested script and the browser deployment recovery into a dedicated module. Existing dashboard modules and database migrations retain their paths.
The September 12 integration merges upstream 3d5ac185 through
PR #16, landed on develop as
c891f43f. Five conflicts were resolved: user routes, frontend manifest and
lockfile, and both CrowdSec nginx configurations. It preserves central Express
error handling, 401/403 semantics, server-side logout revocation, no-store HTML,
CrowdSec telemetry instrumentation, reviewed dependency versions, and nginx
basic-auth bcrypt cost 6. Public CommandError debug payloads were removed by the
security fixes in 8639e5a7; do not reintroduce them during a later merge.
Upstream Argon2id login passwords and recovery codes are integrated. A conditional legacy-hash update prevents a stale login from overwriting a password reset. Token issuance waits out the revocation second rather than issuing future-dated tokens. Preserve the migration, concurrent recovery-code, immediate-login, and logout regression tests when updating shared authentication code.
Local validation passed: 118 backend tests, 10 frontend tests, production build, schema, real-router/browser smoke, 8 Linux sync contracts, and 15 Linux recovery tests. A disposable container check verified Argon2 password reset, session revocation, and MFA clearing. PR checks, the candidate image vulnerability scan, installer variants, and reboot resilience all passed before merging. The post-merge upstream sync succeeded. This records a tested merge, not a guarantee for later upstream revisions.
The September 20 integration merges upstream a1ac84c8 through
PR #29, landed on develop as the
merge commit bfa66942 (parents 7e6aca60 + the reviewed merge 2dfec1a6;
regular merge, ancestry preserved). Thirteen upstream commits, 18 conflicts.
It adopts upstream's per-row second-factor model, which is the one change that
could not be declined: see the resolution notes below. The fork's TOTP replay
protection, backup-code single-use, ACL ownership scoping, DB-backed token
challenges, and reviewed dependency versions are all preserved. Local validation:
163 backend tests, 15 frontend tests, validate-schema, both biome runs, the
production build, and tests/security-invariants.mjs. The three Linux-only python
contracts cannot run on the Windows rig (MSYS path mangling) and were confirmed
environmental by reproducing identical failures on a pristine origin/develop
worktree; all three are green on CI. Every PR check passed before merging, and the
post-merge develop push is green including boot-resilience and the caddy
build-and-scan job.
Upstream rewrote develop again (the previous integration 16116b76 was followed by
58f8a1b9, 968d7e07, and ec092c26), which made the scheduled upstream sync
run fail closed on conflicts. That failure is the intended stop-for-review
behavior, not a CI regression. The prepared merge on fix/upstream-sync-20260913
resolves three files:
backend/app.js: upstream now publishespayload.error.outputfromCommandErrormessages. That reintroduces the raw subprocess stdout/stderr leak (certbot command lines can carry credentials and paths) that8639e5a7removed, so the fork's generic-message handler is kept. The auto-mergederror-object.jsonoutputproperty was reverted for the same reason: the schema must describe what this fork actually returns. The frontend modal changes are kept becauseerr.payload?.error?.outputis simply never set; the translated<T id={err.message}>fallback renders instead.backend/internal/user.js: upstream's avatar rework (id-keyed gravatar cache, magic-byte image detection, stale-extension cleanup, post-insert avatar patch) is adopted, merged with the fork's hardening: the bounded 5s fetch timeout and 1 MiB bounded body read remain, andbackfillGravatarAvatarkeeps backfilling empty avatars on login (now writing id-keyed files). The gravatar contract tests assert the id-keyed file names.backend/routes/users.js: upstream's strict setup whitelist (onlyname,nickname,email,auth, forcedroles: ["admin"]) replaces the fork's field-defaulting block, while the fork's setup-mode race guard (setupCreationInProgress), one-time setup-token verification, token removal on completion, andfinallyrelease are preserved. The tightenedpost.jsonauthobject schema (password type/secret required when present) is accepted; the admin UserModal sends noauthon create and the setup wizard sends exactlyname,nickname,email,auth.
Auto-merged without conflicts: backend/setup.js now seeds the initial admin via
internalUser.create with internal access (the audit entry records user id 1),
backend/schema/paths/users/post.json, the seven frontend certbot-error modals,
and rootfs/usr/local/bin/envs.sh (the upstream Unraid app-template refusal and
the UID/GID/$UID-env root checks; the fork's compose.yaml sets neither, so
the standard deployment is unaffected).
The installer smoke workflow points its self-check at the file under test via a
file:// SELF_URL; the production stale-script guard remains enabled. Channel
selection and password-hash rollback compatibility are documented in the
operations guide.
The scheduled upstream sync run was red because upstream force-pushed develop
again after the previous integration (82a09947); that is the intended
stop-for-review behavior, not a CI regression. Merging the current upstream tip
through PR #29 restores it. The
reviewed merge on fix/upstream-sync-20260920 resolves 18 files; the decisions
that matter:
- Second factors moved to their own rows. Upstream
7efc56c0gives each factor its ownauthrow under atypecolumn (password,totp,totp_pending,backup_code), and its migration20260919230355_auth_factor_rows.jsclearsmetato{}on every password row. The fork's factors lived inauth.meta, so keeping our side was not a different-but-working option: upstream's migration would have wiped the fields those functions read, silently turning 2FA off and locking out the users who rely on it. Adopted, with both fork protections re-expressed on the row model:- TOTP replay stays atomic.
verifyCodeclaims the matched step with an id-scoped conditional patch (WHERE id = ? AND (last_used_step IS NULL OR last_used_step < ?)), so concurrent logins cannot reuse a code, and a run whose enrollment row was replaced mid-verification has nothing left to claim.enablestamps the step the enrollment code came from when it promotestotp_pendingtototp, and that promotion is itself conditional on the row still being pending. - Backup codes stay single-use through the row-existence-guarded delete
(
findById(code.id).delete() === 1), equivalent to the fork's CAS under concurrency. models/auth.jsdrops the now-unusedgetPasswordAuthSnapshotand themetaCaststatic (the DB-cast optimistic lock is obsolete under rows) and addsgetTotpEnrollment.
- TOTP replay stays atomic.
validateAccessListstakes upstream's argument order. The body remains the fork's, which is stricter than upstream's visibility check: non-admins are scoped to their own ACLs viacanAdmin()(try/catch, as inlib/nginx-privilege.js), and a missingaccessthrows so a future caller cannot skip the scoping. This supersedes thevalidateAccessLists(proxyHost, access)order recorded for rc.8 — same semantics, upstream's order, both call sites inproxy-host.jsupdated.- Token challenges stay DB-backed. Upstream's
a59e3db1adds an in-memoryconsumedChallengesmap whose own message concedes that restarts break it. The fork removed that map deliberately, sotoken.jsis kept and the map is not reintroduced. - Dependency versions stay fork-side.
backend/package.json,frontend/package.jsonand both lockfiles are kept: upstream's bumps (6887f082,20b56ee8) are younger than the 7-dayminimumReleaseAgewindow. Both workspaces still install under--frozen-lockfile, and the renovate cron ages the bumps in. access-list.jshashing takes upstream's asyncbcrypt.hashat the fork's cost 6.
Adopted from upstream without argument: the alpine 3.24.2 bump (7dac39b6) in
both Dockerfiles — nginx release-1.31.6 and aws-lc v5.9.0 were already the
fork's pins, so nothing was dropped — the uppercase-hex IPv6 binding acceptance
in envs.sh, the stricter certificate domain pattern in
certificate-object.json, ubuntu-26.04-arm in docker-develop.yml, and the
| default: env.AUTH_REQUEST_*_UPSTREAM fallbacks in proxy_host.conf, which
compose with the fork's _upstream_resolved handling. Four test files were
adapted to the row model (mfa, totp-replay, sqlite-upgrade). The ACL
ownership test was verified to still bite: with the scoping disabled, the hijack
PUT returns 200 instead of 400.
Upstream force-pushed develop again after the September 20 integration, so the
scheduled upstream sync run fails closed until this merge lands — the intended
stop-for-review behavior. Because upstream's rewrite orphans the September 20
merge from upstream's history, the merge-base regressed to 20b56ee8 and git
re-offers ten commits the fork already carries as upstream/develop..develop
noise; patch-id comparison confirms they are identical, and only seven upstream
commits are genuinely new. The reviewed merge on fix/upstream-sync-20260922
resolves 20 files; the decisions that matter:
- Backup-code logic moves into upstream's module.
9d3fa731createsbackend/internal/backup-codes.js(generate, count, create, delete, verify) and97849ffbswitches generation to the Crockford Base32 alphabet with O/I/L input normalization.mfa.jstakes upstream's module calls. The fork's inline loop was the same delete-on-use logic, so the module loses nothing and this also fixes a latent break: the fork's tree referencedgenerateBackupCodeswith no import or definition anywhere. The row-existence-guarded delete (only the request that removes a code counts it as used) survives insideverify. - TOTP reuse stays DB-level.
68fce1e9blocks enrollment-code replay with an in-memoryusedStepsmap. Declined: the fork'senablealready claims the enrollment step with a conditional patch onnpmplus_totp_last_used_step, andverifyCodeclaims login steps the same way — both survive restarts, which upstream's map does not (its own commit message concedes this for the MFA variant). - Token challenges stay session-backed. The September 20 decision stands:
upstream's
consumedChallengesmap is not reintroduced. - Everything else fork-side stays. bcrypt cost 6 in
access-list.js, thecanAdmintry/catch ownership scoping inproxy-host-access-list.js(upstream'svisibility !== "all"variant is the same protection in the same place), the strictaddress/forward_host/domain patterns incommon.jsonand the host schemas, the secret-file compose envs, the emptyAPPSEC_URLwith the fail-closed comment inrootfs/etc/crowdsec.conf.example, the fork README, and the reviewed dependency versions in both manifests and both lockfiles — upstream643cb6a4is Sep 15, inside the 7-dayminimumReleaseAgewindow, so the renovate cron ages it in.
Adopted from upstream without argument: the error-class cleanup
(8a8013ac/1338f918: the unused previous parameter is dropped everywhere,
AuthError moves 400 → 401, PermissionError for expired/invalid tokens in
models/token.js and lib/access.js, call sites in nginx.js, setting.js,
nginx/certificates.js simplified), the app.js trust-proxy change
(app.set("trust proxy", 1) replaces the full trust plus the
x-real-ip→x-forwarded-for rewrite, and the now-redundant limiter
validate override is dropped), the login-form resetForm on TOTP error
(97849ffb), the schema maxLength: 255 additions
(f66dc0b7), and the openresty patch-hash bump in the Dockerfile.
The 401 AuthError forced one further port: the fork's frontend logs out on
any 401 (base.js processResponse), so a login attempt that 401s now
clears the session and reloads the page instead of showing the form error.
Upstream pairs the 401 with PermissionError on every user-attempted
rejection, and token.js now takes that split: wrong password, invalid
TOTP (combined login and /tokens/totp), OIDC identity failures, disabled
password login, and the setup-wizard refusals are PermissionError (403,
rendered as a form error), while dead challenge tokens and consumed sessions
stay AuthError (401, where clearing the session is the intended recovery).
The i18n mapping is unaffected: PermissionError carries messageI18n in
the adopted error.js. The browser smoke caught this only because the
replay check waits for the visible alert — the raw-API tests alone missed
it.
The backend assertions move to the 403 semantics (wrong-password and
setup-token refusals in api.test.js, auth-rate-limits.test.js,
initial-setup.test.js), while the invalid-session-cookie and dead-session
replay checks stay 401; the same split applies to the docker smoke
(security-regressions.mjs, security-ui.mjs), whose three invalid-code
checks became 403 and whose challenge/logged-out replay checks stay 401.
The PUT-without-current-password refusal remains a 400 ValidationError.
Local validation: 163 backend tests, 15 frontend tests, validate-schema,
tests/security-invariants.mjs, pnpm vite build, biome clean on the edited
files, and the full docker-security.mjs integration (two disposable
containers: API privilege/replay checks, modal interactions, the CrowdSec
dashboard harness, fresh first-admin setup with MFA enrollment and
backup-code acknowledgement, visible replay rejection, and proxy creation)
against an image built from this branch. The only local biome complaints are
CRLF work-tree phantoms in untouched files (sqlite-upgrade/totp-replay
tests, the CrowdSec dashboard and the login page), all LF in the index. The
Linux-only python contracts remain unrunnable on the Windows rig
(environmental, as recorded for September 20).
The September 22 reconciliation reached develop as PR #31 but its merge
commit 94e1fd4f was a regular single-parent commit: during preparation the
conflicted merge commit was redone after a git reset --soft HEAD^, and a
plain git commit does not restore the second parent. Upstream's tip
(1338f918) was therefore content-merged but never an ancestor, and GitHub's
banner kept reporting the fork 17 behind. The repair merge f1fa57ff on
develop uses -s ours: no tree change (every upstream change was already
reconciled, tested, and container-smoked in 94e1fd4f and 364f0c00), it
only records the ancestry, so the banner clears and the next sync diffs
against a real merge-base instead of re-offering ten rebased duplicates.
When redoing a conflicted merge commit, redo it as a merge (git checkout -m
or git merge again), never a plain git commit after --soft.
Upstream force-pushed again and added eight genuinely new commits on top of
rebased duplicates of everything already carried (patch-id comparison again;
merge-base still regressed to 20b56ee8). Resolution by file:
backend/internal/token.js,backend/internal/totp.js: ours. Upstream's new "claim the challenge before checking the code, release it on failure" reorder builds on their in-memoryconsumedChallenges/usedStepsMaps and is needed only because those are non-atomic and restart-volatile. The fork's DB-level claims (assertTokenSession/consumeChallengeSessionand the conditionalnpmplus_totp_last_used_steppatch) already make the same race impossible atomically, and verify-then-claim keeps the friendlier behavior (a mistyped code does not burn the challenge; a replayed one still fails).backend/routes/{tokens,oidc,users}.jsand certificates download limiter: adopted upstream's removal ofvalidate: { trustProxy: false }. The suppression exists only to silenceERR_ERL_PERMISSIVE_TRUST_PROXY, which fires only whentrust proxy === true; since the sync adoptedapp.set("trust proxy", 1)the check can never fire, and removing the suppression restores fail-loud behavior if someone ever sets it back totrue.backend/routes/nginx/certificates.js: ours. The fork's multer limits (files: 2, fields: 0, parts: 2, fieldNameSize: 64) already exceed upstream's newfields: 0.Dockerfile: ours + upstream's deltas. The fork pins pip/certbot/luarocks versions and instruments the crowdsec bouncer (instrument-crowdsec-telemetry.py) in this file; upstream renamedRCP_VERtoORP_VER, swapped nginx patch 2 for PR 1756, and added openresty patches 8 (resolver_hosts), 9 (upstream_pipelining), and 10 (reuseport_close_unused_fds, applied with-C1).rootfs/usr/local/nginx/conf/nginx.conf: auto-merged to upstream's newresolver local=on hosts=on ipv6=on(backed by the new resolver_hosts patch) and the reworked quotedalogformat.rootfs/etc/dinit.d/goaccess: upstream's new log-format fields (which must track the nginxalogchange) plus the fork's--external-assets, which upstream never had and which keeps the goaccess page loadable under the fork's strict CSP.rootfs/etc/crowdsec.conf.example: ours (fail-closed emptyAPPSEC_URL).- Dependencies: upstream's bump set split by maturity. Taken:
multer 2.4.0,react-router 8.4.0. Held back as younger than the 7-dayminimumReleaseAgewindow (renovate will age them in):@biomejs/biome 2.5.14,undici 8.11.0(published the day of the merge),@apidevtools/json-schema-ref-parser 16.0.3,@apidevtools/swagger-parser 13.1.0,@tabler/icons-react 3.48.0,@tanstack/react-query 5.103.2,markdown-to-jsx 9.10.3, andreact-intl 12.1.2(a two-major jump, kept at 10.1.26; the bundledintl-messageformat 12.1.2is also inside the window). Lockfiles were regenerated withpnpm install --lockfile-only, which enforces the policy;minimumReleaseAgeExcludePrunethen dropped the now-maturemulter@2.3.0/react-router@8.3.1excludes by itself. - Frontend locale loading: combined, with a fork-specific adjustment.
Upstream's RTL support (
isRTLLocale, dynamictabler.rtl.min.css,dir=rtldocument handling, Persian (fa) and Turkish (tr) translations) was adopted on top of the fork'sinstallDeploymentRecovery()inmain.jsx. The RTL code only usescreateIntl/RawIntlProvider, so it stays compatible with the held-back react-intl 10. Upstream makes the tabler stylesheet itself conditional (dynamic import of eithertabler.min.cssortabler.rtl.min.css); the fork cannot copy that shape because the fork'svite.config.jsui-vendorcode-splitting group matches every@tablermodule, so both dynamically imported stylesheets collapse into the single always-loadedui-vendorcss asset and the RTL-flipped rules apply to LTR pages (this broke the responsive smoke checks: dialogs measured 12px wider than their viewport). Instead,main.jsxkeeps the LTRtabler.min.cssas a static import beforeApp.css(identical cascade and chunk to before the merge), and only RTL locales dynamically importtabler.rtl.min.cssbefore the first render;vite.config.jsexcludestabler.rtl.min.cssfrom theui-vendorgroup so it is emitted as its own asset and can never be concatenated into the always-loaded sheet. .github/workflows/dependency-updates.yml: upstream'sresolver_conf_parsing-updatejob becameopenresty-patches-updateupdating all four ORP patch hashes; kept with the file.
No schema, access-list, proxy-host, compose, or token/totp behavior changed: those conflict regions were resolved with the fork's versions, which are already stricter than upstream's state.
Upstream's same-day "further simplify locale code" pass (through
a2b765f8) was adopted nearly wholesale: changeLocale now persists the
choice and reloads instead of live-swapping a cached intl (dropping
createIntlCache, loadMessages, and applyDocumentLocale),
formatDateTime reads the module-level currentLocale internally so
callers stop threading a locale argument, the tabler stylesheet selection
moved to a top-level await keyed on document.dir (set synchronously at
src/locale module init, so it is readable before the awaits), and
context/LocaleContext.jsx plus hooks/useTheme.js are gone with
ThemeContext providing useThemeState. Two adjustments were required:
api/backend/base.jskeeps the fork's 401 handling: the 401 branch runs beforeresponse.json()so a proxy's HTML error page cannot crash the parse, logout still callsdeleteToken()so a stale token cannot poison the next login,error.statussurvives, and the helpers tolerate a nullparamsand a bareAbortSignal. Upstream's pass removed those capabilities from its own copy; no fork call site passesheadersor bare signals, but the 401 hardening is fork-documented behavior.- The fork-only CrowdSec pages kept their
crowdsec-decisionrendering (gavel icon,Decision #<id>, the i18n'd unknown-type fallback) and were moved onto the locale-freeformatDateTime(value)signature; upstream deleted theuseLocaleStatehook they had been using.
main.jsx adopts the top-level await shape with installDeploymentRecovery()
called before the stylesheet awaits, so the boot guard installs even when a
broken deploy 404s the chunks it awaits.