diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e36dbf9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + pull_request: + workflow_dispatch: + +jobs: + validate: + name: Lint, Check, Build + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2.1.2 + with: + bun-version: latest + + - name: Setup Node.js + uses: actions/setup-node@v6.2.0 + with: + node-version: '22' + + - name: Cache dependencies + uses: actions/cache@v5.0.3 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock', 'bun.lockb') }} + restore-keys: bun-${{ runner.os }}- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Type check + run: bun run check + + - name: Build + run: bun run build + + - name: Validate Cloudflare Worker bundle + run: bunx wrangler deploy --dry-run --outdir .wrangler-dry-run diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml new file mode 100644 index 0000000..5dc4525 --- /dev/null +++ b/.github/workflows/deploy-workers.yml @@ -0,0 +1,77 @@ +# Deploy AtlasOS website to Cloudflare Workers +# Secrets: CLOUDFLARE_API_TOKEN (required), CLOUDFLARE_ACCOUNT_ID (optional, for faster auth) +# https://developers.cloudflare.com/fundamentals/api/get-started/create-token/ + +name: Deploy to Cloudflare Workers + +on: + push: + branches: [main, feat/native-docs] + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + name: Build & Deploy + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2.1.2 + with: + bun-version: latest + + - name: Setup Node.js + uses: actions/setup-node@v6.2.0 + with: + node-version: '22' + + - name: Cache dependencies + uses: actions/cache@v5.0.3 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock', 'bun.lockb') }} + restore-keys: bun-${{ runner.os }}- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Type check + run: bun run check + + - name: Build + run: bun run build + + - name: Validate Cloudflare Worker bundle + run: bunx wrangler deploy --dry-run --outdir .wrangler-dry-run + + - name: Deploy to Cloudflare Workers + id: deploy + uses: cloudflare/wrangler-action@v3.14.1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy + + - name: Deployment summary + if: success() + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} + run: | + echo "## Deployment complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Worker:** atlasos-website" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + [ -n "$DEPLOYMENT_URL" ] && echo "**URL:** $DEPLOYMENT_URL" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 0f277a5..3c8a202 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,12 @@ pnpm-debug.log* # macOS-specific files .DS_Store +# Windows shell redirect artifacts +nul + # jetbrains setting folder .idea/ -# jampack cache +# legacy jampack cache .jampack/ +.wrangler-dry-run/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..dd422c0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# Dependencies +node_modules +bun.lock + +# Build outputs +dist +.astro + +# Content files - Prettier doesn't handle MDX frontmatter well +src/content/**/*.mdx +src/content/**/*.md diff --git a/.prettierrc b/.prettierrc index 2018a87..4432afc 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,8 +2,8 @@ "$schema": "http://json.schemastore.org/prettierrc", "plugins": [ "prettier-plugin-astro", - "prettier-plugin-tailwindcss", - "prettier-plugin-astro-organize-imports" + "prettier-plugin-astro-organize-imports", + "prettier-plugin-tailwindcss" ], "printWidth": 100, "tabWidth": 2, @@ -15,6 +15,7 @@ "arrowParens": "always", "endOfLine": "lf", "tailwindFunctions": ["tw"], + "tailwindStylesheet": "./src/styles/global.css", "overrides": [ { "files": ["*.{js,jsx,ts,tsx}"], diff --git a/.tmp-sidebar-redesign.png b/.tmp-sidebar-redesign.png new file mode 100644 index 0000000..2560c5e Binary files /dev/null and b/.tmp-sidebar-redesign.png differ diff --git a/.tmp-sidebar.png b/.tmp-sidebar.png new file mode 100644 index 0000000..f0540a8 Binary files /dev/null and b/.tmp-sidebar.png differ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 22a1505..3f6382e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,4 +1,13 @@ { - "recommendations": ["astro-build.astro-vscode"], + "recommendations": [ + "astro-build.astro-vscode", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss", + "ms-vscode.vscode-typescript-next", + "usernamehw.errorlens", + "christian-kohler.path-intellisense", + "ms-vscode.vscode-json" + ], "unwantedRecommendations": [] } diff --git a/.vscode/launch.json b/.vscode/launch.json index d642209..8d58e19 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,10 +2,25 @@ "version": "0.2.0", "configurations": [ { - "command": "./node_modules/.bin/astro dev", - "name": "Development server", + "command": "bun run dev", + "name": "Astro Dev Server (Bun)", "request": "launch", - "type": "node-terminal" + "type": "node-terminal", + "cwd": "${workspaceFolder}" + }, + { + "command": "bun run build", + "name": "Build (Bun)", + "request": "launch", + "type": "node-terminal", + "cwd": "${workspaceFolder}" + }, + { + "command": "bun run preview", + "name": "Preview (Bun)", + "request": "launch", + "type": "node-terminal", + "cwd": "${workspaceFolder}" } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..45a44aa --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,141 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.formatOnPaste": false, + "editor.formatOnType": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" + }, + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "editor.rulers": [100, 120], + "editor.wordWrap": "on", + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": true, + "editor.inlineSuggest.enabled": true, + "editor.suggestSelection": "first", + "editor.snippetSuggestions": "top", + + "files.associations": { + "*.astro": "astro" + }, + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/node_modules": true, + "**/dist": true, + "**/.astro": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/**": true, + "**/dist/**": true, + "**/.astro/**": true + }, + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.updateImportsOnFileMove.enabled": "always", + "typescript.suggest.autoImports": true, + "typescript.inlayHints.parameterNames.enabled": "all", + "typescript.inlayHints.variableTypes.enabled": true, + "typescript.inlayHints.propertyDeclarationTypes.enabled": true, + "typescript.inlayHints.functionLikeReturnTypes.enabled": true, + + "javascript.preferences.importModuleSpecifier": "relative", + "javascript.updateImportsOnFileMove.enabled": "always", + "javascript.suggest.autoImports": true, + "javascript.inlayHints.parameterNames.enabled": "all", + "javascript.inlayHints.variableTypes.enabled": true, + "javascript.inlayHints.propertyDeclarationTypes.enabled": true, + "javascript.inlayHints.functionLikeReturnTypes.enabled": true, + + "eslint.enable": true, + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "astro"], + "eslint.useFlatConfig": true, + "eslint.workingDirectories": ["."], + + "prettier.enable": true, + "prettier.requireConfig": true, + "[astro]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[markdown]": { + "editor.formatOnSave": false, + "editor.wordWrap": "on" + }, + "[mdx]": { + "editor.formatOnSave": false, + "editor.wordWrap": "on" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[scss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + + "search.exclude": { + "**/node_modules": true, + "**/dist": true, + "**/.astro": true, + "**/.git": true, + "**/bun.lock": true + }, + + "terminal.integrated.defaultProfile.windows": "PowerShell", + "terminal.integrated.cwd": "${workspaceFolder}", + + "git.autofetch": true, + "git.confirmSync": false, + "git.enableSmartCommit": true, + + "emmet.includeLanguages": { + "astro": "html" + }, + "emmet.triggerExpansionOnTab": true, + + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.ts": "${capture}.js", + "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts", + "*.jsx": "${capture}.js", + "*.tsx": "${capture}.ts", + "tsconfig.json": "tsconfig.*.json", + "package.json": "bun.lock" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1684890 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,76 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "dev", + "type": "shell", + "command": "bun", + "args": ["run", "dev"], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "new" + }, + "isBackground": true, + "runOptions": { + "runOn": "default" + } + }, + { + "label": "build", + "type": "shell", + "command": "bun", + "args": ["run", "build"], + "problemMatcher": ["$tsc"], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "preview", + "type": "shell", + "command": "bun", + "args": ["run", "preview"], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "new" + }, + "isBackground": true + }, + { + "label": "lint", + "type": "shell", + "command": "bun", + "args": ["run", "lint"], + "problemMatcher": ["$eslint-stylish"], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "format", + "type": "shell", + "command": "bun", + "args": ["run", "format"], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "check", + "type": "shell", + "command": "bun", + "args": ["run", "check"], + "problemMatcher": ["$tsc"], + "presentation": { + "reveal": "always", + "panel": "shared" + } + } + ] +} diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md new file mode 100644 index 0000000..e1ca0bb --- /dev/null +++ b/AUDIT_REPORT.md @@ -0,0 +1,656 @@ +# AtlasOS Website -- Full Code Audit Report + +**Date:** February 25, 2026 +**Scope:** Every file under `website/src/`, plus root configuration files +**Files audited:** 80+ files across components, layouts, utilities, styles, pages, and configuration +**Tech stack:** Astro 6 beta, Tailwind CSS v4, MDX content collections, Cloudflare Workers, Pagefind, Satori + Sharp (OG images) + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Configuration & Dependencies](#2-configuration--dependencies) +3. [Constants & Shared Data](#3-constants--shared-data) +4. [Cloudflare Worker (worker.ts)](#4-cloudflare-worker-workerts) +5. [Layouts](#5-layouts) +6. [UI Components](#6-ui-components) +7. [Section Components](#7-section-components) +8. [Docs Components](#8-docs-components) +9. [Layout Components](#9-layout-components) +10. [Core Components](#10-core-components) +11. [Utilities](#11-utilities) +12. [Styles](#12-styles) +13. [Pages](#13-pages) +14. [Integrations](#14-integrations) +15. [TODO/FIXME Scan](#15-todofixme-scan) +16. [Security Summary](#16-security-summary) +17. [Accessibility Summary](#17-accessibility-summary) +18. [Top 10 Recommendations](#18-top-10-recommendations) + +--- + +## 1. Executive Summary + +The AtlasOS website is a well-structured Astro 6 static site with a Cloudflare Workers backend for Microsoft ISO downloads. The codebase is generally clean, uses TypeScript interfaces for props, and has good reduced-motion support. However, there are several categories of issues that should be addressed: + +| Category | Count | Severity | +|----------|-------|----------| +| Security | 3 | High | +| Accessibility | 8 | High | +| Code Quality (will-break) | 2 | High | +| Code Quality (cleanup) | 6 | Medium | +| Hardcoded Values | 12+ | Medium | +| Legal/Compliance | 1 | High | +| Beta Dependencies | 3 | Low | + +**No TODO, FIXME, or HACK comments were found anywhere in the codebase.** + +--- + +## 2. Configuration & Dependencies + +### `package.json` + +| Issue | Severity | Details | +|-------|----------|---------| +| Beta dependencies | Low | `astro@^6.0.0-beta.13`, `@astrojs/mdx@^5.0.0-beta.8`, `@astrojs/check@^0.9.7-beta.1` -- these may have breaking changes before stable release | +| `@cloudflare/puppeteer` in dependencies | Low | Only used by the Cloudflare Worker, not the static site. Could be a devDependency or handled separately | +| Engine requirements | Info | Requires Bun >= 1.3.0 and Node >= 22.12.0 | + +### `astro.config.mjs` (108 lines) + +| Issue | Severity | Details | +|-------|----------|---------| +| `limitInputPixels: false` | Low | Disabling Sharp's pixel limit could allow memory exhaustion if processing very large images | +| `chunkSizeWarningLimit: 1000` | Info | Increased from default 500KB -- intentional but worth noting | +| Hardcoded path alias | Info | `@` alias resolves to `./src` -- standard pattern, no issue | + +### `tsconfig.json` / `eslint.config.js` + +No issues found. Standard Astro/TypeScript configuration. + +--- + +## 3. Constants & Shared Data + +### `src/constants.ts` (22 lines) + +| Issue | Severity | Details | +|-------|----------|---------| +| `ATLAS_VERSION` and `ATLAS_RELEASE_LABEL` hardcoded | Medium | Version `0.5.0-hotfix` and label `Atlas v0.5.0 (October 21 2025)` are hardcoded. These should ideally be sourced from a single place (e.g., environment variable, or derived from `ATLAS_VERSION`) | +| `ATLAS_RELEASE_LABEL` includes date | Medium | The date "October 21 2025" in the release label is manually maintained and could become stale | +| `SITE_URL` duplicated | Low | `SITE_URL` is also set in `astro.config.mjs` as `site: 'https://atlasos.net'`. Two sources of truth | + +--- + +## 4. Cloudflare Worker (`worker.ts`) + +**362 lines** -- Microsoft ISO download proxy + +### Security Issues + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| **Wildcard CORS** | **High** | 52 | `'Access-Control-Allow-Origin': '*'` allows any origin to call the API. Should be restricted to `https://atlasos.net` | +| User-Agent spoofing | Medium | 42-43 | Spoofs Chrome User-Agent to bypass Microsoft's bot detection. While functional, this is fragile and could break | +| No rate limiting | Medium | N/A | API endpoints have no rate limiting. Malicious actors could abuse the proxy to generate ISO links at scale | + +### Code Quality Issues + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| Hardcoded Microsoft product IDs | Medium | 29-32 | `x64: 3262`, `arm64: 3265` -- these will need updating when Microsoft changes Windows versions | +| Hardcoded `MS_PROFILE_ID` | Medium | 23 | `606624d44113` -- undocumented magic string | +| `while` loop without bound | Medium | 269-271 | Random dot selection in animate loop (`while (dotsIsLogo[idx])`) has no guard against infinite loop if all dots are logo dots | +| `handleSkus` generates random `sessionId` | Low | 299 | Falls back to `crypto.randomUUID()` if none provided. This means the session tracking may be unreliable | + +--- + +## 5. Layouts + +### `BaseLayout.astro` (77 lines) + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| **Hardcoded Kapa widget ID** | **High** | 34 | `data-website-id="d6dd374d-575e-4d3a-9462-00878d18f937"` is hardcoded in the template. Should be in an environment variable or constants | +| Kapa widget loaded on every page | Medium | 30-42 | The AI widget script is loaded on all pages including docs, 404, etc. Consider loading it conditionally | +| Third-party script loaded `is:inline` + `async` | Low | 30 | The Kapa script bypasses Astro's bundling. This is intentional but should be documented | +| `kapaLogoUrl` hardcoded | Low | 9 | GitHub avatar URL hardcoded in the layout frontmatter | + +### `DefaultLayout.astro` (45 lines) + +| Issue | Severity | Details | +|-------|----------|---------| +| `role="main"` on `
` | Low | The `role="main"` attribute is redundant on a `
` element (HTML5 semantic) | +| Hardcoded keywords meta tag | Low | SEO keywords are hardcoded directly in the layout | + +### `DocsLayout.astro` (154 lines) + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| Inline CSS custom properties | Low | 68 | `style="--docs-header-height: 3.5rem; --docs-sidebar-width: 18rem; --docs-toc-width: 18rem;"` -- these magic numbers should be in the CSS theme | +| `tabindex="-1"` on main | Info | 74 | Programmatic focus target. Fine, but should be noted | + +--- + +## 6. UI Components + +### `Grid.astro` (46 lines) -- **WILL BREAK** + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| **Dynamic Tailwind classes will be purged** | **High** | 31-34 | Constructs classes like `` `sm:${gridColMap[columns.sm]}` `` which produces strings like `sm:grid-cols-2`. Tailwind v4 scans source files for class literals -- these dynamically-constructed strings will **not** be detected and will be purged from the CSS. Must use a safelist or map to full class strings | + +**Fix:** Replace the string interpolation with a complete map: +```typescript +const responsiveMap: Record> = { + sm: { 1: 'sm:grid-cols-1', 2: 'sm:grid-cols-2', 3: 'sm:grid-cols-3', 4: 'sm:grid-cols-4' }, + md: { 1: 'md:grid-cols-1', 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4' }, + // ... +}; +``` + +### `IconInline.astro` (22 lines) -- **WILL BREAK** + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| **Dynamic Tailwind class will be purged** | **High** | 18 | `` const colorClass = color ? `text-${color}` : '' `` constructs classes like `text-blue-500` dynamically. These will be purged by Tailwind | +| Missing `aria-hidden="true"` | Medium | 22 | Inline decorative icons should be hidden from screen readers | + +### `Card.astro` (58 lines) + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| Dead CSS | Medium | 33-57 | `.card-gradient-hover` and `.card-gradient-hover::before` styles are defined but the class `card-gradient-hover` is never applied in the template. Dead code | + +### `Tabs.astro` (122 lines) + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| **Missing `aria-controls`** | **High** | 15-28 | Tab buttons lack `aria-controls` linking them to their panels | +| **Missing `aria-labelledby`** on panels | **High** | N/A | `TabPanel` does not have `aria-labelledby` referencing the tab button | +| **No keyboard arrow navigation** | **High** | 67-74 | WAI-ARIA Tabs pattern requires Left/Right arrow keys to move between tabs. Only click handlers are present | +| `tabindex` not managed | Medium | N/A | The active tab panel should have `tabindex="0"` for focus management; inactive panels should be hidden from tab order | +| Random ID generation | Low | 8 | `Math.random().toString(36)` for IDs could theoretically collide. Consider using a counter or `crypto.randomUUID()` | + +### `TabPanel.astro` (12 lines) + +| Issue | Severity | Details | +|-------|----------|---------| +| Missing `role="tabpanel"` | High | Panel does not have `role="tabpanel"` attribute | +| Missing `aria-labelledby` | High | No association with the controlling tab button | +| Missing `tabindex="0"` | Medium | Tab panels should be focusable | + +### `Video.astro` (34 lines) + +| Issue | Severity | Line(s) | Details | +|-------|----------|---------|---------| +| **No `` support for captions** | **High** | 27-34 | The `