Skip to content

Commit 0833c53

Browse files
authored
Merge pull request #24 from mangyan1/fix/upstream-sync-20260916
merge upstream develop: plain javascript frontend, new permission model, fork security invariants kept
2 parents c973726 + 6a95e54 commit 0833c53

437 files changed

Lines changed: 3059 additions & 5546 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/caddy.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
3232
with:
3333
driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1
34-
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
34+
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
3535
with:
3636
context: caddy
3737
platforms: linux/amd64
@@ -61,10 +61,10 @@ jobs:
6161
packages: write
6262
steps:
6363
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
64-
- uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
64+
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
6565
with:
6666
platforms: arm64
67-
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
67+
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
6868
with:
6969
driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1
7070
- name: ghcr.io-login
@@ -74,7 +74,7 @@ jobs:
7474
username: ${{ github.repository_owner }}
7575
password: ${{ github.token }}
7676
- id: build
77-
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
77+
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
7878
with:
7979
context: caddy
8080
platforms: linux/amd64,linux/arm64

.github/workflows/docker-develop.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
packages: write
1919
steps:
2020
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21-
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
21+
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
2222
with:
2323
driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1
2424
- name: ghcr.io-login
@@ -32,7 +32,7 @@ jobs:
3232
run: |
3333
jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json
3434
jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json
35-
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
35+
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
3636
with:
3737
context: .
3838
push: ${{ github.event_name != 'pull_request' }}
@@ -51,7 +51,7 @@ jobs:
5151
packages: write
5252
steps:
5353
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
54-
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
54+
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
5555
with:
5656
driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1
5757
- name: ghcr.io-login
@@ -65,7 +65,7 @@ jobs:
6565
run: |
6666
jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json
6767
jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json
68-
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
68+
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
6969
with:
7070
context: .
7171
push: ${{ github.event_name != 'pull_request' }}

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@ All notable changes to the NPMplus Security Fork are documented here. The fork u
44

55
## Unreleased
66

7-
Nothing yet.
7+
### Added
8+
9+
- Reconciled upstream develop through `2fcc605a` manually (the scheduled upstream-sync run had stopped on 42 conflicted files — its fail-closed behavior) and adopted upstream's typescript-to-javascript frontend migration: the react-query client, API modules, components, and pages are plain JavaScript now, and the frontend tsconfig, `tsc` build step, and typescript/`@types/*` devDependencies are gone. The fork-only `.ts`/`.tsx` sources (Anubis/CrowdSec/security UI, deployment recovery, error boundary) remain and run through Node 24 type stripping. Frontend tests run via `node --test test/*.test.ts`.
10+
11+
- Adopted upstream's simplified permission model (`isAdmin`, `canAdmin()`, `canUser(id)`, `can("type:manage")`, `get visibility()`) replacing the per-object permission walk; `access.js` drops to ~120 lines. The nginx privilege guard (raw config and local-path fields stay admin-only) and the CrowdSec route gate now wrap the synchronous `canAdmin()` call, and their tests were updated to the synchronous shape.
12+
13+
- Adopted upstream's nginx control API: config reload now patches `http://localhost/1/control/config` over the `/run/nginx-control.sock` unix socket via the undici client instead of running `nginx -s reload`; the API tests stub the reload since the socket does not exist in the test environment. `undici` 8.10.2 enters the backend manifest (verified 11 days published at merge time, inside the `minimumReleaseAge` window).
14+
15+
- `nickname` is now server-managed: absent from all API schemas and responses, with the `nickname_default` migration backfilling existing rows. API tests no longer send it.
16+
17+
- Every fork security invariant is preserved and re-verified against the merge (127/127 backend tests, 10/10 frontend tests, `vite build`, and `tests/security-invariants.mjs` green): an anonymous missing session is still a 403 at route permission checks rather than upstream's 401 (rejected sessions remain 401); the login, refresh, and OIDC rate limiters and the OIDC `no_redirect` cookie handling are unchanged; gravatar fetching stays bounded (5 s timeout, 1 MiB cap) with avatar cleanup and login-time backfill; the one-time setup-token gate and its field whitelist are kept, and upstream's open `POST /users/setup` endpoint was not taken. `multer` 2.3.0 and `mysql2` 3.24.3 stay fork-side; the remaining upstream dependency bumps that were younger than the `minimumReleaseAge` window were rejected and age out through the renovate cron.
818

919
## v2.15.1-mangyan1.rc.6 - 2026-09-15
1020

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ RUN cd /src/nginx && \
118118
--with-threads \
119119
--with-file-aio \
120120
--with-libatomic \
121+
--with-control-api \
121122
--with-pcre \
122123
--with-pcre-jit \
123124
--without-select_module \
@@ -133,6 +134,7 @@ RUN cd /src/nginx && \
133134
--with-http_gunzip_module \
134135
--with-http_gzip_static_module \
135136
--with-http_sub_module \
137+
--with-http_json_module \
136138
--with-http_addition_module \
137139
--with-http_stub_status_module \
138140
--with-http_auth_request_module \
@@ -180,8 +182,7 @@ RUN apk upgrade --no-cache -a && \
180182
find /app/node_modules -name "*.node" -type f -exec llvm-strip -s {} \; && \
181183
find /app/node_modules -name "*.node" -type f -exec file {} \;
182184
COPY frontend /app
183-
RUN pnpm tsc -b && \
184-
pnpm vite build && \
185+
RUN pnpm vite build && \
185186
find /app/dist -type f ! -name "*.jpg" ! -name "*.png" -print0 | xargs -r0 -P "$(nproc)" -n 1 brotli -q 11 -sf
186187

187188
FROM --platform=$BUILDPLATFORM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS backend

backend/internal/access-list.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { appendFile, rm, unlink, writeFile } from "node:fs/promises";
1+
import { appendFile, rm, writeFile } from "node:fs/promises";
22
import bcrypt from "bcryptjs";
33
import _ from "lodash";
44
import errs from "../lib/error.js";
@@ -21,7 +21,7 @@ const internalAccessList = {
2121
* @returns {Promise}
2222
*/
2323
create: async (access, data) => {
24-
await access.can("access_lists:create", data);
24+
access.can("access_lists:manage");
2525
const row = utils.omitRow(omissions())(
2626
await accessListModel.query().insertAndFetch({
2727
name: data.name,
@@ -101,7 +101,7 @@ const internalAccessList = {
101101
* @return {Promise}
102102
*/
103103
update: async (access, data) => {
104-
await access.can("access_lists:update", data.id);
104+
access.can("access_lists:manage");
105105
const row = await internalAccessList.get(access, { id: data.id });
106106
if (row.id !== data.id) {
107107
// Sanity check that something crazy hasn't happened
@@ -208,7 +208,7 @@ const internalAccessList = {
208208
*/
209209
get: async (access, data, skipMasking) => {
210210
const thisData = data || {};
211-
const accessData = await access.can("access_lists:get", thisData.id);
211+
access.can("access_lists:view");
212212

213213
const query = accessListModel
214214
.query()
@@ -231,7 +231,7 @@ const internalAccessList = {
231231
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_lists.[clients,items]]]")
232232
.first();
233233

234-
if (accessData.permission_visibility !== "all") {
234+
if (access.visibility !== "all") {
235235
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
236236
}
237237

@@ -265,7 +265,7 @@ const internalAccessList = {
265265
* @returns {Promise}
266266
*/
267267
delete: async (access, data) => {
268-
await access.can("access_lists:delete", data.id);
268+
access.can("access_lists:manage");
269269
const row = await internalAccessList.get(access, {
270270
id: data.id,
271271
expand: ["proxy_hosts.[certificate, access_lists.[clients,items]]", "items", "clients"],
@@ -353,11 +353,7 @@ const internalAccessList = {
353353
await internalNginx.reload();
354354

355355
// delete the htpasswd file
356-
try {
357-
await unlink(internalAccessList.getFilename(row));
358-
} catch {
359-
// do nothing
360-
}
356+
await rm(internalAccessList.getFilename(row), { force: true });
361357

362358
// 4. audit log
363359
await internalAuditLog.add(access, {
@@ -378,7 +374,7 @@ const internalAccessList = {
378374
* @returns {Promise}
379375
*/
380376
getAll: async (access, expand, searchQuery) => {
381-
const accessData = await access.can("access_lists:list");
377+
access.can("access_lists:view");
382378

383379
const query = accessListModel
384380
.query()
@@ -400,7 +396,7 @@ const internalAccessList = {
400396
.allowGraph("[owner,items,clients]")
401397
.orderBy("access_list.name", "ASC");
402398

403-
if (accessData.permission_visibility !== "all") {
399+
if (access.visibility !== "all") {
404400
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
405401
}
406402

backend/internal/audit-log.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ const internalAuditLog = {
1111
* @param {String} [searchQuery]
1212
* @returns {Promise}
1313
*/
14-
getAll: async (access, expand, searchQuery) => {
15-
await access.can("auditlog:list");
14+
getAll: (access, expand, searchQuery) => {
15+
access.canAdmin();
1616

1717
const query = auditLogModel
1818
.query()
@@ -43,7 +43,7 @@ const internalAuditLog = {
4343
* @return {Promise}
4444
*/
4545
get: async (access, data) => {
46-
await access.can("auditlog:list");
46+
access.canAdmin();
4747

4848
const query = auditLogModel.query().andWhere("id", data.id).allowGraph("[user]").first();
4949

backend/internal/certificate.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const internalCertificate = {
116116
* @returns {Promise}
117117
*/
118118
create: async (access, data) => {
119-
await access.can("certificates:create", data);
119+
access.can("certificates:manage");
120120
data.owner_user_id = access.token.getUserId(1);
121121

122122
if (data.provider === "letsencrypt") {
@@ -191,7 +191,7 @@ const internalCertificate = {
191191
* @return {Promise}
192192
*/
193193
update: async (access, data) => {
194-
await access.can("certificates:update", data.id);
194+
access.can("certificates:manage");
195195
const row = await internalCertificate.get(access, { id: data.id });
196196

197197
if (row.id !== data.id) {
@@ -231,15 +231,15 @@ const internalCertificate = {
231231
* @return {Promise}
232232
*/
233233
get: async (access, data) => {
234-
const accessData = await access.can("certificates:get", data.id);
234+
access.can("certificates:view");
235235
const query = certificateModel
236236
.query()
237237
.where("is_deleted", 0)
238238
.andWhere("id", data.id)
239239
.allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]")
240240
.first();
241241

242-
if (accessData.permission_visibility !== "all") {
242+
if (access.visibility !== "all") {
243243
query.andWhere("owner_user_id", access.token.getUserId(1));
244244
}
245245

@@ -282,7 +282,7 @@ const internalCertificate = {
282282
* @returns {Promise}
283283
*/
284284
download: async (access, data) => {
285-
await access.can("certificates:get", data);
285+
access.can("certificates:view");
286286
const certificate = await internalCertificate.get(access, data);
287287
if (certificate.provider === "letsencrypt") {
288288
const zipDirectory = internalCertificate.getLiveCertPath(data.id);
@@ -356,7 +356,7 @@ const internalCertificate = {
356356
* @returns {Promise}
357357
*/
358358
delete: async (access, data) => {
359-
await access.can("certificates:delete", data.id);
359+
access.can("certificates:manage");
360360
const row = await internalCertificate.get(access, { id: data.id });
361361

362362
if (!row?.id) {
@@ -411,7 +411,7 @@ const internalCertificate = {
411411
* @returns {Promise}
412412
*/
413413
getAll: async (access, expand, searchQuery) => {
414-
const accessData = await access.can("certificates:list");
414+
access.can("certificates:view");
415415

416416
const query = certificateModel
417417
.query()
@@ -420,7 +420,7 @@ const internalCertificate = {
420420
.allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]")
421421
.orderBy("nice_name", "ASC");
422422

423-
if (accessData.permission_visibility !== "all") {
423+
if (access.visibility !== "all") {
424424
query.andWhere("owner_user_id", access.token.getUserId(1));
425425
}
426426

@@ -496,15 +496,15 @@ const internalCertificate = {
496496

497497
/**
498498
* Validates that the certs provided are good.
499-
* No access required here, nothing is changed or stored.
499+
* Nothing is changed or stored.
500500
*
501501
* @param {Access} access
502502
* @param {Object} data
503503
* @param {Object} data.files
504-
* @returns {Promise}
504+
* @returns {Object}
505505
*/
506-
validate: async (access, data) => {
507-
await access.can("certificates:create");
506+
validate: (access, data) => {
507+
access.can("certificates:manage");
508508
const finalData = {};
509509
for (const [name, [file]] of Object.entries(data.files)) {
510510
if (internalCertificate.allowedSslFiles.includes(name)) {
@@ -536,7 +536,7 @@ const internalCertificate = {
536536
}
537537
const isMtls = row.provider === "mtls";
538538

539-
const validations = await internalCertificate.validate(access, data);
539+
const validations = internalCertificate.validate(access, data);
540540
if (typeof validations.certificate === "undefined") {
541541
throw new error.ValidationError("Certificate file was not provided");
542542
}
@@ -753,7 +753,7 @@ const internalCertificate = {
753753
* @returns {Promise}
754754
*/
755755
renew: async (access, data) => {
756-
await access.can("certificates:update", data);
756+
access.can("certificates:manage");
757757
const certificate = await internalCertificate.get(access, data);
758758

759759
if (certificate.provider === "letsencrypt") {
@@ -910,7 +910,7 @@ const internalCertificate = {
910910
* @returns
911911
*/
912912
testHttpsChallenge: async (access, payload) => {
913-
await access.can("certificates:list");
913+
access.can("certificates:view");
914914

915915
// Create a test challenge file
916916
await writeFile("/data/tls/certbot/acme-challenge/.well-known/acme-challenge/test-challenge", "Success", {

0 commit comments

Comments
 (0)