diff --git a/.github/ISSUE_TEMPLATE/todo-media-storage.md b/.github/ISSUE_TEMPLATE/todo-media-storage.md index ed95847cd..7c72b4eb8 100644 --- a/.github/ISSUE_TEMPLATE/todo-media-storage.md +++ b/.github/ISSUE_TEMPLATE/todo-media-storage.md @@ -40,7 +40,7 @@ Integrate MinIO (or Nhost Storage) for handling file uploads including avatars, ```yaml # docker-compose.yml minio: - image: minio/minio:latest + image: quay.io/minio/minio:latest ports: - '9000:9000' - '9001:9001' diff --git a/.github/deploy/docker/docker-compose.production.yml b/.github/deploy/docker/docker-compose.production.yml index bc2c6667a..c0867d6a4 100644 --- a/.github/deploy/docker/docker-compose.production.yml +++ b/.github/deploy/docker/docker-compose.production.yml @@ -254,7 +254,7 @@ services: # MinIO Storage (Production) # -------------------------------------------------------------------------- storage: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: nchat-storage restart: unless-stopped environment: diff --git a/.github/deploy/docker/docker-compose.staging.yml b/.github/deploy/docker/docker-compose.staging.yml index cedf45fad..d109f346f 100644 --- a/.github/deploy/docker/docker-compose.staging.yml +++ b/.github/deploy/docker/docker-compose.staging.yml @@ -168,7 +168,7 @@ services: # MinIO Storage (S3-compatible) # -------------------------------------------------------------------------- storage: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: nchat-storage-staging restart: unless-stopped ports: diff --git a/.github/deploy/docker/docker-compose.yml b/.github/deploy/docker/docker-compose.yml index c7e67b2d7..5cff5f633 100644 --- a/.github/deploy/docker/docker-compose.yml +++ b/.github/deploy/docker/docker-compose.yml @@ -138,7 +138,7 @@ services: # MinIO Storage (S3-compatible) # -------------------------------------------------------------------------- storage: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: nchat-storage restart: unless-stopped ports: diff --git a/.github/deploy/k8s/minio-deployment.yaml b/.github/deploy/k8s/minio-deployment.yaml index 0b9be2719..8901d94e2 100644 --- a/.github/deploy/k8s/minio-deployment.yaml +++ b/.github/deploy/k8s/minio-deployment.yaml @@ -51,7 +51,7 @@ spec: spec: containers: - name: minio - image: minio/minio:latest + image: quay.io/minio/minio:latest imagePullPolicy: IfNotPresent ports: - name: api diff --git a/.github/wiki/nself-cli/Services.md b/.github/wiki/nself-cli/Services.md index 7b6a45b61..eb5f30d98 100644 --- a/.github/wiki/nself-cli/Services.md +++ b/.github/wiki/nself-cli/Services.md @@ -1204,7 +1204,7 @@ Enable these services via environment variables in `.backend/.env`. | Property | Value | | ---------------- | --------------------- | -| **Image** | `minio/minio:latest` | +| **Image** | `quay.io/minio/minio:latest` | | **API Port** | 9000 | | **Console Port** | 9001 | | **Console URL** | http://localhost:9001 | diff --git a/.github/workflows/hygiene.yml b/.github/workflows/hygiene.yml index c5decaf1f..0f88fbffb 100644 --- a/.github/workflows/hygiene.yml +++ b/.github/workflows/hygiene.yml @@ -280,12 +280,40 @@ jobs: exit 0 fi + # backend/docker-compose.yml is DELIBERATELY tracked, for CI E2E + # provisioning — `nself start --skip-build` needs it present at + # checkout. The Doc-Sync step above already carves it out by name and + # points at .github/wiki/E2E-Backend-Setup.md; this step did not, so + # the two gates contradicted each other: one declared the file + # legitimately tracked, the other refused every commit that touched + # it. The file could be kept but never corrected, which is how it sat + # on a MinIO image that Docker Hub had deleted. + # + # The carve-out is VERIFIED, not blanket. This gate exists to stop + # generated files carrying plaintext credentials into a public repo + # (the ntask incident: a generated compose with the postgres password, + # Hasura admin secret and JWT keys). That protection is what the + # secret check below preserves: every credential-shaped key in this + # file must be a ${...} reference, never a literal. A literal value + # fails the gate exactly as before, so nothing this gate was built to + # catch gets through. + ALLOWED_TRACKED_GENERATED='backend/docker-compose.yml' + FAIL=0 while IFS= read -r f; do [ -f "$f" ] || continue if head -n 5 "$f" | grep -q "# GENERATED BY"; then if git check-ignore -q "$f"; then echo "OK: $f is generated but gitignored." + elif [ "$f" = "$ALLOWED_TRACKED_GENERATED" ]; then + if literals=$(grep -nE '(PASSWORD|SECRET|KEY|TOKEN)[[:space:]]*[:=]' "$f" | grep -v '\${'); then + echo "ERROR: $f is an allowed tracked generated file, but it now carries literal credential values." + echo "$literals" | sed 's/^/ /' + echo " Move the value into .env and reference it as \${VAR}, or de-track the file." + FAIL=1 + else + echo "OK: $f is generated and tracked by design (CI E2E provisioning); no literal credentials." + fi else echo "ERROR: Refusing to commit generated file. Add to .gitignore first." echo " -> $f" diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 60f37159d..daff67166 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -206,7 +206,32 @@ jobs: # (exits 0, prints "(N ignored)") but the raw JSON does not — so the # gate blocked every PR on CVEs already assessed. Apply the same list # here, using jq only (no heredocs: this is inside a YAML block scalar). - IGNORED=$(jq -r '(.pnpm.auditConfig.ignoreCves // [])[]' package.json 2>/dev/null | jq -R . | jq -s . ) + # + # Read the list from the REPOSITORY ROOT, not from frontend/. This step + # runs with working-directory: frontend, so a bare `package.json` here + # is frontend/package.json — whose pnpm.auditConfig is empty. The + # curated list lives in the root package.json (image-size x2, + # extract-zip), so the gate silently applied NO ignores and re-blocked + # on advisories that had already been assessed and accepted. One list, + # at the root, is the point; two would drift. + # + # What is on that list, and why each entry is allowed to be there. + # An advisory may be ignored ONLY while it has no published fix; the + # moment one exists the entry comes off and the dependency is bumped. + # CVE-2025-71330 / CVE-2025-71329 image-size (GHSA-w3rx-r6r6-pgpr, + # GHSA-5p2g-fcmc-qvqq) ICNS/JXL/HEIF infinite-loop DoS. + # first_patched_version NONE against <= 2.0.2, which is still npm + # latest. Build-time only, via Metro. + # CVE-2026-56876 / CVE-2026-19693 extract-zip (GHSA-jmr9-qjv8-65gv, + # GHSA-7pqw-9j4j-h8q3) two symlink path-traversal advisories on the + # same package. first_patched_version NONE against <= 2.0.1, and + # 2.0.1 IS npm latest, so there is nothing to upgrade to. Reaches + # the tree only through devDependencies (@lhci/cli and + # @wdio/appium-service, both via @puppeteer/browsers) unpacking + # archives this repo's CI fetches itself; never shipped to users. + # Re-check each on any dependency sweep: `gh api /advisories/` + # reporting a first_patched_version means the entry must be removed. + IGNORED=$(jq -r '(.pnpm.auditConfig.ignoreCves // [])[]' "${GITHUB_WORKSPACE}/package.json" 2>/dev/null | jq -R . | jq -s . ) IGNORED=${IGNORED:-[]} COUNTS=$(jq -r --argjson ign "$IGNORED" ' (.advisories // {}) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index cf639c0e1..0d955f32e 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -264,7 +264,7 @@ services: # MinIO Object Storage # SECURITY: Bound to localhost only - access via nginx reverse proxy minio: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: ${PROJECT_NAME}_minio restart: unless-stopped user: "1000:1000" diff --git a/package.json b/package.json index 5f398330e..a8795028b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "nodemailer@<9.1.1": ">=9.1.1 <10.0.0", "postcss@<8.5.10": ">=8.5.10", "uuid@<11.1.1": ">=11.1.1", - "@xmldom/xmldom@<0.8.13": ">=0.8.13", + "@xmldom/xmldom@<0.8.15": ">=0.8.15 <0.9.0", + "@xmldom/xmldom@>=0.9.0 <0.9.12": ">=0.9.12", "react": "^19.2.0", "react-dom": "^19.2.0", "@types/react": "19.2.17", @@ -80,7 +81,8 @@ "immutable@5": "5.1.8", "deepmerge-ts@<8.0.0": ">=8.0.0", "browserslist@<4.28.7": "^4.28.7", - "joi@<17.13.6": ">=17.13.6 <18.0.0" + "joi@<17.13.6": ">=17.13.6 <18.0.0", + "js-cookie@<3.0.7": ">=3.0.7" }, "patchedDependencies": { "@capacitor/cli@6.2.1": "patches/@capacitor__cli@6.2.1.patch" @@ -89,7 +91,8 @@ "ignoreCves": [ "CVE-2025-71330", "CVE-2025-71329", - "CVE-2026-56876" + "CVE-2026-56876", + "CVE-2026-19693" ] } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74ccb622a..74902a67c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,7 +36,8 @@ overrides: nodemailer@<9.1.1: '>=9.1.1 <10.0.0' postcss@<8.5.10: '>=8.5.10' uuid@<11.1.1: '>=11.1.1' - '@xmldom/xmldom@<0.8.13': '>=0.8.13' + '@xmldom/xmldom@<0.8.15': '>=0.8.15 <0.9.0' + '@xmldom/xmldom@>=0.9.0 <0.9.12': '>=0.9.12' react: ^19.2.0 react-dom: ^19.2.0 '@types/react': 19.2.17 @@ -66,6 +67,7 @@ overrides: deepmerge-ts@<8.0.0: '>=8.0.0' browserslist@<4.28.7: ^4.28.7 joi@<17.13.6: '>=17.13.6 <18.0.0' + js-cookie@<3.0.7: '>=3.0.7' patchedDependencies: '@capacitor/cli@6.2.1': @@ -6561,12 +6563,12 @@ packages: resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==} engines: {node: '>= 16'} - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} engines: {node: '>=10.0.0'} - '@xmldom/xmldom@0.9.10': - resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + '@xmldom/xmldom@0.9.12': + resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==} engines: {node: '>=14.6'} '@xstate/react@3.2.2': @@ -7805,6 +7807,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cross-argv@2.0.0: resolution: {integrity: sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg==} @@ -10218,10 +10221,8 @@ packages: jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} - js-cookie@3.0.6: - resolution: {integrity: sha512-mYhz0Og/Wv8bZJcBC6rRzYG+rYf8DyQSK3rcUbuGHQIgSsX6uynAIVoaPgqf0udH2AGS953hGFy3w6on1rJzMw==} - engines: {node: '>=20'} - deprecated: Missing corresponding tag/release on GitHub + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} js-file-download@0.4.12: resolution: {integrity: sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==} @@ -11988,6 +11989,7 @@ packages: prom-client@14.2.0: resolution: {integrity: sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==} engines: {node: '>=10'} + deprecated: prom-client has been replaced by @prometheus-io/client promisify-child-process@4.1.2: resolution: {integrity: sha512-APnkIgmaHNJpkAn7k+CrJSi9WMuff5ctYFbD0CO2XIPkM8yO7d/ShouU2clywbpHV/DUsyc4bpJCsNgddNtx4g==} @@ -14876,7 +14878,7 @@ snapshots: '@authenio/xml-encryption@2.0.2': dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 escape-html: 1.0.3 xpath: 0.0.32 @@ -17503,7 +17505,7 @@ snapshots: dependencies: '@simplewebauthn/browser': 9.0.1 fetch-ponyfill: 7.1.0(encoding@0.1.13) - js-cookie: 3.0.6 + js-cookie: 3.0.8 jwt-decode: 4.0.0 xstate: 4.38.3 transitivePeerDependencies: @@ -17519,7 +17521,7 @@ snapshots: '@nhost/react': 3.11.2(@types/react@19.2.17)(encoding@0.1.13)(graphql@16.8.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) graphql: 16.8.1 isomorphic-unfetch: 3.1.0(encoding@0.1.13) - js-cookie: 3.0.6 + js-cookie: 3.0.8 next: 15.5.24(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(@types/node@25.9.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -21421,7 +21423,7 @@ snapshots: '@wdio/reporter': 9.27.1 '@wdio/types': 9.27.1 '@wdio/utils': 9.27.1 - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 change-case: 5.4.4 get-port: 7.2.0 import-meta-resolve: 4.2.0 @@ -21730,9 +21732,9 @@ snapshots: '@xmldom/is-dom-node@1.0.1': {} - '@xmldom/xmldom@0.8.13': {} + '@xmldom/xmldom@0.8.15': {} - '@xmldom/xmldom@0.9.10': {} + '@xmldom/xmldom@0.9.12': {} '@xstate/react@3.2.2(@types/react@19.2.17)(react@19.2.6)(xstate@4.38.3)': dependencies: @@ -21930,7 +21932,7 @@ snapshots: dependencies: '@appium/base-driver': 10.5.2(@types/node@25.9.3) '@appium/support': 7.2.2(@types/node@25.9.3) - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 appium-adb: 14.5.0(@types/node@25.9.3) asyncbox: 6.3.0 axios: 1.19.0 @@ -21982,7 +21984,7 @@ snapshots: '@appium/strongbox': 1.1.1 '@appium/support': 7.2.2(@types/node@25.9.3) '@types/node': 25.9.3 - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 appium-ios-tuntap: 0.2.5(@types/node@25.9.3) axios: 1.19.0 commander: 14.0.3 @@ -22003,7 +22005,7 @@ snapshots: dependencies: '@appium/strongbox': 1.1.1 '@appium/support': 7.2.2(@types/node@25.9.3) - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 appium-ios-tuntap: 0.2.5(@types/node@25.9.3) axios: 1.19.0 commander: 14.0.3 @@ -22024,7 +22026,7 @@ snapshots: appium-ios-simulator@8.1.3(@types/node@25.9.3): dependencies: '@appium/support': 7.2.2(@types/node@25.9.3) - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 appium-xcode: 6.2.2(@types/node@25.9.3) async-lock: 1.4.1 asyncbox: 6.3.0 @@ -26629,7 +26631,7 @@ snapshots: jpeg-js@0.4.4: {} - js-cookie@3.0.6: {} + js-cookie@3.0.8: {} js-file-download@0.4.12: {} @@ -28322,13 +28324,13 @@ snapshots: plist@3.1.1: dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 base64-js: 1.5.1 xmlbuilder: 15.1.1 plist@4.0.0: dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 xmlbuilder: 15.1.1 pluralize@8.0.0: {} @@ -29340,7 +29342,7 @@ snapshots: samlify@2.13.0: dependencies: '@authenio/xml-encryption': 2.0.2 - '@xmldom/xmldom': 0.8.13 + '@xmldom/xmldom': 0.8.15 node-rsa: 1.1.1 xml: 1.0.1 xml-crypto: 6.1.2 @@ -31374,7 +31376,7 @@ snapshots: xml-crypto@6.1.2: dependencies: '@xmldom/is-dom-node': 1.0.1 - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 xpath: 0.0.33 xml-escape@1.1.0: {}