diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml index 6d666593..5c848084 100644 --- a/.github/workflows/jekyll.yml +++ b/.github/workflows/jekyll.yml @@ -8,8 +8,6 @@ on: permissions: contents: read - pages: write - id-token: write concurrency: group: jekyll-site-${{ github.ref }} @@ -24,7 +22,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.2.2" + ruby-version: "3.4.10" bundler: "2.5.23" bundler-cache: true @@ -38,8 +36,10 @@ jobs: - name: Package Pages artifact run: | - tar --dereference --hard-dereference -cvf github-pages.tar _site - gzip -f github-pages.tar + tar --dereference \ + --directory "${GITHUB_WORKSPACE}/_site" \ + --create --file "${GITHUB_WORKSPACE}/github-pages.tar" . + gzip -f "${GITHUB_WORKSPACE}/github-pages.tar" - name: Upload artifact uses: actions/upload-artifact@v6 @@ -54,6 +54,9 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest + permissions: + pages: write + id-token: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true environment: diff --git a/.github/workflows/rebuild-on-search-update.yml b/.github/workflows/rebuild-on-search-update.yml index 8c41fd54..42c35c2c 100644 --- a/.github/workflows/rebuild-on-search-update.yml +++ b/.github/workflows/rebuild-on-search-update.yml @@ -8,8 +8,6 @@ on: permissions: contents: read - pages: write - id-token: write jobs: build: @@ -20,7 +18,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.2.2" + ruby-version: "3.4.10" bundler: "2.5.23" bundler-cache: true @@ -31,8 +29,10 @@ jobs: - name: Package Pages artifact run: | - tar --dereference --hard-dereference -cvf github-pages.tar _site - gzip -f github-pages.tar + tar --dereference \ + --directory "${GITHUB_WORKSPACE}/_site" \ + --create --file "${GITHUB_WORKSPACE}/github-pages.tar" . + gzip -f "${GITHUB_WORKSPACE}/github-pages.tar" - name: Upload artifact uses: actions/upload-artifact@v6 @@ -45,6 +45,9 @@ jobs: deploy: needs: build runs-on: ubuntu-latest + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/teaching-content-checks.yml b/.github/workflows/teaching-content-checks.yml index 7988d3e5..4499de09 100644 --- a/.github/workflows/teaching-content-checks.yml +++ b/.github/workflows/teaching-content-checks.yml @@ -20,6 +20,9 @@ on: - "package.json" - "package-lock.json" +permissions: + contents: read + jobs: markdown-quality: runs-on: ubuntu-latest @@ -35,16 +38,16 @@ jobs: cache: "npm" - name: Install dependencies - run: npm ci + run: npm ci --ignore-scripts - name: Run markdownlint for teaching content run: | - npx markdownlint-cli2 --config .markdownlint-cli2.jsonc \ + ./node_modules/.bin/markdownlint-cli2 --config .markdownlint-cli2.jsonc \ "_teaching/**/*.md" \ "assets/images/teaching/README.md" - name: Check prettier formatting for teaching content run: | - npx prettier --check \ + ./node_modules/.bin/prettier --check \ "_teaching/**/*.md" \ "assets/images/teaching/README.md" diff --git a/.jekyllignore b/.jekyllignore index 3d477435..46476492 100644 --- a/.jekyllignore +++ b/.jekyllignore @@ -1,4 +1,5 @@ .cursor/ .claude/ CLAUDE.md +AGENTS.md .gitignore diff --git a/.ruby-version b/.ruby-version index be94e6f5..84d6c676 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.4.10 diff --git a/CNAME b/CNAME index e143a36f..82f1fd05 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -https://comphy-lab.org \ No newline at end of file +comphy-lab.org diff --git a/Gemfile b/Gemfile index 6fcc32f3..25a37536 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,10 @@ source "https://rubygems.org" -ruby "~> 3.2.0" +ruby "~> 3.4.0" gem "jekyll", "~> 4.3.2" +gem "base64" +gem "csv" gem "webrick", "~> 1.8" gem "jekyll-sitemap" gem "jekyll-include-cache" diff --git a/Gemfile.lock b/Gemfile.lock index 6f8eb9fa..c74066ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,11 @@ GEM specs: addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) bigdecimal (3.1.9) colorator (1.1.0) concurrent-ruby (1.3.7) + csv (3.3.6) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) @@ -176,6 +178,8 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + base64 + csv jekyll (~> 4.3.2) jekyll-include-cache jekyll-seo-tag @@ -184,7 +188,7 @@ DEPENDENCIES webrick (~> 1.8) RUBY VERSION - ruby 3.2.2p53 + ruby 3.4.10 BUNDLED WITH 2.5.23 diff --git a/README.md b/README.md index 656644f1..1e29b3ba 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ A static website for the Computational Multiphase Physics Laboratory, built with 2. **Manual Setup (Alternative)** Prerequisites: - - Ruby `3.2.2` (matches `.ruby-version`) + - Ruby `3.4.10` (matches `.ruby-version`) - Bundler `2.5.23` (`gem install bundler -v 2.5.23`) - Node.js and npm (for linting and testing) @@ -839,7 +839,7 @@ The repository uses automated tools to ensure code quality and consistency: ```bash npm install - npx husky install + ./node_modules/.bin/husky install ``` #### Pre-commit Hooks diff --git a/_config.yml b/_config.yml index 24a1cea9..39d4e34c 100644 --- a/_config.yml +++ b/_config.yml @@ -45,6 +45,7 @@ exclude: - .claude - .claude/** - CLAUDE.md + - AGENTS.md # Design-system handoff bundle — reference only, not built - comphy-lab-design-system - comphy-lab-design-system/** diff --git a/_layouts/default.html b/_layouts/default.html index 1df94023..4def6941 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -130,7 +130,9 @@ - + diff --git a/_layouts/history.html b/_layouts/history.html index ef65c281..d013e375 100644 --- a/_layouts/history.html +++ b/_layouts/history.html @@ -82,7 +82,9 @@ - + diff --git a/_layouts/join-us.html b/_layouts/join-us.html index 38546625..929bcdf5 100644 --- a/_layouts/join-us.html +++ b/_layouts/join-us.html @@ -59,7 +59,9 @@ - + diff --git a/_layouts/team.html b/_layouts/team.html index a1264424..ea54bd9d 100644 --- a/_layouts/team.html +++ b/_layouts/team.html @@ -76,7 +76,9 @@ - + diff --git a/about-legacy.html b/about-legacy.html new file mode 100644 index 00000000..a41135b5 --- /dev/null +++ b/about-legacy.html @@ -0,0 +1,15 @@ +--- +layout: default +title: Redirecting to CoMPhy Lab +permalink: /about.html +sitemap: false +--- + + + +
+

About has moved to the homepage.

+

If you are not redirected automatically, click here.

+
diff --git a/assets/css/join-us.css b/assets/css/join-us.css index 73e2f660..134d2d54 100644 --- a/assets/css/join-us.css +++ b/assets/css/join-us.css @@ -202,7 +202,7 @@ margin: 4rem 0; } -.project-category h3 { +.project-category > h3 { color: var(--join-subheading-color); font-size: 2.2rem; margin-bottom: 2rem; @@ -211,16 +211,16 @@ padding-bottom: 0; } -.project-category h3:after { +.project-category > h3:after { content: none; } /* Different accent colors for each category */ -.soft-matter-category h3:after { +.soft-matter-category > h3:after { background-color: var(--join-phd-accent); } -.free-surface-category h3:after { +.free-surface-category > h3:after { background-color: var(--join-postdoc-accent); } @@ -321,7 +321,7 @@ font-size: 2.4rem; } - .project-category h3 { + .project-category > h3 { font-size: 2rem; } } diff --git a/assets/css/research.css b/assets/css/research.css index c779c347..2ba98fec 100644 --- a/assets/css/research.css +++ b/assets/css/research.css @@ -700,7 +700,7 @@ } .research-content .tags span:nth-of-type(3) { - background: var(--color-primary-alpha-5); + background: var(--color-primary-alpha-05); } [data-theme="dark"] .research-content .tags span:nth-of-type(3) { @@ -708,7 +708,7 @@ } .research-content .tags span:nth-of-type(n + 4) { - background: var(--color-primary-alpha-5); + background: var(--color-primary-alpha-05); } [data-theme="dark"] .research-content .tags span:nth-of-type(n + 4) { diff --git a/assets/css/styles.css b/assets/css/styles.css index 899ddba3..f5e4f93c 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -76,6 +76,7 @@ /* Color System - Transparent Colors */ --color-black-alpha-05: rgba(0, 0, 0, 0.05); + --color-black-alpha-08: rgba(0, 0, 0, 0.08); --color-black-alpha-10: rgba(0, 0, 0, 0.1); --color-black-alpha-15: rgba(0, 0, 0, 0.15); --color-black-alpha-20: rgba(0, 0, 0, 0.2); @@ -89,6 +90,7 @@ --color-white-alpha-30: rgba(255, 255, 255, 0.3); --color-white-alpha-35: rgba(255, 255, 255, 0.35); --color-white-alpha-40: rgba(255, 255, 255, 0.4); + --color-white-alpha-50: rgba(255, 255, 255, 0.5); --color-white-alpha-70: rgba(255, 255, 255, 0.7); --color-white-alpha-85: rgba(255, 255, 255, 0.85); --color-white-alpha-95: rgba(255, 255, 255, 0.95); @@ -105,7 +107,9 @@ --color-blue-bright-alpha-20: rgba(76, 110, 245, 0.2); --color-blue-bright-alpha-25: rgba(76, 110, 245, 0.25); --color-blue-bright-alpha-30: rgba(76, 110, 245, 0.3); + --color-blue-bright-alpha-40: rgba(76, 110, 245, 0.4); --color-blue-bright-alpha-50: rgba(76, 110, 245, 0.5); + --color-blue-bright-alpha-80: rgba(76, 110, 245, 0.8); --color-red-darker-alpha-20: rgba(250, 82, 82, 0.2); --color-header-base-alpha-15: rgba(104, 35, 109, 0.15); --color-header-base-alpha-20: rgba(104, 35, 109, 0.2); @@ -129,6 +133,7 @@ /* Transitions */ --transition-fast: all 0.2s ease; --transition-medium: all 0.3s ease; + --loader-border-color: var(--color-black-alpha-10); /* Spacing System */ --space-xs: 0.5rem; @@ -2323,7 +2328,7 @@ section#featured:after, } /* Join page markdown refinements */ -.join-content .markdown-content h2 { +.join-content.markdown-content h2 { font-size: 2.4rem; color: var(--color-primary); margin: 2.4rem 0 1.2rem; @@ -2331,17 +2336,17 @@ section#featured:after, border-bottom: 2px solid var(--position-h2-border); } -.join-content .markdown-content p { +.join-content.markdown-content p { margin: 0 0 1.2rem; } -.join-content .markdown-content ul, -.join-content .markdown-content ol { +.join-content.markdown-content ul, +.join-content.markdown-content ol { margin: 0 0 1.2rem 2rem; padding: 0; } -.join-content .markdown-content li { +.join-content.markdown-content li { margin: 0.4rem 0; } diff --git a/assets/js/main.js b/assets/js/main.js index 841549c7..ee76d91d 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -445,7 +445,7 @@ // If not on home page, let the browser handle the navigation } else if (href.startsWith("#")) { e.preventDefault(); - const target = document.querySelector(href); + const target = document.getElementById(href.substring(1)); if (target) { target.scrollIntoView({ behavior: "smooth", diff --git a/assets/js/search-manager.js b/assets/js/search-manager.js index e86047dc..2617c181 100644 --- a/assets/js/search-manager.js +++ b/assets/js/search-manager.js @@ -32,6 +32,29 @@ /^(?:Private|0_todo|Admin|Medical|Personal)\b/i, ]; + const PUBLIC_SEARCH_HOSTS = new Set([ + "comphy-lab.org", + "blogs.comphy-lab.org", + ]); + + function isSafeSearchUrl(value) { + if (typeof value !== "string" || value.length === 0) { + return false; + } + + try { + const parsed = new URL(value, window.location.origin); + return ( + (parsed.protocol === "https:" || parsed.protocol === "http:") && + PUBLIC_SEARCH_HOSTS.has(parsed.hostname) && + parsed.username === "" && + parsed.password === "" + ); + } catch (_error) { + return false; + } + } + function isInternalSearchEntry(entry) { const entryIdentityText = [entry?.title, entry?.url] .filter(Boolean) @@ -43,7 +66,10 @@ } function sanitizeSearchData(data) { - return data.filter((entry) => !isInternalSearchEntry(entry)); + return data.filter( + (entry) => + !isInternalSearchEntry(entry) && isSafeSearchUrl(entry?.url) + ); } /** @@ -199,7 +225,7 @@ id: `search-result-${result.refIndex}`, title: result.item.title || "Untitled", handler: () => { - if (result.item.url) { + if (isSafeSearchUrl(result.item.url)) { window.location.href = result.item.url; } }, diff --git a/eslint.config.js b/eslint.config.js index 88b7d4f4..b2d277a4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,13 +1,18 @@ +const js = require("@eslint/js"); +const globals = require("globals"); + module.exports = [ + js.configs.recommended, { languageOptions: { - ecmaVersion: 12, - sourceType: "module", + ecmaVersion: 2021, + sourceType: "script", globals: { - browser: true, - es2021: true, + ...globals.browser, + ...globals.es2021, marked: "readonly", - DOMPurify: "readonly" + DOMPurify: "readonly", + Utils: "readonly" } }, rules: { diff --git a/package-lock.json b/package-lock.json index acf0ef78..a54b8011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@eslint/js": "8.57.1", "eslint": "^8.56.0", + "globals": "13.24.0", "husky": "^8.0.3", "jest": "^30.3.0", "jest-environment-jsdom": "^30.3.0", diff --git a/package.json b/package.json index 58716b66..0bd63bce 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint:md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc", "lint": "npm run lint:js && npm run lint:css && npm run lint:md", "format": "prettier --write assets/js/**/*.js assets/css/**/*.css", - "prepare": "npx husky install", + "prepare": "husky install", "test": "jest --coverage" }, "repository": { @@ -29,7 +29,9 @@ }, "homepage": "https://comphy-lab.org", "devDependencies": { + "@eslint/js": "8.57.1", "eslint": "^8.56.0", + "globals": "13.24.0", "husky": "^8.0.3", "jest": "^30.3.0", "jest-environment-jsdom": "^30.3.0", diff --git a/scripts/fix-line-length.js b/scripts/fix-line-length.js index 9ca0c86b..73ee6c34 100644 --- a/scripts/fix-line-length.js +++ b/scripts/fix-line-length.js @@ -217,6 +217,7 @@ function processJsFiles() { fixedFiles++; } catch (error) { console.error(`Error writing to file ${file}:`, error.message); + throw error; } } } @@ -228,4 +229,4 @@ function processJsFiles() { } // Run the script -processJsFiles(); \ No newline at end of file +processJsFiles(); diff --git a/scripts/generate_seo_tags.rb b/scripts/generate_seo_tags.rb index 1f0be6ec..59dee1d2 100755 --- a/scripts/generate_seo_tags.rb +++ b/scripts/generate_seo_tags.rb @@ -87,10 +87,9 @@ def normalize_url(url) end normalized = Pathname.new(url).cleanpath.to_s - return nil unless normalized.start_with?('/') - # Remove leading slash for file operations result = normalized.sub(/^\//, '') + return nil if result.start_with?('~') # Debug output puts "URL Normalization: #{original_url} -> #{result}" if ENV['DEBUG'] @@ -285,8 +284,8 @@ def update_description_meta(doc, head, description) descriptions = descriptions_by_url[url] || Set.new # Get file path - file_path = File.expand_path(url, site_dir) site_root = File.expand_path(site_dir) + file_path = File.expand_path(File.join(site_root, url)) unless file_path.start_with?("#{site_root}#{File::SEPARATOR}") puts "Skipping unsafe output path for #{url}" if ENV['DEBUG'] diff --git a/scripts/lint-check.sh b/scripts/lint-check.sh index b0e428e2..54eca9fc 100755 --- a/scripts/lint-check.sh +++ b/scripts/lint-check.sh @@ -110,9 +110,15 @@ fi # Run ESLint on JavaScript files with quotes rule JS_DIR="$REPO_ROOT/assets/js" +ESLINT_BIN="$REPO_ROOT/node_modules/.bin/eslint" + +if [[ ! -x "$ESLINT_BIN" ]]; then + echo "Local ESLint is unavailable. Run npm ci first." >&2 + exit 1 +fi if [[ "$FIX_MODE" == "true" ]]; then # Fix mode: run ESLint with --fix flag - npx eslint "$JS_DIR" --rule 'quotes: ["error", "double"]' --fix + "$ESLINT_BIN" "$JS_DIR" --rule 'quotes: ["error", "double"]' --fix if [ $? -eq 0 ]; then echo "Fixed quote style in JavaScript files." else @@ -120,7 +126,7 @@ if [[ "$FIX_MODE" == "true" ]]; then fi else # Check-only mode: run ESLint without --fix - npx eslint "$JS_DIR" --rule 'quotes: ["error", "double"]' + "$ESLINT_BIN" "$JS_DIR" --rule 'quotes: ["error", "double"]' ESLINT_EXIT_CODE=$? if [ $ESLINT_EXIT_CODE -eq 0 ]; then echo "No quote style issues found in JavaScript files." @@ -130,4 +136,4 @@ else fi fi -echo "Lint check completed!" \ No newline at end of file +echo "Lint check completed!" diff --git a/scripts/setup.sh b/scripts/setup.sh index 95c5603b..fc26b9f8 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -151,7 +151,7 @@ echo "🔨 Building site and generating search database..." # Install husky hooks echo "" echo "🪝 Installing Git hooks (husky)..." -npx husky install +./node_modules/.bin/husky install # Run simple validation tests echo "" diff --git a/tests/fix-line-length-actual.test.js b/tests/fix-line-length-actual.test.js index 50a2ef91..e7a75438 100644 --- a/tests/fix-line-length-actual.test.js +++ b/tests/fix-line-length-actual.test.js @@ -118,6 +118,20 @@ describe('fix-line-length.js actual implementation', () => { expect(mockFs.writeFileSync).toHaveBeenCalledTimes(0); }); + it('should fail when a rewritten file cannot be written', () => { + mockFs.readdirSync.mockReturnValue(['long-lines.js']); + mockFs.readFileSync.mockReturnValue( + 'const value = {first: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", second: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"};' + ); + mockFs.writeFileSync.mockImplementation(() => { + throw new Error('Permission denied'); + }); + + expect(() => { + require('../scripts/fix-line-length.js'); + }).toThrow('Permission denied'); + }); + it('should log summary of processed files', () => { mockFs.readdirSync.mockReturnValue(['test1.js', 'test2.js']); mockFs.readFileSync.mockReturnValue('short line'); @@ -129,4 +143,4 @@ describe('fix-line-length.js actual implementation', () => { expect(console.log).toHaveBeenCalledWith(expect.stringContaining('0 lines exceeded 80 characters')); expect(console.log).toHaveBeenCalledWith(expect.stringContaining('0 files were modified')); }); -}); \ No newline at end of file +}); diff --git a/tests/search-manager.test.js b/tests/search-manager.test.js index 229245b1..1c303bb2 100644 --- a/tests/search-manager.test.js +++ b/tests/search-manager.test.js @@ -79,4 +79,40 @@ describe("search-manager sanitization", () => { "https://comphy-lab.org/.agents/skills/add-paper/SKILL/#badge-shapes" ); }); + + it("rejects unsafe and unapproved search result URLs", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve([ + { + title: "Script URL", + url: "javascript:alert(document.domain)", + content: "malicious search entry", + }, + { + title: "Credentialed URL", + url: "https://attacker@comphy-lab.org/", + content: "misleading search entry", + }, + { + title: "External redirect", + url: "https://example.net/phishing", + content: "unapproved search entry", + }, + { + title: "Public blog post", + url: "https://blogs.comphy-lab.org/public-post/", + content: "approved search entry", + }, + ]), + }); + + require("../assets/js/search-manager.js"); + + const data = await window.SearchManager.loadSearchDatabase(); + + expect(data).toHaveLength(1); + expect(data[0].title).toBe("Public blog post"); + }); });