commit a0fd0e0b3c846bbcdcf19123fe78e0668e118e68 Author: Ender Date: Wed Oct 22 00:50:29 2025 +0200 chore: initialize monorepo (pnpm workspace), add PLAN.md and .gitignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dbf85c0 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +ADMIN_PASSWORD= +OPENAI_API_KEY= +GHOST_ADMIN_API_KEY= +S3_BUCKET= +S3_REGION= +S3_ACCESS_KEY= +S3_SECRET_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e3d7cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Node +node_modules/ + +# Build outputs +/dist/ +/apps/**/dist/ +/packages/**/dist/ + +# Env & secrets +.env +.env.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# OS / IDE +.DS_Store +*.swp +.idea/ +.vscode/ diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..58b9ae2 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,90 @@ +# VoxBlog Admin Project Plan + +## Vision +Voice-first authoring tool for single-user Ghost blog. Capture audio, refine with AI, manage rich media, and publish seamlessly via a secure admin dashboard. + +## Architecture Snapshot +- **Frontend**: `apps/admin` – React + TypeScript, Vite, Material UI, authenticated single-user dashboard. +- **Backend**: `apps/api` – Node.js (Express) providing auth, media upload, OpenAI & Ghost integrations, leveraging shared utilities. +- **Storage**: Configurable AWS S3 (preferred) with local fallback during development. +- **Shared**: `packages/` for shared TypeScript types, client SDK, and utility modules. + +## Milestones & Tasks +- **M1 · Access & Shell** (Scope: Goals 1 + infrastructure) + - [x] Scaffold workspace structure (frontend, backend, shared packages). + - [x] Implement .env management & secrets handling guidelines. + - [x] Build password gate (frontend form + backend verification). + - [x] Connect FE<->BE via Vite proxy and enable CORS. + - [x] Load .env in API with explicit path. + - [ ] Bootstrap base admin layout with navigation placeholders. + - [ ] Document manual test checklist for auth flow. +- **M2 · Voice Capture Pipeline** (Scope: Goal 2) + - [ ] Add browser audio recorder UI & permissions handling. + - [ ] Stream/upload audio blobs to backend endpoint. + - [ ] Persist raw audio (S3/local) with metadata. +- **M3 · Speech-to-Text Integration** (Scope: Goal 3) + - [ ] Invoke OpenAI STT API server-side. + - [ ] Surface transcript in rich editor state with status feedback. + - [ ] Log conversion lifecycle for debug. +- **M4 · Rich Editor Enhancements** (Scope: Goal 4) + - [ ] Integrate block-based editor (e.g., TipTap/Rich text) with custom nodes. + - [ ] Implement file/image upload widget wired to storage. + - [ ] Support color picker, code blocks, and metadata fields. +- **M5 · AI Editing Tools** (Scope: Goal 5) + - [ ] Prompt templates for tone/style suggestions via OpenAI. + - [ ] Inline improvement workflow with diff/revert capabilities. +- **M6 · Ghost Publication Flow** (Scope: Goal 6) + - [ ] Map editor content to Ghost post payload. + - [ ] Implement publish/draft triggers with status reports. + - [ ] Handle tags, feature image, and canonical URL settings. +- **M7 · Media Management** (Scope: Goal 7) + - [ ] Centralize media library view with reuse. + - [ ] Background cleanup/retention policies. +- **M8 · UX Polish & Hardening** (Scope: Goal 8) + - [ ] Loading/error states across workflows. + - [ ] Responsive layout tuning & accessibility audit. + - [ ] Smoke test scripts for manual verification. + +## Environment & Tooling TODOs +- **Core tooling** + - [ ] Configure PNPM workspaces (or Nx/Turbo) for multi-app repo. + - [ ] ESLint + Prettier shared config. + - [ ] Commit hooks (lint-staged, Husky) optional. +- **Secrets** + - [ ] `.env.example` for common keys (ADMIN_PASSWORD_HASH, OPENAI_API_KEY, GHOST_ADMIN_API_KEY, S3 credentials). + - [ ] Instructions for local secret population. + +## Tooling Decisions +- **Dependency manager**: Adopt PNPM with workspace support for mono-repo friendliness and fast installs. +- **Task runner**: Use Turborepo for orchestrating build/test scripts across apps/packages. +- **Package structure**: Maintain `apps/` for runtime targets and `packages/` for shared libraries. + +## Immediate Next Actions +- [ ] Create admin layout shell (header/sidebar, container) +- [ ] Persist auth state (cookie/localStorage flag after success) +- [ ] Add simple health route `/api/health` and error handler +- [ ] Begin audio capture UI (mic permission + basic recorder) + +## Scaffolding Plan (Draft) +- **Frontend (`apps/admin`)** + - `pnpm create vite apps/admin --template react-ts` + - Add Material UI (`pnpm add @mui/material @mui/icons-material @emotion/react @emotion/styled -C apps/admin`). +- **Backend (`apps/api`)** + - `pnpm dlx degit expressjs/express apps/api` + - Install TypeScript + tooling (`pnpm add -D typescript ts-node-dev @types/node @types/express -C apps/api`). +- **Shared packages** + - `pnpm create @tsconfig/bases packages/config-ts` (or manual `tsconfig` shared file). + - Create `packages/types` for shared TypeScript definitions. +- **Workspace root** + - Initialize PNPM workspace: `pnpm init`, add `pnpm-workspace.yaml` with `apps/**` and `packages/**`. + - Configure Turborepo: `pnpm add -D turbo`, add `turbo.json` with build/dev/lint pipelines. + +## Risks & Assumptions +- **OpenAI & Ghost API access** available with required scopes. +- **Single admin user** requirement simplifies auth; if multi-user emerges, revisit architecture. +- **Browser recording support** assumed for target browsers (Chrome/Edge latest). + +## References +- Ghost Admin API docs +- OpenAI Whisper/Speech-to-Text API docs +- AWS S3 SDK for Node.js diff --git a/apps/admin/.gitignore b/apps/admin/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/admin/README.md b/apps/admin/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/apps/admin/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/apps/admin/eslint.config.js b/apps/admin/eslint.config.js new file mode 100644 index 0000000..b19330b --- /dev/null +++ b/apps/admin/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/apps/admin/index.html b/apps/admin/index.html new file mode 100644 index 0000000..4ff6eca --- /dev/null +++ b/apps/admin/index.html @@ -0,0 +1,13 @@ + + + + + + + admin + + +
+ + + diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..1967cb5 --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,34 @@ +{ + "name": "admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.4", + "@mui/material": "^7.3.4", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } +} diff --git a/apps/admin/public/vite.svg b/apps/admin/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/admin/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/src/App.css b/apps/admin/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/apps/admin/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx new file mode 100644 index 0000000..78abf77 --- /dev/null +++ b/apps/admin/src/App.tsx @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react'; +import AuthGate from './components/AuthGate'; +import EditorShell from './components/EditorShell'; +import './App.css'; + +function App() { + const [authenticated, setAuthenticated] = useState(false); + + useEffect(() => { + const flag = localStorage.getItem('voxblog_authed'); + setAuthenticated(flag === '1'); + }, []); + + const handleLogout = () => { + localStorage.removeItem('voxblog_authed'); + setAuthenticated(false); + }; + + return ( +
+ {authenticated + ? + : { + localStorage.setItem('voxblog_authed', '1'); + setAuthenticated(true); + }} /> + } +
+ ); +} + +export default App; diff --git a/apps/admin/src/assets/react.svg b/apps/admin/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/admin/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/src/components/AuthGate.tsx b/apps/admin/src/components/AuthGate.tsx new file mode 100644 index 0000000..82585d4 --- /dev/null +++ b/apps/admin/src/components/AuthGate.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import { Box, TextField, Button, Typography } from '@mui/material'; + +export default function AuthGate({ onAuth }: { onAuth: () => void }) { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const res = await fetch('/api/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }) + }); + + if (res.ok) onAuth(); + else setError('Invalid password'); + } catch (err) { + setError('Connection failed'); + } + }; + + return ( + + VoxBlog Admin +
+ setPassword(e.target.value)} + error={!!error} + helperText={error} + sx={{ mb: 2 }} + /> + + +
+ ); +} diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx new file mode 100644 index 0000000..fa80b0f --- /dev/null +++ b/apps/admin/src/components/EditorShell.tsx @@ -0,0 +1,9 @@ +import { Typography } from '@mui/material'; + +export default function EditorShell() { + return ( + + Welcome to VoxBlog Editor + + ); +} diff --git a/apps/admin/src/index.css b/apps/admin/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/apps/admin/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/admin/src/main.tsx b/apps/admin/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/apps/admin/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/admin/tsconfig.app.json b/apps/admin/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/apps/admin/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/admin/tsconfig.node.json b/apps/admin/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/apps/admin/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts new file mode 100644 index 0000000..3575045 --- /dev/null +++ b/apps/admin/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true + } + } + } +}) diff --git a/apps/api/.editorconfig b/apps/api/.editorconfig new file mode 100644 index 0000000..12cf111 --- /dev/null +++ b/apps/api/.editorconfig @@ -0,0 +1,11 @@ +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.js,*.json,*.yml}] +indent_size = 2 +indent_style = space diff --git a/apps/api/.eslintignore b/apps/api/.eslintignore new file mode 100644 index 0000000..62562b7 --- /dev/null +++ b/apps/api/.eslintignore @@ -0,0 +1,2 @@ +coverage +node_modules diff --git a/apps/api/.eslintrc.yml b/apps/api/.eslintrc.yml new file mode 100644 index 0000000..f9359bf --- /dev/null +++ b/apps/api/.eslintrc.yml @@ -0,0 +1,14 @@ +root: true +env: + es2022: true + node: true +rules: + eol-last: error + eqeqeq: [error, allow-null] + indent: [error, 2, { MemberExpression: "off", SwitchCase: 1 }] + no-trailing-spaces: error + no-unused-vars: [error, { vars: all, args: none, ignoreRestSiblings: true }] + no-restricted-globals: + - error + - name: Buffer + message: Use `import { Buffer } from "node:buffer"` instead of the global Buffer. diff --git a/apps/api/.github/dependabot.yml b/apps/api/.github/dependabot.yml new file mode 100644 index 0000000..a6096a4 --- /dev/null +++ b/apps/api/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + time: "23:00" + timezone: Europe/London + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] \ No newline at end of file diff --git a/apps/api/.github/workflows/ci.yml b/apps/api/.github/workflows/ci.yml new file mode 100644 index 0000000..ee6159c --- /dev/null +++ b/apps/api/.github/workflows/ci.yml @@ -0,0 +1,117 @@ +name: ci + +on: + push: + branches: + - master + - develop + - '4.x' + - '5.x' + - '5.0' + paths-ignore: + - '*.md' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +# Cancel in progress workflows +# in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: 'lts/*' + + - name: Install dependencies + run: npm install --ignore-scripts --include=dev + + - name: Run lint + run: npm run lint + + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [18, 19, 20, 21, 22, 23, 24, 25] + # Node.js release schedule: https://nodejs.org/en/about/releases/ + + name: Node.js ${{ matrix.node-version }} - ${{matrix.os}} + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Configure npm loglevel + run: | + npm config set loglevel error + shell: bash + + - name: Install dependencies + run: npm install + + - name: Output Node and NPM versions + run: | + echo "Node.js version: $(node -v)" + echo "NPM version: $(npm -v)" + + - name: Run tests + shell: bash + run: npm run test-ci + + - name: Upload code coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-node-${{ matrix.node-version }}-${{ matrix.os }} + path: ./coverage/lcov.info + retention-days: 1 + + coverage: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Install lcov + shell: bash + run: sudo apt-get -y install lcov + + - name: Collect coverage reports + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + path: ./coverage + pattern: coverage-node-* + + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info + + - name: Upload coverage report + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 + with: + file: ./lcov.info diff --git a/apps/api/.github/workflows/codeql.yml b/apps/api/.github/workflows/codeql.yml new file mode 100644 index 0000000..a8477b3 --- /dev/null +++ b/apps/api/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "0 0 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [javascript, actions] + + steps: + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 + with: + languages: ${{ matrix.language }} + config: | + paths-ignore: + - test + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 diff --git a/apps/api/.github/workflows/legacy.yml b/apps/api/.github/workflows/legacy.yml new file mode 100644 index 0000000..b1c0653 --- /dev/null +++ b/apps/api/.github/workflows/legacy.yml @@ -0,0 +1,101 @@ +name: legacy + +on: + push: + branches: + - master + - develop + - '4.x' + - '5.x' + - '5.0' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + workflow_dispatch: + +permissions: + contents: read + +# Cancel in progress workflows +# in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [16, 17] + # Node.js release schedule: https://nodejs.org/en/about/releases/ + + name: Node.js ${{ matrix.node-version }} - ${{matrix.os}} + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Configure npm loglevel + run: | + npm config set loglevel error + shell: bash + + - name: Install dependencies + run: npm install + + - name: Output Node and NPM versions + run: | + echo "Node.js version: $(node -v)" + echo "NPM version: $(npm -v)" + + - name: Run tests + shell: bash + run: npm run test-ci + + - name: Upload code coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-node-${{ matrix.node-version }}-${{ matrix.os }} + path: ./coverage/lcov.info + retention-days: 1 + + coverage: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Install lcov + shell: bash + run: sudo apt-get -y install lcov + + - name: Collect coverage reports + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + path: ./coverage + pattern: coverage-node-* + + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info + + - name: Upload coverage report + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 + with: + file: ./lcov.info diff --git a/apps/api/.github/workflows/scorecard.yml b/apps/api/.github/workflows/scorecard.yml new file mode 100644 index 0000000..00ae5f1 --- /dev/null +++ b/apps/api/.github/workflows/scorecard.yml @@ -0,0 +1,72 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 + with: + sarif_file: results.sarif diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..768368c --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,20 @@ +# npm +node_modules +package-lock.json +npm-shrinkwrap.json +*.log +*.gz + +# Yarn +yarn-error.log +yarn.lock + +# Coveralls +.nyc_output +coverage + +# Benchmarking +benchmarks/graphs + +# ignore additional files using core.excludesFile +# https://git-scm.com/docs/gitignore diff --git a/apps/api/.npmrc b/apps/api/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/apps/api/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/apps/api/History.md b/apps/api/History.md new file mode 100644 index 0000000..5b6cba5 --- /dev/null +++ b/apps/api/History.md @@ -0,0 +1,3858 @@ +5.1.0 / 2025-03-31 +======================== + +* Add support for `Uint8Array` in `res.send()` +* Add support for ETag option in `res.sendFile()` +* Add support for multiple links with the same rel in `res.links()` +* Add funding field to package.json +* perf: use loop for acceptParams +* refactor: prefix built-in node module imports +* deps: remove `setprototypeof` +* deps: remove `safe-buffer` +* deps: remove `utils-merge` +* deps: remove `methods` +* deps: remove `depd` +* deps: `debug@^4.4.0` +* deps: `body-parser@^2.2.0` +* deps: `router@^2.2.0` +* deps: `content-type@^1.0.5` +* deps: `finalhandler@^2.1.0` +* deps: `qs@^6.14.0` +* deps: `server-static@2.2.0` +* deps: `type-is@2.0.1` + +5.0.1 / 2024-10-08 +========== + +* Update `cookie` semver lock to address [CVE-2024-47764](https://nvd.nist.gov/vuln/detail/CVE-2024-47764) + +5.0.0 / 2024-09-10 +========================= +* remove: + - `path-is-absolute` dependency - use `path.isAbsolute` instead +* breaking: + * `res.status()` accepts only integers, and input must be greater than 99 and less than 1000 + * will throw a `RangeError: Invalid status code: ${code}. Status code must be greater than 99 and less than 1000.` for inputs outside this range + * will throw a `TypeError: Invalid status code: ${code}. Status code must be an integer.` for non integer inputs + * deps: send@1.0.0 + * `res.redirect('back')` and `res.location('back')` is no longer a supported magic string, explicitly use `req.get('Referrer') || '/'`. +* change: + - `res.clearCookie` will ignore user provided `maxAge` and `expires` options +* deps: cookie-signature@^1.2.1 +* deps: debug@4.3.6 +* deps: merge-descriptors@^2.0.0 +* deps: serve-static@^2.1.0 +* deps: qs@6.13.0 +* deps: accepts@^2.0.0 +* deps: mime-types@^3.0.0 + - `application/javascript` => `text/javascript` +* deps: type-is@^2.0.0 +* deps: content-disposition@^1.0.0 +* deps: finalhandler@^2.0.0 +* deps: fresh@^2.0.0 +* deps: body-parser@^2.0.1 +* deps: send@^1.1.0 + +5.0.0-beta.3 / 2024-03-25 +========================= + +This incorporates all changes after 4.19.1 up to 4.19.2. + +5.0.0-beta.2 / 2024-03-20 +========================= + +This incorporates all changes after 4.17.2 up to 4.19.1. + +5.0.0-beta.1 / 2022-02-14 +========================= + +This is the first Express 5.0 beta release, based off 4.17.2 and includes +changes from 5.0.0-alpha.8. + + * change: + - Default "query parser" setting to `'simple'` + - Requires Node.js 4+ + - Use `mime-types` for file to content type mapping + * deps: array-flatten@3.0.0 + * deps: body-parser@2.0.0-beta.1 + - `req.body` is no longer always initialized to `{}` + - `urlencoded` parser now defaults `extended` to `false` + - Use `on-finished` to determine when body read + * deps: router@2.0.0-beta.1 + - Add new `?`, `*`, and `+` parameter modifiers + - Internalize private `router.process_params` method + - Matching group expressions are only RegExp syntax + - Named matching groups no longer available by position in `req.params` + - Regular expressions can only be used in a matching group + - Remove `debug` dependency + - Special `*` path segment behavior removed + - deps: array-flatten@3.0.0 + - deps: parseurl@~1.3.3 + - deps: path-to-regexp@3.2.0 + - deps: setprototypeof@1.2.0 + * deps: send@1.0.0-beta.1 + - Change `dotfiles` option default to `'ignore'` + - Remove `hidden` option; use `dotfiles` option instead + - Use `mime-types` for file to content type mapping + - deps: debug@3.1.0 + * deps: serve-static@2.0.0-beta.1 + - Change `dotfiles` option default to `'ignore'` + - Remove `hidden` option; use `dotfiles` option instead + - Use `mime-types` for file to content type mapping + - Remove `express.static.mime` export; use `mime-types` package instead + - deps: send@1.0.0-beta.1 + +5.0.0-alpha.8 / 2020-03-25 +========================== + +This is the eighth Express 5.0 alpha release, based off 4.17.1 and includes +changes from 5.0.0-alpha.7. + +5.0.0-alpha.7 / 2018-10-26 +========================== + +This is the seventh Express 5.0 alpha release, based off 4.16.4 and includes +changes from 5.0.0-alpha.6. + +The major change with this alpha is the basic support for returned, rejected +Promises in the router. + + * remove: + - `path-to-regexp` dependency + * deps: debug@3.1.0 + - Add `DEBUG_HIDE_DATE` environment variable + - Change timer to per-namespace instead of global + - Change non-TTY date format + - Remove `DEBUG_FD` environment variable support + - Support 256 namespace colors + * deps: router@2.0.0-alpha.1 + - Add basic support for returned, rejected Promises + - Fix JSDoc for `Router` constructor + - deps: debug@3.1.0 + - deps: parseurl@~1.3.2 + - deps: setprototypeof@1.1.0 + - deps: utils-merge@1.0.1 + +5.0.0-alpha.6 / 2017-09-24 +========================== + +This is the sixth Express 5.0 alpha release, based off 4.15.5 and includes +changes from 5.0.0-alpha.5. + + * remove: + - `res.redirect(url, status)` signature - use `res.redirect(status, url)` + - `res.send(status, body)` signature - use `res.status(status).send(body)` + * deps: router@~1.3.1 + - deps: debug@2.6.8 + +5.0.0-alpha.5 / 2017-03-06 +========================== + +This is the fifth Express 5.0 alpha release, based off 4.15.2 and includes +changes from 5.0.0-alpha.4. + +5.0.0-alpha.4 / 2017-03-01 +========================== + +This is the fourth Express 5.0 alpha release, based off 4.15.0 and includes +changes from 5.0.0-alpha.3. + + * remove: + - Remove Express 3.x middleware error stubs + * deps: router@~1.3.0 + - Add `next("router")` to exit from router + - Fix case where `router.use` skipped requests routes did not + - Skip routing when `req.url` is not set + - Use `%o` in path debug to tell types apart + - deps: debug@2.6.1 + - deps: setprototypeof@1.0.3 + - perf: add fast match path for `*` route + +5.0.0-alpha.3 / 2017-01-28 +========================== + +This is the third Express 5.0 alpha release, based off 4.14.1 and includes +changes from 5.0.0-alpha.2. + + * remove: + - `res.json(status, obj)` signature - use `res.status(status).json(obj)` + - `res.jsonp(status, obj)` signature - use `res.status(status).jsonp(obj)` + - `res.vary()` (no arguments) -- provide a field name as an argument + * deps: array-flatten@2.1.1 + * deps: path-is-absolute@1.0.1 + * deps: router@~1.1.5 + - deps: array-flatten@2.0.1 + - deps: methods@~1.1.2 + - deps: parseurl@~1.3.1 + - deps: setprototypeof@1.0.2 + +5.0.0-alpha.2 / 2015-07-06 +========================== + +This is the second Express 5.0 alpha release, based off 4.13.1 and includes +changes from 5.0.0-alpha.1. + + * remove: + - `app.param(fn)` + - `req.param()` -- use `req.params`, `req.body`, or `req.query` instead + * change: + - `res.render` callback is always async, even for sync view engines + - The leading `:` character in `name` for `app.param(name, fn)` is no longer removed + - Use `router` module for routing + - Use `path-is-absolute` module for absolute path detection + +5.0.0-alpha.1 / 2014-11-06 +========================== + +This is the first Express 5.0 alpha release, based off 4.10.1. + + * remove: + - `app.del` - use `app.delete` + - `req.acceptsCharset` - use `req.acceptsCharsets` + - `req.acceptsEncoding` - use `req.acceptsEncodings` + - `req.acceptsLanguage` - use `req.acceptsLanguages` + - `res.json(obj, status)` signature - use `res.json(status, obj)` + - `res.jsonp(obj, status)` signature - use `res.jsonp(status, obj)` + - `res.send(body, status)` signature - use `res.send(status, body)` + - `res.send(status)` signature - use `res.sendStatus(status)` + - `res.sendfile` - use `res.sendFile` instead + - `express.query` middleware + * change: + - `req.host` now returns host (`hostname:port`) - use `req.hostname` for only hostname + - `req.query` is now a getter instead of a plain property + * add: + - `app.router` is a reference to the base router + +4.20.0 / 2024-09-10 +========== + * deps: serve-static@0.16.0 + * Remove link renderization in html while redirecting + * deps: send@0.19.0 + * Remove link renderization in html while redirecting + * deps: body-parser@0.6.0 + * add `depth` option to customize the depth level in the parser + * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) + * Remove link renderization in html while using `res.redirect` + * deps: path-to-regexp@0.1.10 + - Adds support for named matching groups in the routes using a regex + - Adds backtracking protection to parameters without regexes defined + * deps: encodeurl@~2.0.0 + - Removes encoding of `\`, `|`, and `^` to align better with URL spec + * Deprecate passing `options.maxAge` and `options.expires` to `res.clearCookie` + - Will be ignored in v5, clearCookie will set a cookie with an expires in the past to instruct clients to delete the cookie + +4.19.2 / 2024-03-25 +========== + + * Improved fix for open redirect allow list bypass + +4.19.1 / 2024-03-20 +========== + + * Allow passing non-strings to res.location with new encoding handling checks + +4.19.0 / 2024-03-20 +========== + + * Prevent open redirect allow list bypass due to encodeurl + * deps: cookie@0.6.0 + +4.18.3 / 2024-02-29 +========== + + * Fix routing requests without method + * deps: body-parser@1.20.2 + - Fix strict json error message on Node.js 19+ + - deps: content-type@~1.0.5 + - deps: raw-body@2.5.2 + * deps: cookie@0.6.0 + - Add `partitioned` option + +4.18.2 / 2022-10-08 +=================== + + * Fix regression routing a large stack in a single route + * deps: body-parser@1.20.1 + - deps: qs@6.11.0 + - perf: remove unnecessary object clone + * deps: qs@6.11.0 + +4.18.1 / 2022-04-29 +=================== + + * Fix hanging on large stack of sync routes + +4.18.0 / 2022-04-25 +=================== + + * Add "root" option to `res.download` + * Allow `options` without `filename` in `res.download` + * Deprecate string and non-integer arguments to `res.status` + * Fix behavior of `null`/`undefined` as `maxAge` in `res.cookie` + * Fix handling very large stacks of sync middleware + * Ignore `Object.prototype` values in settings through `app.set`/`app.get` + * Invoke `default` with same arguments as types in `res.format` + * Support proper 205 responses using `res.send` + * Use `http-errors` for `res.format` error + * deps: body-parser@1.20.0 + - Fix error message for json parse whitespace in `strict` + - Fix internal error when inflated body exceeds limit + - Prevent loss of async hooks context + - Prevent hanging when request already read + - deps: depd@2.0.0 + - deps: http-errors@2.0.0 + - deps: on-finished@2.4.1 + - deps: qs@6.10.3 + - deps: raw-body@2.5.1 + * deps: cookie@0.5.0 + - Add `priority` option + - Fix `expires` option to reject invalid dates + * deps: depd@2.0.0 + - Replace internal `eval` usage with `Function` constructor + - Use instance methods on `process` to check for listeners + * deps: finalhandler@1.2.0 + - Remove set content headers that break response + - deps: on-finished@2.4.1 + - deps: statuses@2.0.1 + * deps: on-finished@2.4.1 + - Prevent loss of async hooks context + * deps: qs@6.10.3 + * deps: send@0.18.0 + - Fix emitted 416 error missing headers property + - Limit the headers removed for 304 response + - deps: depd@2.0.0 + - deps: destroy@1.2.0 + - deps: http-errors@2.0.0 + - deps: on-finished@2.4.1 + - deps: statuses@2.0.1 + * deps: serve-static@1.15.0 + - deps: send@0.18.0 + * deps: statuses@2.0.1 + - Remove code 306 + - Rename `425 Unordered Collection` to standard `425 Too Early` + +4.17.3 / 2022-02-16 +=================== + + * deps: accepts@~1.3.8 + - deps: mime-types@~2.1.34 + - deps: negotiator@0.6.3 + * deps: body-parser@1.19.2 + - deps: bytes@3.1.2 + - deps: qs@6.9.7 + - deps: raw-body@2.4.3 + * deps: cookie@0.4.2 + * deps: qs@6.9.7 + * Fix handling of `__proto__` keys + * pref: remove unnecessary regexp for trust proxy + +4.17.2 / 2021-12-16 +=================== + + * Fix handling of `undefined` in `res.jsonp` + * Fix handling of `undefined` when `"json escape"` is enabled + * Fix incorrect middleware execution with unanchored `RegExp`s + * Fix `res.jsonp(obj, status)` deprecation message + * Fix typo in `res.is` JSDoc + * deps: body-parser@1.19.1 + - deps: bytes@3.1.1 + - deps: http-errors@1.8.1 + - deps: qs@6.9.6 + - deps: raw-body@2.4.2 + - deps: safe-buffer@5.2.1 + - deps: type-is@~1.6.18 + * deps: content-disposition@0.5.4 + - deps: safe-buffer@5.2.1 + * deps: cookie@0.4.1 + - Fix `maxAge` option to reject invalid values + * deps: proxy-addr@~2.0.7 + - Use `req.socket` over deprecated `req.connection` + - deps: forwarded@0.2.0 + - deps: ipaddr.js@1.9.1 + * deps: qs@6.9.6 + * deps: safe-buffer@5.2.1 + * deps: send@0.17.2 + - deps: http-errors@1.8.1 + - deps: ms@2.1.3 + - pref: ignore empty http tokens + * deps: serve-static@1.14.2 + - deps: send@0.17.2 + * deps: setprototypeof@1.2.0 + +4.17.1 / 2019-05-25 +=================== + + * Revert "Improve error message for `null`/`undefined` to `res.status`" + +4.17.0 / 2019-05-16 +=================== + + * Add `express.raw` to parse bodies into `Buffer` + * Add `express.text` to parse bodies into string + * Improve error message for non-strings to `res.sendFile` + * Improve error message for `null`/`undefined` to `res.status` + * Support multiple hosts in `X-Forwarded-Host` + * deps: accepts@~1.3.7 + * deps: body-parser@1.19.0 + - Add encoding MIK + - Add petabyte (`pb`) support + - Fix parsing array brackets after index + - deps: bytes@3.1.0 + - deps: http-errors@1.7.2 + - deps: iconv-lite@0.4.24 + - deps: qs@6.7.0 + - deps: raw-body@2.4.0 + - deps: type-is@~1.6.17 + * deps: content-disposition@0.5.3 + * deps: cookie@0.4.0 + - Add `SameSite=None` support + * deps: finalhandler@~1.1.2 + - Set stricter `Content-Security-Policy` header + - deps: parseurl@~1.3.3 + - deps: statuses@~1.5.0 + * deps: parseurl@~1.3.3 + * deps: proxy-addr@~2.0.5 + - deps: ipaddr.js@1.9.0 + * deps: qs@6.7.0 + - Fix parsing array brackets after index + * deps: range-parser@~1.2.1 + * deps: send@0.17.1 + - Set stricter CSP header in redirect & error responses + - deps: http-errors@~1.7.2 + - deps: mime@1.6.0 + - deps: ms@2.1.1 + - deps: range-parser@~1.2.1 + - deps: statuses@~1.5.0 + - perf: remove redundant `path.normalize` call + * deps: serve-static@1.14.1 + - Set stricter CSP header in redirect response + - deps: parseurl@~1.3.3 + - deps: send@0.17.1 + * deps: setprototypeof@1.1.1 + * deps: statuses@~1.5.0 + - Add `103 Early Hints` + * deps: type-is@~1.6.18 + - deps: mime-types@~2.1.24 + - perf: prevent internal `throw` on invalid type + +4.16.4 / 2018-10-10 +=================== + + * Fix issue where `"Request aborted"` may be logged in `res.sendfile` + * Fix JSDoc for `Router` constructor + * deps: body-parser@1.18.3 + - Fix deprecation warnings on Node.js 10+ + - Fix stack trace for strict json parse error + - deps: depd@~1.1.2 + - deps: http-errors@~1.6.3 + - deps: iconv-lite@0.4.23 + - deps: qs@6.5.2 + - deps: raw-body@2.3.3 + - deps: type-is@~1.6.16 + * deps: proxy-addr@~2.0.4 + - deps: ipaddr.js@1.8.0 + * deps: qs@6.5.2 + * deps: safe-buffer@5.1.2 + +4.16.3 / 2018-03-12 +=================== + + * deps: accepts@~1.3.5 + - deps: mime-types@~2.1.18 + * deps: depd@~1.1.2 + - perf: remove argument reassignment + * deps: encodeurl@~1.0.2 + - Fix encoding `%` as last character + * deps: finalhandler@1.1.1 + - Fix 404 output for bad / missing pathnames + - deps: encodeurl@~1.0.2 + - deps: statuses@~1.4.0 + * deps: proxy-addr@~2.0.3 + - deps: ipaddr.js@1.6.0 + * deps: send@0.16.2 + - Fix incorrect end tag in default error & redirects + - deps: depd@~1.1.2 + - deps: encodeurl@~1.0.2 + - deps: statuses@~1.4.0 + * deps: serve-static@1.13.2 + - Fix incorrect end tag in redirects + - deps: encodeurl@~1.0.2 + - deps: send@0.16.2 + * deps: statuses@~1.4.0 + * deps: type-is@~1.6.16 + - deps: mime-types@~2.1.18 + +4.16.2 / 2017-10-09 +=================== + + * Fix `TypeError` in `res.send` when given `Buffer` and `ETag` header set + * perf: skip parsing of entire `X-Forwarded-Proto` header + +4.16.1 / 2017-09-29 +=================== + + * deps: send@0.16.1 + * deps: serve-static@1.13.1 + - Fix regression when `root` is incorrectly set to a file + - deps: send@0.16.1 + +4.16.0 / 2017-09-28 +=================== + + * Add `"json escape"` setting for `res.json` and `res.jsonp` + * Add `express.json` and `express.urlencoded` to parse bodies + * Add `options` argument to `res.download` + * Improve error message when autoloading invalid view engine + * Improve error messages when non-function provided as middleware + * Skip `Buffer` encoding when not generating ETag for small response + * Use `safe-buffer` for improved Buffer API + * deps: accepts@~1.3.4 + - deps: mime-types@~2.1.16 + * deps: content-type@~1.0.4 + - perf: remove argument reassignment + - perf: skip parameter parsing when no parameters + * deps: etag@~1.8.1 + - perf: replace regular expression with substring + * deps: finalhandler@1.1.0 + - Use `res.headersSent` when available + * deps: parseurl@~1.3.2 + - perf: reduce overhead for full URLs + - perf: unroll the "fast-path" `RegExp` + * deps: proxy-addr@~2.0.2 + - Fix trimming leading / trailing OWS in `X-Forwarded-For` + - deps: forwarded@~0.1.2 + - deps: ipaddr.js@1.5.2 + - perf: reduce overhead when no `X-Forwarded-For` header + * deps: qs@6.5.1 + - Fix parsing & compacting very deep objects + * deps: send@0.16.0 + - Add 70 new types for file extensions + - Add `immutable` option + - Fix missing `` in default error & redirects + - Set charset as "UTF-8" for .js and .json + - Use instance methods on steam to check for listeners + - deps: mime@1.4.1 + - perf: improve path validation speed + * deps: serve-static@1.13.0 + - Add 70 new types for file extensions + - Add `immutable` option + - Set charset as "UTF-8" for .js and .json + - deps: send@0.16.0 + * deps: setprototypeof@1.1.0 + * deps: utils-merge@1.0.1 + * deps: vary@~1.1.2 + - perf: improve header token parsing speed + * perf: re-use options object when generating ETags + * perf: remove dead `.charset` set in `res.jsonp` + +4.15.5 / 2017-09-24 +=================== + + * deps: debug@2.6.9 + * deps: finalhandler@~1.0.6 + - deps: debug@2.6.9 + - deps: parseurl@~1.3.2 + * deps: fresh@0.5.2 + - Fix handling of modified headers with invalid dates + - perf: improve ETag match loop + - perf: improve `If-None-Match` token parsing + * deps: send@0.15.6 + - Fix handling of modified headers with invalid dates + - deps: debug@2.6.9 + - deps: etag@~1.8.1 + - deps: fresh@0.5.2 + - perf: improve `If-Match` token parsing + * deps: serve-static@1.12.6 + - deps: parseurl@~1.3.2 + - deps: send@0.15.6 + - perf: improve slash collapsing + +4.15.4 / 2017-08-06 +=================== + + * deps: debug@2.6.8 + * deps: depd@~1.1.1 + - Remove unnecessary `Buffer` loading + * deps: finalhandler@~1.0.4 + - deps: debug@2.6.8 + * deps: proxy-addr@~1.1.5 + - Fix array argument being altered + - deps: ipaddr.js@1.4.0 + * deps: qs@6.5.0 + * deps: send@0.15.4 + - deps: debug@2.6.8 + - deps: depd@~1.1.1 + - deps: http-errors@~1.6.2 + * deps: serve-static@1.12.4 + - deps: send@0.15.4 + +4.15.3 / 2017-05-16 +=================== + + * Fix error when `res.set` cannot add charset to `Content-Type` + * deps: debug@2.6.7 + - Fix `DEBUG_MAX_ARRAY_LENGTH` + - deps: ms@2.0.0 + * deps: finalhandler@~1.0.3 + - Fix missing `` in HTML document + - deps: debug@2.6.7 + * deps: proxy-addr@~1.1.4 + - deps: ipaddr.js@1.3.0 + * deps: send@0.15.3 + - deps: debug@2.6.7 + - deps: ms@2.0.0 + * deps: serve-static@1.12.3 + - deps: send@0.15.3 + * deps: type-is@~1.6.15 + - deps: mime-types@~2.1.15 + * deps: vary@~1.1.1 + - perf: hoist regular expression + +4.15.2 / 2017-03-06 +=================== + + * deps: qs@6.4.0 + - Fix regression parsing keys starting with `[` + +4.15.1 / 2017-03-05 +=================== + + * deps: send@0.15.1 + - Fix issue when `Date.parse` does not return `NaN` on invalid date + - Fix strict violation in broken environments + * deps: serve-static@1.12.1 + - Fix issue when `Date.parse` does not return `NaN` on invalid date + - deps: send@0.15.1 + +4.15.0 / 2017-03-01 +=================== + + * Add debug message when loading view engine + * Add `next("router")` to exit from router + * Fix case where `router.use` skipped requests routes did not + * Remove usage of `res._headers` private field + - Improves compatibility with Node.js 8 nightly + * Skip routing when `req.url` is not set + * Use `%o` in path debug to tell types apart + * Use `Object.create` to setup request & response prototypes + * Use `setprototypeof` module to replace `__proto__` setting + * Use `statuses` instead of `http` module for status messages + * deps: debug@2.6.1 + - Allow colors in workers + - Deprecated `DEBUG_FD` environment variable set to `3` or higher + - Fix error when running under React Native + - Use same color for same namespace + - deps: ms@0.7.2 + * deps: etag@~1.8.0 + - Use SHA1 instead of MD5 for ETag hashing + - Works with FIPS 140-2 OpenSSL configuration + * deps: finalhandler@~1.0.0 + - Fix exception when `err` cannot be converted to a string + - Fully URL-encode the pathname in the 404 + - Only include the pathname in the 404 message + - Send complete HTML document + - Set `Content-Security-Policy: default-src 'self'` header + - deps: debug@2.6.1 + * deps: fresh@0.5.0 + - Fix false detection of `no-cache` request directive + - Fix incorrect result when `If-None-Match` has both `*` and ETags + - Fix weak `ETag` matching to match spec + - perf: delay reading header values until needed + - perf: enable strict mode + - perf: hoist regular expressions + - perf: remove duplicate conditional + - perf: remove unnecessary boolean coercions + - perf: skip checking modified time if ETag check failed + - perf: skip parsing `If-None-Match` when no `ETag` header + - perf: use `Date.parse` instead of `new Date` + * deps: qs@6.3.1 + - Fix array parsing from skipping empty values + - Fix compacting nested arrays + * deps: send@0.15.0 + - Fix false detection of `no-cache` request directive + - Fix incorrect result when `If-None-Match` has both `*` and ETags + - Fix weak `ETag` matching to match spec + - Remove usage of `res._headers` private field + - Support `If-Match` and `If-Unmodified-Since` headers + - Use `res.getHeaderNames()` when available + - Use `res.headersSent` when available + - deps: debug@2.6.1 + - deps: etag@~1.8.0 + - deps: fresh@0.5.0 + - deps: http-errors@~1.6.1 + * deps: serve-static@1.12.0 + - Fix false detection of `no-cache` request directive + - Fix incorrect result when `If-None-Match` has both `*` and ETags + - Fix weak `ETag` matching to match spec + - Remove usage of `res._headers` private field + - Send complete HTML document in redirect response + - Set default CSP header in redirect response + - Support `If-Match` and `If-Unmodified-Since` headers + - Use `res.getHeaderNames()` when available + - Use `res.headersSent` when available + - deps: send@0.15.0 + * perf: add fast match path for `*` route + * perf: improve `req.ips` performance + +4.14.1 / 2017-01-28 +=================== + + * deps: content-disposition@0.5.2 + * deps: finalhandler@0.5.1 + - Fix exception when `err.headers` is not an object + - deps: statuses@~1.3.1 + - perf: hoist regular expressions + - perf: remove duplicate validation path + * deps: proxy-addr@~1.1.3 + - deps: ipaddr.js@1.2.0 + * deps: send@0.14.2 + - deps: http-errors@~1.5.1 + - deps: ms@0.7.2 + - deps: statuses@~1.3.1 + * deps: serve-static@~1.11.2 + - deps: send@0.14.2 + * deps: type-is@~1.6.14 + - deps: mime-types@~2.1.13 + +4.14.0 / 2016-06-16 +=================== + + * Add `acceptRanges` option to `res.sendFile`/`res.sendfile` + * Add `cacheControl` option to `res.sendFile`/`res.sendfile` + * Add `options` argument to `req.range` + - Includes the `combine` option + * Encode URL in `res.location`/`res.redirect` if not already encoded + * Fix some redirect handling in `res.sendFile`/`res.sendfile` + * Fix Windows absolute path check using forward slashes + * Improve error with invalid arguments to `req.get()` + * Improve performance for `res.json`/`res.jsonp` in most cases + * Improve `Range` header handling in `res.sendFile`/`res.sendfile` + * deps: accepts@~1.3.3 + - Fix including type extensions in parameters in `Accept` parsing + - Fix parsing `Accept` parameters with quoted equals + - Fix parsing `Accept` parameters with quoted semicolons + - Many performance improvements + - deps: mime-types@~2.1.11 + - deps: negotiator@0.6.1 + * deps: content-type@~1.0.2 + - perf: enable strict mode + * deps: cookie@0.3.1 + - Add `sameSite` option + - Fix cookie `Max-Age` to never be a floating point number + - Improve error message when `encode` is not a function + - Improve error message when `expires` is not a `Date` + - Throw better error for invalid argument to parse + - Throw on invalid values provided to `serialize` + - perf: enable strict mode + - perf: hoist regular expression + - perf: use for loop in parse + - perf: use string concatenation for serialization + * deps: finalhandler@0.5.0 + - Change invalid or non-numeric status code to 500 + - Overwrite status message to match set status code + - Prefer `err.statusCode` if `err.status` is invalid + - Set response headers from `err.headers` object + - Use `statuses` instead of `http` module for status messages + * deps: proxy-addr@~1.1.2 + - Fix accepting various invalid netmasks + - Fix IPv6-mapped IPv4 validation edge cases + - IPv4 netmasks must be contiguous + - IPv6 addresses cannot be used as a netmask + - deps: ipaddr.js@1.1.1 + * deps: qs@6.2.0 + - Add `decoder` option in `parse` function + * deps: range-parser@~1.2.0 + - Add `combine` option to combine overlapping ranges + - Fix incorrectly returning -1 when there is at least one valid range + - perf: remove internal function + * deps: send@0.14.1 + - Add `acceptRanges` option + - Add `cacheControl` option + - Attempt to combine multiple ranges into single range + - Correctly inherit from `Stream` class + - Fix `Content-Range` header in 416 responses when using `start`/`end` options + - Fix `Content-Range` header missing from default 416 responses + - Fix redirect error when `path` contains raw non-URL characters + - Fix redirect when `path` starts with multiple forward slashes + - Ignore non-byte `Range` headers + - deps: http-errors@~1.5.0 + - deps: range-parser@~1.2.0 + - deps: statuses@~1.3.0 + - perf: remove argument reassignment + * deps: serve-static@~1.11.1 + - Add `acceptRanges` option + - Add `cacheControl` option + - Attempt to combine multiple ranges into single range + - Fix redirect error when `req.url` contains raw non-URL characters + - Ignore non-byte `Range` headers + - Use status code 301 for redirects + - deps: send@0.14.1 + * deps: type-is@~1.6.13 + - Fix type error when given invalid type to match against + - deps: mime-types@~2.1.11 + * deps: vary@~1.1.0 + - Only accept valid field names in the `field` argument + * perf: use strict equality when possible + +4.13.4 / 2016-01-21 +=================== + + * deps: content-disposition@0.5.1 + - perf: enable strict mode + * deps: cookie@0.1.5 + - Throw on invalid values provided to `serialize` + * deps: depd@~1.1.0 + - Support web browser loading + - perf: enable strict mode + * deps: escape-html@~1.0.3 + - perf: enable strict mode + - perf: optimize string replacement + - perf: use faster string coercion + * deps: finalhandler@0.4.1 + - deps: escape-html@~1.0.3 + * deps: merge-descriptors@1.0.1 + - perf: enable strict mode + * deps: methods@~1.1.2 + - perf: enable strict mode + * deps: parseurl@~1.3.1 + - perf: enable strict mode + * deps: proxy-addr@~1.0.10 + - deps: ipaddr.js@1.0.5 + - perf: enable strict mode + * deps: range-parser@~1.0.3 + - perf: enable strict mode + * deps: send@0.13.1 + - deps: depd@~1.1.0 + - deps: destroy@~1.0.4 + - deps: escape-html@~1.0.3 + - deps: range-parser@~1.0.3 + * deps: serve-static@~1.10.2 + - deps: escape-html@~1.0.3 + - deps: parseurl@~1.3.0 + - deps: send@0.13.1 + +4.13.3 / 2015-08-02 +=================== + + * Fix infinite loop condition using `mergeParams: true` + * Fix inner numeric indices incorrectly altering parent `req.params` + +4.13.2 / 2015-07-31 +=================== + + * deps: accepts@~1.2.12 + - deps: mime-types@~2.1.4 + * deps: array-flatten@1.1.1 + - perf: enable strict mode + * deps: path-to-regexp@0.1.7 + - Fix regression with escaped round brackets and matching groups + * deps: type-is@~1.6.6 + - deps: mime-types@~2.1.4 + +4.13.1 / 2015-07-05 +=================== + + * deps: accepts@~1.2.10 + - deps: mime-types@~2.1.2 + * deps: qs@4.0.0 + - Fix dropping parameters like `hasOwnProperty` + - Fix various parsing edge cases + * deps: type-is@~1.6.4 + - deps: mime-types@~2.1.2 + - perf: enable strict mode + - perf: remove argument reassignment + +4.13.0 / 2015-06-20 +=================== + + * Add settings to debug output + * Fix `res.format` error when only `default` provided + * Fix issue where `next('route')` in `app.param` would incorrectly skip values + * Fix hiding platform issues with `decodeURIComponent` + - Only `URIError`s are a 400 + * Fix using `*` before params in routes + * Fix using capture groups before params in routes + * Simplify `res.cookie` to call `res.append` + * Use `array-flatten` module for flattening arrays + * deps: accepts@~1.2.9 + - deps: mime-types@~2.1.1 + - perf: avoid argument reassignment & argument slice + - perf: avoid negotiator recursive construction + - perf: enable strict mode + - perf: remove unnecessary bitwise operator + * deps: cookie@0.1.3 + - perf: deduce the scope of try-catch deopt + - perf: remove argument reassignments + * deps: escape-html@1.0.2 + * deps: etag@~1.7.0 + - Always include entity length in ETags for hash length extensions + - Generate non-Stats ETags using MD5 only (no longer CRC32) + - Improve stat performance by removing hashing + - Improve support for JXcore + - Remove base64 padding in ETags to shorten + - Support "fake" stats objects in environments without fs + - Use MD5 instead of MD4 in weak ETags over 1KB + * deps: finalhandler@0.4.0 + - Fix a false-positive when unpiping in Node.js 0.8 + - Support `statusCode` property on `Error` objects + - Use `unpipe` module for unpiping requests + - deps: escape-html@1.0.2 + - deps: on-finished@~2.3.0 + - perf: enable strict mode + - perf: remove argument reassignment + * deps: fresh@0.3.0 + - Add weak `ETag` matching support + * deps: on-finished@~2.3.0 + - Add defined behavior for HTTP `CONNECT` requests + - Add defined behavior for HTTP `Upgrade` requests + - deps: ee-first@1.1.1 + * deps: path-to-regexp@0.1.6 + * deps: send@0.13.0 + - Allow Node.js HTTP server to set `Date` response header + - Fix incorrectly removing `Content-Location` on 304 response + - Improve the default redirect response headers + - Send appropriate headers on default error response + - Use `http-errors` for standard emitted errors + - Use `statuses` instead of `http` module for status messages + - deps: escape-html@1.0.2 + - deps: etag@~1.7.0 + - deps: fresh@0.3.0 + - deps: on-finished@~2.3.0 + - perf: enable strict mode + - perf: remove unnecessary array allocations + * deps: serve-static@~1.10.0 + - Add `fallthrough` option + - Fix reading options from options prototype + - Improve the default redirect response headers + - Malformed URLs now `next()` instead of 400 + - deps: escape-html@1.0.2 + - deps: send@0.13.0 + - perf: enable strict mode + - perf: remove argument reassignment + * deps: type-is@~1.6.3 + - deps: mime-types@~2.1.1 + - perf: reduce try block size + - perf: remove bitwise operations + * perf: enable strict mode + * perf: isolate `app.render` try block + * perf: remove argument reassignments in application + * perf: remove argument reassignments in request prototype + * perf: remove argument reassignments in response prototype + * perf: remove argument reassignments in routing + * perf: remove argument reassignments in `View` + * perf: skip attempting to decode zero length string + * perf: use saved reference to `http.STATUS_CODES` + +4.12.4 / 2015-05-17 +=================== + + * deps: accepts@~1.2.7 + - deps: mime-types@~2.0.11 + - deps: negotiator@0.5.3 + * deps: debug@~2.2.0 + - deps: ms@0.7.1 + * deps: depd@~1.0.1 + * deps: etag@~1.6.0 + - Improve support for JXcore + - Support "fake" stats objects in environments without `fs` + * deps: finalhandler@0.3.6 + - deps: debug@~2.2.0 + - deps: on-finished@~2.2.1 + * deps: on-finished@~2.2.1 + - Fix `isFinished(req)` when data buffered + * deps: proxy-addr@~1.0.8 + - deps: ipaddr.js@1.0.1 + * deps: qs@2.4.2 + - Fix allowing parameters like `constructor` + * deps: send@0.12.3 + - deps: debug@~2.2.0 + - deps: depd@~1.0.1 + - deps: etag@~1.6.0 + - deps: ms@0.7.1 + - deps: on-finished@~2.2.1 + * deps: serve-static@~1.9.3 + - deps: send@0.12.3 + * deps: type-is@~1.6.2 + - deps: mime-types@~2.0.11 + +4.12.3 / 2015-03-17 +=================== + + * deps: accepts@~1.2.5 + - deps: mime-types@~2.0.10 + * deps: debug@~2.1.3 + - Fix high intensity foreground color for bold + - deps: ms@0.7.0 + * deps: finalhandler@0.3.4 + - deps: debug@~2.1.3 + * deps: proxy-addr@~1.0.7 + - deps: ipaddr.js@0.1.9 + * deps: qs@2.4.1 + - Fix error when parameter `hasOwnProperty` is present + * deps: send@0.12.2 + - Throw errors early for invalid `extensions` or `index` options + - deps: debug@~2.1.3 + * deps: serve-static@~1.9.2 + - deps: send@0.12.2 + * deps: type-is@~1.6.1 + - deps: mime-types@~2.0.10 + +4.12.2 / 2015-03-02 +=================== + + * Fix regression where `"Request aborted"` is logged using `res.sendFile` + +4.12.1 / 2015-03-01 +=================== + + * Fix constructing application with non-configurable prototype properties + * Fix `ECONNRESET` errors from `res.sendFile` usage + * Fix `req.host` when using "trust proxy" hops count + * Fix `req.protocol`/`req.secure` when using "trust proxy" hops count + * Fix wrong `code` on aborted connections from `res.sendFile` + * deps: merge-descriptors@1.0.0 + +4.12.0 / 2015-02-23 +=================== + + * Fix `"trust proxy"` setting to inherit when app is mounted + * Generate `ETag`s for all request responses + - No longer restricted to only responses for `GET` and `HEAD` requests + * Use `content-type` to parse `Content-Type` headers + * deps: accepts@~1.2.4 + - Fix preference sorting to be stable for long acceptable lists + - deps: mime-types@~2.0.9 + - deps: negotiator@0.5.1 + * deps: cookie-signature@1.0.6 + * deps: send@0.12.1 + - Always read the stat size from the file + - Fix mutating passed-in `options` + - deps: mime@1.3.4 + * deps: serve-static@~1.9.1 + - deps: send@0.12.1 + * deps: type-is@~1.6.0 + - fix argument reassignment + - fix false-positives in `hasBody` `Transfer-Encoding` check + - support wildcard for both type and subtype (`*/*`) + - deps: mime-types@~2.0.9 + +4.11.2 / 2015-02-01 +=================== + + * Fix `res.redirect` double-calling `res.end` for `HEAD` requests + * deps: accepts@~1.2.3 + - deps: mime-types@~2.0.8 + * deps: proxy-addr@~1.0.6 + - deps: ipaddr.js@0.1.8 + * deps: type-is@~1.5.6 + - deps: mime-types@~2.0.8 + +4.11.1 / 2015-01-20 +=================== + + * deps: send@0.11.1 + - Fix root path disclosure + * deps: serve-static@~1.8.1 + - Fix redirect loop in Node.js 0.11.14 + - Fix root path disclosure + - deps: send@0.11.1 + +4.11.0 / 2015-01-13 +=================== + + * Add `res.append(field, val)` to append headers + * Deprecate leading `:` in `name` for `app.param(name, fn)` + * Deprecate `req.param()` -- use `req.params`, `req.body`, or `req.query` instead + * Deprecate `app.param(fn)` + * Fix `OPTIONS` responses to include the `HEAD` method properly + * Fix `res.sendFile` not always detecting aborted connection + * Match routes iteratively to prevent stack overflows + * deps: accepts@~1.2.2 + - deps: mime-types@~2.0.7 + - deps: negotiator@0.5.0 + * deps: send@0.11.0 + - deps: debug@~2.1.1 + - deps: etag@~1.5.1 + - deps: ms@0.7.0 + - deps: on-finished@~2.2.0 + * deps: serve-static@~1.8.0 + - deps: send@0.11.0 + +4.10.8 / 2015-01-13 +=================== + + * Fix crash from error within `OPTIONS` response handler + * deps: proxy-addr@~1.0.5 + - deps: ipaddr.js@0.1.6 + +4.10.7 / 2015-01-04 +=================== + + * Fix `Allow` header for `OPTIONS` to not contain duplicate methods + * Fix incorrect "Request aborted" for `res.sendFile` when `HEAD` or 304 + * deps: debug@~2.1.1 + * deps: finalhandler@0.3.3 + - deps: debug@~2.1.1 + - deps: on-finished@~2.2.0 + * deps: methods@~1.1.1 + * deps: on-finished@~2.2.0 + * deps: serve-static@~1.7.2 + - Fix potential open redirect when mounted at root + * deps: type-is@~1.5.5 + - deps: mime-types@~2.0.7 + +4.10.6 / 2014-12-12 +=================== + + * Fix exception in `req.fresh`/`req.stale` without response headers + +4.10.5 / 2014-12-10 +=================== + + * Fix `res.send` double-calling `res.end` for `HEAD` requests + * deps: accepts@~1.1.4 + - deps: mime-types@~2.0.4 + * deps: type-is@~1.5.4 + - deps: mime-types@~2.0.4 + +4.10.4 / 2014-11-24 +=================== + + * Fix `res.sendfile` logging standard write errors + +4.10.3 / 2014-11-23 +=================== + + * Fix `res.sendFile` logging standard write errors + * deps: etag@~1.5.1 + * deps: proxy-addr@~1.0.4 + - deps: ipaddr.js@0.1.5 + * deps: qs@2.3.3 + - Fix `arrayLimit` behavior + +4.10.2 / 2014-11-09 +=================== + + * Correctly invoke async router callback asynchronously + * deps: accepts@~1.1.3 + - deps: mime-types@~2.0.3 + * deps: type-is@~1.5.3 + - deps: mime-types@~2.0.3 + +4.10.1 / 2014-10-28 +=================== + + * Fix handling of URLs containing `://` in the path + * deps: qs@2.3.2 + - Fix parsing of mixed objects and values + +4.10.0 / 2014-10-23 +=================== + + * Add support for `app.set('views', array)` + - Views are looked up in sequence in array of directories + * Fix `res.send(status)` to mention `res.sendStatus(status)` + * Fix handling of invalid empty URLs + * Use `content-disposition` module for `res.attachment`/`res.download` + - Sends standards-compliant `Content-Disposition` header + - Full Unicode support + * Use `path.resolve` in view lookup + * deps: debug@~2.1.0 + - Implement `DEBUG_FD` env variable support + * deps: depd@~1.0.0 + * deps: etag@~1.5.0 + - Improve string performance + - Slightly improve speed for weak ETags over 1KB + * deps: finalhandler@0.3.2 + - Terminate in progress response only on error + - Use `on-finished` to determine request status + - deps: debug@~2.1.0 + - deps: on-finished@~2.1.1 + * deps: on-finished@~2.1.1 + - Fix handling of pipelined requests + * deps: qs@2.3.0 + - Fix parsing of mixed implicit and explicit arrays + * deps: send@0.10.1 + - deps: debug@~2.1.0 + - deps: depd@~1.0.0 + - deps: etag@~1.5.0 + - deps: on-finished@~2.1.1 + * deps: serve-static@~1.7.1 + - deps: send@0.10.1 + +4.9.8 / 2014-10-17 +================== + + * Fix `res.redirect` body when redirect status specified + * deps: accepts@~1.1.2 + - Fix error when media type has invalid parameter + - deps: negotiator@0.4.9 + +4.9.7 / 2014-10-10 +================== + + * Fix using same param name in array of paths + +4.9.6 / 2014-10-08 +================== + + * deps: accepts@~1.1.1 + - deps: mime-types@~2.0.2 + - deps: negotiator@0.4.8 + * deps: serve-static@~1.6.4 + - Fix redirect loop when index file serving disabled + * deps: type-is@~1.5.2 + - deps: mime-types@~2.0.2 + +4.9.5 / 2014-09-24 +================== + + * deps: etag@~1.4.0 + * deps: proxy-addr@~1.0.3 + - Use `forwarded` npm module + * deps: send@0.9.3 + - deps: etag@~1.4.0 + * deps: serve-static@~1.6.3 + - deps: send@0.9.3 + +4.9.4 / 2014-09-19 +================== + + * deps: qs@2.2.4 + - Fix issue with object keys starting with numbers truncated + +4.9.3 / 2014-09-18 +================== + + * deps: proxy-addr@~1.0.2 + - Fix a global leak when multiple subnets are trusted + - deps: ipaddr.js@0.1.3 + +4.9.2 / 2014-09-17 +================== + + * Fix regression for empty string `path` in `app.use` + * Fix `router.use` to accept array of middleware without path + * Improve error message for bad `app.use` arguments + +4.9.1 / 2014-09-16 +================== + + * Fix `app.use` to accept array of middleware without path + * deps: depd@0.4.5 + * deps: etag@~1.3.1 + * deps: send@0.9.2 + - deps: depd@0.4.5 + - deps: etag@~1.3.1 + - deps: range-parser@~1.0.2 + * deps: serve-static@~1.6.2 + - deps: send@0.9.2 + +4.9.0 / 2014-09-08 +================== + + * Add `res.sendStatus` + * Invoke callback for sendfile when client aborts + - Applies to `res.sendFile`, `res.sendfile`, and `res.download` + - `err` will be populated with request aborted error + * Support IP address host in `req.subdomains` + * Use `etag` to generate `ETag` headers + * deps: accepts@~1.1.0 + - update `mime-types` + * deps: cookie-signature@1.0.5 + * deps: debug@~2.0.0 + * deps: finalhandler@0.2.0 + - Set `X-Content-Type-Options: nosniff` header + - deps: debug@~2.0.0 + * deps: fresh@0.2.4 + * deps: media-typer@0.3.0 + - Throw error when parameter format invalid on parse + * deps: qs@2.2.3 + - Fix issue where first empty value in array is discarded + * deps: range-parser@~1.0.2 + * deps: send@0.9.1 + - Add `lastModified` option + - Use `etag` to generate `ETag` header + - deps: debug@~2.0.0 + - deps: fresh@0.2.4 + * deps: serve-static@~1.6.1 + - Add `lastModified` option + - deps: send@0.9.1 + * deps: type-is@~1.5.1 + - fix `hasbody` to be true for `content-length: 0` + - deps: media-typer@0.3.0 + - deps: mime-types@~2.0.1 + * deps: vary@~1.0.0 + - Accept valid `Vary` header string as `field` + +4.8.8 / 2014-09-04 +================== + + * deps: send@0.8.5 + - Fix a path traversal issue when using `root` + - Fix malicious path detection for empty string path + * deps: serve-static@~1.5.4 + - deps: send@0.8.5 + +4.8.7 / 2014-08-29 +================== + + * deps: qs@2.2.2 + - Remove unnecessary cloning + +4.8.6 / 2014-08-27 +================== + + * deps: qs@2.2.0 + - Array parsing fix + - Performance improvements + +4.8.5 / 2014-08-18 +================== + + * deps: send@0.8.3 + - deps: destroy@1.0.3 + - deps: on-finished@2.1.0 + * deps: serve-static@~1.5.3 + - deps: send@0.8.3 + +4.8.4 / 2014-08-14 +================== + + * deps: qs@1.2.2 + * deps: send@0.8.2 + - Work around `fd` leak in Node.js 0.10 for `fs.ReadStream` + * deps: serve-static@~1.5.2 + - deps: send@0.8.2 + +4.8.3 / 2014-08-10 +================== + + * deps: parseurl@~1.3.0 + * deps: qs@1.2.1 + * deps: serve-static@~1.5.1 + - Fix parsing of weird `req.originalUrl` values + - deps: parseurl@~1.3.0 + - deps: utils-merge@1.0.0 + +4.8.2 / 2014-08-07 +================== + + * deps: qs@1.2.0 + - Fix parsing array of objects + +4.8.1 / 2014-08-06 +================== + + * fix incorrect deprecation warnings on `res.download` + * deps: qs@1.1.0 + - Accept urlencoded square brackets + - Accept empty values in implicit array notation + +4.8.0 / 2014-08-05 +================== + + * add `res.sendFile` + - accepts a file system path instead of a URL + - requires an absolute path or `root` option specified + * deprecate `res.sendfile` -- use `res.sendFile` instead + * support mounted app as any argument to `app.use()` + * deps: qs@1.0.2 + - Complete rewrite + - Limits array length to 20 + - Limits object depth to 5 + - Limits parameters to 1,000 + * deps: send@0.8.1 + - Add `extensions` option + * deps: serve-static@~1.5.0 + - Add `extensions` option + - deps: send@0.8.1 + +4.7.4 / 2014-08-04 +================== + + * fix `res.sendfile` regression for serving directory index files + * deps: send@0.7.4 + - Fix incorrect 403 on Windows and Node.js 0.11 + - Fix serving index files without root dir + * deps: serve-static@~1.4.4 + - deps: send@0.7.4 + +4.7.3 / 2014-08-04 +================== + + * deps: send@0.7.3 + - Fix incorrect 403 on Windows and Node.js 0.11 + * deps: serve-static@~1.4.3 + - Fix incorrect 403 on Windows and Node.js 0.11 + - deps: send@0.7.3 + +4.7.2 / 2014-07-27 +================== + + * deps: depd@0.4.4 + - Work-around v8 generating empty stack traces + * deps: send@0.7.2 + - deps: depd@0.4.4 + * deps: serve-static@~1.4.2 + +4.7.1 / 2014-07-26 +================== + + * deps: depd@0.4.3 + - Fix exception when global `Error.stackTraceLimit` is too low + * deps: send@0.7.1 + - deps: depd@0.4.3 + * deps: serve-static@~1.4.1 + +4.7.0 / 2014-07-25 +================== + + * fix `req.protocol` for proxy-direct connections + * configurable query parser with `app.set('query parser', parser)` + - `app.set('query parser', 'extended')` parse with "qs" module + - `app.set('query parser', 'simple')` parse with "querystring" core module + - `app.set('query parser', false)` disable query string parsing + - `app.set('query parser', true)` enable simple parsing + * deprecate `res.json(status, obj)` -- use `res.status(status).json(obj)` instead + * deprecate `res.jsonp(status, obj)` -- use `res.status(status).jsonp(obj)` instead + * deprecate `res.send(status, body)` -- use `res.status(status).send(body)` instead + * deps: debug@1.0.4 + * deps: depd@0.4.2 + - Add `TRACE_DEPRECATION` environment variable + - Remove non-standard grey color from color output + - Support `--no-deprecation` argument + - Support `--trace-deprecation` argument + * deps: finalhandler@0.1.0 + - Respond after request fully read + - deps: debug@1.0.4 + * deps: parseurl@~1.2.0 + - Cache URLs based on original value + - Remove no-longer-needed URL mis-parse work-around + - Simplify the "fast-path" `RegExp` + * deps: send@0.7.0 + - Add `dotfiles` option + - Cap `maxAge` value to 1 year + - deps: debug@1.0.4 + - deps: depd@0.4.2 + * deps: serve-static@~1.4.0 + - deps: parseurl@~1.2.0 + - deps: send@0.7.0 + * perf: prevent multiple `Buffer` creation in `res.send` + +4.6.1 / 2014-07-12 +================== + + * fix `subapp.mountpath` regression for `app.use(subapp)` + +4.6.0 / 2014-07-11 +================== + + * accept multiple callbacks to `app.use()` + * add explicit "Rosetta Flash JSONP abuse" protection + - previous versions are not vulnerable; this is just explicit protection + * catch errors in multiple `req.param(name, fn)` handlers + * deprecate `res.redirect(url, status)` -- use `res.redirect(status, url)` instead + * fix `res.send(status, num)` to send `num` as json (not error) + * remove unnecessary escaping when `res.jsonp` returns JSON response + * support non-string `path` in `app.use(path, fn)` + - supports array of paths + - supports `RegExp` + * router: fix optimization on router exit + * router: refactor location of `try` blocks + * router: speed up standard `app.use(fn)` + * deps: debug@1.0.3 + - Add support for multiple wildcards in namespaces + * deps: finalhandler@0.0.3 + - deps: debug@1.0.3 + * deps: methods@1.1.0 + - add `CONNECT` + * deps: parseurl@~1.1.3 + - faster parsing of href-only URLs + * deps: path-to-regexp@0.1.3 + * deps: send@0.6.0 + - deps: debug@1.0.3 + * deps: serve-static@~1.3.2 + - deps: parseurl@~1.1.3 + - deps: send@0.6.0 + * perf: fix arguments reassign deopt in some `res` methods + +4.5.1 / 2014-07-06 +================== + + * fix routing regression when altering `req.method` + +4.5.0 / 2014-07-04 +================== + + * add deprecation message to non-plural `req.accepts*` + * add deprecation message to `res.send(body, status)` + * add deprecation message to `res.vary()` + * add `headers` option to `res.sendfile` + - use to set headers on successful file transfer + * add `mergeParams` option to `Router` + - merges `req.params` from parent routes + * add `req.hostname` -- correct name for what `req.host` returns + * deprecate things with `depd` module + * deprecate `req.host` -- use `req.hostname` instead + * fix behavior when handling request without routes + * fix handling when `route.all` is only route + * invoke `router.param()` only when route matches + * restore `req.params` after invoking router + * use `finalhandler` for final response handling + * use `media-typer` to alter content-type charset + * deps: accepts@~1.0.7 + * deps: send@0.5.0 + - Accept string for `maxage` (converted by `ms`) + - Include link in default redirect response + * deps: serve-static@~1.3.0 + - Accept string for `maxAge` (converted by `ms`) + - Add `setHeaders` option + - Include HTML link in redirect response + - deps: send@0.5.0 + * deps: type-is@~1.3.2 + +4.4.5 / 2014-06-26 +================== + + * deps: cookie-signature@1.0.4 + - fix for timing attacks + +4.4.4 / 2014-06-20 +================== + + * fix `res.attachment` Unicode filenames in Safari + * fix "trim prefix" debug message in `express:router` + * deps: accepts@~1.0.5 + * deps: buffer-crc32@0.2.3 + +4.4.3 / 2014-06-11 +================== + + * fix persistence of modified `req.params[name]` from `app.param()` + * deps: accepts@1.0.3 + - deps: negotiator@0.4.6 + * deps: debug@1.0.2 + * deps: send@0.4.3 + - Do not throw uncatchable error on file open race condition + - Use `escape-html` for HTML escaping + - deps: debug@1.0.2 + - deps: finished@1.2.2 + - deps: fresh@0.2.2 + * deps: serve-static@1.2.3 + - Do not throw uncatchable error on file open race condition + - deps: send@0.4.3 + +4.4.2 / 2014-06-09 +================== + + * fix catching errors from top-level handlers + * use `vary` module for `res.vary` + * deps: debug@1.0.1 + * deps: proxy-addr@1.0.1 + * deps: send@0.4.2 + - fix "event emitter leak" warnings + - deps: debug@1.0.1 + - deps: finished@1.2.1 + * deps: serve-static@1.2.2 + - fix "event emitter leak" warnings + - deps: send@0.4.2 + * deps: type-is@1.2.1 + +4.4.1 / 2014-06-02 +================== + + * deps: methods@1.0.1 + * deps: send@0.4.1 + - Send `max-age` in `Cache-Control` in correct format + * deps: serve-static@1.2.1 + - use `escape-html` for escaping + - deps: send@0.4.1 + +4.4.0 / 2014-05-30 +================== + + * custom etag control with `app.set('etag', val)` + - `app.set('etag', function(body, encoding){ return '"etag"' })` custom etag generation + - `app.set('etag', 'weak')` weak tag + - `app.set('etag', 'strong')` strong etag + - `app.set('etag', false)` turn off + - `app.set('etag', true)` standard etag + * mark `res.send` ETag as weak and reduce collisions + * update accepts to 1.0.2 + - Fix interpretation when header not in request + * update send to 0.4.0 + - Calculate ETag with md5 for reduced collisions + - Ignore stream errors after request ends + - deps: debug@0.8.1 + * update serve-static to 1.2.0 + - Calculate ETag with md5 for reduced collisions + - Ignore stream errors after request ends + - deps: send@0.4.0 + +4.3.2 / 2014-05-28 +================== + + * fix handling of errors from `router.param()` callbacks + +4.3.1 / 2014-05-23 +================== + + * revert "fix behavior of multiple `app.VERB` for the same path" + - this caused a regression in the order of route execution + +4.3.0 / 2014-05-21 +================== + + * add `req.baseUrl` to access the path stripped from `req.url` in routes + * fix behavior of multiple `app.VERB` for the same path + * fix issue routing requests among sub routers + * invoke `router.param()` only when necessary instead of every match + * proper proxy trust with `app.set('trust proxy', trust)` + - `app.set('trust proxy', 1)` trust first hop + - `app.set('trust proxy', 'loopback')` trust loopback addresses + - `app.set('trust proxy', '10.0.0.1')` trust single IP + - `app.set('trust proxy', '10.0.0.1/16')` trust subnet + - `app.set('trust proxy', '10.0.0.1, 10.0.0.2')` trust list + - `app.set('trust proxy', false)` turn off + - `app.set('trust proxy', true)` trust everything + * set proper `charset` in `Content-Type` for `res.send` + * update type-is to 1.2.0 + - support suffix matching + +4.2.0 / 2014-05-11 +================== + + * deprecate `app.del()` -- use `app.delete()` instead + * deprecate `res.json(obj, status)` -- use `res.json(status, obj)` instead + - the edge-case `res.json(status, num)` requires `res.status(status).json(num)` + * deprecate `res.jsonp(obj, status)` -- use `res.jsonp(status, obj)` instead + - the edge-case `res.jsonp(status, num)` requires `res.status(status).jsonp(num)` + * fix `req.next` when inside router instance + * include `ETag` header in `HEAD` requests + * keep previous `Content-Type` for `res.jsonp` + * support PURGE method + - add `app.purge` + - add `router.purge` + - include PURGE in `app.all` + * update debug to 0.8.0 + - add `enable()` method + - change from stderr to stdout + * update methods to 1.0.0 + - add PURGE + +4.1.2 / 2014-05-08 +================== + + * fix `req.host` for IPv6 literals + * fix `res.jsonp` error if callback param is object + +4.1.1 / 2014-04-27 +================== + + * fix package.json to reflect supported node version + +4.1.0 / 2014-04-24 +================== + + * pass options from `res.sendfile` to `send` + * preserve casing of headers in `res.header` and `res.set` + * support unicode file names in `res.attachment` and `res.download` + * update accepts to 1.0.1 + - deps: negotiator@0.4.0 + * update cookie to 0.1.2 + - Fix for maxAge == 0 + - made compat with expires field + * update send to 0.3.0 + - Accept API options in options object + - Coerce option types + - Control whether to generate etags + - Default directory access to 403 when index disabled + - Fix sending files with dots without root set + - Include file path in etag + - Make "Can't set headers after they are sent." catchable + - Send full entity-body for multi range requests + - Set etags to "weak" + - Support "If-Range" header + - Support multiple index paths + - deps: mime@1.2.11 + * update serve-static to 1.1.0 + - Accept options directly to `send` module + - Resolve relative paths at middleware setup + - Use parseurl to parse the URL from request + - deps: send@0.3.0 + * update type-is to 1.1.0 + - add non-array values support + - add `multipart` as a shorthand + +4.0.0 / 2014-04-09 +================== + + * remove: + - node 0.8 support + - connect and connect's patches except for charset handling + - express(1) - moved to [express-generator](https://github.com/expressjs/generator) + - `express.createServer()` - it has been deprecated for a long time. Use `express()` + - `app.configure` - use logic in your own app code + - `app.router` - is removed + - `req.auth` - use `basic-auth` instead + - `req.accepted*` - use `req.accepts*()` instead + - `res.location` - relative URL resolution is removed + - `res.charset` - include the charset in the content type when using `res.set()` + - all bundled middleware except `static` + * change: + - `app.route` -> `app.mountpath` when mounting an express app in another express app + - `json spaces` no longer enabled by default in development + - `req.accepts*` -> `req.accepts*s` - i.e. `req.acceptsEncoding` -> `req.acceptsEncodings` + - `req.params` is now an object instead of an array + - `res.locals` is no longer a function. It is a plain js object. Treat it as such. + - `res.headerSent` -> `res.headersSent` to match node.js ServerResponse object + * refactor: + - `req.accepts*` with [accepts](https://github.com/expressjs/accepts) + - `req.is` with [type-is](https://github.com/expressjs/type-is) + - [path-to-regexp](https://github.com/component/path-to-regexp) + * add: + - `app.router()` - returns the app Router instance + - `app.route()` - Proxy to the app's `Router#route()` method to create a new route + - Router & Route - public API + +3.21.2 / 2015-07-31 +=================== + + * deps: connect@2.30.2 + - deps: body-parser@~1.13.3 + - deps: compression@~1.5.2 + - deps: errorhandler@~1.4.2 + - deps: method-override@~2.3.5 + - deps: serve-index@~1.7.2 + - deps: type-is@~1.6.6 + - deps: vhost@~3.0.1 + * deps: vary@~1.0.1 + - Fix setting empty header from empty `field` + - perf: enable strict mode + - perf: remove argument reassignments + +3.21.1 / 2015-07-05 +=================== + + * deps: basic-auth@~1.0.3 + * deps: connect@2.30.1 + - deps: body-parser@~1.13.2 + - deps: compression@~1.5.1 + - deps: errorhandler@~1.4.1 + - deps: morgan@~1.6.1 + - deps: pause@0.1.0 + - deps: qs@4.0.0 + - deps: serve-index@~1.7.1 + - deps: type-is@~1.6.4 + +3.21.0 / 2015-06-18 +=================== + + * deps: basic-auth@1.0.2 + - perf: enable strict mode + - perf: hoist regular expression + - perf: parse with regular expressions + - perf: remove argument reassignment + * deps: connect@2.30.0 + - deps: body-parser@~1.13.1 + - deps: bytes@2.1.0 + - deps: compression@~1.5.0 + - deps: cookie@0.1.3 + - deps: cookie-parser@~1.3.5 + - deps: csurf@~1.8.3 + - deps: errorhandler@~1.4.0 + - deps: express-session@~1.11.3 + - deps: finalhandler@0.4.0 + - deps: fresh@0.3.0 + - deps: morgan@~1.6.0 + - deps: serve-favicon@~2.3.0 + - deps: serve-index@~1.7.0 + - deps: serve-static@~1.10.0 + - deps: type-is@~1.6.3 + * deps: cookie@0.1.3 + - perf: deduce the scope of try-catch deopt + - perf: remove argument reassignments + * deps: escape-html@1.0.2 + * deps: etag@~1.7.0 + - Always include entity length in ETags for hash length extensions + - Generate non-Stats ETags using MD5 only (no longer CRC32) + - Improve stat performance by removing hashing + - Improve support for JXcore + - Remove base64 padding in ETags to shorten + - Support "fake" stats objects in environments without fs + - Use MD5 instead of MD4 in weak ETags over 1KB + * deps: fresh@0.3.0 + - Add weak `ETag` matching support + * deps: mkdirp@0.5.1 + - Work in global strict mode + * deps: send@0.13.0 + - Allow Node.js HTTP server to set `Date` response header + - Fix incorrectly removing `Content-Location` on 304 response + - Improve the default redirect response headers + - Send appropriate headers on default error response + - Use `http-errors` for standard emitted errors + - Use `statuses` instead of `http` module for status messages + - deps: escape-html@1.0.2 + - deps: etag@~1.7.0 + - deps: fresh@0.3.0 + - deps: on-finished@~2.3.0 + - perf: enable strict mode + - perf: remove unnecessary array allocations + +3.20.3 / 2015-05-17 +=================== + + * deps: connect@2.29.2 + - deps: body-parser@~1.12.4 + - deps: compression@~1.4.4 + - deps: connect-timeout@~1.6.2 + - deps: debug@~2.2.0 + - deps: depd@~1.0.1 + - deps: errorhandler@~1.3.6 + - deps: finalhandler@0.3.6 + - deps: method-override@~2.3.3 + - deps: morgan@~1.5.3 + - deps: qs@2.4.2 + - deps: response-time@~2.3.1 + - deps: serve-favicon@~2.2.1 + - deps: serve-index@~1.6.4 + - deps: serve-static@~1.9.3 + - deps: type-is@~1.6.2 + * deps: debug@~2.2.0 + - deps: ms@0.7.1 + * deps: depd@~1.0.1 + * deps: proxy-addr@~1.0.8 + - deps: ipaddr.js@1.0.1 + * deps: send@0.12.3 + - deps: debug@~2.2.0 + - deps: depd@~1.0.1 + - deps: etag@~1.6.0 + - deps: ms@0.7.1 + - deps: on-finished@~2.2.1 + +3.20.2 / 2015-03-16 +=================== + + * deps: connect@2.29.1 + - deps: body-parser@~1.12.2 + - deps: compression@~1.4.3 + - deps: connect-timeout@~1.6.1 + - deps: debug@~2.1.3 + - deps: errorhandler@~1.3.5 + - deps: express-session@~1.10.4 + - deps: finalhandler@0.3.4 + - deps: method-override@~2.3.2 + - deps: morgan@~1.5.2 + - deps: qs@2.4.1 + - deps: serve-index@~1.6.3 + - deps: serve-static@~1.9.2 + - deps: type-is@~1.6.1 + * deps: debug@~2.1.3 + - Fix high intensity foreground color for bold + - deps: ms@0.7.0 + * deps: merge-descriptors@1.0.0 + * deps: proxy-addr@~1.0.7 + - deps: ipaddr.js@0.1.9 + * deps: send@0.12.2 + - Throw errors early for invalid `extensions` or `index` options + - deps: debug@~2.1.3 + +3.20.1 / 2015-02-28 +=================== + + * Fix `req.host` when using "trust proxy" hops count + * Fix `req.protocol`/`req.secure` when using "trust proxy" hops count + +3.20.0 / 2015-02-18 +=================== + + * Fix `"trust proxy"` setting to inherit when app is mounted + * Generate `ETag`s for all request responses + - No longer restricted to only responses for `GET` and `HEAD` requests + * Use `content-type` to parse `Content-Type` headers + * deps: connect@2.29.0 + - Use `content-type` to parse `Content-Type` headers + - deps: body-parser@~1.12.0 + - deps: compression@~1.4.1 + - deps: connect-timeout@~1.6.0 + - deps: cookie-parser@~1.3.4 + - deps: cookie-signature@1.0.6 + - deps: csurf@~1.7.0 + - deps: errorhandler@~1.3.4 + - deps: express-session@~1.10.3 + - deps: http-errors@~1.3.1 + - deps: response-time@~2.3.0 + - deps: serve-index@~1.6.2 + - deps: serve-static@~1.9.1 + - deps: type-is@~1.6.0 + * deps: cookie-signature@1.0.6 + * deps: send@0.12.1 + - Always read the stat size from the file + - Fix mutating passed-in `options` + - deps: mime@1.3.4 + +3.19.2 / 2015-02-01 +=================== + + * deps: connect@2.28.3 + - deps: compression@~1.3.1 + - deps: csurf@~1.6.6 + - deps: errorhandler@~1.3.3 + - deps: express-session@~1.10.2 + - deps: serve-index@~1.6.1 + - deps: type-is@~1.5.6 + * deps: proxy-addr@~1.0.6 + - deps: ipaddr.js@0.1.8 + +3.19.1 / 2015-01-20 +=================== + + * deps: connect@2.28.2 + - deps: body-parser@~1.10.2 + - deps: serve-static@~1.8.1 + * deps: send@0.11.1 + - Fix root path disclosure + +3.19.0 / 2015-01-09 +=================== + + * Fix `OPTIONS` responses to include the `HEAD` method property + * Use `readline` for prompt in `express(1)` + * deps: commander@2.6.0 + * deps: connect@2.28.1 + - deps: body-parser@~1.10.1 + - deps: compression@~1.3.0 + - deps: connect-timeout@~1.5.0 + - deps: csurf@~1.6.4 + - deps: debug@~2.1.1 + - deps: errorhandler@~1.3.2 + - deps: express-session@~1.10.1 + - deps: finalhandler@0.3.3 + - deps: method-override@~2.3.1 + - deps: morgan@~1.5.1 + - deps: serve-favicon@~2.2.0 + - deps: serve-index@~1.6.0 + - deps: serve-static@~1.8.0 + - deps: type-is@~1.5.5 + * deps: debug@~2.1.1 + * deps: methods@~1.1.1 + * deps: proxy-addr@~1.0.5 + - deps: ipaddr.js@0.1.6 + * deps: send@0.11.0 + - deps: debug@~2.1.1 + - deps: etag@~1.5.1 + - deps: ms@0.7.0 + - deps: on-finished@~2.2.0 + +3.18.6 / 2014-12-12 +=================== + + * Fix exception in `req.fresh`/`req.stale` without response headers + +3.18.5 / 2014-12-11 +=================== + + * deps: connect@2.27.6 + - deps: compression@~1.2.2 + - deps: express-session@~1.9.3 + - deps: http-errors@~1.2.8 + - deps: serve-index@~1.5.3 + - deps: type-is@~1.5.4 + +3.18.4 / 2014-11-23 +=================== + + * deps: connect@2.27.4 + - deps: body-parser@~1.9.3 + - deps: compression@~1.2.1 + - deps: errorhandler@~1.2.3 + - deps: express-session@~1.9.2 + - deps: qs@2.3.3 + - deps: serve-favicon@~2.1.7 + - deps: serve-static@~1.5.1 + - deps: type-is@~1.5.3 + * deps: etag@~1.5.1 + * deps: proxy-addr@~1.0.4 + - deps: ipaddr.js@0.1.5 + +3.18.3 / 2014-11-09 +=================== + + * deps: connect@2.27.3 + - Correctly invoke async callback asynchronously + - deps: csurf@~1.6.3 + +3.18.2 / 2014-10-28 +=================== + + * deps: connect@2.27.2 + - Fix handling of URLs containing `://` in the path + - deps: body-parser@~1.9.2 + - deps: qs@2.3.2 + +3.18.1 / 2014-10-22 +=================== + + * Fix internal `utils.merge` deprecation warnings + * deps: connect@2.27.1 + - deps: body-parser@~1.9.1 + - deps: express-session@~1.9.1 + - deps: finalhandler@0.3.2 + - deps: morgan@~1.4.1 + - deps: qs@2.3.0 + - deps: serve-static@~1.7.1 + * deps: send@0.10.1 + - deps: on-finished@~2.1.1 + +3.18.0 / 2014-10-17 +=================== + + * Use `content-disposition` module for `res.attachment`/`res.download` + - Sends standards-compliant `Content-Disposition` header + - Full Unicode support + * Use `etag` module to generate `ETag` headers + * deps: connect@2.27.0 + - Use `http-errors` module for creating errors + - Use `utils-merge` module for merging objects + - deps: body-parser@~1.9.0 + - deps: compression@~1.2.0 + - deps: connect-timeout@~1.4.0 + - deps: debug@~2.1.0 + - deps: depd@~1.0.0 + - deps: express-session@~1.9.0 + - deps: finalhandler@0.3.1 + - deps: method-override@~2.3.0 + - deps: morgan@~1.4.0 + - deps: response-time@~2.2.0 + - deps: serve-favicon@~2.1.6 + - deps: serve-index@~1.5.0 + - deps: serve-static@~1.7.0 + * deps: debug@~2.1.0 + - Implement `DEBUG_FD` env variable support + * deps: depd@~1.0.0 + * deps: send@0.10.0 + - deps: debug@~2.1.0 + - deps: depd@~1.0.0 + - deps: etag@~1.5.0 + +3.17.8 / 2014-10-15 +=================== + + * deps: connect@2.26.6 + - deps: compression@~1.1.2 + - deps: csurf@~1.6.2 + - deps: errorhandler@~1.2.2 + +3.17.7 / 2014-10-08 +=================== + + * deps: connect@2.26.5 + - Fix accepting non-object arguments to `logger` + - deps: serve-static@~1.6.4 + +3.17.6 / 2014-10-02 +=================== + + * deps: connect@2.26.4 + - deps: morgan@~1.3.2 + - deps: type-is@~1.5.2 + +3.17.5 / 2014-09-24 +=================== + + * deps: connect@2.26.3 + - deps: body-parser@~1.8.4 + - deps: serve-favicon@~2.1.5 + - deps: serve-static@~1.6.3 + * deps: proxy-addr@~1.0.3 + - Use `forwarded` npm module + * deps: send@0.9.3 + - deps: etag@~1.4.0 + +3.17.4 / 2014-09-19 +=================== + + * deps: connect@2.26.2 + - deps: body-parser@~1.8.3 + - deps: qs@2.2.4 + +3.17.3 / 2014-09-18 +=================== + + * deps: proxy-addr@~1.0.2 + - Fix a global leak when multiple subnets are trusted + - deps: ipaddr.js@0.1.3 + +3.17.2 / 2014-09-15 +=================== + + * Use `crc` instead of `buffer-crc32` for speed + * deps: connect@2.26.1 + - deps: body-parser@~1.8.2 + - deps: depd@0.4.5 + - deps: express-session@~1.8.2 + - deps: morgan@~1.3.1 + - deps: serve-favicon@~2.1.3 + - deps: serve-static@~1.6.2 + * deps: depd@0.4.5 + * deps: send@0.9.2 + - deps: depd@0.4.5 + - deps: etag@~1.3.1 + - deps: range-parser@~1.0.2 + +3.17.1 / 2014-09-08 +=================== + + * Fix error in `req.subdomains` on empty host + +3.17.0 / 2014-09-08 +=================== + + * Support `X-Forwarded-Host` in `req.subdomains` + * Support IP address host in `req.subdomains` + * deps: connect@2.26.0 + - deps: body-parser@~1.8.1 + - deps: compression@~1.1.0 + - deps: connect-timeout@~1.3.0 + - deps: cookie-parser@~1.3.3 + - deps: cookie-signature@1.0.5 + - deps: csurf@~1.6.1 + - deps: debug@~2.0.0 + - deps: errorhandler@~1.2.0 + - deps: express-session@~1.8.1 + - deps: finalhandler@0.2.0 + - deps: fresh@0.2.4 + - deps: media-typer@0.3.0 + - deps: method-override@~2.2.0 + - deps: morgan@~1.3.0 + - deps: qs@2.2.3 + - deps: serve-favicon@~2.1.3 + - deps: serve-index@~1.2.1 + - deps: serve-static@~1.6.1 + - deps: type-is@~1.5.1 + - deps: vhost@~3.0.0 + * deps: cookie-signature@1.0.5 + * deps: debug@~2.0.0 + * deps: fresh@0.2.4 + * deps: media-typer@0.3.0 + - Throw error when parameter format invalid on parse + * deps: range-parser@~1.0.2 + * deps: send@0.9.1 + - Add `lastModified` option + - Use `etag` to generate `ETag` header + - deps: debug@~2.0.0 + - deps: fresh@0.2.4 + * deps: vary@~1.0.0 + - Accept valid `Vary` header string as `field` + +3.16.10 / 2014-09-04 +==================== + + * deps: connect@2.25.10 + - deps: serve-static@~1.5.4 + * deps: send@0.8.5 + - Fix a path traversal issue when using `root` + - Fix malicious path detection for empty string path + +3.16.9 / 2014-08-29 +=================== + + * deps: connect@2.25.9 + - deps: body-parser@~1.6.7 + - deps: qs@2.2.2 + +3.16.8 / 2014-08-27 +=================== + + * deps: connect@2.25.8 + - deps: body-parser@~1.6.6 + - deps: csurf@~1.4.1 + - deps: qs@2.2.0 + +3.16.7 / 2014-08-18 +=================== + + * deps: connect@2.25.7 + - deps: body-parser@~1.6.5 + - deps: express-session@~1.7.6 + - deps: morgan@~1.2.3 + - deps: serve-static@~1.5.3 + * deps: send@0.8.3 + - deps: destroy@1.0.3 + - deps: on-finished@2.1.0 + +3.16.6 / 2014-08-14 +=================== + + * deps: connect@2.25.6 + - deps: body-parser@~1.6.4 + - deps: qs@1.2.2 + - deps: serve-static@~1.5.2 + * deps: send@0.8.2 + - Work around `fd` leak in Node.js 0.10 for `fs.ReadStream` + +3.16.5 / 2014-08-11 +=================== + + * deps: connect@2.25.5 + - Fix backwards compatibility in `logger` + +3.16.4 / 2014-08-10 +=================== + + * Fix original URL parsing in `res.location` + * deps: connect@2.25.4 + - Fix `query` middleware breaking with argument + - deps: body-parser@~1.6.3 + - deps: compression@~1.0.11 + - deps: connect-timeout@~1.2.2 + - deps: express-session@~1.7.5 + - deps: method-override@~2.1.3 + - deps: on-headers@~1.0.0 + - deps: parseurl@~1.3.0 + - deps: qs@1.2.1 + - deps: response-time@~2.0.1 + - deps: serve-index@~1.1.6 + - deps: serve-static@~1.5.1 + * deps: parseurl@~1.3.0 + +3.16.3 / 2014-08-07 +=================== + + * deps: connect@2.25.3 + - deps: multiparty@3.3.2 + +3.16.2 / 2014-08-07 +=================== + + * deps: connect@2.25.2 + - deps: body-parser@~1.6.2 + - deps: qs@1.2.0 + +3.16.1 / 2014-08-06 +=================== + + * deps: connect@2.25.1 + - deps: body-parser@~1.6.1 + - deps: qs@1.1.0 + +3.16.0 / 2014-08-05 +=================== + + * deps: connect@2.25.0 + - deps: body-parser@~1.6.0 + - deps: compression@~1.0.10 + - deps: csurf@~1.4.0 + - deps: express-session@~1.7.4 + - deps: qs@1.0.2 + - deps: serve-static@~1.5.0 + * deps: send@0.8.1 + - Add `extensions` option + +3.15.3 / 2014-08-04 +=================== + + * fix `res.sendfile` regression for serving directory index files + * deps: connect@2.24.3 + - deps: serve-index@~1.1.5 + - deps: serve-static@~1.4.4 + * deps: send@0.7.4 + - Fix incorrect 403 on Windows and Node.js 0.11 + - Fix serving index files without root dir + +3.15.2 / 2014-07-27 +=================== + + * deps: connect@2.24.2 + - deps: body-parser@~1.5.2 + - deps: depd@0.4.4 + - deps: express-session@~1.7.2 + - deps: morgan@~1.2.2 + - deps: serve-static@~1.4.2 + * deps: depd@0.4.4 + - Work-around v8 generating empty stack traces + * deps: send@0.7.2 + - deps: depd@0.4.4 + +3.15.1 / 2014-07-26 +=================== + + * deps: connect@2.24.1 + - deps: body-parser@~1.5.1 + - deps: depd@0.4.3 + - deps: express-session@~1.7.1 + - deps: morgan@~1.2.1 + - deps: serve-index@~1.1.4 + - deps: serve-static@~1.4.1 + * deps: depd@0.4.3 + - Fix exception when global `Error.stackTraceLimit` is too low + * deps: send@0.7.1 + - deps: depd@0.4.3 + +3.15.0 / 2014-07-22 +=================== + + * Fix `req.protocol` for proxy-direct connections + * Pass options from `res.sendfile` to `send` + * deps: connect@2.24.0 + - deps: body-parser@~1.5.0 + - deps: compression@~1.0.9 + - deps: connect-timeout@~1.2.1 + - deps: debug@1.0.4 + - deps: depd@0.4.2 + - deps: express-session@~1.7.0 + - deps: finalhandler@0.1.0 + - deps: method-override@~2.1.2 + - deps: morgan@~1.2.0 + - deps: multiparty@3.3.1 + - deps: parseurl@~1.2.0 + - deps: serve-static@~1.4.0 + * deps: debug@1.0.4 + * deps: depd@0.4.2 + - Add `TRACE_DEPRECATION` environment variable + - Remove non-standard grey color from color output + - Support `--no-deprecation` argument + - Support `--trace-deprecation` argument + * deps: parseurl@~1.2.0 + - Cache URLs based on original value + - Remove no-longer-needed URL mis-parse work-around + - Simplify the "fast-path" `RegExp` + * deps: send@0.7.0 + - Add `dotfiles` option + - Cap `maxAge` value to 1 year + - deps: debug@1.0.4 + - deps: depd@0.4.2 + +3.14.0 / 2014-07-11 +=================== + + * add explicit "Rosetta Flash JSONP abuse" protection + - previous versions are not vulnerable; this is just explicit protection + * deprecate `res.redirect(url, status)` -- use `res.redirect(status, url)` instead + * fix `res.send(status, num)` to send `num` as json (not error) + * remove unnecessary escaping when `res.jsonp` returns JSON response + * deps: basic-auth@1.0.0 + - support empty password + - support empty username + * deps: connect@2.23.0 + - deps: debug@1.0.3 + - deps: express-session@~1.6.4 + - deps: method-override@~2.1.0 + - deps: parseurl@~1.1.3 + - deps: serve-static@~1.3.1 + * deps: debug@1.0.3 + - Add support for multiple wildcards in namespaces + * deps: methods@1.1.0 + - add `CONNECT` + * deps: parseurl@~1.1.3 + - faster parsing of href-only URLs + +3.13.0 / 2014-07-03 +=================== + + * add deprecation message to `app.configure` + * add deprecation message to `req.auth` + * use `basic-auth` to parse `Authorization` header + * deps: connect@2.22.0 + - deps: csurf@~1.3.0 + - deps: express-session@~1.6.1 + - deps: multiparty@3.3.0 + - deps: serve-static@~1.3.0 + * deps: send@0.5.0 + - Accept string for `maxage` (converted by `ms`) + - Include link in default redirect response + +3.12.1 / 2014-06-26 +=================== + + * deps: connect@2.21.1 + - deps: cookie-parser@1.3.2 + - deps: cookie-signature@1.0.4 + - deps: express-session@~1.5.2 + - deps: type-is@~1.3.2 + * deps: cookie-signature@1.0.4 + - fix for timing attacks + +3.12.0 / 2014-06-21 +=================== + + * use `media-typer` to alter content-type charset + * deps: connect@2.21.0 + - deprecate `connect(middleware)` -- use `app.use(middleware)` instead + - deprecate `connect.createServer()` -- use `connect()` instead + - fix `res.setHeader()` patch to work with get -> append -> set pattern + - deps: compression@~1.0.8 + - deps: errorhandler@~1.1.1 + - deps: express-session@~1.5.0 + - deps: serve-index@~1.1.3 + +3.11.0 / 2014-06-19 +=================== + + * deprecate things with `depd` module + * deps: buffer-crc32@0.2.3 + * deps: connect@2.20.2 + - deprecate `verify` option to `json` -- use `body-parser` npm module instead + - deprecate `verify` option to `urlencoded` -- use `body-parser` npm module instead + - deprecate things with `depd` module + - use `finalhandler` for final response handling + - use `media-typer` to parse `content-type` for charset + - deps: body-parser@1.4.3 + - deps: connect-timeout@1.1.1 + - deps: cookie-parser@1.3.1 + - deps: csurf@1.2.2 + - deps: errorhandler@1.1.0 + - deps: express-session@1.4.0 + - deps: multiparty@3.2.9 + - deps: serve-index@1.1.2 + - deps: type-is@1.3.1 + - deps: vhost@2.0.0 + +3.10.5 / 2014-06-11 +=================== + + * deps: connect@2.19.6 + - deps: body-parser@1.3.1 + - deps: compression@1.0.7 + - deps: debug@1.0.2 + - deps: serve-index@1.1.1 + - deps: serve-static@1.2.3 + * deps: debug@1.0.2 + * deps: send@0.4.3 + - Do not throw uncatchable error on file open race condition + - Use `escape-html` for HTML escaping + - deps: debug@1.0.2 + - deps: finished@1.2.2 + - deps: fresh@0.2.2 + +3.10.4 / 2014-06-09 +=================== + + * deps: connect@2.19.5 + - fix "event emitter leak" warnings + - deps: csurf@1.2.1 + - deps: debug@1.0.1 + - deps: serve-static@1.2.2 + - deps: type-is@1.2.1 + * deps: debug@1.0.1 + * deps: send@0.4.2 + - fix "event emitter leak" warnings + - deps: finished@1.2.1 + - deps: debug@1.0.1 + +3.10.3 / 2014-06-05 +=================== + + * use `vary` module for `res.vary` + * deps: connect@2.19.4 + - deps: errorhandler@1.0.2 + - deps: method-override@2.0.2 + - deps: serve-favicon@2.0.1 + * deps: debug@1.0.0 + +3.10.2 / 2014-06-03 +=================== + + * deps: connect@2.19.3 + - deps: compression@1.0.6 + +3.10.1 / 2014-06-03 +=================== + + * deps: connect@2.19.2 + - deps: compression@1.0.4 + * deps: proxy-addr@1.0.1 + +3.10.0 / 2014-06-02 +=================== + + * deps: connect@2.19.1 + - deprecate `methodOverride()` -- use `method-override` npm module instead + - deps: body-parser@1.3.0 + - deps: method-override@2.0.1 + - deps: multiparty@3.2.8 + - deps: response-time@2.0.0 + - deps: serve-static@1.2.1 + * deps: methods@1.0.1 + * deps: send@0.4.1 + - Send `max-age` in `Cache-Control` in correct format + +3.9.0 / 2014-05-30 +================== + + * custom etag control with `app.set('etag', val)` + - `app.set('etag', function(body, encoding){ return '"etag"' })` custom etag generation + - `app.set('etag', 'weak')` weak tag + - `app.set('etag', 'strong')` strong etag + - `app.set('etag', false)` turn off + - `app.set('etag', true)` standard etag + * Include ETag in HEAD requests + * mark `res.send` ETag as weak and reduce collisions + * update connect to 2.18.0 + - deps: compression@1.0.3 + - deps: serve-index@1.1.0 + - deps: serve-static@1.2.0 + * update send to 0.4.0 + - Calculate ETag with md5 for reduced collisions + - Ignore stream errors after request ends + - deps: debug@0.8.1 + +3.8.1 / 2014-05-27 +================== + + * update connect to 2.17.3 + - deps: body-parser@1.2.2 + - deps: express-session@1.2.1 + - deps: method-override@1.0.2 + +3.8.0 / 2014-05-21 +================== + + * keep previous `Content-Type` for `res.jsonp` + * set proper `charset` in `Content-Type` for `res.send` + * update connect to 2.17.1 + - fix `res.charset` appending charset when `content-type` has one + - deps: express-session@1.2.0 + - deps: morgan@1.1.1 + - deps: serve-index@1.0.3 + +3.7.0 / 2014-05-18 +================== + + * proper proxy trust with `app.set('trust proxy', trust)` + - `app.set('trust proxy', 1)` trust first hop + - `app.set('trust proxy', 'loopback')` trust loopback addresses + - `app.set('trust proxy', '10.0.0.1')` trust single IP + - `app.set('trust proxy', '10.0.0.1/16')` trust subnet + - `app.set('trust proxy', '10.0.0.1, 10.0.0.2')` trust list + - `app.set('trust proxy', false)` turn off + - `app.set('trust proxy', true)` trust everything + * update connect to 2.16.2 + - deprecate `res.headerSent` -- use `res.headersSent` + - deprecate `res.on("header")` -- use on-headers module instead + - fix edge-case in `res.appendHeader` that would append in wrong order + - json: use body-parser + - urlencoded: use body-parser + - dep: bytes@1.0.0 + - dep: cookie-parser@1.1.0 + - dep: csurf@1.2.0 + - dep: express-session@1.1.0 + - dep: method-override@1.0.1 + +3.6.0 / 2014-05-09 +================== + + * deprecate `app.del()` -- use `app.delete()` instead + * deprecate `res.json(obj, status)` -- use `res.json(status, obj)` instead + - the edge-case `res.json(status, num)` requires `res.status(status).json(num)` + * deprecate `res.jsonp(obj, status)` -- use `res.jsonp(status, obj)` instead + - the edge-case `res.jsonp(status, num)` requires `res.status(status).jsonp(num)` + * support PURGE method + - add `app.purge` + - add `router.purge` + - include PURGE in `app.all` + * update connect to 2.15.0 + * Add `res.appendHeader` + * Call error stack even when response has been sent + * Patch `res.headerSent` to return Boolean + * Patch `res.headersSent` for node.js 0.8 + * Prevent default 404 handler after response sent + * dep: compression@1.0.2 + * dep: connect-timeout@1.1.0 + * dep: debug@^0.8.0 + * dep: errorhandler@1.0.1 + * dep: express-session@1.0.4 + * dep: morgan@1.0.1 + * dep: serve-favicon@2.0.0 + * dep: serve-index@1.0.2 + * update debug to 0.8.0 + * add `enable()` method + * change from stderr to stdout + * update methods to 1.0.0 + - add PURGE + * update mkdirp to 0.5.0 + +3.5.3 / 2014-05-08 +================== + + * fix `req.host` for IPv6 literals + * fix `res.jsonp` error if callback param is object + +3.5.2 / 2014-04-24 +================== + + * update connect to 2.14.5 + * update cookie to 0.1.2 + * update mkdirp to 0.4.0 + * update send to 0.3.0 + +3.5.1 / 2014-03-25 +================== + + * pin less-middleware in generated app + +3.5.0 / 2014-03-06 +================== + + * bump deps + +3.4.8 / 2014-01-13 +================== + + * prevent incorrect automatic OPTIONS responses #1868 @dpatti + * update binary and examples for jade 1.0 #1876 @yossi, #1877 @reqshark, #1892 @matheusazzi + * throw 400 in case of malformed paths @rlidwka + +3.4.7 / 2013-12-10 +================== + + * update connect + +3.4.6 / 2013-12-01 +================== + + * update connect (raw-body) + +3.4.5 / 2013-11-27 +================== + + * update connect + * res.location: remove leading ./ #1802 @kapouer + * res.redirect: fix `res.redirect('toString') #1829 @michaelficarra + * res.send: always send ETag when content-length > 0 + * router: add Router.all() method + +3.4.4 / 2013-10-29 +================== + + * update connect + * update supertest + * update methods + * express(1): replace bodyParser() with urlencoded() and json() #1795 @chirag04 + +3.4.3 / 2013-10-23 +================== + + * update connect + +3.4.2 / 2013-10-18 +================== + + * update connect + * downgrade commander + +3.4.1 / 2013-10-15 +================== + + * update connect + * update commander + * jsonp: check if callback is a function + * router: wrap encodeURIComponent in a try/catch #1735 (@lxe) + * res.format: now includes charset @1747 (@sorribas) + * res.links: allow multiple calls @1746 (@sorribas) + +3.4.0 / 2013-09-07 +================== + + * add res.vary(). Closes #1682 + * update connect + +3.3.8 / 2013-09-02 +================== + + * update connect + +3.3.7 / 2013-08-28 +================== + + * update connect + +3.3.6 / 2013-08-27 +================== + + * Revert "remove charset from json responses. Closes #1631" (causes issues in some clients) + * add: req.accepts take an argument list + +3.3.4 / 2013-07-08 +================== + + * update send and connect + +3.3.3 / 2013-07-04 +================== + + * update connect + +3.3.2 / 2013-07-03 +================== + + * update connect + * update send + * remove .version export + +3.3.1 / 2013-06-27 +================== + + * update connect + +3.3.0 / 2013-06-26 +================== + + * update connect + * add support for multiple X-Forwarded-Proto values. Closes #1646 + * change: remove charset from json responses. Closes #1631 + * change: return actual booleans from req.accept* functions + * fix jsonp callback array throw + +3.2.6 / 2013-06-02 +================== + + * update connect + +3.2.5 / 2013-05-21 +================== + + * update connect + * update node-cookie + * add: throw a meaningful error when there is no default engine + * change generation of ETags with res.send() to GET requests only. Closes #1619 + +3.2.4 / 2013-05-09 +================== + + * fix `req.subdomains` when no Host is present + * fix `req.host` when no Host is present, return undefined + +3.2.3 / 2013-05-07 +================== + + * update connect / qs + +3.2.2 / 2013-05-03 +================== + + * update qs + +3.2.1 / 2013-04-29 +================== + + * add app.VERB() paths array deprecation warning + * update connect + * update qs and remove all ~ semver crap + * fix: accept number as value of Signed Cookie + +3.2.0 / 2013-04-15 +================== + + * add "view" constructor setting to override view behaviour + * add req.acceptsEncoding(name) + * add req.acceptedEncodings + * revert cookie signature change causing session race conditions + * fix sorting of Accept values of the same quality + +3.1.2 / 2013-04-12 +================== + + * add support for custom Accept parameters + * update cookie-signature + +3.1.1 / 2013-04-01 +================== + + * add X-Forwarded-Host support to `req.host` + * fix relative redirects + * update mkdirp + * update buffer-crc32 + * remove legacy app.configure() method from app template. + +3.1.0 / 2013-01-25 +================== + + * add support for leading "." in "view engine" setting + * add array support to `res.set()` + * add node 0.8.x to travis.yml + * add "subdomain offset" setting for tweaking `req.subdomains` + * add `res.location(url)` implementing `res.redirect()`-like setting of Location + * use app.get() for x-powered-by setting for inheritance + * fix colons in passwords for `req.auth` + +3.0.6 / 2013-01-04 +================== + + * add http verb methods to Router + * update connect + * fix mangling of the `res.cookie()` options object + * fix jsonp whitespace escape. Closes #1132 + +3.0.5 / 2012-12-19 +================== + + * add throwing when a non-function is passed to a route + * fix: explicitly remove Transfer-Encoding header from 204 and 304 responses + * revert "add 'etag' option" + +3.0.4 / 2012-12-05 +================== + + * add 'etag' option to disable `res.send()` Etags + * add escaping of urls in text/plain in `res.redirect()` + for old browsers interpreting as html + * change crc32 module for a more liberal license + * update connect + +3.0.3 / 2012-11-13 +================== + + * update connect + * update cookie module + * fix cookie max-age + +3.0.2 / 2012-11-08 +================== + + * add OPTIONS to cors example. Closes #1398 + * fix route chaining regression. Closes #1397 + +3.0.1 / 2012-11-01 +================== + + * update connect + +3.0.0 / 2012-10-23 +================== + + * add `make clean` + * add "Basic" check to req.auth + * add `req.auth` test coverage + * add cb && cb(payload) to `res.jsonp()`. Closes #1374 + * add backwards compat for `res.redirect()` status. Closes #1336 + * add support for `res.json()` to retain previously defined Content-Types. Closes #1349 + * update connect + * change `res.redirect()` to utilize a pathname-relative Location again. Closes #1382 + * remove non-primitive string support for `res.send()` + * fix view-locals example. Closes #1370 + * fix route-separation example + +3.0.0rc5 / 2012-09-18 +================== + + * update connect + * add redis search example + * add static-files example + * add "x-powered-by" setting (`app.disable('x-powered-by')`) + * add "application/octet-stream" redirect Accept test case. Closes #1317 + +3.0.0rc4 / 2012-08-30 +================== + + * add `res.jsonp()`. Closes #1307 + * add "verbose errors" option to error-pages example + * add another route example to express(1) so people are not so confused + * add redis online user activity tracking example + * update connect dep + * fix etag quoting. Closes #1310 + * fix error-pages 404 status + * fix jsonp callback char restrictions + * remove old OPTIONS default response + +3.0.0rc3 / 2012-08-13 +================== + + * update connect dep + * fix signed cookies to work with `connect.cookieParser()` ("s:" prefix was missing) [tnydwrds] + * fix `res.render()` clobbering of "locals" + +3.0.0rc2 / 2012-08-03 +================== + + * add CORS example + * update connect dep + * deprecate `.createServer()` & remove old stale examples + * fix: escape `res.redirect()` link + * fix vhost example + +3.0.0rc1 / 2012-07-24 +================== + + * add more examples to view-locals + * add scheme-relative redirects (`res.redirect("//foo.com")`) support + * update cookie dep + * update connect dep + * update send dep + * fix `express(1)` -h flag, use -H for hogan. Closes #1245 + * fix `res.sendfile()` socket error handling regression + +3.0.0beta7 / 2012-07-16 +================== + + * update connect dep for `send()` root normalization regression + +3.0.0beta6 / 2012-07-13 +================== + + * add `err.view` property for view errors. Closes #1226 + * add "jsonp callback name" setting + * add support for "/foo/:bar*" non-greedy matches + * change `res.sendfile()` to use `send()` module + * change `res.send` to use "response-send" module + * remove `app.locals.use` and `res.locals.use`, use regular middleware + +3.0.0beta5 / 2012-07-03 +================== + + * add "make check" support + * add route-map example + * add `res.json(obj, status)` support back for BC + * add "methods" dep, remove internal methods module + * update connect dep + * update auth example to utilize cores pbkdf2 + * updated tests to use "supertest" + +3.0.0beta4 / 2012-06-25 +================== + + * Added `req.auth` + * Added `req.range(size)` + * Added `res.links(obj)` + * Added `res.send(body, status)` support back for backwards compat + * Added `.default()` support to `res.format()` + * Added 2xx / 304 check to `req.fresh` + * Revert "Added + support to the router" + * Fixed `res.send()` freshness check, respect res.statusCode + +3.0.0beta3 / 2012-06-15 +================== + + * Added hogan `--hjs` to express(1) [nullfirm] + * Added another example to content-negotiation + * Added `fresh` dep + * Changed: `res.send()` always checks freshness + * Fixed: expose connects mime module. Closes #1165 + +3.0.0beta2 / 2012-06-06 +================== + + * Added `+` support to the router + * Added `req.host` + * Changed `req.param()` to check route first + * Update connect dep + +3.0.0beta1 / 2012-06-01 +================== + + * Added `res.format()` callback to override default 406 behaviour + * Fixed `res.redirect()` 406. Closes #1154 + +3.0.0alpha5 / 2012-05-30 +================== + + * Added `req.ip` + * Added `{ signed: true }` option to `res.cookie()` + * Removed `res.signedCookie()` + * Changed: dont reverse `req.ips` + * Fixed "trust proxy" setting check for `req.ips` + +3.0.0alpha4 / 2012-05-09 +================== + + * Added: allow `[]` in jsonp callback. Closes #1128 + * Added `PORT` env var support in generated template. Closes #1118 [benatkin] + * Updated: connect 2.2.2 + +3.0.0alpha3 / 2012-05-04 +================== + + * Added public `app.routes`. Closes #887 + * Added _view-locals_ example + * Added _mvc_ example + * Added `res.locals.use()`. Closes #1120 + * Added conditional-GET support to `res.send()` + * Added: coerce `res.set()` values to strings + * Changed: moved `static()` in generated apps below router + * Changed: `res.send()` only set ETag when not previously set + * Changed connect 2.2.1 dep + * Changed: `make test` now runs unit / acceptance tests + * Fixed req/res proto inheritance + +3.0.0alpha2 / 2012-04-26 +================== + + * Added `make benchmark` back + * Added `res.send()` support for `String` objects + * Added client-side data exposing example + * Added `res.header()` and `req.header()` aliases for BC + * Added `express.createServer()` for BC + * Perf: memoize parsed urls + * Perf: connect 2.2.0 dep + * Changed: make `expressInit()` middleware self-aware + * Fixed: use app.get() for all core settings + * Fixed redis session example + * Fixed session example. Closes #1105 + * Fixed generated express dep. Closes #1078 + +3.0.0alpha1 / 2012-04-15 +================== + + * Added `app.locals.use(callback)` + * Added `app.locals` object + * Added `app.locals(obj)` + * Added `res.locals` object + * Added `res.locals(obj)` + * Added `res.format()` for content-negotiation + * Added `app.engine()` + * Added `res.cookie()` JSON cookie support + * Added "trust proxy" setting + * Added `req.subdomains` + * Added `req.protocol` + * Added `req.secure` + * Added `req.path` + * Added `req.ips` + * Added `req.fresh` + * Added `req.stale` + * Added comma-delimited / array support for `req.accepts()` + * Added debug instrumentation + * Added `res.set(obj)` + * Added `res.set(field, value)` + * Added `res.get(field)` + * Added `app.get(setting)`. Closes #842 + * Added `req.acceptsLanguage()` + * Added `req.acceptsCharset()` + * Added `req.accepted` + * Added `req.acceptedLanguages` + * Added `req.acceptedCharsets` + * Added "json replacer" setting + * Added "json spaces" setting + * Added X-Forwarded-Proto support to `res.redirect()`. Closes #92 + * Added `--less` support to express(1) + * Added `express.response` prototype + * Added `express.request` prototype + * Added `express.application` prototype + * Added `app.path()` + * Added `app.render()` + * Added `res.type()` to replace `res.contentType()` + * Changed: `res.redirect()` to add relative support + * Changed: enable "jsonp callback" by default + * Changed: renamed "case sensitive routes" to "case sensitive routing" + * Rewrite of all tests with mocha + * Removed "root" setting + * Removed `res.redirect('home')` support + * Removed `req.notify()` + * Removed `app.register()` + * Removed `app.redirect()` + * Removed `app.is()` + * Removed `app.helpers()` + * Removed `app.dynamicHelpers()` + * Fixed `res.sendfile()` with non-GET. Closes #723 + * Fixed express(1) public dir for windows. Closes #866 + +2.5.9/ 2012-04-02 +================== + + * Added support for PURGE request method [pbuyle] + * Fixed `express(1)` generated app `app.address()` before `listening` [mmalecki] + +2.5.8 / 2012-02-08 +================== + + * Update mkdirp dep. Closes #991 + +2.5.7 / 2012-02-06 +================== + + * Fixed `app.all` duplicate DELETE requests [mscdex] + +2.5.6 / 2012-01-13 +================== + + * Updated hamljs dev dep. Closes #953 + +2.5.5 / 2012-01-08 +================== + + * Fixed: set `filename` on cached templates [matthewleon] + +2.5.4 / 2012-01-02 +================== + + * Fixed `express(1)` eol on 0.4.x. Closes #947 + +2.5.3 / 2011-12-30 +================== + + * Fixed `req.is()` when a charset is present + +2.5.2 / 2011-12-10 +================== + + * Fixed: express(1) LF -> CRLF for windows + +2.5.1 / 2011-11-17 +================== + + * Changed: updated connect to 1.8.x + * Removed sass.js support from express(1) + +2.5.0 / 2011-10-24 +================== + + * Added ./routes dir for generated app by default + * Added npm install reminder to express(1) app gen + * Added 0.5.x support + * Removed `make test-cov` since it wont work with node 0.5.x + * Fixed express(1) public dir for windows. Closes #866 + +2.4.7 / 2011-10-05 +================== + + * Added mkdirp to express(1). Closes #795 + * Added simple _json-config_ example + * Added shorthand for the parsed request's pathname via `req.path` + * Changed connect dep to 1.7.x to fix npm issue... + * Fixed `res.redirect()` __HEAD__ support. [reported by xerox] + * Fixed `req.flash()`, only escape args + * Fixed absolute path checking on windows. Closes #829 [reported by andrewpmckenzie] + +2.4.6 / 2011-08-22 +================== + + * Fixed multiple param callback regression. Closes #824 [reported by TroyGoode] + +2.4.5 / 2011-08-19 +================== + + * Added support for routes to handle errors. Closes #809 + * Added `app.routes.all()`. Closes #803 + * Added "basepath" setting to work in conjunction with reverse proxies etc. + * Refactored `Route` to use a single array of callbacks + * Added support for multiple callbacks for `app.param()`. Closes #801 +Closes #805 + * Changed: removed .call(self) for route callbacks + * Dependency: `qs >= 0.3.1` + * Fixed `res.redirect()` on windows due to `join()` usage. Closes #808 + +2.4.4 / 2011-08-05 +================== + + * Fixed `res.header()` intention of a set, even when `undefined` + * Fixed `*`, value no longer required + * Fixed `res.send(204)` support. Closes #771 + +2.4.3 / 2011-07-14 +================== + + * Added docs for `status` option special-case. Closes #739 + * Fixed `options.filename`, exposing the view path to template engines + +2.4.2. / 2011-07-06 +================== + + * Revert "removed jsonp stripping" for XSS + +2.4.1 / 2011-07-06 +================== + + * Added `res.json()` JSONP support. Closes #737 + * Added _extending-templates_ example. Closes #730 + * Added "strict routing" setting for trailing slashes + * Added support for multiple envs in `app.configure()` calls. Closes #735 + * Changed: `res.send()` using `res.json()` + * Changed: when cookie `path === null` don't default it + * Changed; default cookie path to "home" setting. Closes #731 + * Removed _pids/logs_ creation from express(1) + +2.4.0 / 2011-06-28 +================== + + * Added chainable `res.status(code)` + * Added `res.json()`, an explicit version of `res.send(obj)` + * Added simple web-service example + +2.3.12 / 2011-06-22 +================== + + * \#express is now on freenode! come join! + * Added `req.get(field, param)` + * Added links to Japanese documentation, thanks @hideyukisaito! + * Added; the `express(1)` generated app outputs the env + * Added `content-negotiation` example + * Dependency: connect >= 1.5.1 < 2.0.0 + * Fixed view layout bug. Closes #720 + * Fixed; ignore body on 304. Closes #701 + +2.3.11 / 2011-06-04 +================== + + * Added `npm test` + * Removed generation of dummy test file from `express(1)` + * Fixed; `express(1)` adds express as a dep + * Fixed; prune on `prepublish` + +2.3.10 / 2011-05-27 +================== + + * Added `req.route`, exposing the current route + * Added _package.json_ generation support to `express(1)` + * Fixed call to `app.param()` function for optional params. Closes #682 + +2.3.9 / 2011-05-25 +================== + + * Fixed bug-ish with `../' in `res.partial()` calls + +2.3.8 / 2011-05-24 +================== + + * Fixed `app.options()` + +2.3.7 / 2011-05-23 +================== + + * Added route `Collection`, ex: `app.get('/user/:id').remove();` + * Added support for `app.param(fn)` to define param logic + * Removed `app.param()` support for callback with return value + * Removed module.parent check from express(1) generated app. Closes #670 + * Refactored router. Closes #639 + +2.3.6 / 2011-05-20 +================== + + * Changed; using devDependencies instead of git submodules + * Fixed redis session example + * Fixed markdown example + * Fixed view caching, should not be enabled in development + +2.3.5 / 2011-05-20 +================== + + * Added export `.view` as alias for `.View` + +2.3.4 / 2011-05-08 +================== + + * Added `./examples/say` + * Fixed `res.sendfile()` bug preventing the transfer of files with spaces + +2.3.3 / 2011-05-03 +================== + + * Added "case sensitive routes" option. + * Changed; split methods supported per rfc [slaskis] + * Fixed route-specific middleware when using the same callback function several times + +2.3.2 / 2011-04-27 +================== + + * Fixed view hints + +2.3.1 / 2011-04-26 +================== + + * Added `app.match()` as `app.match.all()` + * Added `app.lookup()` as `app.lookup.all()` + * Added `app.remove()` for `app.remove.all()` + * Added `app.remove.VERB()` + * Fixed template caching collision issue. Closes #644 + * Moved router over from connect and started refactor + +2.3.0 / 2011-04-25 +================== + + * Added options support to `res.clearCookie()` + * Added `res.helpers()` as alias of `res.locals()` + * Added; json defaults to UTF-8 with `res.send()`. Closes #632. [Daniel * Dependency `connect >= 1.4.0` + * Changed; auto set Content-Type in res.attachement [Aaron Heckmann] + * Renamed "cache views" to "view cache". Closes #628 + * Fixed caching of views when using several apps. Closes #637 + * Fixed gotcha invoking `app.param()` callbacks once per route middleware. +Closes #638 + * Fixed partial lookup precedence. Closes #631 +Shaw] + +2.2.2 / 2011-04-12 +================== + + * Added second callback support for `res.download()` connection errors + * Fixed `filename` option passing to template engine + +2.2.1 / 2011-04-04 +================== + + * Added `layout(path)` helper to change the layout within a view. Closes #610 + * Fixed `partial()` collection object support. + Previously only anything with `.length` would work. + When `.length` is present one must still be aware of holes, + however now `{ collection: {foo: 'bar'}}` is valid, exposes + `keyInCollection` and `keysInCollection`. + + * Performance improved with better view caching + * Removed `request` and `response` locals + * Changed; errorHandler page title is now `Express` instead of `Connect` + +2.2.0 / 2011-03-30 +================== + + * Added `app.lookup.VERB()`, ex `app.lookup.put('/user/:id')`. Closes #606 + * Added `app.match.VERB()`, ex `app.match.put('/user/12')`. Closes #606 + * Added `app.VERB(path)` as alias of `app.lookup.VERB()`. + * Dependency `connect >= 1.2.0` + +2.1.1 / 2011-03-29 +================== + + * Added; expose `err.view` object when failing to locate a view + * Fixed `res.partial()` call `next(err)` when no callback is given [reported by aheckmann] + * Fixed; `res.send(undefined)` responds with 204 [aheckmann] + +2.1.0 / 2011-03-24 +================== + + * Added `/_?` partial lookup support. Closes #447 + * Added `request`, `response`, and `app` local variables + * Added `settings` local variable, containing the app's settings + * Added `req.flash()` exception if `req.session` is not available + * Added `res.send(bool)` support (json response) + * Fixed stylus example for latest version + * Fixed; wrap try/catch around `res.render()` + +2.0.0 / 2011-03-17 +================== + + * Fixed up index view path alternative. + * Changed; `res.locals()` without object returns the locals + +2.0.0rc3 / 2011-03-17 +================== + + * Added `res.locals(obj)` to compliment `res.local(key, val)` + * Added `res.partial()` callback support + * Fixed recursive error reporting issue in `res.render()` + +2.0.0rc2 / 2011-03-17 +================== + + * Changed; `partial()` "locals" are now optional + * Fixed `SlowBuffer` support. Closes #584 [reported by tyrda01] + * Fixed .filename view engine option [reported by drudge] + * Fixed blog example + * Fixed `{req,res}.app` reference when mounting [Ben Weaver] + +2.0.0rc / 2011-03-14 +================== + + * Fixed; expose `HTTPSServer` constructor + * Fixed express(1) default test charset. Closes #579 [reported by secoif] + * Fixed; default charset to utf-8 instead of utf8 for lame IE [reported by NickP] + +2.0.0beta3 / 2011-03-09 +================== + + * Added support for `res.contentType()` literal + The original `res.contentType('.json')`, + `res.contentType('application/json')`, and `res.contentType('json')` + will work now. + * Added `res.render()` status option support back + * Added charset option for `res.render()` + * Added `.charset` support (via connect 1.0.4) + * Added view resolution hints when in development and a lookup fails + * Added layout lookup support relative to the page view. + For example while rendering `./views/user/index.jade` if you create + `./views/user/layout.jade` it will be used in favour of the root layout. + * Fixed `res.redirect()`. RFC states absolute url [reported by unlink] + * Fixed; default `res.send()` string charset to utf8 + * Removed `Partial` constructor (not currently used) + +2.0.0beta2 / 2011-03-07 +================== + + * Added res.render() `.locals` support back to aid in migration process + * Fixed flash example + +2.0.0beta / 2011-03-03 +================== + + * Added HTTPS support + * Added `res.cookie()` maxAge support + * Added `req.header()` _Referrer_ / _Referer_ special-case, either works + * Added mount support for `res.redirect()`, now respects the mount-point + * Added `union()` util, taking place of `merge(clone())` combo + * Added stylus support to express(1) generated app + * Added secret to session middleware used in examples and generated app + * Added `res.local(name, val)` for progressive view locals + * Added default param support to `req.param(name, default)` + * Added `app.disabled()` and `app.enabled()` + * Added `app.register()` support for omitting leading ".", either works + * Added `res.partial()`, using the same interface as `partial()` within a view. Closes #539 + * Added `app.param()` to map route params to async/sync logic + * Added; aliased `app.helpers()` as `app.locals()`. Closes #481 + * Added extname with no leading "." support to `res.contentType()` + * Added `cache views` setting, defaulting to enabled in "production" env + * Added index file partial resolution, eg: partial('user') may try _views/user/index.jade_. + * Added `req.accepts()` support for extensions + * Changed; `res.download()` and `res.sendfile()` now utilize Connect's + static file server `connect.static.send()`. + * Changed; replaced `connect.utils.mime()` with npm _mime_ module + * Changed; allow `req.query` to be pre-defined (via middleware or other parent + * Changed view partial resolution, now relative to parent view + * Changed view engine signature. no longer `engine.render(str, options, callback)`, now `engine.compile(str, options) -> Function`, the returned function accepts `fn(locals)`. + * Fixed `req.param()` bug returning Array.prototype methods. Closes #552 + * Fixed; using `Stream#pipe()` instead of `sys.pump()` in `res.sendfile()` + * Fixed; using _qs_ module instead of _querystring_ + * Fixed; strip unsafe chars from jsonp callbacks + * Removed "stream threshold" setting + +1.0.8 / 2011-03-01 +================== + + * Allow `req.query` to be pre-defined (via middleware or other parent app) + * "connect": ">= 0.5.0 < 1.0.0". Closes #547 + * Removed the long deprecated __EXPRESS_ENV__ support + +1.0.7 / 2011-02-07 +================== + + * Fixed `render()` setting inheritance. + Mounted apps would not inherit "view engine" + +1.0.6 / 2011-02-07 +================== + + * Fixed `view engine` setting bug when period is in dirname + +1.0.5 / 2011-02-05 +================== + + * Added secret to generated app `session()` call + +1.0.4 / 2011-02-05 +================== + + * Added `qs` dependency to _package.json_ + * Fixed namespaced `require()`s for latest connect support + +1.0.3 / 2011-01-13 +================== + + * Remove unsafe characters from JSONP callback names [Ryan Grove] + +1.0.2 / 2011-01-10 +================== + + * Removed nested require, using `connect.router` + +1.0.1 / 2010-12-29 +================== + + * Fixed for middleware stacked via `createServer()` + previously the `foo` middleware passed to `createServer(foo)` + would not have access to Express methods such as `res.send()` + or props like `req.query` etc. + +1.0.0 / 2010-11-16 +================== + + * Added; deduce partial object names from the last segment. + For example by default `partial('forum/post', postObject)` will + give you the _post_ object, providing a meaningful default. + * Added http status code string representation to `res.redirect()` body + * Added; `res.redirect()` supporting _text/plain_ and _text/html_ via __Accept__. + * Added `req.is()` to aid in content negotiation + * Added partial local inheritance [suggested by masylum]. Closes #102 + providing access to parent template locals. + * Added _-s, --session[s]_ flag to express(1) to add session related middleware + * Added _--template_ flag to express(1) to specify the + template engine to use. + * Added _--css_ flag to express(1) to specify the + stylesheet engine to use (or just plain css by default). + * Added `app.all()` support [thanks aheckmann] + * Added partial direct object support. + You may now `partial('user', user)` providing the "user" local, + vs previously `partial('user', { object: user })`. + * Added _route-separation_ example since many people question ways + to do this with CommonJS modules. Also view the _blog_ example for + an alternative. + * Performance; caching view path derived partial object names + * Fixed partial local inheritance precedence. [reported by Nick Poulden] Closes #454 + * Fixed jsonp support; _text/javascript_ as per mailinglist discussion + +1.0.0rc4 / 2010-10-14 +================== + + * Added _NODE_ENV_ support, _EXPRESS_ENV_ is deprecated and will be removed in 1.0.0 + * Added route-middleware support (very helpful, see the [docs](http://expressjs.com/guide.html#Route-Middleware)) + * Added _jsonp callback_ setting to enable/disable jsonp autowrapping [Dav Glass] + * Added callback query check on response.send to autowrap JSON objects for simple webservice implementations [Dav Glass] + * Added `partial()` support for array-like collections. Closes #434 + * Added support for swappable querystring parsers + * Added session usage docs. Closes #443 + * Added dynamic helper caching. Closes #439 [suggested by maritz] + * Added authentication example + * Added basic Range support to `res.sendfile()` (and `res.download()` etc) + * Changed; `express(1)` generated app using 2 spaces instead of 4 + * Default env to "development" again [aheckmann] + * Removed _context_ option is no more, use "scope" + * Fixed; exposing _./support_ libs to examples so they can run without installs + * Fixed mvc example + +1.0.0rc3 / 2010-09-20 +================== + + * Added confirmation for `express(1)` app generation. Closes #391 + * Added extending of flash formatters via `app.flashFormatters` + * Added flash formatter support. Closes #411 + * Added streaming support to `res.sendfile()` using `sys.pump()` when >= "stream threshold" + * Added _stream threshold_ setting for `res.sendfile()` + * Added `res.send()` __HEAD__ support + * Added `res.clearCookie()` + * Added `res.cookie()` + * Added `res.render()` headers option + * Added `res.redirect()` response bodies + * Added `res.render()` status option support. Closes #425 [thanks aheckmann] + * Fixed `res.sendfile()` responding with 403 on malicious path + * Fixed `res.download()` bug; when an error occurs remove _Content-Disposition_ + * Fixed; mounted apps settings now inherit from parent app [aheckmann] + * Fixed; stripping Content-Length / Content-Type when 204 + * Fixed `res.send()` 204. Closes #419 + * Fixed multiple _Set-Cookie_ headers via `res.header()`. Closes #402 + * Fixed bug messing with error handlers when `listenFD()` is called instead of `listen()`. [thanks guillermo] + + +1.0.0rc2 / 2010-08-17 +================== + + * Added `app.register()` for template engine mapping. Closes #390 + * Added `res.render()` callback support as second argument (no options) + * Added callback support to `res.download()` + * Added callback support for `res.sendfile()` + * Added support for middleware access via `express.middlewareName()` vs `connect.middlewareName()` + * Added "partials" setting to docs + * Added default expresso tests to `express(1)` generated app. Closes #384 + * Fixed `res.sendfile()` error handling, defer via `next()` + * Fixed `res.render()` callback when a layout is used [thanks guillermo] + * Fixed; `make install` creating ~/.node_libraries when not present + * Fixed issue preventing error handlers from being defined anywhere. Closes #387 + +1.0.0rc / 2010-07-28 +================== + + * Added mounted hook. Closes #369 + * Added connect dependency to _package.json_ + + * Removed "reload views" setting and support code + development env never caches, production always caches. + + * Removed _param_ in route callbacks, signature is now + simply (req, res, next), previously (req, res, params, next). + Use _req.params_ for path captures, _req.query_ for GET params. + + * Fixed "home" setting + * Fixed middleware/router precedence issue. Closes #366 + * Fixed; _configure()_ callbacks called immediately. Closes #368 + +1.0.0beta2 / 2010-07-23 +================== + + * Added more examples + * Added; exporting `Server` constructor + * Added `Server#helpers()` for view locals + * Added `Server#dynamicHelpers()` for dynamic view locals. Closes #349 + * Added support for absolute view paths + * Added; _home_ setting defaults to `Server#route` for mounted apps. Closes #363 + * Added Guillermo Rauch to the contributor list + * Added support for "as" for non-collection partials. Closes #341 + * Fixed _install.sh_, ensuring _~/.node_libraries_ exists. Closes #362 [thanks jf] + * Fixed `res.render()` exceptions, now passed to `next()` when no callback is given [thanks guillermo] + * Fixed instanceof `Array` checks, now `Array.isArray()` + * Fixed express(1) expansion of public dirs. Closes #348 + * Fixed middleware precedence. Closes #345 + * Fixed view watcher, now async [thanks aheckmann] + +1.0.0beta / 2010-07-15 +================== + + * Re-write + - much faster + - much lighter + - Check [ExpressJS.com](http://expressjs.com) for migration guide and updated docs + +0.14.0 / 2010-06-15 +================== + + * Utilize relative requires + * Added Static bufferSize option [aheckmann] + * Fixed caching of view and partial subdirectories [aheckmann] + * Fixed mime.type() comments now that ".ext" is not supported + * Updated haml submodule + * Updated class submodule + * Removed bin/express + +0.13.0 / 2010-06-01 +================== + + * Added node v0.1.97 compatibility + * Added support for deleting cookies via Request#cookie('key', null) + * Updated haml submodule + * Fixed not-found page, now using charset utf-8 + * Fixed show-exceptions page, now using charset utf-8 + * Fixed view support due to fs.readFile Buffers + * Changed; mime.type() no longer accepts ".type" due to node extname() changes + +0.12.0 / 2010-05-22 +================== + + * Added node v0.1.96 compatibility + * Added view `helpers` export which act as additional local variables + * Updated haml submodule + * Changed ETag; removed inode, modified time only + * Fixed LF to CRLF for setting multiple cookies + * Fixed cookie compilation; values are now urlencoded + * Fixed cookies parsing; accepts quoted values and url escaped cookies + +0.11.0 / 2010-05-06 +================== + + * Added support for layouts using different engines + - this.render('page.html.haml', { layout: 'super-cool-layout.html.ejs' }) + - this.render('page.html.haml', { layout: 'foo' }) // assumes 'foo.html.haml' + - this.render('page.html.haml', { layout: false }) // no layout + * Updated ext submodule + * Updated haml submodule + * Fixed EJS partial support by passing along the context. Issue #307 + +0.10.1 / 2010-05-03 +================== + + * Fixed binary uploads. + +0.10.0 / 2010-04-30 +================== + + * Added charset support via Request#charset (automatically assigned to 'UTF-8' when respond()'s + encoding is set to 'utf8' or 'utf-8'). + * Added "encoding" option to Request#render(). Closes #299 + * Added "dump exceptions" setting, which is enabled by default. + * Added simple ejs template engine support + * Added error response support for text/plain, application/json. Closes #297 + * Added callback function param to Request#error() + * Added Request#sendHead() + * Added Request#stream() + * Added support for Request#respond(304, null) for empty response bodies + * Added ETag support to Request#sendfile() + * Added options to Request#sendfile(), passed to fs.createReadStream() + * Added filename arg to Request#download() + * Performance enhanced due to pre-reversing plugins so that plugins.reverse() is not called on each request + * Performance enhanced by preventing several calls to toLowerCase() in Router#match() + * Changed; Request#sendfile() now streams + * Changed; Renamed Request#halt() to Request#respond(). Closes #289 + * Changed; Using sys.inspect() instead of JSON.encode() for error output + * Changed; run() returns the http.Server instance. Closes #298 + * Changed; Defaulting Server#host to null (INADDR_ANY) + * Changed; Logger "common" format scale of 0.4f + * Removed Logger "request" format + * Fixed; Catching ENOENT in view caching, preventing error when "views/partials" is not found + * Fixed several issues with http client + * Fixed Logger Content-Length output + * Fixed bug preventing Opera from retaining the generated session id. Closes #292 + +0.9.0 / 2010-04-14 +================== + + * Added DSL level error() route support + * Added DSL level notFound() route support + * Added Request#error() + * Added Request#notFound() + * Added Request#render() callback function. Closes #258 + * Added "max upload size" setting + * Added "magic" variables to collection partials (\_\_index\_\_, \_\_length\_\_, \_\_isFirst\_\_, \_\_isLast\_\_). Closes #254 + * Added [haml.js](http://github.com/visionmedia/haml.js) submodule; removed haml-js + * Added callback function support to Request#halt() as 3rd/4th arg + * Added preprocessing of route param wildcards using param(). Closes #251 + * Added view partial support (with collections etc.) + * Fixed bug preventing falsey params (such as ?page=0). Closes #286 + * Fixed setting of multiple cookies. Closes #199 + * Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml) + * Changed; session cookie is now httpOnly + * Changed; Request is no longer global + * Changed; Event is no longer global + * Changed; "sys" module is no longer global + * Changed; moved Request#download to Static plugin where it belongs + * Changed; Request instance created before body parsing. Closes #262 + * Changed; Pre-caching views in memory when "cache view contents" is enabled. Closes #253 + * Changed; Pre-caching view partials in memory when "cache view partials" is enabled + * Updated support to node --version 0.1.90 + * Updated dependencies + * Removed set("session cookie") in favour of use(Session, { cookie: { ... }}) + * Removed utils.mixin(); use Object#mergeDeep() + +0.8.0 / 2010-03-19 +================== + + * Added coffeescript example app. Closes #242 + * Changed; cache api now async friendly. Closes #240 + * Removed deprecated 'express/static' support. Use 'express/plugins/static' + +0.7.6 / 2010-03-19 +================== + + * Added Request#isXHR. Closes #229 + * Added `make install` (for the executable) + * Added `express` executable for setting up simple app templates + * Added "GET /public/*" to Static plugin, defaulting to /public + * Added Static plugin + * Fixed; Request#render() only calls cache.get() once + * Fixed; Namespacing View caches with "view:" + * Fixed; Namespacing Static caches with "static:" + * Fixed; Both example apps now use the Static plugin + * Fixed set("views"). Closes #239 + * Fixed missing space for combined log format + * Deprecated Request#sendfile() and 'express/static' + * Removed Server#running + +0.7.5 / 2010-03-16 +================== + + * Added Request#flash() support without args, now returns all flashes + * Updated ext submodule + +0.7.4 / 2010-03-16 +================== + + * Fixed session reaper + * Changed; class.js replacing js-oo Class implementation (quite a bit faster, no browser cruft) + +0.7.3 / 2010-03-16 +================== + + * Added package.json + * Fixed requiring of haml / sass due to kiwi removal + +0.7.2 / 2010-03-16 +================== + + * Fixed GIT submodules (HAH!) + +0.7.1 / 2010-03-16 +================== + + * Changed; Express now using submodules again until a PM is adopted + * Changed; chat example using millisecond conversions from ext + +0.7.0 / 2010-03-15 +================== + + * Added Request#pass() support (finds the next matching route, or the given path) + * Added Logger plugin (default "common" format replaces CommonLogger) + * Removed Profiler plugin + * Removed CommonLogger plugin + +0.6.0 / 2010-03-11 +================== + + * Added seed.yml for kiwi package management support + * Added HTTP client query string support when method is GET. Closes #205 + + * Added support for arbitrary view engines. + For example "foo.engine.html" will now require('engine'), + the exports from this module are cached after the first require(). + + * Added async plugin support + + * Removed usage of RESTful route funcs as http client + get() etc, use http.get() and friends + + * Removed custom exceptions + +0.5.0 / 2010-03-10 +================== + + * Added ext dependency (library of js extensions) + * Removed extname() / basename() utils. Use path module + * Removed toArray() util. Use arguments.values + * Removed escapeRegexp() util. Use RegExp.escape() + * Removed process.mixin() dependency. Use utils.mixin() + * Removed Collection + * Removed ElementCollection + * Shameless self promotion of ebook "Advanced JavaScript" (http://dev-mag.com) ;) + +0.4.0 / 2010-02-11 +================== + + * Added flash() example to sample upload app + * Added high level restful http client module (express/http) + * Changed; RESTful route functions double as HTTP clients. Closes #69 + * Changed; throwing error when routes are added at runtime + * Changed; defaulting render() context to the current Request. Closes #197 + * Updated haml submodule + +0.3.0 / 2010-02-11 +================== + + * Updated haml / sass submodules. Closes #200 + * Added flash message support. Closes #64 + * Added accepts() now allows multiple args. fixes #117 + * Added support for plugins to halt. Closes #189 + * Added alternate layout support. Closes #119 + * Removed Route#run(). Closes #188 + * Fixed broken specs due to use(Cookie) missing + +0.2.1 / 2010-02-05 +================== + + * Added "plot" format option for Profiler (for gnuplot processing) + * Added request number to Profiler plugin + * Fixed binary encoding for multipart file uploads, was previously defaulting to UTF8 + * Fixed issue with routes not firing when not files are present. Closes #184 + * Fixed process.Promise -> events.Promise + +0.2.0 / 2010-02-03 +================== + + * Added parseParam() support for name[] etc. (allows for file inputs with "multiple" attr) Closes #180 + * Added Both Cache and Session option "reapInterval" may be "reapEvery". Closes #174 + * Added expiration support to cache api with reaper. Closes #133 + * Added cache Store.Memory#reap() + * Added Cache; cache api now uses first class Cache instances + * Added abstract session Store. Closes #172 + * Changed; cache Memory.Store#get() utilizing Collection + * Renamed MemoryStore -> Store.Memory + * Fixed use() of the same plugin several time will always use latest options. Closes #176 + +0.1.0 / 2010-02-03 +================== + + * Changed; Hooks (before / after) pass request as arg as well as evaluated in their context + * Updated node support to 0.1.27 Closes #169 + * Updated dirname(__filename) -> __dirname + * Updated libxmljs support to v0.2.0 + * Added session support with memory store / reaping + * Added quick uid() helper + * Added multi-part upload support + * Added Sass.js support / submodule + * Added production env caching view contents and static files + * Added static file caching. Closes #136 + * Added cache plugin with memory stores + * Added support to StaticFile so that it works with non-textual files. + * Removed dirname() helper + * Removed several globals (now their modules must be required) + +0.0.2 / 2010-01-10 +================== + + * Added view benchmarks; currently haml vs ejs + * Added Request#attachment() specs. Closes #116 + * Added use of node's parseQuery() util. Closes #123 + * Added `make init` for submodules + * Updated Haml + * Updated sample chat app to show messages on load + * Updated libxmljs parseString -> parseHtmlString + * Fixed `make init` to work with older versions of git + * Fixed specs can now run independent specs for those who can't build deps. Closes #127 + * Fixed issues introduced by the node url module changes. Closes 126. + * Fixed two assertions failing due to Collection#keys() returning strings + * Fixed faulty Collection#toArray() spec due to keys() returning strings + * Fixed `make test` now builds libxmljs.node before testing + +0.0.1 / 2010-01-03 +================== + + * Initial release diff --git a/apps/api/LICENSE b/apps/api/LICENSE new file mode 100644 index 0000000..aa927e4 --- /dev/null +++ b/apps/api/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/apps/api/Readme.md b/apps/api/Readme.md new file mode 100644 index 0000000..d62b982 --- /dev/null +++ b/apps/api/Readme.md @@ -0,0 +1,275 @@ +[![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](https://expressjs.com/) + +**Fast, unopinionated, minimalist web framework for [Node.js](https://nodejs.org).** + +**This project has a [Code of Conduct].** + +## Table of contents + +- [Table of contents](#table-of-contents) +- [Installation](#installation) +- [Features](#features) +- [Docs \& Community](#docs--community) +- [Quick Start](#quick-start) +- [Philosophy](#philosophy) +- [Examples](#examples) +- [Contributing](#contributing) + - [Security Issues](#security-issues) + - [Running Tests](#running-tests) +- [Current project team members](#current-project-team-members) + - [TC (Technical Committee)](#tc-technical-committee) + - [TC emeriti members](#tc-emeriti-members) + - [Triagers](#triagers) + - [Emeritus Triagers](#emeritus-triagers) +- [License](#license) + + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-downloads-url] +[![Linux Build][github-actions-ci-image]][github-actions-ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] +[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] + + +```js +import express from 'express' + +const app = express() + +app.get('/', (req, res) => { + res.send('Hello World') +}) + +app.listen(3000, () => { + console.log('Server is running on http://localhost:3000') +}) +``` + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). + +Before installing, [download and install Node.js](https://nodejs.org/en/download/). +Node.js 18 or higher is required. + +If this is a brand new project, make sure to create a `package.json` first with +the [`npm init` command](https://docs.npmjs.com/creating-a-package-json-file). + +Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```bash +npm install express +``` + +Follow [our installing guide](https://expressjs.com/en/starter/installing.html) +for more information. + +## Features + + * Robust routing + * Focus on high performance + * Super-high test coverage + * HTTP helpers (redirection, caching, etc) + * View system supporting 14+ template engines + * Content negotiation + * Executable for generating applications quickly + +## Docs & Community + + * [Website and Documentation](https://expressjs.com/) - [[website repo](https://github.com/expressjs/expressjs.com)] + * [GitHub Organization](https://github.com/expressjs) for Official Middleware & Modules + * [Github Discussions](https://github.com/expressjs/discussions) for discussion on the development and usage of Express + +**PROTIP** Be sure to read the [migration guide to v5](https://expressjs.com/en/guide/migrating-5) + +## Quick Start + + The quickest way to get started with express is to utilize the executable [`express(1)`](https://github.com/expressjs/generator) to generate an application as shown below: + + Install the executable. The executable's major version will match Express's: + +```bash +npm install -g express-generator@4 +``` + + Create the app: + +```bash +express /tmp/foo && cd /tmp/foo +``` + + Install dependencies: + +```bash +npm install +``` + + Start the server: + +```bash +npm start +``` + + View the website at: http://localhost:3000 + +## Philosophy + + The Express philosophy is to provide small, robust tooling for HTTP servers, making + it a great solution for single page applications, websites, hybrids, or public + HTTP APIs. + + Express does not force you to use any specific ORM or template engine. With support for over + 14 template engines via [@ladjs/consolidate](https://github.com/ladjs/consolidate), + you can quickly craft your perfect framework. + +## Examples + + To view the examples, clone the Express repository: + +```bash +git clone https://github.com/expressjs/express.git --depth 1 && cd express +``` + + Then install the dependencies: + +```bash +npm install +``` + + Then run whichever example you want: + +```bash +node examples/content-negotiation +``` + +## Contributing + +The Express.js project welcomes all constructive contributions. Contributions take many forms, +from code for bug fixes and enhancements, to additions and fixes to documentation, additional +tests, triaging incoming pull requests and issues, and more! + +See the [Contributing Guide] for more technical details on contributing. + +### Security Issues + +If you discover a security vulnerability in Express, please see [Security Policies and Procedures](SECURITY.md). + +### Running Tests + +To run the test suite, first install the dependencies: + +```bash +npm install +``` + +Then run `npm test`: + +```bash +npm test +``` + +## Current project team members + +For information about the governance of the express.js project, see [GOVERNANCE.md](https://github.com/expressjs/discussions/blob/HEAD/docs/GOVERNANCE.md). + +The original author of Express is [TJ Holowaychuk](https://github.com/tj) + +[List of all contributors](https://github.com/expressjs/express/graphs/contributors) + +### TC (Technical Committee) + +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [jonchurch](https://github.com/jonchurch) - **Jon Church** +* [wesleytodd](https://github.com/wesleytodd) - **Wes Todd** +* [LinusU](https://github.com/LinusU) - **Linus Unnebäck** +* [blakeembrey](https://github.com/blakeembrey) - **Blake Embrey** +* [sheplu](https://github.com/sheplu) - **Jean Burellier** +* [crandmck](https://github.com/crandmck) - **Rand McKinney** +* [ctcpip](https://github.com/ctcpip) - **Chris de Almeida** + +
+TC emeriti members + +#### TC emeriti members + + * [dougwilson](https://github.com/dougwilson) - **Douglas Wilson** + * [hacksparrow](https://github.com/hacksparrow) - **Hage Yaapa** + * [jonathanong](https://github.com/jonathanong) - **jongleberry** + * [niftylettuce](https://github.com/niftylettuce) - **niftylettuce** + * [troygoode](https://github.com/troygoode) - **Troy Goode** +
+ + +### Triagers + +* [aravindvnair99](https://github.com/aravindvnair99) - **Aravind Nair** +* [bjohansebas](https://github.com/bjohansebas) - **Sebastian Beltran** +* [carpasse](https://github.com/carpasse) - **Carlos Serrano** +* [CBID2](https://github.com/CBID2) - **Christine Belzie** +* [dpopp07](https://github.com/dpopp07) - **Dustin Popp** +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [3imed-jaberi](https://github.com/3imed-jaberi) - **Imed Jaberi** +* [IamLizu](https://github.com/IamLizu) - **S M Mahmudul Hasan** (he/him) +* [Phillip9587](https://github.com/Phillip9587) - **Phillip Barta** +* [Sushmeet](https://github.com/Sushmeet) - **Sushmeet Sunger** +* [rxmarbles](https://github.com/rxmarbles) **Rick Markins** (He/him) + +
+Triagers emeriti members + +#### Emeritus Triagers + + * [AuggieH](https://github.com/AuggieH) - **Auggie Hudak** + * [G-Rath](https://github.com/G-Rath) - **Gareth Jones** + * [MohammadXroid](https://github.com/MohammadXroid) - **Mohammad Ayashi** + * [NawafSwe](https://github.com/NawafSwe) - **Nawaf Alsharqi** + * [NotMoni](https://github.com/NotMoni) - **Moni** + * [VigneshMurugan](https://github.com/VigneshMurugan) - **Vignesh Murugan** + * [davidmashe](https://github.com/davidmashe) - **David Ashe** + * [digitaIfabric](https://github.com/digitaIfabric) - **David** + * [e-l-i-s-e](https://github.com/e-l-i-s-e) - **Elise Bonner** + * [fed135](https://github.com/fed135) - **Frederic Charette** + * [firmanJS](https://github.com/firmanJS) - **Firman Abdul Hakim** + * [getspooky](https://github.com/getspooky) - **Yasser Ameur** + * [ghinks](https://github.com/ghinks) - **Glenn** + * [ghousemohamed](https://github.com/ghousemohamed) - **Ghouse Mohamed** + * [gireeshpunathil](https://github.com/gireeshpunathil) - **Gireesh Punathil** + * [jake32321](https://github.com/jake32321) - **Jake Reed** + * [jonchurch](https://github.com/jonchurch) - **Jon Church** + * [lekanikotun](https://github.com/lekanikotun) - **Troy Goode** + * [marsonya](https://github.com/marsonya) - **Lekan Ikotun** + * [mastermatt](https://github.com/mastermatt) - **Matt R. Wilson** + * [maxakuru](https://github.com/maxakuru) - **Max Edell** + * [mlrawlings](https://github.com/mlrawlings) - **Michael Rawlings** + * [rodion-arr](https://github.com/rodion-arr) - **Rodion Abdurakhimov** + * [sheplu](https://github.com/sheplu) - **Jean Burellier** + * [tarunyadav1](https://github.com/tarunyadav1) - **Tarun yadav** + * [tunniclm](https://github.com/tunniclm) - **Mike Tunnicliffe** + * [enyoghasim](https://github.com/enyoghasim) - **David Enyoghasim** + * [0ss](https://github.com/0ss) - **Salah** + * [import-brain](https://github.com/import-brain) - **Eric Cheng** (he/him) + * [dakshkhetan](https://github.com/dakshkhetan) - **Daksh Khetan** (he/him) + * [lucasraziel](https://github.com/lucasraziel) - **Lucas Soares Do Rego** + * [mertcanaltin](https://github.com/mertcanaltin) - **Mert Can Altin** + +
+ + +## License + + [MIT](LICENSE) + +[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/express/master +[coveralls-url]: https://coveralls.io/r/expressjs/express?branch=master +[github-actions-ci-image]: https://badgen.net/github/checks/expressjs/express/master?label=CI +[github-actions-ci-url]: https://github.com/expressjs/express/actions/workflows/ci.yml +[npm-downloads-image]: https://badgen.net/npm/dm/express +[npm-downloads-url]: https://npmcharts.com/compare/express?minimal=true +[npm-url]: https://npmjs.org/package/express +[npm-version-image]: https://badgen.net/npm/v/express +[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/express/badge +[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/express +[Code of Conduct]: https://github.com/expressjs/.github/blob/HEAD/CODE_OF_CONDUCT.md +[Contributing Guide]: https://github.com/expressjs/.github/blob/HEAD/CONTRIBUTING.md diff --git a/apps/api/SECURITY.md b/apps/api/SECURITY.md new file mode 100644 index 0000000..ff106d6 --- /dev/null +++ b/apps/api/SECURITY.md @@ -0,0 +1,56 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for the Express +project. + + * [Reporting a Bug](#reporting-a-bug) + * [Disclosure Policy](#disclosure-policy) + * [Comments on this Policy](#comments-on-this-policy) + +## Reporting a Bug + +The Express team and community take all security bugs in Express seriously. +Thank you for improving the security of Express. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by emailing `express-security@lists.openjsf.org`. + +To ensure the timely response to your report, please ensure that the entirety +of the report is contained within the email body and not solely behind a web +link or an attachment. + +The lead maintainer will acknowledge your email within 48 hours, and will send a +more detailed response within 48 hours indicating the next steps in handling +your report. After the initial reply to your report, the security team will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. + +Report security bugs in third-party modules to the person or team maintaining +the module. + +## Pre-release Versions + +Alpha and Beta releases are unstable and **not suitable for production use**. +Vulnerabilities found in pre-releases should be reported according to the [Reporting a Bug](#reporting-a-bug) section. +Due to the unstable nature of the branch it is not guaranteed that any fixes will be released in the next pre-release. + +## Disclosure Policy + +When the security team receives a security bug report, they will assign it to a +primary handler. This person will coordinate the fix and release process, +involving the following steps: + + * Confirm the problem and determine the affected versions. + * Audit code to find any potential similar problems. + * Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible to npm. + +## The Express Threat Model + +We are currently working on a new version of the security model, the most updated version can be found [here](https://github.com/expressjs/security-wg/blob/main/docs/ThreatModel.md) + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/apps/api/benchmarks/Makefile b/apps/api/benchmarks/Makefile new file mode 100644 index 0000000..ed1ddfc --- /dev/null +++ b/apps/api/benchmarks/Makefile @@ -0,0 +1,17 @@ + +all: + @./run 1 middleware 50 + @./run 5 middleware 50 + @./run 10 middleware 50 + @./run 15 middleware 50 + @./run 20 middleware 50 + @./run 30 middleware 50 + @./run 50 middleware 50 + @./run 100 middleware 50 + @./run 10 middleware 100 + @./run 10 middleware 250 + @./run 10 middleware 500 + @./run 10 middleware 1000 + @echo + +.PHONY: all diff --git a/apps/api/benchmarks/README.md b/apps/api/benchmarks/README.md new file mode 100644 index 0000000..1894c52 --- /dev/null +++ b/apps/api/benchmarks/README.md @@ -0,0 +1,34 @@ +# Express Benchmarks + +## Installation + +You will need to install [wrk](https://github.com/wg/wrk/blob/master/INSTALL) in order to run the benchmarks. + +## Running + +To run the benchmarks, first install the dependencies `npm i`, then run `make` + +The output will look something like this: + +``` + 50 connections + 1 middleware + 7.15ms + 6784.01 + + [...redacted...] + + 1000 connections + 10 middleware + 139.21ms + 6155.19 + +``` + +### Tip: Include Node.js version in output + +You can use `make && node -v` to include the node.js version in the output. + +### Tip: Save the results to a file + +You can use `make > results.log` to save the results to a file `results.log`. diff --git a/apps/api/benchmarks/middleware.js b/apps/api/benchmarks/middleware.js new file mode 100644 index 0000000..fed97ba --- /dev/null +++ b/apps/api/benchmarks/middleware.js @@ -0,0 +1,20 @@ + +var express = require('..'); +var app = express(); + +// number of middleware + +var n = parseInt(process.env.MW || '1', 10); +console.log(' %s middleware', n); + +while (n--) { + app.use(function(req, res, next){ + next(); + }); +} + +app.use(function(req, res){ + res.send('Hello World') +}); + +app.listen(3333); diff --git a/apps/api/benchmarks/run b/apps/api/benchmarks/run new file mode 100755 index 0000000..ec8f55d --- /dev/null +++ b/apps/api/benchmarks/run @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +echo +MW=$1 node $2 & +pid=$! + +echo " $3 connections" + +sleep 2 + +wrk 'http://localhost:3333/?foo[bar]=baz' \ + -d 3 \ + -c $3 \ + -t 8 \ + | grep 'Requests/sec\|Latency' \ + | awk '{ print " " $2 }' + +kill $pid diff --git a/apps/api/examples/README.md b/apps/api/examples/README.md new file mode 100644 index 0000000..bd1f1f6 --- /dev/null +++ b/apps/api/examples/README.md @@ -0,0 +1,29 @@ +# Express examples + +This page contains list of examples using Express. + +- [auth](./auth) - Authentication with login and password +- [content-negotiation](./content-negotiation) - HTTP content negotiation +- [cookie-sessions](./cookie-sessions) - Working with cookie-based sessions +- [cookies](./cookies) - Working with cookies +- [downloads](./downloads) - Transferring files to client +- [ejs](./ejs) - Working with Embedded JavaScript templating (ejs) +- [error-pages](./error-pages) - Creating error pages +- [error](./error) - Working with error middleware +- [hello-world](./hello-world) - Simple request handler +- [markdown](./markdown) - Markdown as template engine +- [multi-router](./multi-router) - Working with multiple Express routers +- [mvc](./mvc) - MVC-style controllers +- [online](./online) - Tracking online user activity with `online` and `redis` packages +- [params](./params) - Working with route parameters +- [resource](./resource) - Multiple HTTP operations on the same resource +- [route-map](./route-map) - Organizing routes using a map +- [route-middleware](./route-middleware) - Working with route middleware +- [route-separation](./route-separation) - Organizing routes per each resource +- [search](./search) - Search API +- [session](./session) - User sessions +- [static-files](./static-files) - Serving static files +- [vhost](./vhost) - Working with virtual hosts +- [view-constructor](./view-constructor) - Rendering views dynamically +- [view-locals](./view-locals) - Saving data in request object between middleware calls +- [web-service](./web-service) - Simple API service diff --git a/apps/api/examples/auth/index.js b/apps/api/examples/auth/index.js new file mode 100644 index 0000000..40b73e6 --- /dev/null +++ b/apps/api/examples/auth/index.js @@ -0,0 +1,134 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../..'); +var hash = require('pbkdf2-password')() +var path = require('node:path'); +var session = require('express-session'); + +var app = module.exports = express(); + +// config + +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// middleware + +app.use(express.urlencoded()) +app.use(session({ + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + secret: 'shhhh, very secret' +})); + +// Session-persisted message middleware + +app.use(function(req, res, next){ + var err = req.session.error; + var msg = req.session.success; + delete req.session.error; + delete req.session.success; + res.locals.message = ''; + if (err) res.locals.message = '

' + err + '

'; + if (msg) res.locals.message = '

' + msg + '

'; + next(); +}); + +// dummy database + +var users = { + tj: { name: 'tj' } +}; + +// when you create a user, generate a salt +// and hash the password ('foobar' is the pass here) + +hash({ password: 'foobar' }, function (err, pass, salt, hash) { + if (err) throw err; + // store the salt & hash in the "db" + users.tj.salt = salt; + users.tj.hash = hash; +}); + + +// Authenticate using our plain-object database of doom! + +function authenticate(name, pass, fn) { + if (!module.parent) console.log('authenticating %s:%s', name, pass); + var user = users[name]; + // query the db for the given username + if (!user) return fn(null, null) + // apply the same algorithm to the POSTed password, applying + // the hash against the pass / salt, if there is a match we + // found the user + hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) { + if (err) return fn(err); + if (hash === user.hash) return fn(null, user) + fn(null, null) + }); +} + +function restrict(req, res, next) { + if (req.session.user) { + next(); + } else { + req.session.error = 'Access denied!'; + res.redirect('/login'); + } +} + +app.get('/', function(req, res){ + res.redirect('/login'); +}); + +app.get('/restricted', restrict, function(req, res){ + res.send('Wahoo! restricted area, click to logout'); +}); + +app.get('/logout', function(req, res){ + // destroy the user's session to log them out + // will be re-created next request + req.session.destroy(function(){ + res.redirect('/'); + }); +}); + +app.get('/login', function(req, res){ + res.render('login'); +}); + +app.post('/login', function (req, res, next) { + if (!req.body) return res.sendStatus(400) + authenticate(req.body.username, req.body.password, function(err, user){ + if (err) return next(err) + if (user) { + // Regenerate session when signing in + // to prevent fixation + req.session.regenerate(function(){ + // Store the user's primary key + // in the session store to be retrieved, + // or in this case the entire user object + req.session.user = user; + req.session.success = 'Authenticated as ' + user.name + + ' click to logout. ' + + ' You may now access /restricted.'; + res.redirect(req.get('Referrer') || '/'); + }); + } else { + req.session.error = 'Authentication failed, please check your ' + + ' username and password.' + + ' (use "tj" and "foobar")'; + res.redirect('/login'); + } + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/auth/views/foot.ejs b/apps/api/examples/auth/views/foot.ejs new file mode 100644 index 0000000..b605728 --- /dev/null +++ b/apps/api/examples/auth/views/foot.ejs @@ -0,0 +1,2 @@ + + diff --git a/apps/api/examples/auth/views/head.ejs b/apps/api/examples/auth/views/head.ejs new file mode 100644 index 0000000..c623b5c --- /dev/null +++ b/apps/api/examples/auth/views/head.ejs @@ -0,0 +1,20 @@ + + + + + + <%= title %> + + + diff --git a/apps/api/examples/auth/views/login.ejs b/apps/api/examples/auth/views/login.ejs new file mode 100644 index 0000000..181c36c --- /dev/null +++ b/apps/api/examples/auth/views/login.ejs @@ -0,0 +1,21 @@ + +<%- include('head', { title: 'Authentication Example' }) -%> + +

Login

+<%- message %> +Try accessing /restricted, then authenticate with "tj" and "foobar". +
+

+ + +

+

+ + +

+

+ +

+
+ +<%- include('foot') -%> diff --git a/apps/api/examples/content-negotiation/db.js b/apps/api/examples/content-negotiation/db.js new file mode 100644 index 0000000..f59b23b --- /dev/null +++ b/apps/api/examples/content-negotiation/db.js @@ -0,0 +1,9 @@ +'use strict' + +var users = []; + +users.push({ name: 'Tobi' }); +users.push({ name: 'Loki' }); +users.push({ name: 'Jane' }); + +module.exports = users; diff --git a/apps/api/examples/content-negotiation/index.js b/apps/api/examples/content-negotiation/index.js new file mode 100644 index 0000000..280a4e2 --- /dev/null +++ b/apps/api/examples/content-negotiation/index.js @@ -0,0 +1,46 @@ +'use strict' + +var express = require('../../'); +var app = module.exports = express(); +var users = require('./db'); + +// so either you can deal with different types of formatting +// for expected response in index.js +app.get('/', function(req, res){ + res.format({ + html: function(){ + res.send('
    ' + users.map(function(user){ + return '
  • ' + user.name + '
  • '; + }).join('') + '
'); + }, + + text: function(){ + res.send(users.map(function(user){ + return ' - ' + user.name + '\n'; + }).join('')); + }, + + json: function(){ + res.json(users); + } + }); +}); + +// or you could write a tiny middleware like +// this to add a layer of abstraction +// and make things a bit more declarative: + +function format(path) { + var obj = require(path); + return function(req, res){ + res.format(obj); + }; +} + +app.get('/users', format('./users')); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/content-negotiation/users.js b/apps/api/examples/content-negotiation/users.js new file mode 100644 index 0000000..fe703e7 --- /dev/null +++ b/apps/api/examples/content-negotiation/users.js @@ -0,0 +1,19 @@ +'use strict' + +var users = require('./db'); + +exports.html = function(req, res){ + res.send('
    ' + users.map(function(user){ + return '
  • ' + user.name + '
  • '; + }).join('') + '
'); +}; + +exports.text = function(req, res){ + res.send(users.map(function(user){ + return ' - ' + user.name + '\n'; + }).join('')); +}; + +exports.json = function(req, res){ + res.json(users); +}; diff --git a/apps/api/examples/cookie-sessions/index.js b/apps/api/examples/cookie-sessions/index.js new file mode 100644 index 0000000..83b6fae --- /dev/null +++ b/apps/api/examples/cookie-sessions/index.js @@ -0,0 +1,25 @@ +'use strict' + +/** + * Module dependencies. + */ + +var cookieSession = require('cookie-session'); +var express = require('../../'); + +var app = module.exports = express(); + +// add req.session cookie support +app.use(cookieSession({ secret: 'manny is cool' })); + +// do something with the session +app.get('/', function (req, res) { + req.session.count = (req.session.count || 0) + 1 + res.send('viewed ' + req.session.count + ' times\n') +}) + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/cookies/index.js b/apps/api/examples/cookies/index.js new file mode 100644 index 0000000..0620cb4 --- /dev/null +++ b/apps/api/examples/cookies/index.js @@ -0,0 +1,53 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); +var app = module.exports = express(); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); + +// custom log format +if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) + +// parses request cookies, populating +// req.cookies and req.signedCookies +// when the secret is passed, used +// for signing the cookies. +app.use(cookieParser('my secret here')); + +// parses x-www-form-urlencoded +app.use(express.urlencoded()) + +app.get('/', function(req, res){ + if (req.cookies.remember) { + res.send('Remembered :). Click to forget!.'); + } else { + res.send('

Check to ' + + '.

'); + } +}); + +app.get('/forget', function(req, res){ + res.clearCookie('remember'); + res.redirect(req.get('Referrer') || '/'); +}); + +app.post('/', function(req, res){ + var minute = 60000; + + if (req.body && req.body.remember) { + res.cookie('remember', 1, { maxAge: minute }) + } + + res.redirect(req.get('Referrer') || '/'); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/downloads/files/CCTV大赛上海分赛区.txt b/apps/api/examples/downloads/files/CCTV大赛上海分赛区.txt new file mode 100644 index 0000000..3b049c3 --- /dev/null +++ b/apps/api/examples/downloads/files/CCTV大赛上海分赛区.txt @@ -0,0 +1,2 @@ +Only for test. +The file name is faked. \ No newline at end of file diff --git a/apps/api/examples/downloads/files/amazing.txt b/apps/api/examples/downloads/files/amazing.txt new file mode 100644 index 0000000..c478ec5 --- /dev/null +++ b/apps/api/examples/downloads/files/amazing.txt @@ -0,0 +1 @@ +what an amazing download \ No newline at end of file diff --git a/apps/api/examples/downloads/files/notes/groceries.txt b/apps/api/examples/downloads/files/notes/groceries.txt new file mode 100644 index 0000000..fb43877 --- /dev/null +++ b/apps/api/examples/downloads/files/notes/groceries.txt @@ -0,0 +1,3 @@ +* milk +* eggs +* bread diff --git a/apps/api/examples/downloads/index.js b/apps/api/examples/downloads/index.js new file mode 100644 index 0000000..ddc549f --- /dev/null +++ b/apps/api/examples/downloads/index.js @@ -0,0 +1,40 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); +var path = require('node:path'); + +var app = module.exports = express(); + +// path to where the files are stored on disk +var FILES_DIR = path.join(__dirname, 'files') + +app.get('/', function(req, res){ + res.send('') +}); + +// /files/* is accessed via req.params[0] +// but here we name it :file +app.get('/files/*file', function (req, res, next) { + res.download(req.params.file.join('/'), { root: FILES_DIR }, function (err) { + if (!err) return; // file sent + if (err.status !== 404) return next(err); // non-404 error + // file for download not found + res.statusCode = 404; + res.send('Cant find that file, sorry!'); + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/ejs/index.js b/apps/api/examples/ejs/index.js new file mode 100644 index 0000000..0940d06 --- /dev/null +++ b/apps/api/examples/ejs/index.js @@ -0,0 +1,57 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); +var path = require('node:path'); + +var app = module.exports = express(); + +// Register ejs as .html. If we did +// not call this, we would need to +// name our views foo.ejs instead +// of foo.html. The __express method +// is simply a function that engines +// use to hook into the Express view +// system by default, so if we want +// to change "foo.ejs" to "foo.html" +// we simply pass _any_ function, in this +// case `ejs.__express`. + +app.engine('.html', require('ejs').__express); + +// Optional since express defaults to CWD/views + +app.set('views', path.join(__dirname, 'views')); + +// Path to our public directory + +app.use(express.static(path.join(__dirname, 'public'))); + +// Without this you would need to +// supply the extension to res.render() +// ex: res.render('users.html'). +app.set('view engine', 'html'); + +// Dummy users +var users = [ + { name: 'tobi', email: 'tobi@learnboost.com' }, + { name: 'loki', email: 'loki@learnboost.com' }, + { name: 'jane', email: 'jane@learnboost.com' } +]; + +app.get('/', function(req, res){ + res.render('users', { + users: users, + title: "EJS example", + header: "Some users" + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/ejs/public/stylesheets/style.css b/apps/api/examples/ejs/public/stylesheets/style.css new file mode 100644 index 0000000..db60b89 --- /dev/null +++ b/apps/api/examples/ejs/public/stylesheets/style.css @@ -0,0 +1,4 @@ +body { + padding: 50px 80px; + font: 14px "Helvetica Neue", "Lucida Grande", Arial, sans-serif; +} diff --git a/apps/api/examples/ejs/views/footer.html b/apps/api/examples/ejs/views/footer.html new file mode 100644 index 0000000..308b1d0 --- /dev/null +++ b/apps/api/examples/ejs/views/footer.html @@ -0,0 +1,2 @@ + + diff --git a/apps/api/examples/ejs/views/header.html b/apps/api/examples/ejs/views/header.html new file mode 100644 index 0000000..c642a15 --- /dev/null +++ b/apps/api/examples/ejs/views/header.html @@ -0,0 +1,9 @@ + + + + + + <%= title %> + + + diff --git a/apps/api/examples/ejs/views/users.html b/apps/api/examples/ejs/views/users.html new file mode 100644 index 0000000..dad2462 --- /dev/null +++ b/apps/api/examples/ejs/views/users.html @@ -0,0 +1,10 @@ +<%- include('header.html') -%> + +

Users

+
    + <% users.forEach(function(user){ %> +
  • <%= user.name %> <<%= user.email %>>
  • + <% }) %> +
+ +<%- include('footer.html') -%> diff --git a/apps/api/examples/error-pages/index.js b/apps/api/examples/error-pages/index.js new file mode 100644 index 0000000..0863120 --- /dev/null +++ b/apps/api/examples/error-pages/index.js @@ -0,0 +1,103 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); +var path = require('node:path'); +var app = module.exports = express(); +var logger = require('morgan'); +var silent = process.env.NODE_ENV === 'test' + +// general config +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// our custom "verbose errors" setting +// which we can use in the templates +// via settings['verbose errors'] +app.enable('verbose errors'); + +// disable them in production +// use $ NODE_ENV=production node examples/error-pages +if (app.settings.env === 'production') app.disable('verbose errors') + +silent || app.use(logger('dev')); + +// Routes + +app.get('/', function(req, res){ + res.render('index.ejs'); +}); + +app.get('/404', function(req, res, next){ + // trigger a 404 since no other middleware + // will match /404 after this one, and we're not + // responding here + next(); +}); + +app.get('/403', function(req, res, next){ + // trigger a 403 error + var err = new Error('not allowed!'); + err.status = 403; + next(err); +}); + +app.get('/500', function(req, res, next){ + // trigger a generic (500) error + next(new Error('keyboard cat!')); +}); + +// Error handlers + +// Since this is the last non-error-handling +// middleware use()d, we assume 404, as nothing else +// responded. + +// $ curl http://localhost:3000/notfound +// $ curl http://localhost:3000/notfound -H "Accept: application/json" +// $ curl http://localhost:3000/notfound -H "Accept: text/plain" + +app.use(function(req, res, next){ + res.status(404); + + res.format({ + html: function () { + res.render('404', { url: req.url }) + }, + json: function () { + res.json({ error: 'Not found' }) + }, + default: function () { + res.type('txt').send('Not found') + } + }) +}); + +// error-handling middleware, take the same form +// as regular middleware, however they require an +// arity of 4, aka the signature (err, req, res, next). +// when connect has an error, it will invoke ONLY error-handling +// middleware. + +// If we were to next() here any remaining non-error-handling +// middleware would then be executed, or if we next(err) to +// continue passing the error, only error-handling middleware +// would remain being executed, however here +// we simply respond with an error page. + +app.use(function(err, req, res, next){ + // we may use properties of the error object + // here and next(err) appropriately, or if + // we possibly recovered from the error, simply next(). + res.status(err.status || 500); + res.render('500', { error: err }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/error-pages/views/404.ejs b/apps/api/examples/error-pages/views/404.ejs new file mode 100644 index 0000000..d992ce0 --- /dev/null +++ b/apps/api/examples/error-pages/views/404.ejs @@ -0,0 +1,3 @@ +<%- include('error_header') -%> +

Cannot find <%= url %>

+<%- include('footer') -%> diff --git a/apps/api/examples/error-pages/views/500.ejs b/apps/api/examples/error-pages/views/500.ejs new file mode 100644 index 0000000..c9d855f --- /dev/null +++ b/apps/api/examples/error-pages/views/500.ejs @@ -0,0 +1,8 @@ +<%- include('error_header') -%> +

Error: <%= error.message %>

+<% if (settings['verbose errors']) { %> +
<%= error.stack %>
+<% } else { %> +

An error occurred!

+<% } %> +<%- include('footer') -%> diff --git a/apps/api/examples/error-pages/views/error_header.ejs b/apps/api/examples/error-pages/views/error_header.ejs new file mode 100644 index 0000000..b2451ab --- /dev/null +++ b/apps/api/examples/error-pages/views/error_header.ejs @@ -0,0 +1,10 @@ + + + + + +Error + + + +

An error occurred!

diff --git a/apps/api/examples/error-pages/views/footer.ejs b/apps/api/examples/error-pages/views/footer.ejs new file mode 100644 index 0000000..308b1d0 --- /dev/null +++ b/apps/api/examples/error-pages/views/footer.ejs @@ -0,0 +1,2 @@ + + diff --git a/apps/api/examples/error-pages/views/index.ejs b/apps/api/examples/error-pages/views/index.ejs new file mode 100644 index 0000000..ae8c928 --- /dev/null +++ b/apps/api/examples/error-pages/views/index.ejs @@ -0,0 +1,20 @@ + + + + + +Custom Pages Example + + + +

My Site

+

Pages Example

+ + + + + diff --git a/apps/api/examples/error/index.js b/apps/api/examples/error/index.js new file mode 100644 index 0000000..d733a81 --- /dev/null +++ b/apps/api/examples/error/index.js @@ -0,0 +1,53 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); +var logger = require('morgan'); +var app = module.exports = express(); +var test = app.get('env') === 'test' + +if (!test) app.use(logger('dev')); + +// error handling middleware have an arity of 4 +// instead of the typical (req, res, next), +// otherwise they behave exactly like regular +// middleware, you may have several of them, +// in different orders etc. + +function error(err, req, res, next) { + // log it + if (!test) console.error(err.stack); + + // respond with 500 "Internal Server Error". + res.status(500); + res.send('Internal Server Error'); +} + +app.get('/', function () { + // Caught and passed down to the errorHandler middleware + throw new Error('something broke!'); +}); + +app.get('/next', function(req, res, next){ + // We can also pass exceptions to next() + // The reason for process.nextTick() is to show that + // next() can be called inside an async operation, + // in real life it can be a DB read or HTTP request. + process.nextTick(function(){ + next(new Error('oh no!')); + }); +}); + +// the error handler is placed after routes +// if it were above it would not receive errors +// from app.get() etc +app.use(error); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/hello-world/index.js b/apps/api/examples/hello-world/index.js new file mode 100644 index 0000000..8c1855c --- /dev/null +++ b/apps/api/examples/hello-world/index.js @@ -0,0 +1,15 @@ +'use strict' + +var express = require('../../'); + +var app = module.exports = express() + +app.get('/', function(req, res){ + res.send('Hello World'); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/markdown/index.js b/apps/api/examples/markdown/index.js new file mode 100644 index 0000000..53e40ac --- /dev/null +++ b/apps/api/examples/markdown/index.js @@ -0,0 +1,44 @@ +'use strict' + +/** + * Module dependencies. + */ + +var escapeHtml = require('escape-html'); +var express = require('../..'); +var fs = require('node:fs'); +var marked = require('marked'); +var path = require('node:path'); + +var app = module.exports = express(); + +// register .md as an engine in express view system + +app.engine('md', function(path, options, fn){ + fs.readFile(path, 'utf8', function(err, str){ + if (err) return fn(err); + var html = marked.parse(str).replace(/\{([^}]+)\}/g, function(_, name){ + return escapeHtml(options[name] || ''); + }); + fn(null, html); + }); +}); + +app.set('views', path.join(__dirname, 'views')); + +// make it the default, so we don't need .md +app.set('view engine', 'md'); + +app.get('/', function(req, res){ + res.render('index', { title: 'Markdown Example' }); +}); + +app.get('/fail', function(req, res){ + res.render('missing', { title: 'Markdown Example' }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/markdown/views/index.md b/apps/api/examples/markdown/views/index.md new file mode 100644 index 0000000..b0b2e71 --- /dev/null +++ b/apps/api/examples/markdown/views/index.md @@ -0,0 +1,4 @@ + +# {title} + +Just an example view rendered with _markdown_. \ No newline at end of file diff --git a/apps/api/examples/multi-router/controllers/api_v1.js b/apps/api/examples/multi-router/controllers/api_v1.js new file mode 100644 index 0000000..a301e3e --- /dev/null +++ b/apps/api/examples/multi-router/controllers/api_v1.js @@ -0,0 +1,15 @@ +'use strict' + +var express = require('../../..'); + +var apiv1 = express.Router(); + +apiv1.get('/', function(req, res) { + res.send('Hello from APIv1 root route.'); +}); + +apiv1.get('/users', function(req, res) { + res.send('List of APIv1 users.'); +}); + +module.exports = apiv1; diff --git a/apps/api/examples/multi-router/controllers/api_v2.js b/apps/api/examples/multi-router/controllers/api_v2.js new file mode 100644 index 0000000..e997fb1 --- /dev/null +++ b/apps/api/examples/multi-router/controllers/api_v2.js @@ -0,0 +1,15 @@ +'use strict' + +var express = require('../../..'); + +var apiv2 = express.Router(); + +apiv2.get('/', function(req, res) { + res.send('Hello from APIv2 root route.'); +}); + +apiv2.get('/users', function(req, res) { + res.send('List of APIv2 users.'); +}); + +module.exports = apiv2; diff --git a/apps/api/examples/multi-router/index.js b/apps/api/examples/multi-router/index.js new file mode 100644 index 0000000..dbfd284 --- /dev/null +++ b/apps/api/examples/multi-router/index.js @@ -0,0 +1,18 @@ +'use strict' + +var express = require('../..'); + +var app = module.exports = express(); + +app.use('/api/v1', require('./controllers/api_v1')); +app.use('/api/v2', require('./controllers/api_v2')); + +app.get('/', function(req, res) { + res.send('Hello from root route.') +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/mvc/controllers/main/index.js b/apps/api/examples/mvc/controllers/main/index.js new file mode 100644 index 0000000..74cde19 --- /dev/null +++ b/apps/api/examples/mvc/controllers/main/index.js @@ -0,0 +1,5 @@ +'use strict' + +exports.index = function(req, res){ + res.redirect('/users'); +}; diff --git a/apps/api/examples/mvc/controllers/pet/index.js b/apps/api/examples/mvc/controllers/pet/index.js new file mode 100644 index 0000000..214160f --- /dev/null +++ b/apps/api/examples/mvc/controllers/pet/index.js @@ -0,0 +1,31 @@ +'use strict' + +/** + * Module dependencies. + */ + +var db = require('../../db'); + +exports.engine = 'ejs'; + +exports.before = function(req, res, next){ + var pet = db.pets[req.params.pet_id]; + if (!pet) return next('route'); + req.pet = pet; + next(); +}; + +exports.show = function(req, res, next){ + res.render('show', { pet: req.pet }); +}; + +exports.edit = function(req, res, next){ + res.render('edit', { pet: req.pet }); +}; + +exports.update = function(req, res, next){ + var body = req.body; + req.pet.name = body.pet.name; + res.message('Information updated!'); + res.redirect('/pet/' + req.pet.id); +}; diff --git a/apps/api/examples/mvc/controllers/pet/views/edit.ejs b/apps/api/examples/mvc/controllers/pet/views/edit.ejs new file mode 100644 index 0000000..655666e --- /dev/null +++ b/apps/api/examples/mvc/controllers/pet/views/edit.ejs @@ -0,0 +1,17 @@ + + + + + + +Edit <%= pet.name %> + + + +

<%= pet.name %>

+
+ + +
+ + diff --git a/apps/api/examples/mvc/controllers/pet/views/show.ejs b/apps/api/examples/mvc/controllers/pet/views/show.ejs new file mode 100644 index 0000000..7e1e338 --- /dev/null +++ b/apps/api/examples/mvc/controllers/pet/views/show.ejs @@ -0,0 +1,15 @@ + + + + + + +<%= pet.name %> + + + +

<%= pet.name %> edit

+ +

You are viewing <%= pet.name %>

+ + diff --git a/apps/api/examples/mvc/controllers/user-pet/index.js b/apps/api/examples/mvc/controllers/user-pet/index.js new file mode 100644 index 0000000..42a29ad --- /dev/null +++ b/apps/api/examples/mvc/controllers/user-pet/index.js @@ -0,0 +1,22 @@ +'use strict' + +/** + * Module dependencies. + */ + +var db = require('../../db'); + +exports.name = 'pet'; +exports.prefix = '/user/:user_id'; + +exports.create = function(req, res, next){ + var id = req.params.user_id; + var user = db.users[id]; + var body = req.body; + if (!user) return next('route'); + var pet = { name: body.pet.name }; + pet.id = db.pets.push(pet) - 1; + user.pets.push(pet); + res.message('Added pet ' + body.pet.name); + res.redirect('/user/' + id); +}; diff --git a/apps/api/examples/mvc/controllers/user/index.js b/apps/api/examples/mvc/controllers/user/index.js new file mode 100644 index 0000000..ec3ae4c --- /dev/null +++ b/apps/api/examples/mvc/controllers/user/index.js @@ -0,0 +1,41 @@ +'use strict' + +/** + * Module dependencies. + */ + +var db = require('../../db'); + +exports.engine = 'hbs'; + +exports.before = function(req, res, next){ + var id = req.params.user_id; + if (!id) return next(); + // pretend to query a database... + process.nextTick(function(){ + req.user = db.users[id]; + // cant find that user + if (!req.user) return next('route'); + // found it, move on to the routes + next(); + }); +}; + +exports.list = function(req, res, next){ + res.render('list', { users: db.users }); +}; + +exports.edit = function(req, res, next){ + res.render('edit', { user: req.user }); +}; + +exports.show = function(req, res, next){ + res.render('show', { user: req.user }); +}; + +exports.update = function(req, res, next){ + var body = req.body; + req.user.name = body.user.name; + res.message('Information updated!'); + res.redirect('/user/' + req.user.id); +}; diff --git a/apps/api/examples/mvc/controllers/user/views/edit.hbs b/apps/api/examples/mvc/controllers/user/views/edit.hbs new file mode 100644 index 0000000..2be7ddc --- /dev/null +++ b/apps/api/examples/mvc/controllers/user/views/edit.hbs @@ -0,0 +1,27 @@ + + + + + + + Edit {{user.name}} + + +

{{user.name}}

+
+ + + +
+ +
+ + + +
+ + diff --git a/apps/api/examples/mvc/controllers/user/views/list.hbs b/apps/api/examples/mvc/controllers/user/views/list.hbs new file mode 100644 index 0000000..448c66f --- /dev/null +++ b/apps/api/examples/mvc/controllers/user/views/list.hbs @@ -0,0 +1,18 @@ + + + + + + + Users + + +

Users

+

Click a user below to view their pets.

+
    + {{#each users}} +
  • {{name}}
  • + {{/each}} +
+ + diff --git a/apps/api/examples/mvc/controllers/user/views/show.hbs b/apps/api/examples/mvc/controllers/user/views/show.hbs new file mode 100644 index 0000000..f3fccfe --- /dev/null +++ b/apps/api/examples/mvc/controllers/user/views/show.hbs @@ -0,0 +1,31 @@ + + + + + + + {{user.name}} + + +

{{user.name}} edit

+ +{{#if hasMessages}} +
    + {{#each messages}} +
  • {{this}}
  • + {{/each}} +
+{{/if}} + +{{#if user.pets.length}} +

View {{user.name}}'s pets:

+
    + {{#each user.pets}} +
  • {{name}}
  • + {{/each}} +
+{{else}} +

No pets!

+{{/if}} + + diff --git a/apps/api/examples/mvc/db.js b/apps/api/examples/mvc/db.js new file mode 100644 index 0000000..94d1480 --- /dev/null +++ b/apps/api/examples/mvc/db.js @@ -0,0 +1,16 @@ +'use strict' + +// faux database + +var pets = exports.pets = []; + +pets.push({ name: 'Tobi', id: 0 }); +pets.push({ name: 'Loki', id: 1 }); +pets.push({ name: 'Jane', id: 2 }); +pets.push({ name: 'Raul', id: 3 }); + +var users = exports.users = []; + +users.push({ name: 'TJ', pets: [pets[0], pets[1], pets[2]], id: 0 }); +users.push({ name: 'Guillermo', pets: [pets[3]], id: 1 }); +users.push({ name: 'Nathan', pets: [], id: 2 }); diff --git a/apps/api/examples/mvc/index.js b/apps/api/examples/mvc/index.js new file mode 100644 index 0000000..1d8aa0e --- /dev/null +++ b/apps/api/examples/mvc/index.js @@ -0,0 +1,95 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../..'); +var logger = require('morgan'); +var path = require('node:path'); +var session = require('express-session'); +var methodOverride = require('method-override'); + +var app = module.exports = express(); + +// set our default template engine to "ejs" +// which prevents the need for using file extensions +app.set('view engine', 'ejs'); + +// set views for error and 404 pages +app.set('views', path.join(__dirname, 'views')); + +// define a custom res.message() method +// which stores messages in the session +app.response.message = function(msg){ + // reference `req.session` via the `this.req` reference + var sess = this.req.session; + // simply add the msg to an array for later + sess.messages = sess.messages || []; + sess.messages.push(msg); + return this; +}; + +// log +if (!module.parent) app.use(logger('dev')); + +// serve static files +app.use(express.static(path.join(__dirname, 'public'))); + +// session support +app.use(session({ + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + secret: 'some secret here' +})); + +// parse request bodies (req.body) +app.use(express.urlencoded({ extended: true })) + +// allow overriding methods in query (?_method=put) +app.use(methodOverride('_method')); + +// expose the "messages" local variable when views are rendered +app.use(function(req, res, next){ + var msgs = req.session.messages || []; + + // expose "messages" local variable + res.locals.messages = msgs; + + // expose "hasMessages" + res.locals.hasMessages = !! msgs.length; + + /* This is equivalent: + res.locals({ + messages: msgs, + hasMessages: !! msgs.length + }); + */ + + next(); + // empty or "flush" the messages so they + // don't build up + req.session.messages = []; +}); + +// load controllers +require('./lib/boot')(app, { verbose: !module.parent }); + +app.use(function(err, req, res, next){ + // log it + if (!module.parent) console.error(err.stack); + + // error page + res.status(500).render('5xx'); +}); + +// assume 404 since no middleware responded +app.use(function(req, res, next){ + res.status(404).render('404', { url: req.originalUrl }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/mvc/lib/boot.js b/apps/api/examples/mvc/lib/boot.js new file mode 100644 index 0000000..fc2ab0f --- /dev/null +++ b/apps/api/examples/mvc/lib/boot.js @@ -0,0 +1,83 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../..'); +var fs = require('node:fs'); +var path = require('node:path'); + +module.exports = function(parent, options){ + var dir = path.join(__dirname, '..', 'controllers'); + var verbose = options.verbose; + fs.readdirSync(dir).forEach(function(name){ + var file = path.join(dir, name) + if (!fs.statSync(file).isDirectory()) return; + verbose && console.log('\n %s:', name); + var obj = require(file); + var name = obj.name || name; + var prefix = obj.prefix || ''; + var app = express(); + var handler; + var method; + var url; + + // allow specifying the view engine + if (obj.engine) app.set('view engine', obj.engine); + app.set('views', path.join(__dirname, '..', 'controllers', name, 'views')); + + // generate routes based + // on the exported methods + for (var key in obj) { + // "reserved" exports + if (~['name', 'prefix', 'engine', 'before'].indexOf(key)) continue; + // route exports + switch (key) { + case 'show': + method = 'get'; + url = '/' + name + '/:' + name + '_id'; + break; + case 'list': + method = 'get'; + url = '/' + name + 's'; + break; + case 'edit': + method = 'get'; + url = '/' + name + '/:' + name + '_id/edit'; + break; + case 'update': + method = 'put'; + url = '/' + name + '/:' + name + '_id'; + break; + case 'create': + method = 'post'; + url = '/' + name; + break; + case 'index': + method = 'get'; + url = '/'; + break; + default: + /* istanbul ignore next */ + throw new Error('unrecognized route: ' + name + '.' + key); + } + + // setup + handler = obj[key]; + url = prefix + url; + + // before middleware support + if (obj.before) { + app[method](url, obj.before, handler); + verbose && console.log(' %s %s -> before -> %s', method.toUpperCase(), url, key); + } else { + app[method](url, handler); + verbose && console.log(' %s %s -> %s', method.toUpperCase(), url, key); + } + } + + // mount the app + parent.use(app); + }); +}; diff --git a/apps/api/examples/mvc/public/style.css b/apps/api/examples/mvc/public/style.css new file mode 100644 index 0000000..8a23f9d --- /dev/null +++ b/apps/api/examples/mvc/public/style.css @@ -0,0 +1,14 @@ +body { + padding: 50px; + font: 16px "Helvetica Neue", Helvetica, Arial, sans-serif; +} +a { + color: #107aff; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +h1 a { + font-size: 16px; +} diff --git a/apps/api/examples/mvc/views/404.ejs b/apps/api/examples/mvc/views/404.ejs new file mode 100644 index 0000000..21a86f8 --- /dev/null +++ b/apps/api/examples/mvc/views/404.ejs @@ -0,0 +1,13 @@ + + + + + + Not Found + + + +

404: Not Found

+

Sorry we can't find <%= url %>

+ + diff --git a/apps/api/examples/mvc/views/5xx.ejs b/apps/api/examples/mvc/views/5xx.ejs new file mode 100644 index 0000000..190f580 --- /dev/null +++ b/apps/api/examples/mvc/views/5xx.ejs @@ -0,0 +1,13 @@ + + + + + + Internal Server Error + + + +

500: Internal Server Error

+

Looks like something blew up!

+ + diff --git a/apps/api/examples/online/index.js b/apps/api/examples/online/index.js new file mode 100644 index 0000000..0b5fdff --- /dev/null +++ b/apps/api/examples/online/index.js @@ -0,0 +1,61 @@ +'use strict' + +// install redis first: +// https://redis.io/ + +// then: +// $ npm install redis online +// $ redis-server + +/** + * Module dependencies. + */ + +var express = require('../..'); +var online = require('online'); +var redis = require('redis'); +var db = redis.createClient(); + +// online + +online = online(db); + +// app + +var app = express(); + +// activity tracking, in this case using +// the UA string, you would use req.user.id etc + +app.use(function(req, res, next){ + // fire-and-forget + online.add(req.headers['user-agent']); + next(); +}); + +/** + * List helper. + */ + +function list(ids) { + return '
    ' + ids.map(function(id){ + return '
  • ' + id + '
  • '; + }).join('') + '
'; +} + +/** + * GET users online. + */ + +app.get('/', function(req, res, next){ + online.last(5, function(err, ids){ + if (err) return next(err); + res.send('

Users online: ' + ids.length + '

' + list(ids)); + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/params/index.js b/apps/api/examples/params/index.js new file mode 100644 index 0000000..11eef51 --- /dev/null +++ b/apps/api/examples/params/index.js @@ -0,0 +1,74 @@ +'use strict' + +/** + * Module dependencies. + */ + +var createError = require('http-errors') +var express = require('../../'); +var app = module.exports = express(); + +// Faux database + +var users = [ + { name: 'tj' } + , { name: 'tobi' } + , { name: 'loki' } + , { name: 'jane' } + , { name: 'bandit' } +]; + +// Convert :to and :from to integers + +app.param(['to', 'from'], function(req, res, next, num, name){ + req.params[name] = parseInt(num, 10); + if( isNaN(req.params[name]) ){ + next(createError(400, 'failed to parseInt '+num)); + } else { + next(); + } +}); + +// Load user by id + +app.param('user', function(req, res, next, id){ + req.user = users[id] + if (req.user) { + next(); + } else { + next(createError(404, 'failed to find user')); + } +}); + +/** + * GET index. + */ + +app.get('/', function(req, res){ + res.send('Visit /user/0 or /users/0-2'); +}); + +/** + * GET :user. + */ + +app.get('/user/:user', function (req, res) { + res.send('user ' + req.user.name); +}); + +/** + * GET users :from - :to. + */ + +app.get('/users/:from-:to', function (req, res) { + var from = req.params.from; + var to = req.params.to; + var names = users.map(function(user){ return user.name; }); + res.send('users ' + names.slice(from, to + 1).join(', ')); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/resource/index.js b/apps/api/examples/resource/index.js new file mode 100644 index 0000000..627ab24 --- /dev/null +++ b/apps/api/examples/resource/index.js @@ -0,0 +1,95 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); + +var app = module.exports = express(); + +// Ad-hoc example resource method + +app.resource = function(path, obj) { + this.get(path, obj.index); + this.get(path + '/:a..:b{.:format}', function(req, res){ + var a = parseInt(req.params.a, 10); + var b = parseInt(req.params.b, 10); + var format = req.params.format; + obj.range(req, res, a, b, format); + }); + this.get(path + '/:id', obj.show); + this.delete(path + '/:id', function(req, res){ + var id = parseInt(req.params.id, 10); + obj.destroy(req, res, id); + }); +}; + +// Fake records + +var users = [ + { name: 'tj' } + , { name: 'ciaran' } + , { name: 'aaron' } + , { name: 'guillermo' } + , { name: 'simon' } + , { name: 'tobi' } +]; + +// Fake controller. + +var User = { + index: function(req, res){ + res.send(users); + }, + show: function(req, res){ + res.send(users[req.params.id] || { error: 'Cannot find user' }); + }, + destroy: function(req, res, id){ + var destroyed = id in users; + delete users[id]; + res.send(destroyed ? 'destroyed' : 'Cannot find user'); + }, + range: function(req, res, a, b, format){ + var range = users.slice(a, b + 1); + switch (format) { + case 'json': + res.send(range); + break; + case 'html': + default: + var html = '
    ' + range.map(function(user){ + return '
  • ' + user.name + '
  • '; + }).join('\n') + '
'; + res.send(html); + break; + } + } +}; + +// curl http://localhost:3000/users -- responds with all users +// curl http://localhost:3000/users/1 -- responds with user 1 +// curl http://localhost:3000/users/4 -- responds with error +// curl http://localhost:3000/users/1..3 -- responds with several users +// curl -X DELETE http://localhost:3000/users/1 -- deletes the user + +app.resource('/users', User); + +app.get('/', function(req, res){ + res.send([ + '

Examples:

    ' + , '
  • GET /users
  • ' + , '
  • GET /users/1
  • ' + , '
  • GET /users/3
  • ' + , '
  • GET /users/1..3
  • ' + , '
  • GET /users/1..3.json
  • ' + , '
  • DELETE /users/4
  • ' + , '
' + ].join('\n')); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/route-map/index.js b/apps/api/examples/route-map/index.js new file mode 100644 index 0000000..2bc28bd --- /dev/null +++ b/apps/api/examples/route-map/index.js @@ -0,0 +1,75 @@ +'use strict' + +/** + * Module dependencies. + */ + +var escapeHtml = require('escape-html') +var express = require('../../lib/express'); + +var verbose = process.env.NODE_ENV !== 'test' + +var app = module.exports = express(); + +app.map = function(a, route){ + route = route || ''; + for (var key in a) { + switch (typeof a[key]) { + // { '/path': { ... }} + case 'object': + app.map(a[key], route + key); + break; + // get: function(){ ... } + case 'function': + if (verbose) console.log('%s %s', key, route); + app[key](route, a[key]); + break; + } + } +}; + +var users = { + list: function(req, res){ + res.send('user list'); + }, + + get: function(req, res){ + res.send('user ' + escapeHtml(req.params.uid)) + }, + + delete: function(req, res){ + res.send('delete users'); + } +}; + +var pets = { + list: function(req, res){ + res.send('user ' + escapeHtml(req.params.uid) + '\'s pets') + }, + + delete: function(req, res){ + res.send('delete ' + escapeHtml(req.params.uid) + '\'s pet ' + escapeHtml(req.params.pid)) + } +}; + +app.map({ + '/users': { + get: users.list, + delete: users.delete, + '/:uid': { + get: users.get, + '/pets': { + get: pets.list, + '/:pid': { + delete: pets.delete + } + } + } + } +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/route-middleware/index.js b/apps/api/examples/route-middleware/index.js new file mode 100644 index 0000000..44ec13a --- /dev/null +++ b/apps/api/examples/route-middleware/index.js @@ -0,0 +1,90 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../lib/express'); + +var app = express(); + +// Example requests: +// curl http://localhost:3000/user/0 +// curl http://localhost:3000/user/0/edit +// curl http://localhost:3000/user/1 +// curl http://localhost:3000/user/1/edit (unauthorized since this is not you) +// curl -X DELETE http://localhost:3000/user/0 (unauthorized since you are not an admin) + +// Dummy users +var users = [ + { id: 0, name: 'tj', email: 'tj@vision-media.ca', role: 'member' } + , { id: 1, name: 'ciaran', email: 'ciaranj@gmail.com', role: 'member' } + , { id: 2, name: 'aaron', email: 'aaron.heckmann+github@gmail.com', role: 'admin' } +]; + +function loadUser(req, res, next) { + // You would fetch your user from the db + var user = users[req.params.id]; + if (user) { + req.user = user; + next(); + } else { + next(new Error('Failed to load user ' + req.params.id)); + } +} + +function andRestrictToSelf(req, res, next) { + // If our authenticated user is the user we are viewing + // then everything is fine :) + if (req.authenticatedUser.id === req.user.id) { + next(); + } else { + // You may want to implement specific exceptions + // such as UnauthorizedError or similar so that you + // can handle these can be special-cased in an error handler + // (view ./examples/pages for this) + next(new Error('Unauthorized')); + } +} + +function andRestrictTo(role) { + return function(req, res, next) { + if (req.authenticatedUser.role === role) { + next(); + } else { + next(new Error('Unauthorized')); + } + } +} + +// Middleware for faux authentication +// you would of course implement something real, +// but this illustrates how an authenticated user +// may interact with middleware + +app.use(function(req, res, next){ + req.authenticatedUser = users[0]; + next(); +}); + +app.get('/', function(req, res){ + res.redirect('/user/0'); +}); + +app.get('/user/:id', loadUser, function(req, res){ + res.send('Viewing user ' + req.user.name); +}); + +app.get('/user/:id/edit', loadUser, andRestrictToSelf, function(req, res){ + res.send('Editing user ' + req.user.name); +}); + +app.delete('/user/:id', loadUser, andRestrictTo('admin'), function(req, res){ + res.send('Deleted user ' + req.user.name); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/route-separation/index.js b/apps/api/examples/route-separation/index.js new file mode 100644 index 0000000..0a29c94 --- /dev/null +++ b/apps/api/examples/route-separation/index.js @@ -0,0 +1,55 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../..'); +var path = require('node:path'); +var app = express(); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var methodOverride = require('method-override'); +var site = require('./site'); +var post = require('./post'); +var user = require('./user'); + +module.exports = app; + +// Config + +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +/* istanbul ignore next */ +if (!module.parent) { + app.use(logger('dev')); +} + +app.use(methodOverride('_method')); +app.use(cookieParser()); +app.use(express.urlencoded({ extended: true })) +app.use(express.static(path.join(__dirname, 'public'))); + +// General + +app.get('/', site.index); + +// User + +app.get('/users', user.list); +app.all('/user/:id{/:op}', user.load); +app.get('/user/:id', user.view); +app.get('/user/:id/view', user.view); +app.get('/user/:id/edit', user.edit); +app.put('/user/:id/edit', user.update); + +// Posts + +app.get('/posts', post.list); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/route-separation/post.js b/apps/api/examples/route-separation/post.js new file mode 100644 index 0000000..3a8e3a2 --- /dev/null +++ b/apps/api/examples/route-separation/post.js @@ -0,0 +1,13 @@ +'use strict' + +// Fake posts database + +var posts = [ + { title: 'Foo', body: 'some foo bar' }, + { title: 'Foo bar', body: 'more foo bar' }, + { title: 'Foo bar baz', body: 'more foo bar baz' } +]; + +exports.list = function(req, res){ + res.render('posts', { title: 'Posts', posts: posts }); +}; diff --git a/apps/api/examples/route-separation/public/style.css b/apps/api/examples/route-separation/public/style.css new file mode 100644 index 0000000..d699784 --- /dev/null +++ b/apps/api/examples/route-separation/public/style.css @@ -0,0 +1,24 @@ +body { + padding: 50px; + font: 14px "Helvetica Neue", Arial, sans-serif; +} +a { + color: #00AEFF; + text-decoration: none; +} +a.edit { + color: #000; + opacity: .3; +} +a.edit::before { + content: ' ['; +} +a.edit::after { + content: ']'; +} +dt { + font-weight: bold; +} +dd { + margin: 15px; +} \ No newline at end of file diff --git a/apps/api/examples/route-separation/site.js b/apps/api/examples/route-separation/site.js new file mode 100644 index 0000000..aee36d1 --- /dev/null +++ b/apps/api/examples/route-separation/site.js @@ -0,0 +1,5 @@ +'use strict' + +exports.index = function(req, res){ + res.render('index', { title: 'Route Separation Example' }); +}; diff --git a/apps/api/examples/route-separation/user.js b/apps/api/examples/route-separation/user.js new file mode 100644 index 0000000..bc6fbd7 --- /dev/null +++ b/apps/api/examples/route-separation/user.js @@ -0,0 +1,47 @@ +'use strict' + +// Fake user database + +var users = [ + { name: 'TJ', email: 'tj@vision-media.ca' }, + { name: 'Tobi', email: 'tobi@vision-media.ca' } +]; + +exports.list = function(req, res){ + res.render('users', { title: 'Users', users: users }); +}; + +exports.load = function(req, res, next){ + var id = req.params.id; + req.user = users[id]; + if (req.user) { + next(); + } else { + var err = new Error('cannot find user ' + id); + err.status = 404; + next(err); + } +}; + +exports.view = function(req, res){ + res.render('users/view', { + title: 'Viewing user ' + req.user.name, + user: req.user + }); +}; + +exports.edit = function(req, res){ + res.render('users/edit', { + title: 'Editing user ' + req.user.name, + user: req.user + }); +}; + +exports.update = function(req, res){ + // Normally you would handle all kinds of + // validation and save back to the db + var user = req.body.user; + req.user.name = user.name; + req.user.email = user.email; + res.redirect(req.get('Referrer') || '/'); +}; diff --git a/apps/api/examples/route-separation/views/footer.ejs b/apps/api/examples/route-separation/views/footer.ejs new file mode 100644 index 0000000..308b1d0 --- /dev/null +++ b/apps/api/examples/route-separation/views/footer.ejs @@ -0,0 +1,2 @@ + + diff --git a/apps/api/examples/route-separation/views/header.ejs b/apps/api/examples/route-separation/views/header.ejs new file mode 100644 index 0000000..4300325 --- /dev/null +++ b/apps/api/examples/route-separation/views/header.ejs @@ -0,0 +1,9 @@ + + + + + + <%= title %> + + + diff --git a/apps/api/examples/route-separation/views/index.ejs b/apps/api/examples/route-separation/views/index.ejs new file mode 100644 index 0000000..7fd7587 --- /dev/null +++ b/apps/api/examples/route-separation/views/index.ejs @@ -0,0 +1,10 @@ +<%- include('header') -%> + +

<%= title %>

+ +
    +
  • Visit the users page.
  • +
  • Visit the posts page.
  • +
+ +<%- include('footer') -%> diff --git a/apps/api/examples/route-separation/views/posts/index.ejs b/apps/api/examples/route-separation/views/posts/index.ejs new file mode 100644 index 0000000..cbcebff --- /dev/null +++ b/apps/api/examples/route-separation/views/posts/index.ejs @@ -0,0 +1,12 @@ +<%- include('../header') -%> + +

Posts

+ +
+ <% posts.forEach(function(post) { %> +
<%= post.title %>
+
<%= post.body %>
+ <% }) %> +
+ +<%- include('../footer') -%> diff --git a/apps/api/examples/route-separation/views/users/edit.ejs b/apps/api/examples/route-separation/views/users/edit.ejs new file mode 100644 index 0000000..6df78a9 --- /dev/null +++ b/apps/api/examples/route-separation/views/users/edit.ejs @@ -0,0 +1,23 @@ +<%- include('../header') -%> + +

Editing <%= user.name %>

+ +
+
+

+ Name: + +

+ +

+ Email: + +

+ +

+ +

+
+
+ +<%- include('../footer') -%> diff --git a/apps/api/examples/route-separation/views/users/index.ejs b/apps/api/examples/route-separation/views/users/index.ejs new file mode 100644 index 0000000..949412a --- /dev/null +++ b/apps/api/examples/route-separation/views/users/index.ejs @@ -0,0 +1,14 @@ +<%- include('../header') -%> + +

<%= title %>

+ +
+ <% users.forEach(function(user, index) { %> +
  • + <%= user.name %> + edit +
  • + <% }) %> +
    + +<%- include('../footer') -%> diff --git a/apps/api/examples/route-separation/views/users/view.ejs b/apps/api/examples/route-separation/views/users/view.ejs new file mode 100644 index 0000000..457bd53 --- /dev/null +++ b/apps/api/examples/route-separation/views/users/view.ejs @@ -0,0 +1,9 @@ +<%- include('../header') -%> + +

    <%= user.name %>

    + +
    +

    Email: <%= user.email %>

    +
    + +<%- include('../footer') -%> diff --git a/apps/api/examples/search/index.js b/apps/api/examples/search/index.js new file mode 100644 index 0000000..951e0d4 --- /dev/null +++ b/apps/api/examples/search/index.js @@ -0,0 +1,61 @@ +'use strict' + +// install redis first: +// https://redis.io/ + +// then: +// $ npm install redis +// $ redis-server + +/** + * Module dependencies. + */ + +var express = require('../..'); +var path = require('node:path'); +var redis = require('redis'); + +var db = redis.createClient(); + +// npm install redis + +var app = express(); + +app.use(express.static(path.join(__dirname, 'public'))); + +// populate search + +db.sadd('ferret', 'tobi'); +db.sadd('ferret', 'loki'); +db.sadd('ferret', 'jane'); +db.sadd('cat', 'manny'); +db.sadd('cat', 'luna'); + +/** + * GET search for :query. + */ + +app.get('/search/:query?', function(req, res, next){ + var query = req.params.query; + db.smembers(query, function(err, vals){ + if (err) return next(err); + res.send(vals); + }); +}); + +/** + * GET client javascript. Here we use sendFile() + * because serving __dirname with the static() middleware + * would also mean serving our server "index.js" and the "search.jade" + * template. + */ + +app.get('/client.js', function(req, res){ + res.sendFile(path.join(__dirname, 'client.js')); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/search/public/client.js b/apps/api/examples/search/public/client.js new file mode 100644 index 0000000..cd43faf --- /dev/null +++ b/apps/api/examples/search/public/client.js @@ -0,0 +1,15 @@ +'use strict' + +var search = document.querySelector('[type=search]'); +var code = document.querySelector('pre'); + +search.addEventListener('keyup', function(){ + var xhr = new XMLHttpRequest; + xhr.open('GET', '/search/' + search.value, true); + xhr.onreadystatechange = function(){ + if (xhr.readyState === 4) { + code.textContent = xhr.responseText; + } + }; + xhr.send(); +}, false); diff --git a/apps/api/examples/search/public/index.html b/apps/api/examples/search/public/index.html new file mode 100644 index 0000000..7353644 --- /dev/null +++ b/apps/api/examples/search/public/index.html @@ -0,0 +1,21 @@ + + + + + + Search example + + + +

    Search

    +

    Try searching for "ferret" or "cat".

    + +
    
    +  
    +
    +
    diff --git a/apps/api/examples/session/index.js b/apps/api/examples/session/index.js
    new file mode 100644
    index 0000000..2bb2b10
    --- /dev/null
    +++ b/apps/api/examples/session/index.js
    @@ -0,0 +1,37 @@
    +'use strict'
    +
    +// install redis first:
    +// https://redis.io/
    +
    +// then:
    +// $ npm install redis
    +// $ redis-server
    +
    +var express = require('../..');
    +var session = require('express-session');
    +
    +var app = express();
    +
    +// Populates req.session
    +app.use(session({
    +  resave: false, // don't save session if unmodified
    +  saveUninitialized: false, // don't create session until something stored
    +  secret: 'keyboard cat'
    +}));
    +
    +app.get('/', function(req, res){
    +  var body = '';
    +  if (req.session.views) {
    +    ++req.session.views;
    +  } else {
    +    req.session.views = 1;
    +    body += '

    First time visiting? view this page in several browsers :)

    '; + } + res.send(body + '

    viewed ' + req.session.views + ' times.

    '); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/session/redis.js b/apps/api/examples/session/redis.js new file mode 100644 index 0000000..bbbdc7f --- /dev/null +++ b/apps/api/examples/session/redis.js @@ -0,0 +1,39 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../..'); +var logger = require('morgan'); +var session = require('express-session'); + +// pass the express to the connect redis module +// allowing it to inherit from session.Store +var RedisStore = require('connect-redis')(session); + +var app = express(); + +app.use(logger('dev')); + +// Populates req.session +app.use(session({ + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + secret: 'keyboard cat', + store: new RedisStore +})); + +app.get('/', function(req, res){ + var body = ''; + if (req.session.views) { + ++req.session.views; + } else { + req.session.views = 1; + body += '

    First time visiting? view this page in several browsers :)

    '; + } + res.send(body + '

    viewed ' + req.session.views + ' times.

    '); +}); + +app.listen(3000); +console.log('Express app started on port 3000'); diff --git a/apps/api/examples/static-files/index.js b/apps/api/examples/static-files/index.js new file mode 100644 index 0000000..b7c697a --- /dev/null +++ b/apps/api/examples/static-files/index.js @@ -0,0 +1,43 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../..'); +var logger = require('morgan'); +var path = require('node:path'); +var app = express(); + +// log requests +app.use(logger('dev')); + +// express on its own has no notion +// of a "file". The express.static() +// middleware checks for a file matching +// the `req.path` within the directory +// that you pass it. In this case "GET /js/app.js" +// will look for "./public/js/app.js". + +app.use(express.static(path.join(__dirname, 'public'))); + +// if you wanted to "prefix" you may use +// the mounting feature of Connect, for example +// "GET /static/js/app.js" instead of "GET /js/app.js". +// The mount-path "/static" is simply removed before +// passing control to the express.static() middleware, +// thus it serves the file correctly by ignoring "/static" +app.use('/static', express.static(path.join(__dirname, 'public'))); + +// if for some reason you want to serve files from +// several directories, you can use express.static() +// multiple times! Here we're passing "./public/css", +// this will allow "GET /style.css" instead of "GET /css/style.css": +app.use(express.static(path.join(__dirname, 'public', 'css'))); + +app.listen(3000); +console.log('listening on port 3000'); +console.log('try:'); +console.log(' GET /hello.txt'); +console.log(' GET /js/app.js'); +console.log(' GET /css/style.css'); diff --git a/apps/api/examples/static-files/public/css/style.css b/apps/api/examples/static-files/public/css/style.css new file mode 100644 index 0000000..7fc28f9 --- /dev/null +++ b/apps/api/examples/static-files/public/css/style.css @@ -0,0 +1,3 @@ +body { + +} \ No newline at end of file diff --git a/apps/api/examples/static-files/public/hello.txt b/apps/api/examples/static-files/public/hello.txt new file mode 100644 index 0000000..2b31011 --- /dev/null +++ b/apps/api/examples/static-files/public/hello.txt @@ -0,0 +1 @@ +hey \ No newline at end of file diff --git a/apps/api/examples/static-files/public/js/app.js b/apps/api/examples/static-files/public/js/app.js new file mode 100644 index 0000000..775eb73 --- /dev/null +++ b/apps/api/examples/static-files/public/js/app.js @@ -0,0 +1 @@ +// foo diff --git a/apps/api/examples/vhost/index.js b/apps/api/examples/vhost/index.js new file mode 100644 index 0000000..a949935 --- /dev/null +++ b/apps/api/examples/vhost/index.js @@ -0,0 +1,53 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../..'); +var logger = require('morgan'); +var vhost = require('vhost'); + +/* +edit /etc/hosts: + +127.0.0.1 foo.example.com +127.0.0.1 bar.example.com +127.0.0.1 example.com +*/ + +// Main server app + +var main = express(); + +if (!module.parent) main.use(logger('dev')); + +main.get('/', function(req, res){ + res.send('Hello from main app!'); +}); + +main.get('/:sub', function(req, res){ + res.send('requested ' + req.params.sub); +}); + +// Redirect app + +var redirect = express(); + +redirect.use(function(req, res){ + if (!module.parent) console.log(req.vhost); + res.redirect('http://example.com:3000/' + req.vhost[0]); +}); + +// Vhost app + +var app = module.exports = express(); + +app.use(vhost('*.example.com', redirect)); // Serves all subdomains via Redirect app +app.use(vhost('example.com', main)); // Serves top level domain via Main server app + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/view-constructor/github-view.js b/apps/api/examples/view-constructor/github-view.js new file mode 100644 index 0000000..eabfb2d --- /dev/null +++ b/apps/api/examples/view-constructor/github-view.js @@ -0,0 +1,53 @@ +'use strict' + +/** + * Module dependencies. + */ + +var https = require('node:https'); +var path = require('node:path'); +var extname = path.extname; + +/** + * Expose `GithubView`. + */ + +module.exports = GithubView; + +/** + * Custom view that fetches and renders + * remove github templates. You could + * render templates from a database etc. + */ + +function GithubView(name, options){ + this.name = name; + options = options || {}; + this.engine = options.engines[extname(name)]; + // "root" is the app.set('views') setting, however + // in your own implementation you could ignore this + this.path = '/' + options.root + '/master/' + name; +} + +/** + * Render the view. + */ + +GithubView.prototype.render = function(options, fn){ + var self = this; + var opts = { + host: 'raw.githubusercontent.com', + port: 443, + path: this.path, + method: 'GET' + }; + + https.request(opts, function(res) { + var buf = ''; + res.setEncoding('utf8'); + res.on('data', function(str){ buf += str }); + res.on('end', function(){ + self.engine(buf, options, fn); + }); + }).end(); +}; diff --git a/apps/api/examples/view-constructor/index.js b/apps/api/examples/view-constructor/index.js new file mode 100644 index 0000000..3d67367 --- /dev/null +++ b/apps/api/examples/view-constructor/index.js @@ -0,0 +1,48 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); +var GithubView = require('./github-view'); +var md = require('marked').parse; + +var app = module.exports = express(); + +// register .md as an engine in express view system +app.engine('md', function(str, options, fn){ + try { + var html = md(str); + html = html.replace(/\{([^}]+)\}/g, function(_, name){ + return options[name] || ''; + }); + fn(null, html); + } catch(err) { + fn(err); + } +}); + +// pointing to a particular github repo to load files from it +app.set('views', 'expressjs/express'); + +// register a new view constructor +app.set('view', GithubView); + +app.get('/', function(req, res){ + // rendering a view relative to the repo. + // app.locals, res.locals, and locals passed + // work like they normally would + res.render('examples/markdown/views/index.md', { title: 'Example' }); +}); + +app.get('/Readme.md', function(req, res){ + // rendering a view from https://github.com/expressjs/express/blob/master/Readme.md + res.render('Readme.md'); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/view-locals/index.js b/apps/api/examples/view-locals/index.js new file mode 100644 index 0000000..e635560 --- /dev/null +++ b/apps/api/examples/view-locals/index.js @@ -0,0 +1,155 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../..'); +var path = require('node:path'); +var User = require('./user'); +var app = express(); + +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// filter ferrets only + +function ferrets(user) { + return user.species === 'ferret' +} + +// naive nesting approach, +// delegating errors to next(err) +// in order to expose the "count" +// and "users" locals + +app.get('/', function(req, res, next){ + User.count(function(err, count){ + if (err) return next(err); + User.all(function(err, users){ + if (err) return next(err); + res.render('index', { + title: 'Users', + count: count, + users: users.filter(ferrets) + }); + }) + }) +}); + + + + +// this approach is cleaner, +// less nesting and we have +// the variables available +// on the request object + +function count(req, res, next) { + User.count(function(err, count){ + if (err) return next(err); + req.count = count; + next(); + }) +} + +function users(req, res, next) { + User.all(function(err, users){ + if (err) return next(err); + req.users = users; + next(); + }) +} + +app.get('/middleware', count, users, function (req, res) { + res.render('index', { + title: 'Users', + count: req.count, + users: req.users.filter(ferrets) + }); +}); + + + + +// this approach is much like the last +// however we're explicitly exposing +// the locals within each middleware +// +// note that this may not always work +// well, for example here we filter +// the users in the middleware, which +// may not be ideal for our application. +// so in that sense the previous example +// is more flexible with `req.users`. + +function count2(req, res, next) { + User.count(function(err, count){ + if (err) return next(err); + res.locals.count = count; + next(); + }) +} + +function users2(req, res, next) { + User.all(function(err, users){ + if (err) return next(err); + res.locals.users = users.filter(ferrets); + next(); + }) +} + +app.get('/middleware-locals', count2, users2, function (req, res) { + // you can see now how we have much less + // to pass to res.render(). If we have + // several routes related to users this + // can be a great productivity booster + res.render('index', { title: 'Users' }); +}); + +// keep in mind that middleware may be placed anywhere +// and in various combinations, so if you have locals +// that you wish to make available to all subsequent +// middleware/routes you can do something like this: + +/* + +app.use(function(req, res, next){ + res.locals.user = req.user; + res.locals.sess = req.session; + next(); +}); + +*/ + +// or suppose you have some /admin +// "global" local variables: + +/* + +app.use('/api', function(req, res, next){ + res.locals.user = req.user; + res.locals.sess = req.session; + next(); +}); + +*/ + +// the following is effectively the same, +// but uses a route instead: + +/* + +app.all('/api/*', function(req, res, next){ + res.locals.user = req.user; + res.locals.sess = req.session; + next(); +}); + +*/ + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/examples/view-locals/user.js b/apps/api/examples/view-locals/user.js new file mode 100644 index 0000000..aaa6f85 --- /dev/null +++ b/apps/api/examples/view-locals/user.js @@ -0,0 +1,36 @@ +'use strict' + +module.exports = User; + +// faux model + +function User(name, age, species) { + this.name = name; + this.age = age; + this.species = species; +} + +User.all = function(fn){ + // process.nextTick makes sure this function API + // behaves in an asynchronous manner, like if it + // was a real DB query to read all users. + process.nextTick(function(){ + fn(null, users); + }); +}; + +User.count = function(fn){ + process.nextTick(function(){ + fn(null, users.length); + }); +}; + +// faux database + +var users = []; + +users.push(new User('Tobi', 2, 'ferret')); +users.push(new User('Loki', 1, 'ferret')); +users.push(new User('Jane', 6, 'ferret')); +users.push(new User('Luna', 1, 'cat')); +users.push(new User('Manny', 1, 'cat')); diff --git a/apps/api/examples/view-locals/views/index.ejs b/apps/api/examples/view-locals/views/index.ejs new file mode 100644 index 0000000..287f34b --- /dev/null +++ b/apps/api/examples/view-locals/views/index.ejs @@ -0,0 +1,20 @@ + + + + + + <%= title %> + + + +

    <%= title %>

    + <% users.forEach(function(user) { %> +
  • <%= user.name %> is a <% user.age %> year old <%= user.species %>
  • + <% }); %> + + diff --git a/apps/api/examples/web-service/index.js b/apps/api/examples/web-service/index.js new file mode 100644 index 0000000..d1a9036 --- /dev/null +++ b/apps/api/examples/web-service/index.js @@ -0,0 +1,117 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require('../../'); + +var app = module.exports = express(); + +// create an error with .status. we +// can then use the property in our +// custom error handler (Connect respects this prop as well) + +function error(status, msg) { + var err = new Error(msg); + err.status = status; + return err; +} + +// if we wanted to supply more than JSON, we could +// use something similar to the content-negotiation +// example. + +// here we validate the API key, +// by mounting this middleware to /api +// meaning only paths prefixed with "/api" +// will cause this middleware to be invoked + +app.use('/api', function(req, res, next){ + var key = req.query['api-key']; + + // key isn't present + if (!key) return next(error(400, 'api key required')); + + // key is invalid + if (apiKeys.indexOf(key) === -1) return next(error(401, 'invalid api key')) + + // all good, store req.key for route access + req.key = key; + next(); +}); + +// map of valid api keys, typically mapped to +// account info with some sort of database like redis. +// api keys do _not_ serve as authentication, merely to +// track API usage or help prevent malicious behavior etc. + +var apiKeys = ['foo', 'bar', 'baz']; + +// these two objects will serve as our faux database + +var repos = [ + { name: 'express', url: 'https://github.com/expressjs/express' }, + { name: 'stylus', url: 'https://github.com/learnboost/stylus' }, + { name: 'cluster', url: 'https://github.com/learnboost/cluster' } +]; + +var users = [ + { name: 'tobi' } + , { name: 'loki' } + , { name: 'jane' } +]; + +var userRepos = { + tobi: [repos[0], repos[1]] + , loki: [repos[1]] + , jane: [repos[2]] +}; + +// we now can assume the api key is valid, +// and simply expose the data + +// example: http://localhost:3000/api/users/?api-key=foo +app.get('/api/users', function (req, res) { + res.send(users); +}); + +// example: http://localhost:3000/api/repos/?api-key=foo +app.get('/api/repos', function (req, res) { + res.send(repos); +}); + +// example: http://localhost:3000/api/user/tobi/repos/?api-key=foo +app.get('/api/user/:name/repos', function(req, res, next){ + var name = req.params.name; + var user = userRepos[name]; + + if (user) res.send(user); + else next(); +}); + +// middleware with an arity of 4 are considered +// error handling middleware. When you next(err) +// it will be passed through the defined middleware +// in order, but ONLY those with an arity of 4, ignoring +// regular middleware. +app.use(function(err, req, res, next){ + // whatever you want here, feel free to populate + // properties on `err` to treat it differently in here. + res.status(err.status || 500); + res.send({ error: err.message }); +}); + +// our custom JSON 404 middleware. Since it's placed last +// it will be the last middleware called, if all others +// invoke next() and do not respond. +app.use(function(req, res){ + res.status(404); + res.send({ error: "Sorry, can't find that" }) +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/apps/api/index.js b/apps/api/index.js new file mode 100644 index 0000000..d219b0c --- /dev/null +++ b/apps/api/index.js @@ -0,0 +1,11 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +module.exports = require('./lib/express'); diff --git a/apps/api/lib/application.js b/apps/api/lib/application.js new file mode 100644 index 0000000..cf6d78c --- /dev/null +++ b/apps/api/lib/application.js @@ -0,0 +1,631 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var finalhandler = require('finalhandler'); +var debug = require('debug')('express:application'); +var View = require('./view'); +var http = require('node:http'); +var methods = require('./utils').methods; +var compileETag = require('./utils').compileETag; +var compileQueryParser = require('./utils').compileQueryParser; +var compileTrust = require('./utils').compileTrust; +var resolve = require('node:path').resolve; +var once = require('once') +var Router = require('router'); + +/** + * Module variables. + * @private + */ + +var slice = Array.prototype.slice; +var flatten = Array.prototype.flat; + +/** + * Application prototype. + */ + +var app = exports = module.exports = {}; + +/** + * Variable for trust proxy inheritance back-compat + * @private + */ + +var trustProxyDefaultSymbol = '@@symbol:trust_proxy_default'; + +/** + * Initialize the server. + * + * - setup default configuration + * - setup default middleware + * - setup route reflection methods + * + * @private + */ + +app.init = function init() { + var router = null; + + this.cache = Object.create(null); + this.engines = Object.create(null); + this.settings = Object.create(null); + + this.defaultConfiguration(); + + // Setup getting to lazily add base router + Object.defineProperty(this, 'router', { + configurable: true, + enumerable: true, + get: function getrouter() { + if (router === null) { + router = new Router({ + caseSensitive: this.enabled('case sensitive routing'), + strict: this.enabled('strict routing') + }); + } + + return router; + } + }); +}; + +/** + * Initialize application configuration. + * @private + */ + +app.defaultConfiguration = function defaultConfiguration() { + var env = process.env.NODE_ENV || 'development'; + + // default settings + this.enable('x-powered-by'); + this.set('etag', 'weak'); + this.set('env', env); + this.set('query parser', 'simple') + this.set('subdomain offset', 2); + this.set('trust proxy', false); + + // trust proxy inherit back-compat + Object.defineProperty(this.settings, trustProxyDefaultSymbol, { + configurable: true, + value: true + }); + + debug('booting in %s mode', env); + + this.on('mount', function onmount(parent) { + // inherit trust proxy + if (this.settings[trustProxyDefaultSymbol] === true + && typeof parent.settings['trust proxy fn'] === 'function') { + delete this.settings['trust proxy']; + delete this.settings['trust proxy fn']; + } + + // inherit protos + Object.setPrototypeOf(this.request, parent.request) + Object.setPrototypeOf(this.response, parent.response) + Object.setPrototypeOf(this.engines, parent.engines) + Object.setPrototypeOf(this.settings, parent.settings) + }); + + // setup locals + this.locals = Object.create(null); + + // top-most app is mounted at / + this.mountpath = '/'; + + // default locals + this.locals.settings = this.settings; + + // default configuration + this.set('view', View); + this.set('views', resolve('views')); + this.set('jsonp callback name', 'callback'); + + if (env === 'production') { + this.enable('view cache'); + } +}; + +/** + * Dispatch a req, res pair into the application. Starts pipeline processing. + * + * If no callback is provided, then default error handlers will respond + * in the event of an error bubbling through the stack. + * + * @private + */ + +app.handle = function handle(req, res, callback) { + // final handler + var done = callback || finalhandler(req, res, { + env: this.get('env'), + onerror: logerror.bind(this) + }); + + // set powered by header + if (this.enabled('x-powered-by')) { + res.setHeader('X-Powered-By', 'Express'); + } + + // set circular references + req.res = res; + res.req = req; + + // alter the prototypes + Object.setPrototypeOf(req, this.request) + Object.setPrototypeOf(res, this.response) + + // setup locals + if (!res.locals) { + res.locals = Object.create(null); + } + + this.router.handle(req, res, done); +}; + +/** + * Proxy `Router#use()` to add middleware to the app router. + * See Router#use() documentation for details. + * + * If the _fn_ parameter is an express app, then it will be + * mounted at the _route_ specified. + * + * @public + */ + +app.use = function use(fn) { + var offset = 0; + var path = '/'; + + // default path to '/' + // disambiguate app.use([fn]) + if (typeof fn !== 'function') { + var arg = fn; + + while (Array.isArray(arg) && arg.length !== 0) { + arg = arg[0]; + } + + // first arg is the path + if (typeof arg !== 'function') { + offset = 1; + path = fn; + } + } + + var fns = flatten.call(slice.call(arguments, offset), Infinity); + + if (fns.length === 0) { + throw new TypeError('app.use() requires a middleware function') + } + + // get router + var router = this.router; + + fns.forEach(function (fn) { + // non-express app + if (!fn || !fn.handle || !fn.set) { + return router.use(path, fn); + } + + debug('.use app under %s', path); + fn.mountpath = path; + fn.parent = this; + + // restore .app property on req and res + router.use(path, function mounted_app(req, res, next) { + var orig = req.app; + fn.handle(req, res, function (err) { + Object.setPrototypeOf(req, orig.request) + Object.setPrototypeOf(res, orig.response) + next(err); + }); + }); + + // mounted an app + fn.emit('mount', this); + }, this); + + return this; +}; + +/** + * Proxy to the app `Router#route()` + * Returns a new `Route` instance for the _path_. + * + * Routes are isolated middleware stacks for specific paths. + * See the Route api docs for details. + * + * @public + */ + +app.route = function route(path) { + return this.router.route(path); +}; + +/** + * Register the given template engine callback `fn` + * as `ext`. + * + * By default will `require()` the engine based on the + * file extension. For example if you try to render + * a "foo.ejs" file Express will invoke the following internally: + * + * app.engine('ejs', require('ejs').__express); + * + * For engines that do not provide `.__express` out of the box, + * or if you wish to "map" a different extension to the template engine + * you may use this method. For example mapping the EJS template engine to + * ".html" files: + * + * app.engine('html', require('ejs').renderFile); + * + * In this case EJS provides a `.renderFile()` method with + * the same signature that Express expects: `(path, options, callback)`, + * though note that it aliases this method as `ejs.__express` internally + * so if you're using ".ejs" extensions you don't need to do anything. + * + * Some template engines do not follow this convention, the + * [Consolidate.js](https://github.com/tj/consolidate.js) + * library was created to map all of node's popular template + * engines to follow this convention, thus allowing them to + * work seamlessly within Express. + * + * @param {String} ext + * @param {Function} fn + * @return {app} for chaining + * @public + */ + +app.engine = function engine(ext, fn) { + if (typeof fn !== 'function') { + throw new Error('callback function required'); + } + + // get file extension + var extension = ext[0] !== '.' + ? '.' + ext + : ext; + + // store engine + this.engines[extension] = fn; + + return this; +}; + +/** + * Proxy to `Router#param()` with one added api feature. The _name_ parameter + * can be an array of names. + * + * See the Router#param() docs for more details. + * + * @param {String|Array} name + * @param {Function} fn + * @return {app} for chaining + * @public + */ + +app.param = function param(name, fn) { + if (Array.isArray(name)) { + for (var i = 0; i < name.length; i++) { + this.param(name[i], fn); + } + + return this; + } + + this.router.param(name, fn); + + return this; +}; + +/** + * Assign `setting` to `val`, or return `setting`'s value. + * + * app.set('foo', 'bar'); + * app.set('foo'); + * // => "bar" + * + * Mounted servers inherit their parent server's settings. + * + * @param {String} setting + * @param {*} [val] + * @return {Server} for chaining + * @public + */ + +app.set = function set(setting, val) { + if (arguments.length === 1) { + // app.get(setting) + return this.settings[setting]; + } + + debug('set "%s" to %o', setting, val); + + // set value + this.settings[setting] = val; + + // trigger matched settings + switch (setting) { + case 'etag': + this.set('etag fn', compileETag(val)); + break; + case 'query parser': + this.set('query parser fn', compileQueryParser(val)); + break; + case 'trust proxy': + this.set('trust proxy fn', compileTrust(val)); + + // trust proxy inherit back-compat + Object.defineProperty(this.settings, trustProxyDefaultSymbol, { + configurable: true, + value: false + }); + + break; + } + + return this; +}; + +/** + * Return the app's absolute pathname + * based on the parent(s) that have + * mounted it. + * + * For example if the application was + * mounted as "/admin", which itself + * was mounted as "/blog" then the + * return value would be "/blog/admin". + * + * @return {String} + * @private + */ + +app.path = function path() { + return this.parent + ? this.parent.path() + this.mountpath + : ''; +}; + +/** + * Check if `setting` is enabled (truthy). + * + * app.enabled('foo') + * // => false + * + * app.enable('foo') + * app.enabled('foo') + * // => true + * + * @param {String} setting + * @return {Boolean} + * @public + */ + +app.enabled = function enabled(setting) { + return Boolean(this.set(setting)); +}; + +/** + * Check if `setting` is disabled. + * + * app.disabled('foo') + * // => true + * + * app.enable('foo') + * app.disabled('foo') + * // => false + * + * @param {String} setting + * @return {Boolean} + * @public + */ + +app.disabled = function disabled(setting) { + return !this.set(setting); +}; + +/** + * Enable `setting`. + * + * @param {String} setting + * @return {app} for chaining + * @public + */ + +app.enable = function enable(setting) { + return this.set(setting, true); +}; + +/** + * Disable `setting`. + * + * @param {String} setting + * @return {app} for chaining + * @public + */ + +app.disable = function disable(setting) { + return this.set(setting, false); +}; + +/** + * Delegate `.VERB(...)` calls to `router.VERB(...)`. + */ + +methods.forEach(function (method) { + app[method] = function (path) { + if (method === 'get' && arguments.length === 1) { + // app.get(setting) + return this.set(path); + } + + var route = this.route(path); + route[method].apply(route, slice.call(arguments, 1)); + return this; + }; +}); + +/** + * Special-cased "all" method, applying the given route `path`, + * middleware, and callback to _every_ HTTP method. + * + * @param {String} path + * @param {Function} ... + * @return {app} for chaining + * @public + */ + +app.all = function all(path) { + var route = this.route(path); + var args = slice.call(arguments, 1); + + for (var i = 0; i < methods.length; i++) { + route[methods[i]].apply(route, args); + } + + return this; +}; + +/** + * Render the given view `name` name with `options` + * and a callback accepting an error and the + * rendered template string. + * + * Example: + * + * app.render('email', { name: 'Tobi' }, function(err, html){ + * // ... + * }) + * + * @param {String} name + * @param {Object|Function} options or fn + * @param {Function} callback + * @public + */ + +app.render = function render(name, options, callback) { + var cache = this.cache; + var done = callback; + var engines = this.engines; + var opts = options; + var view; + + // support callback function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; + } + + // merge options + var renderOptions = { ...this.locals, ...opts._locals, ...opts }; + + // set .cache unless explicitly provided + if (renderOptions.cache == null) { + renderOptions.cache = this.enabled('view cache'); + } + + // primed cache + if (renderOptions.cache) { + view = cache[name]; + } + + // view + if (!view) { + var View = this.get('view'); + + view = new View(name, { + defaultEngine: this.get('view engine'), + root: this.get('views'), + engines: engines + }); + + if (!view.path) { + var dirs = Array.isArray(view.root) && view.root.length > 1 + ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"' + : 'directory "' + view.root + '"' + var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs); + err.view = view; + return done(err); + } + + // prime the cache + if (renderOptions.cache) { + cache[name] = view; + } + } + + // render + tryRender(view, renderOptions, done); +}; + +/** + * Listen for connections. + * + * A node `http.Server` is returned, with this + * application (which is a `Function`) as its + * callback. If you wish to create both an HTTP + * and HTTPS server you may do so with the "http" + * and "https" modules as shown here: + * + * var http = require('node:http') + * , https = require('node:https') + * , express = require('express') + * , app = express(); + * + * http.createServer(app).listen(80); + * https.createServer({ ... }, app).listen(443); + * + * @return {http.Server} + * @public + */ + +app.listen = function listen() { + var server = http.createServer(this) + var args = Array.prototype.slice.call(arguments) + if (typeof args[args.length - 1] === 'function') { + var done = args[args.length - 1] = once(args[args.length - 1]) + server.once('error', done) + } + return server.listen.apply(server, args) +} + +/** + * Log error using console.error. + * + * @param {Error} err + * @private + */ + +function logerror(err) { + /* istanbul ignore next */ + if (this.get('env') !== 'test') console.error(err.stack || err.toString()); +} + +/** + * Try rendering a view. + * @private + */ + +function tryRender(view, options, callback) { + try { + view.render(options, callback); + } catch (err) { + callback(err); + } +} diff --git a/apps/api/lib/express.js b/apps/api/lib/express.js new file mode 100644 index 0000000..2d502eb --- /dev/null +++ b/apps/api/lib/express.js @@ -0,0 +1,81 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + */ + +var bodyParser = require('body-parser') +var EventEmitter = require('node:events').EventEmitter; +var mixin = require('merge-descriptors'); +var proto = require('./application'); +var Router = require('router'); +var req = require('./request'); +var res = require('./response'); + +/** + * Expose `createApplication()`. + */ + +exports = module.exports = createApplication; + +/** + * Create an express application. + * + * @return {Function} + * @api public + */ + +function createApplication() { + var app = function(req, res, next) { + app.handle(req, res, next); + }; + + mixin(app, EventEmitter.prototype, false); + mixin(app, proto, false); + + // expose the prototype that will get set on requests + app.request = Object.create(req, { + app: { configurable: true, enumerable: true, writable: true, value: app } + }) + + // expose the prototype that will get set on responses + app.response = Object.create(res, { + app: { configurable: true, enumerable: true, writable: true, value: app } + }) + + app.init(); + return app; +} + +/** + * Expose the prototypes. + */ + +exports.application = proto; +exports.request = req; +exports.response = res; + +/** + * Expose constructors. + */ + +exports.Route = Router.Route; +exports.Router = Router; + +/** + * Expose middleware + */ + +exports.json = bodyParser.json +exports.raw = bodyParser.raw +exports.static = require('serve-static'); +exports.text = bodyParser.text +exports.urlencoded = bodyParser.urlencoded diff --git a/apps/api/lib/request.js b/apps/api/lib/request.js new file mode 100644 index 0000000..69990da --- /dev/null +++ b/apps/api/lib/request.js @@ -0,0 +1,514 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var accepts = require('accepts'); +var isIP = require('node:net').isIP; +var typeis = require('type-is'); +var http = require('node:http'); +var fresh = require('fresh'); +var parseRange = require('range-parser'); +var parse = require('parseurl'); +var proxyaddr = require('proxy-addr'); + +/** + * Request prototype. + * @public + */ + +var req = Object.create(http.IncomingMessage.prototype) + +/** + * Module exports. + * @public + */ + +module.exports = req + +/** + * Return request header. + * + * The `Referrer` header field is special-cased, + * both `Referrer` and `Referer` are interchangeable. + * + * Examples: + * + * req.get('Content-Type'); + * // => "text/plain" + * + * req.get('content-type'); + * // => "text/plain" + * + * req.get('Something'); + * // => undefined + * + * Aliased as `req.header()`. + * + * @param {String} name + * @return {String} + * @public + */ + +req.get = +req.header = function header(name) { + if (!name) { + throw new TypeError('name argument is required to req.get'); + } + + if (typeof name !== 'string') { + throw new TypeError('name must be a string to req.get'); + } + + var lc = name.toLowerCase(); + + switch (lc) { + case 'referer': + case 'referrer': + return this.headers.referrer + || this.headers.referer; + default: + return this.headers[lc]; + } +}; + +/** + * To do: update docs. + * + * Check if the given `type(s)` is acceptable, returning + * the best match when true, otherwise `undefined`, in which + * case you should respond with 406 "Not Acceptable". + * + * The `type` value may be a single MIME type string + * such as "application/json", an extension name + * such as "json", a comma-delimited list such as "json, html, text/plain", + * an argument list such as `"json", "html", "text/plain"`, + * or an array `["json", "html", "text/plain"]`. When a list + * or array is given, the _best_ match, if any is returned. + * + * Examples: + * + * // Accept: text/html + * req.accepts('html'); + * // => "html" + * + * // Accept: text/*, application/json + * req.accepts('html'); + * // => "html" + * req.accepts('text/html'); + * // => "text/html" + * req.accepts('json, text'); + * // => "json" + * req.accepts('application/json'); + * // => "application/json" + * + * // Accept: text/*, application/json + * req.accepts('image/png'); + * req.accepts('png'); + * // => undefined + * + * // Accept: text/*;q=.5, application/json + * req.accepts(['html', 'json']); + * req.accepts('html', 'json'); + * req.accepts('html, json'); + * // => "json" + * + * @param {String|Array} type(s) + * @return {String|Array|Boolean} + * @public + */ + +req.accepts = function(){ + var accept = accepts(this); + return accept.types.apply(accept, arguments); +}; + +/** + * Check if the given `encoding`s are accepted. + * + * @param {String} ...encoding + * @return {String|Array} + * @public + */ + +req.acceptsEncodings = function(){ + var accept = accepts(this); + return accept.encodings.apply(accept, arguments); +}; + +/** + * Check if the given `charset`s are acceptable, + * otherwise you should respond with 406 "Not Acceptable". + * + * @param {String} ...charset + * @return {String|Array} + * @public + */ + +req.acceptsCharsets = function(){ + var accept = accepts(this); + return accept.charsets.apply(accept, arguments); +}; + +/** + * Check if the given `lang`s are acceptable, + * otherwise you should respond with 406 "Not Acceptable". + * + * @param {String} ...lang + * @return {String|Array} + * @public + */ + +req.acceptsLanguages = function(...languages) { + return accepts(this).languages(...languages); +}; + +/** + * Parse Range header field, capping to the given `size`. + * + * Unspecified ranges such as "0-" require knowledge of your resource length. In + * the case of a byte range this is of course the total number of bytes. If the + * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, + * and `-2` when syntactically invalid. + * + * When ranges are returned, the array has a "type" property which is the type of + * range that is required (most commonly, "bytes"). Each array element is an object + * with a "start" and "end" property for the portion of the range. + * + * The "combine" option can be set to `true` and overlapping & adjacent ranges + * will be combined into a single range. + * + * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" + * should respond with 4 users when available, not 3. + * + * @param {number} size + * @param {object} [options] + * @param {boolean} [options.combine=false] + * @return {number|array} + * @public + */ + +req.range = function range(size, options) { + var range = this.get('Range'); + if (!range) return; + return parseRange(size, range, options); +}; + +/** + * Parse the query string of `req.url`. + * + * This uses the "query parser" setting to parse the raw + * string into an object. + * + * @return {String} + * @api public + */ + +defineGetter(req, 'query', function query(){ + var queryparse = this.app.get('query parser fn'); + + if (!queryparse) { + // parsing is disabled + return Object.create(null); + } + + var querystring = parse(this).query; + + return queryparse(querystring); +}); + +/** + * Check if the incoming request contains the "Content-Type" + * header field, and it contains the given mime `type`. + * + * Examples: + * + * // With Content-Type: text/html; charset=utf-8 + * req.is('html'); + * req.is('text/html'); + * req.is('text/*'); + * // => true + * + * // When Content-Type is application/json + * req.is('json'); + * req.is('application/json'); + * req.is('application/*'); + * // => true + * + * req.is('html'); + * // => false + * + * @param {String|Array} types... + * @return {String|false|null} + * @public + */ + +req.is = function is(types) { + var arr = types; + + // support flattened arguments + if (!Array.isArray(types)) { + arr = new Array(arguments.length); + for (var i = 0; i < arr.length; i++) { + arr[i] = arguments[i]; + } + } + + return typeis(this, arr); +}; + +/** + * Return the protocol string "http" or "https" + * when requested with TLS. When the "trust proxy" + * setting trusts the socket address, the + * "X-Forwarded-Proto" header field will be trusted + * and used if present. + * + * If you're running behind a reverse proxy that + * supplies https for you this may be enabled. + * + * @return {String} + * @public + */ + +defineGetter(req, 'protocol', function protocol(){ + var proto = this.socket.encrypted + ? 'https' + : 'http'; + var trust = this.app.get('trust proxy fn'); + + if (!trust(this.socket.remoteAddress, 0)) { + return proto; + } + + // Note: X-Forwarded-Proto is normally only ever a + // single value, but this is to be safe. + var header = this.get('X-Forwarded-Proto') || proto + var index = header.indexOf(',') + + return index !== -1 + ? header.substring(0, index).trim() + : header.trim() +}); + +/** + * Short-hand for: + * + * req.protocol === 'https' + * + * @return {Boolean} + * @public + */ + +defineGetter(req, 'secure', function secure(){ + return this.protocol === 'https'; +}); + +/** + * Return the remote address from the trusted proxy. + * + * The is the remote address on the socket unless + * "trust proxy" is set. + * + * @return {String} + * @public + */ + +defineGetter(req, 'ip', function ip(){ + var trust = this.app.get('trust proxy fn'); + return proxyaddr(this, trust); +}); + +/** + * When "trust proxy" is set, trusted proxy addresses + client. + * + * For example if the value were "client, proxy1, proxy2" + * you would receive the array `["client", "proxy1", "proxy2"]` + * where "proxy2" is the furthest down-stream and "proxy1" and + * "proxy2" were trusted. + * + * @return {Array} + * @public + */ + +defineGetter(req, 'ips', function ips() { + var trust = this.app.get('trust proxy fn'); + var addrs = proxyaddr.all(this, trust); + + // reverse the order (to farthest -> closest) + // and remove socket address + addrs.reverse().pop() + + return addrs +}); + +/** + * Return subdomains as an array. + * + * Subdomains are the dot-separated parts of the host before the main domain of + * the app. By default, the domain of the app is assumed to be the last two + * parts of the host. This can be changed by setting "subdomain offset". + * + * For example, if the domain is "tobi.ferrets.example.com": + * If "subdomain offset" is not set, req.subdomains is `["ferrets", "tobi"]`. + * If "subdomain offset" is 3, req.subdomains is `["tobi"]`. + * + * @return {Array} + * @public + */ + +defineGetter(req, 'subdomains', function subdomains() { + var hostname = this.hostname; + + if (!hostname) return []; + + var offset = this.app.get('subdomain offset'); + var subdomains = !isIP(hostname) + ? hostname.split('.').reverse() + : [hostname]; + + return subdomains.slice(offset); +}); + +/** + * Short-hand for `url.parse(req.url).pathname`. + * + * @return {String} + * @public + */ + +defineGetter(req, 'path', function path() { + return parse(this).pathname; +}); + +/** + * Parse the "Host" header field to a host. + * + * When the "trust proxy" setting trusts the socket + * address, the "X-Forwarded-Host" header field will + * be trusted. + * + * @return {String} + * @public + */ + +defineGetter(req, 'host', function host(){ + var trust = this.app.get('trust proxy fn'); + var val = this.get('X-Forwarded-Host'); + + if (!val || !trust(this.socket.remoteAddress, 0)) { + val = this.get('Host'); + } else if (val.indexOf(',') !== -1) { + // Note: X-Forwarded-Host is normally only ever a + // single value, but this is to be safe. + val = val.substring(0, val.indexOf(',')).trimRight() + } + + return val || undefined; +}); + +/** + * Parse the "Host" header field to a hostname. + * + * When the "trust proxy" setting trusts the socket + * address, the "X-Forwarded-Host" header field will + * be trusted. + * + * @return {String} + * @api public + */ + +defineGetter(req, 'hostname', function hostname(){ + var host = this.host; + + if (!host) return; + + // IPv6 literal support + var offset = host[0] === '[' + ? host.indexOf(']') + 1 + : 0; + var index = host.indexOf(':', offset); + + return index !== -1 + ? host.substring(0, index) + : host; +}); + +/** + * Check if the request is fresh, aka + * Last-Modified or the ETag + * still match. + * + * @return {Boolean} + * @public + */ + +defineGetter(req, 'fresh', function(){ + var method = this.method; + var res = this.res + var status = res.statusCode + + // GET or HEAD for weak freshness validation only + if ('GET' !== method && 'HEAD' !== method) return false; + + // 2xx or 304 as per rfc2616 14.26 + if ((status >= 200 && status < 300) || 304 === status) { + return fresh(this.headers, { + 'etag': res.get('ETag'), + 'last-modified': res.get('Last-Modified') + }) + } + + return false; +}); + +/** + * Check if the request is stale, aka + * "Last-Modified" and / or the "ETag" for the + * resource has changed. + * + * @return {Boolean} + * @public + */ + +defineGetter(req, 'stale', function stale(){ + return !this.fresh; +}); + +/** + * Check if the request was an _XMLHttpRequest_. + * + * @return {Boolean} + * @public + */ + +defineGetter(req, 'xhr', function xhr(){ + var val = this.get('X-Requested-With') || ''; + return val.toLowerCase() === 'xmlhttprequest'; +}); + +/** + * Helper function for creating a getter on an object. + * + * @param {Object} obj + * @param {String} name + * @param {Function} getter + * @private + */ +function defineGetter(obj, name, getter) { + Object.defineProperty(obj, name, { + configurable: true, + enumerable: true, + get: getter + }); +} diff --git a/apps/api/lib/response.js b/apps/api/lib/response.js new file mode 100644 index 0000000..7a2f0ec --- /dev/null +++ b/apps/api/lib/response.js @@ -0,0 +1,1053 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var contentDisposition = require('content-disposition'); +var createError = require('http-errors') +var deprecate = require('depd')('express'); +var encodeUrl = require('encodeurl'); +var escapeHtml = require('escape-html'); +var http = require('node:http'); +var onFinished = require('on-finished'); +var mime = require('mime-types') +var path = require('node:path'); +var pathIsAbsolute = require('node:path').isAbsolute; +var statuses = require('statuses') +var sign = require('cookie-signature').sign; +var normalizeType = require('./utils').normalizeType; +var normalizeTypes = require('./utils').normalizeTypes; +var setCharset = require('./utils').setCharset; +var cookie = require('cookie'); +var send = require('send'); +var extname = path.extname; +var resolve = path.resolve; +var vary = require('vary'); +const { Buffer } = require('node:buffer'); + +/** + * Response prototype. + * @public + */ + +var res = Object.create(http.ServerResponse.prototype) + +/** + * Module exports. + * @public + */ + +module.exports = res + +/** + * Set the HTTP status code for the response. + * + * Expects an integer value between 100 and 999 inclusive. + * Throws an error if the provided status code is not an integer or if it's outside the allowable range. + * + * @param {number} code - The HTTP status code to set. + * @return {ServerResponse} - Returns itself for chaining methods. + * @throws {TypeError} If `code` is not an integer. + * @throws {RangeError} If `code` is outside the range 100 to 999. + * @public + */ + +res.status = function status(code) { + // Check if the status code is not an integer + if (!Number.isInteger(code)) { + throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`); + } + // Check if the status code is outside of Node's valid range + if (code < 100 || code > 999) { + throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`); + } + + this.statusCode = code; + return this; +}; + +/** + * Set Link header field with the given `links`. + * + * Examples: + * + * res.links({ + * next: 'http://api.example.com/users?page=2', + * last: 'http://api.example.com/users?page=5', + * pages: [ + * 'http://api.example.com/users?page=1', + * 'http://api.example.com/users?page=2' + * ] + * }); + * + * @param {Object} links + * @return {ServerResponse} + * @public + */ + +res.links = function(links) { + var link = this.get('Link') || ''; + if (link) link += ', '; + return this.set('Link', link + Object.keys(links).map(function(rel) { + // Allow multiple links if links[rel] is an array + if (Array.isArray(links[rel])) { + return links[rel].map(function (singleLink) { + return `<${singleLink}>; rel="${rel}"`; + }).join(', '); + } else { + return `<${links[rel]}>; rel="${rel}"`; + } + }).join(', ')); +}; + +/** + * Send a response. + * + * Examples: + * + * res.send(Buffer.from('wahoo')); + * res.send({ some: 'json' }); + * res.send('

    some html

    '); + * + * @param {string|number|boolean|object|Buffer} body + * @public + */ + +res.send = function send(body) { + var chunk = body; + var encoding; + var req = this.req; + var type; + + // settings + var app = this.app; + + switch (typeof chunk) { + // string defaulting to html + case 'string': + if (!this.get('Content-Type')) { + this.type('html'); + } + break; + case 'boolean': + case 'number': + case 'object': + if (chunk === null) { + chunk = ''; + } else if (ArrayBuffer.isView(chunk)) { + if (!this.get('Content-Type')) { + this.type('bin'); + } + } else { + return this.json(chunk); + } + break; + } + + // write strings in utf-8 + if (typeof chunk === 'string') { + encoding = 'utf8'; + type = this.get('Content-Type'); + + // reflect this in content-type + if (typeof type === 'string') { + this.set('Content-Type', setCharset(type, 'utf-8')); + } + } + + // determine if ETag should be generated + var etagFn = app.get('etag fn') + var generateETag = !this.get('ETag') && typeof etagFn === 'function' + + // populate Content-Length + var len + if (chunk !== undefined) { + if (Buffer.isBuffer(chunk)) { + // get length of Buffer + len = chunk.length + } else if (!generateETag && chunk.length < 1000) { + // just calculate length when no ETag + small chunk + len = Buffer.byteLength(chunk, encoding) + } else { + // convert chunk to Buffer and calculate + chunk = Buffer.from(chunk, encoding) + encoding = undefined; + len = chunk.length + } + + this.set('Content-Length', len); + } + + // populate ETag + var etag; + if (generateETag && len !== undefined) { + if ((etag = etagFn(chunk, encoding))) { + this.set('ETag', etag); + } + } + + // freshness + if (req.fresh) this.status(304); + + // strip irrelevant headers + if (204 === this.statusCode || 304 === this.statusCode) { + this.removeHeader('Content-Type'); + this.removeHeader('Content-Length'); + this.removeHeader('Transfer-Encoding'); + chunk = ''; + } + + // alter headers for 205 + if (this.statusCode === 205) { + this.set('Content-Length', '0') + this.removeHeader('Transfer-Encoding') + chunk = '' + } + + if (req.method === 'HEAD') { + // skip body for HEAD + this.end(); + } else { + // respond + this.end(chunk, encoding); + } + + return this; +}; + +/** + * Send JSON response. + * + * Examples: + * + * res.json(null); + * res.json({ user: 'tj' }); + * + * @param {string|number|boolean|object} obj + * @public + */ + +res.json = function json(obj) { + // settings + var app = this.app; + var escape = app.get('json escape') + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(obj, replacer, spaces, escape) + + // content-type + if (!this.get('Content-Type')) { + this.set('Content-Type', 'application/json'); + } + + return this.send(body); +}; + +/** + * Send JSON response with JSONP callback support. + * + * Examples: + * + * res.jsonp(null); + * res.jsonp({ user: 'tj' }); + * + * @param {string|number|boolean|object} obj + * @public + */ + +res.jsonp = function jsonp(obj) { + // settings + var app = this.app; + var escape = app.get('json escape') + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(obj, replacer, spaces, escape) + var callback = this.req.query[app.get('jsonp callback name')]; + + // content-type + if (!this.get('Content-Type')) { + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'application/json'); + } + + // fixup callback + if (Array.isArray(callback)) { + callback = callback[0]; + } + + // jsonp + if (typeof callback === 'string' && callback.length !== 0) { + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'text/javascript'); + + // restrict callback charset + callback = callback.replace(/[^\[\]\w$.]/g, ''); + + if (body === undefined) { + // empty argument + body = '' + } else if (typeof body === 'string') { + // replace chars not allowed in JavaScript that are in JSON + body = body + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') + } + + // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" + // the typeof check is just to reduce client error noise + body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; + } + + return this.send(body); +}; + +/** + * Send given HTTP status code. + * + * Sets the response status to `statusCode` and the body of the + * response to the standard description from node's http.STATUS_CODES + * or the statusCode number if no description. + * + * Examples: + * + * res.sendStatus(200); + * + * @param {number} statusCode + * @public + */ + +res.sendStatus = function sendStatus(statusCode) { + var body = statuses.message[statusCode] || String(statusCode) + + this.status(statusCode); + this.type('txt'); + + return this.send(body); +}; + +/** + * Transfer the file at the given `path`. + * + * Automatically sets the _Content-Type_ response header field. + * The callback `callback(err)` is invoked when the transfer is complete + * or when an error occurs. Be sure to check `res.headersSent` + * if you wish to attempt responding, as the header and some data + * may have already been transferred. + * + * Options: + * + * - `maxAge` defaulting to 0 (can be string converted by `ms`) + * - `root` root directory for relative filenames + * - `headers` object of headers to serve with file + * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them + * + * Other options are passed along to `send`. + * + * Examples: + * + * The following example illustrates how `res.sendFile()` may + * be used as an alternative for the `static()` middleware for + * dynamic situations. The code backing `res.sendFile()` is actually + * the same code, so HTTP cache support etc is identical. + * + * app.get('/user/:uid/photos/:file', function(req, res){ + * var uid = req.params.uid + * , file = req.params.file; + * + * req.user.mayViewFilesFrom(uid, function(yes){ + * if (yes) { + * res.sendFile('/uploads/' + uid + '/' + file); + * } else { + * res.send(403, 'Sorry! you cant see that.'); + * } + * }); + * }); + * + * @public + */ + +res.sendFile = function sendFile(path, options, callback) { + var done = callback; + var req = this.req; + var res = this; + var next = req.next; + var opts = options || {}; + + if (!path) { + throw new TypeError('path argument is required to res.sendFile'); + } + + if (typeof path !== 'string') { + throw new TypeError('path must be a string to res.sendFile') + } + + // support function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; + } + + if (!opts.root && !pathIsAbsolute(path)) { + throw new TypeError('path must be absolute or specify root to res.sendFile'); + } + + // create file stream + var pathname = encodeURI(path); + + // wire application etag option to send + opts.etag = this.app.enabled('etag'); + var file = send(req, pathname, opts); + + // transfer + sendfile(res, file, opts, function (err) { + if (done) return done(err); + if (err && err.code === 'EISDIR') return next(); + + // next() all but write errors + if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { + next(err); + } + }); +}; + +/** + * Transfer the file at the given `path` as an attachment. + * + * Optionally providing an alternate attachment `filename`, + * and optional callback `callback(err)`. The callback is invoked + * when the data transfer is complete, or when an error has + * occurred. Be sure to check `res.headersSent` if you plan to respond. + * + * Optionally providing an `options` object to use with `res.sendFile()`. + * This function will set the `Content-Disposition` header, overriding + * any `Content-Disposition` header passed as header options in order + * to set the attachment and filename. + * + * This method uses `res.sendFile()`. + * + * @public + */ + +res.download = function download (path, filename, options, callback) { + var done = callback; + var name = filename; + var opts = options || null + + // support function as second or third arg + if (typeof filename === 'function') { + done = filename; + name = null; + opts = null + } else if (typeof options === 'function') { + done = options + opts = null + } + + // support optional filename, where options may be in it's place + if (typeof filename === 'object' && + (typeof options === 'function' || options === undefined)) { + name = null + opts = filename + } + + // set Content-Disposition when file is sent + var headers = { + 'Content-Disposition': contentDisposition(name || path) + }; + + // merge user-provided headers + if (opts && opts.headers) { + var keys = Object.keys(opts.headers) + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + if (key.toLowerCase() !== 'content-disposition') { + headers[key] = opts.headers[key] + } + } + } + + // merge user-provided options + opts = Object.create(opts) + opts.headers = headers + + // Resolve the full path for sendFile + var fullPath = !opts.root + ? resolve(path) + : path + + // send file + return this.sendFile(fullPath, opts, done) +}; + +/** + * Set _Content-Type_ response header with `type` through `mime.contentType()` + * when it does not contain "/", or set the Content-Type to `type` otherwise. + * When no mapping is found though `mime.contentType()`, the type is set to + * "application/octet-stream". + * + * Examples: + * + * res.type('.html'); + * res.type('html'); + * res.type('json'); + * res.type('application/json'); + * res.type('png'); + * + * @param {String} type + * @return {ServerResponse} for chaining + * @public + */ + +res.contentType = +res.type = function contentType(type) { + var ct = type.indexOf('/') === -1 + ? (mime.contentType(type) || 'application/octet-stream') + : type; + + return this.set('Content-Type', ct); +}; + +/** + * Respond to the Acceptable formats using an `obj` + * of mime-type callbacks. + * + * This method uses `req.accepted`, an array of + * acceptable types ordered by their quality values. + * When "Accept" is not present the _first_ callback + * is invoked, otherwise the first match is used. When + * no match is performed the server responds with + * 406 "Not Acceptable". + * + * Content-Type is set for you, however if you choose + * you may alter this within the callback using `res.type()` + * or `res.set('Content-Type', ...)`. + * + * res.format({ + * 'text/plain': function(){ + * res.send('hey'); + * }, + * + * 'text/html': function(){ + * res.send('

    hey

    '); + * }, + * + * 'application/json': function () { + * res.send({ message: 'hey' }); + * } + * }); + * + * In addition to canonicalized MIME types you may + * also use extnames mapped to these types: + * + * res.format({ + * text: function(){ + * res.send('hey'); + * }, + * + * html: function(){ + * res.send('

    hey

    '); + * }, + * + * json: function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * By default Express passes an `Error` + * with a `.status` of 406 to `next(err)` + * if a match is not made. If you provide + * a `.default` callback it will be invoked + * instead. + * + * @param {Object} obj + * @return {ServerResponse} for chaining + * @public + */ + +res.format = function(obj){ + var req = this.req; + var next = req.next; + + var keys = Object.keys(obj) + .filter(function (v) { return v !== 'default' }) + + var key = keys.length > 0 + ? req.accepts(keys) + : false; + + this.vary("Accept"); + + if (key) { + this.set('Content-Type', normalizeType(key).value); + obj[key](req, this, next); + } else if (obj.default) { + obj.default(req, this, next) + } else { + next(createError(406, { + types: normalizeTypes(keys).map(function (o) { return o.value }) + })) + } + + return this; +}; + +/** + * Set _Content-Disposition_ header to _attachment_ with optional `filename`. + * + * @param {String} filename + * @return {ServerResponse} + * @public + */ + +res.attachment = function attachment(filename) { + if (filename) { + this.type(extname(filename)); + } + + this.set('Content-Disposition', contentDisposition(filename)); + + return this; +}; + +/** + * Append additional header `field` with value `val`. + * + * Example: + * + * res.append('Link', ['', '']); + * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); + * res.append('Warning', '199 Miscellaneous warning'); + * + * @param {String} field + * @param {String|Array} val + * @return {ServerResponse} for chaining + * @public + */ + +res.append = function append(field, val) { + var prev = this.get(field); + var value = val; + + if (prev) { + // concat the new and prev vals + value = Array.isArray(prev) ? prev.concat(val) + : Array.isArray(val) ? [prev].concat(val) + : [prev, val] + } + + return this.set(field, value); +}; + +/** + * Set header `field` to `val`, or pass + * an object of header fields. + * + * Examples: + * + * res.set('Foo', ['bar', 'baz']); + * res.set('Accept', 'application/json'); + * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); + * + * Aliased as `res.header()`. + * + * When the set header is "Content-Type", the type is expanded to include + * the charset if not present using `mime.contentType()`. + * + * @param {String|Object} field + * @param {String|Array} val + * @return {ServerResponse} for chaining + * @public + */ + +res.set = +res.header = function header(field, val) { + if (arguments.length === 2) { + var value = Array.isArray(val) + ? val.map(String) + : String(val); + + // add charset to content-type + if (field.toLowerCase() === 'content-type') { + if (Array.isArray(value)) { + throw new TypeError('Content-Type cannot be set to an Array'); + } + value = mime.contentType(value) + } + + this.setHeader(field, value); + } else { + for (var key in field) { + this.set(key, field[key]); + } + } + return this; +}; + +/** + * Get value for header `field`. + * + * @param {String} field + * @return {String} + * @public + */ + +res.get = function(field){ + return this.getHeader(field); +}; + +/** + * Clear cookie `name`. + * + * @param {String} name + * @param {Object} [options] + * @return {ServerResponse} for chaining + * @public + */ + +res.clearCookie = function clearCookie(name, options) { + // Force cookie expiration by setting expires to the past + const opts = { path: '/', ...options, expires: new Date(1)}; + // ensure maxAge is not passed + delete opts.maxAge + + return this.cookie(name, '', opts); +}; + +/** + * Set cookie `name` to `value`, with the given `options`. + * + * Options: + * + * - `maxAge` max-age in milliseconds, converted to `expires` + * - `signed` sign the cookie + * - `path` defaults to "/" + * + * Examples: + * + * // "Remember Me" for 15 minutes + * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); + * + * // same as above + * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) + * + * @param {String} name + * @param {String|Object} value + * @param {Object} [options] + * @return {ServerResponse} for chaining + * @public + */ + +res.cookie = function (name, value, options) { + var opts = { ...options }; + var secret = this.req.secret; + var signed = opts.signed; + + if (signed && !secret) { + throw new Error('cookieParser("secret") required for signed cookies'); + } + + var val = typeof value === 'object' + ? 'j:' + JSON.stringify(value) + : String(value); + + if (signed) { + val = 's:' + sign(val, secret); + } + + if (opts.maxAge != null) { + var maxAge = opts.maxAge - 0 + + if (!isNaN(maxAge)) { + opts.expires = new Date(Date.now() + maxAge) + opts.maxAge = Math.floor(maxAge / 1000) + } + } + + if (opts.path == null) { + opts.path = '/'; + } + + this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); + + return this; +}; + +/** + * Set the location header to `url`. + * + * The given `url` can also be "back", which redirects + * to the _Referrer_ or _Referer_ headers or "/". + * + * Examples: + * + * res.location('/foo/bar').; + * res.location('http://example.com'); + * res.location('../login'); + * + * @param {String} url + * @return {ServerResponse} for chaining + * @public + */ + +res.location = function location(url) { + return this.set('Location', encodeUrl(url)); +}; + +/** + * Redirect to the given `url` with optional response `status` + * defaulting to 302. + * + * Examples: + * + * res.redirect('/foo/bar'); + * res.redirect('http://example.com'); + * res.redirect(301, 'http://example.com'); + * res.redirect('../login'); // /blog/post/1 -> /blog/login + * + * @public + */ + +res.redirect = function redirect(url) { + var address = url; + var body; + var status = 302; + + // allow status / url + if (arguments.length === 2) { + status = arguments[0] + address = arguments[1] + } + + if (!address) { + deprecate('Provide a url argument'); + } + + if (typeof address !== 'string') { + deprecate('Url must be a string'); + } + + if (typeof status !== 'number') { + deprecate('Status must be a number'); + } + + // Set location header + address = this.location(address).get('Location'); + + // Support text/{plain,html} by default + this.format({ + text: function(){ + body = statuses.message[status] + '. Redirecting to ' + address + }, + + html: function(){ + var u = escapeHtml(address); + body = '

    ' + statuses.message[status] + '. Redirecting to ' + u + '

    ' + }, + + default: function(){ + body = ''; + } + }); + + // Respond + this.status(status); + this.set('Content-Length', Buffer.byteLength(body)); + + if (this.req.method === 'HEAD') { + this.end(); + } else { + this.end(body); + } +}; + +/** + * Add `field` to Vary. If already present in the Vary set, then + * this call is simply ignored. + * + * @param {Array|String} field + * @return {ServerResponse} for chaining + * @public + */ + +res.vary = function(field){ + vary(this, field); + + return this; +}; + +/** + * Render `view` with the given `options` and optional callback `fn`. + * When a callback function is given a response will _not_ be made + * automatically, otherwise a response of _200_ and _text/html_ is given. + * + * Options: + * + * - `cache` boolean hinting to the engine it should cache + * - `filename` filename of the view being rendered + * + * @public + */ + +res.render = function render(view, options, callback) { + var app = this.req.app; + var done = callback; + var opts = options || {}; + var req = this.req; + var self = this; + + // support callback function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; + } + + // merge res.locals + opts._locals = self.locals; + + // default callback to respond + done = done || function (err, str) { + if (err) return req.next(err); + self.send(str); + }; + + // render + app.render(view, opts, done); +}; + +// pipe the send file stream +function sendfile(res, file, options, callback) { + var done = false; + var streaming; + + // request aborted + function onaborted() { + if (done) return; + done = true; + + var err = new Error('Request aborted'); + err.code = 'ECONNABORTED'; + callback(err); + } + + // directory + function ondirectory() { + if (done) return; + done = true; + + var err = new Error('EISDIR, read'); + err.code = 'EISDIR'; + callback(err); + } + + // errors + function onerror(err) { + if (done) return; + done = true; + callback(err); + } + + // ended + function onend() { + if (done) return; + done = true; + callback(); + } + + // file + function onfile() { + streaming = false; + } + + // finished + function onfinish(err) { + if (err && err.code === 'ECONNRESET') return onaborted(); + if (err) return onerror(err); + if (done) return; + + setImmediate(function () { + if (streaming !== false && !done) { + onaborted(); + return; + } + + if (done) return; + done = true; + callback(); + }); + } + + // streaming + function onstream() { + streaming = true; + } + + file.on('directory', ondirectory); + file.on('end', onend); + file.on('error', onerror); + file.on('file', onfile); + file.on('stream', onstream); + onFinished(res, onfinish); + + if (options.headers) { + // set headers on successful transfer + file.on('headers', function headers(res) { + var obj = options.headers; + var keys = Object.keys(obj); + + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + res.setHeader(k, obj[k]); + } + }); + } + + // pipe + file.pipe(res); +} + +/** + * Stringify JSON, like JSON.stringify, but v8 optimized, with the + * ability to escape characters that can trigger HTML sniffing. + * + * @param {*} value + * @param {function} replacer + * @param {number} spaces + * @param {boolean} escape + * @returns {string} + * @private + */ + +function stringify (value, replacer, spaces, escape) { + // v8 checks arguments.length for optimizing simple call + // https://bugs.chromium.org/p/v8/issues/detail?id=4730 + var json = replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); + + if (escape && typeof json === 'string') { + json = json.replace(/[<>&]/g, function (c) { + switch (c.charCodeAt(0)) { + case 0x3c: + return '\\u003c' + case 0x3e: + return '\\u003e' + case 0x26: + return '\\u0026' + /* istanbul ignore next: unreachable default */ + default: + return c + } + }) + } + + return json +} diff --git a/apps/api/lib/utils.js b/apps/api/lib/utils.js new file mode 100644 index 0000000..4f21e7e --- /dev/null +++ b/apps/api/lib/utils.js @@ -0,0 +1,271 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @api private + */ + +var { METHODS } = require('node:http'); +var contentType = require('content-type'); +var etag = require('etag'); +var mime = require('mime-types') +var proxyaddr = require('proxy-addr'); +var qs = require('qs'); +var querystring = require('node:querystring'); +const { Buffer } = require('node:buffer'); + + +/** + * A list of lowercased HTTP methods that are supported by Node.js. + * @api private + */ +exports.methods = METHODS.map((method) => method.toLowerCase()); + +/** + * Return strong ETag for `body`. + * + * @param {String|Buffer} body + * @param {String} [encoding] + * @return {String} + * @api private + */ + +exports.etag = createETagGenerator({ weak: false }) + +/** + * Return weak ETag for `body`. + * + * @param {String|Buffer} body + * @param {String} [encoding] + * @return {String} + * @api private + */ + +exports.wetag = createETagGenerator({ weak: true }) + +/** + * Normalize the given `type`, for example "html" becomes "text/html". + * + * @param {String} type + * @return {Object} + * @api private + */ + +exports.normalizeType = function(type){ + return ~type.indexOf('/') + ? acceptParams(type) + : { value: (mime.lookup(type) || 'application/octet-stream'), params: {} } +}; + +/** + * Normalize `types`, for example "html" becomes "text/html". + * + * @param {Array} types + * @return {Array} + * @api private + */ + +exports.normalizeTypes = function(types) { + return types.map(exports.normalizeType); +}; + + +/** + * Parse accept params `str` returning an + * object with `.value`, `.quality` and `.params`. + * + * @param {String} str + * @return {Object} + * @api private + */ + +function acceptParams (str) { + var length = str.length; + var colonIndex = str.indexOf(';'); + var index = colonIndex === -1 ? length : colonIndex; + var ret = { value: str.slice(0, index).trim(), quality: 1, params: {} }; + + while (index < length) { + var splitIndex = str.indexOf('=', index); + if (splitIndex === -1) break; + + var colonIndex = str.indexOf(';', index); + var endIndex = colonIndex === -1 ? length : colonIndex; + + if (splitIndex > endIndex) { + index = str.lastIndexOf(';', splitIndex - 1) + 1; + continue; + } + + var key = str.slice(index, splitIndex).trim(); + var value = str.slice(splitIndex + 1, endIndex).trim(); + + if (key === 'q') { + ret.quality = parseFloat(value); + } else { + ret.params[key] = value; + } + + index = endIndex + 1; + } + + return ret; +} + +/** + * Compile "etag" value to function. + * + * @param {Boolean|String|Function} val + * @return {Function} + * @api private + */ + +exports.compileETag = function(val) { + var fn; + + if (typeof val === 'function') { + return val; + } + + switch (val) { + case true: + case 'weak': + fn = exports.wetag; + break; + case false: + break; + case 'strong': + fn = exports.etag; + break; + default: + throw new TypeError('unknown value for etag function: ' + val); + } + + return fn; +} + +/** + * Compile "query parser" value to function. + * + * @param {String|Function} val + * @return {Function} + * @api private + */ + +exports.compileQueryParser = function compileQueryParser(val) { + var fn; + + if (typeof val === 'function') { + return val; + } + + switch (val) { + case true: + case 'simple': + fn = querystring.parse; + break; + case false: + break; + case 'extended': + fn = parseExtendedQueryString; + break; + default: + throw new TypeError('unknown value for query parser function: ' + val); + } + + return fn; +} + +/** + * Compile "proxy trust" value to function. + * + * @param {Boolean|String|Number|Array|Function} val + * @return {Function} + * @api private + */ + +exports.compileTrust = function(val) { + if (typeof val === 'function') return val; + + if (val === true) { + // Support plain true/false + return function(){ return true }; + } + + if (typeof val === 'number') { + // Support trusting hop count + return function(a, i){ return i < val }; + } + + if (typeof val === 'string') { + // Support comma-separated values + val = val.split(',') + .map(function (v) { return v.trim() }) + } + + return proxyaddr.compile(val || []); +} + +/** + * Set the charset in a given Content-Type string. + * + * @param {String} type + * @param {String} charset + * @return {String} + * @api private + */ + +exports.setCharset = function setCharset(type, charset) { + if (!type || !charset) { + return type; + } + + // parse type + var parsed = contentType.parse(type); + + // set charset + parsed.parameters.charset = charset; + + // format type + return contentType.format(parsed); +}; + +/** + * Create an ETag generator function, generating ETags with + * the given options. + * + * @param {object} options + * @return {function} + * @private + */ + +function createETagGenerator (options) { + return function generateETag (body, encoding) { + var buf = !Buffer.isBuffer(body) + ? Buffer.from(body, encoding) + : body + + return etag(buf, options) + } +} + +/** + * Parse an extended query string with qs. + * + * @param {String} str + * @return {Object} + * @private + */ + +function parseExtendedQueryString(str) { + return qs.parse(str, { + allowPrototypes: true + }); +} diff --git a/apps/api/lib/view.js b/apps/api/lib/view.js new file mode 100644 index 0000000..d66b4a2 --- /dev/null +++ b/apps/api/lib/view.js @@ -0,0 +1,205 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var debug = require('debug')('express:view'); +var path = require('node:path'); +var fs = require('node:fs'); + +/** + * Module variables. + * @private + */ + +var dirname = path.dirname; +var basename = path.basename; +var extname = path.extname; +var join = path.join; +var resolve = path.resolve; + +/** + * Module exports. + * @public + */ + +module.exports = View; + +/** + * Initialize a new `View` with the given `name`. + * + * Options: + * + * - `defaultEngine` the default template engine name + * - `engines` template engine require() cache + * - `root` root path for view lookup + * + * @param {string} name + * @param {object} options + * @public + */ + +function View(name, options) { + var opts = options || {}; + + this.defaultEngine = opts.defaultEngine; + this.ext = extname(name); + this.name = name; + this.root = opts.root; + + if (!this.ext && !this.defaultEngine) { + throw new Error('No default engine was specified and no extension was provided.'); + } + + var fileName = name; + + if (!this.ext) { + // get extension from default engine name + this.ext = this.defaultEngine[0] !== '.' + ? '.' + this.defaultEngine + : this.defaultEngine; + + fileName += this.ext; + } + + if (!opts.engines[this.ext]) { + // load engine + var mod = this.ext.slice(1) + debug('require "%s"', mod) + + // default engine export + var fn = require(mod).__express + + if (typeof fn !== 'function') { + throw new Error('Module "' + mod + '" does not provide a view engine.') + } + + opts.engines[this.ext] = fn + } + + // store loaded engine + this.engine = opts.engines[this.ext]; + + // lookup path + this.path = this.lookup(fileName); +} + +/** + * Lookup view by the given `name` + * + * @param {string} name + * @private + */ + +View.prototype.lookup = function lookup(name) { + var path; + var roots = [].concat(this.root); + + debug('lookup "%s"', name); + + for (var i = 0; i < roots.length && !path; i++) { + var root = roots[i]; + + // resolve the path + var loc = resolve(root, name); + var dir = dirname(loc); + var file = basename(loc); + + // resolve the file + path = this.resolve(dir, file); + } + + return path; +}; + +/** + * Render with the given options. + * + * @param {object} options + * @param {function} callback + * @private + */ + +View.prototype.render = function render(options, callback) { + var sync = true; + + debug('render "%s"', this.path); + + // render, normalizing sync callbacks + this.engine(this.path, options, function onRender() { + if (!sync) { + return callback.apply(this, arguments); + } + + // copy arguments + var args = new Array(arguments.length); + var cntx = this; + + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + // force callback to be async + return process.nextTick(function renderTick() { + return callback.apply(cntx, args); + }); + }); + + sync = false; +}; + +/** + * Resolve the file within the given directory. + * + * @param {string} dir + * @param {string} file + * @private + */ + +View.prototype.resolve = function resolve(dir, file) { + var ext = this.ext; + + // . + var path = join(dir, file); + var stat = tryStat(path); + + if (stat && stat.isFile()) { + return path; + } + + // /index. + path = join(dir, basename(file, ext), 'index' + ext); + stat = tryStat(path); + + if (stat && stat.isFile()) { + return path; + } +}; + +/** + * Return a stat, maybe. + * + * @param {string} path + * @return {fs.Stats} + * @private + */ + +function tryStat(path) { + debug('stat "%s"', path); + + try { + return fs.statSync(path); + } catch (e) { + return undefined; + } +} diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..cb729a5 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,108 @@ +{ + "name": "express", + "description": "Fast, unopinionated, minimalist web framework", + "version": "5.1.0", + "author": "TJ Holowaychuk ", + "contributors": [ + "Aaron Heckmann ", + "Ciaran Jessup ", + "Douglas Christopher Wilson ", + "Guillermo Rauch ", + "Jonathan Ong ", + "Roman Shtylman ", + "Young Jae Sim " + ], + "license": "MIT", + "repository": "expressjs/express", + "homepage": "https://expressjs.com/", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, + "keywords": [ + "express", + "framework", + "sinatra", + "web", + "http", + "rest", + "restful", + "router", + "app", + "api" + ], + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "cors": "^2.8.5", + "debug": "^4.4.0", + "depd": "^2.0.0", + "dotenv": "^17.2.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "express": "^5.1.0", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/node": "^24.6.0", + "after": "0.8.2", + "connect-redis": "^8.0.1", + "cookie-parser": "1.4.7", + "cookie-session": "2.1.1", + "ejs": "^3.1.10", + "eslint": "8.47.0", + "express-session": "^1.18.1", + "hbs": "4.2.0", + "marked": "^15.0.3", + "method-override": "3.0.0", + "mocha": "^10.7.3", + "morgan": "1.10.1", + "nyc": "^17.1.0", + "pbkdf2-password": "1.2.1", + "supertest": "^6.3.0", + "ts-node-dev": "^2.0.0", + "typescript": "~5.9.3", + "vhost": "~3.0.2" + }, + "engines": { + "node": ">= 18" + }, + "files": [ + "LICENSE", + "Readme.md", + "index.js", + "lib/" + ], + "scripts": { + "dev": "ts-node-dev src/index.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "mocha --require test/support/env --reporter spec --check-leaks test/ test/acceptance/", + "test-ci": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=html --reporter=text npm test", + "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" + } +} diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts new file mode 100644 index 0000000..cf91aa0 --- /dev/null +++ b/apps/api/src/auth.ts @@ -0,0 +1,22 @@ +import express from 'express'; +const router = express.Router(); + +router.post('/', (req, res) => { + const { password } = req.body; + console.log('Auth attempt:', { + received: password, + expected: process.env.ADMIN_PASSWORD + }); + + if (password === process.env.ADMIN_PASSWORD) { + res.status(200).json({ success: true }); + } else { + console.log('Auth failed - password mismatch'); + res.status(401).json({ + success: false, + error: 'Invalid credentials' + }); + } +}); + +export default router; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..b63eb59 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,28 @@ +import path from 'path'; +import dotenv from 'dotenv'; +dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); +import express from 'express'; +import cors from 'cors'; +import authRouter from './auth'; + +const app = express(); +console.log('ENV ADMIN_PASSWORD loaded:', Boolean(process.env.ADMIN_PASSWORD)); + +// Middleware +app.use(cors({ + origin: 'http://localhost:5173', + credentials: true +})); +app.use(express.json()); + +// API routes +app.use('/api/auth', authRouter); +app.get('/api/health', (_req, res) => { + res.json({ ok: true }); +}); + +// Start server +const PORT = process.env.PORT || 3001; +app.listen(PORT, () => { + console.log(`API server running on port ${PORT}`); +}); diff --git a/apps/api/test/Route.js b/apps/api/test/Route.js new file mode 100644 index 0000000..e4b73c7 --- /dev/null +++ b/apps/api/test/Route.js @@ -0,0 +1,274 @@ +'use strict' + +var after = require('after'); +var assert = require('node:assert') +var express = require('../') + , Route = express.Route + , methods = require('../lib/utils').methods + +describe('Route', function(){ + it('should work without handlers', function(done) { + var req = { method: 'GET', url: '/' } + var route = new Route('/foo') + route.dispatch(req, {}, done) + }) + + it('should not stack overflow with a large sync stack', function (done) { + this.timeout(5000) // long-running test + + var req = { method: 'GET', url: '/' } + var route = new Route('/foo') + + route.get(function (req, res, next) { + req.counter = 0 + next() + }) + + for (var i = 0; i < 6000; i++) { + route.all(function (req, res, next) { + req.counter++ + next() + }) + } + + route.get(function (req, res, next) { + req.called = true + next() + }) + + route.dispatch(req, {}, function (err) { + if (err) return done(err) + assert.ok(req.called) + assert.strictEqual(req.counter, 6000) + done() + }) + }) + + describe('.all', function(){ + it('should add handler', function(done){ + var req = { method: 'GET', url: '/' }; + var route = new Route('/foo'); + + route.all(function(req, res, next) { + req.called = true; + next(); + }); + + route.dispatch(req, {}, function (err) { + if (err) return done(err); + assert.ok(req.called) + done(); + }); + }) + + it('should handle VERBS', function(done) { + var count = 0; + var route = new Route('/foo'); + var cb = after(methods.length, function (err) { + if (err) return done(err); + assert.strictEqual(count, methods.length) + done(); + }); + + route.all(function(req, res, next) { + count++; + next(); + }); + + methods.forEach(function testMethod(method) { + var req = { method: method, url: '/' }; + route.dispatch(req, {}, cb); + }); + }) + + it('should stack', function(done) { + var req = { count: 0, method: 'GET', url: '/' }; + var route = new Route('/foo'); + + route.all(function(req, res, next) { + req.count++; + next(); + }); + + route.all(function(req, res, next) { + req.count++; + next(); + }); + + route.dispatch(req, {}, function (err) { + if (err) return done(err); + assert.strictEqual(req.count, 2) + done(); + }); + }) + }) + + describe('.VERB', function(){ + it('should support .get', function(done){ + var req = { method: 'GET', url: '/' }; + var route = new Route(''); + + route.get(function(req, res, next) { + req.called = true; + next(); + }) + + route.dispatch(req, {}, function (err) { + if (err) return done(err); + assert.ok(req.called) + done(); + }); + }) + + it('should limit to just .VERB', function(done){ + var req = { method: 'POST', url: '/' }; + var route = new Route(''); + + route.get(function () { + throw new Error('not me!'); + }) + + route.post(function(req, res, next) { + req.called = true; + next(); + }) + + route.dispatch(req, {}, function (err) { + if (err) return done(err); + assert.ok(req.called) + done(); + }); + }) + + it('should allow fallthrough', function(done){ + var req = { order: '', method: 'GET', url: '/' }; + var route = new Route(''); + + route.get(function(req, res, next) { + req.order += 'a'; + next(); + }) + + route.all(function(req, res, next) { + req.order += 'b'; + next(); + }); + + route.get(function(req, res, next) { + req.order += 'c'; + next(); + }) + + route.dispatch(req, {}, function (err) { + if (err) return done(err); + assert.strictEqual(req.order, 'abc') + done(); + }); + }) + }) + + describe('errors', function(){ + it('should handle errors via arity 4 functions', function(done){ + var req = { order: '', method: 'GET', url: '/' }; + var route = new Route(''); + + route.all(function(req, res, next){ + next(new Error('foobar')); + }); + + route.all(function(req, res, next){ + req.order += '0'; + next(); + }); + + route.all(function(err, req, res, next){ + req.order += 'a'; + next(err); + }); + + route.dispatch(req, {}, function (err) { + assert.ok(err) + assert.strictEqual(err.message, 'foobar') + assert.strictEqual(req.order, 'a') + done(); + }); + }) + + it('should handle throw', function(done) { + var req = { order: '', method: 'GET', url: '/' }; + var route = new Route(''); + + route.all(function () { + throw new Error('foobar'); + }); + + route.all(function(req, res, next){ + req.order += '0'; + next(); + }); + + route.all(function(err, req, res, next){ + req.order += 'a'; + next(err); + }); + + route.dispatch(req, {}, function (err) { + assert.ok(err) + assert.strictEqual(err.message, 'foobar') + assert.strictEqual(req.order, 'a') + done(); + }); + }); + + it('should handle throwing inside error handlers', function(done) { + var req = { method: 'GET', url: '/' }; + var route = new Route(''); + + route.get(function () { + throw new Error('boom!'); + }); + + route.get(function(err, req, res, next){ + throw new Error('oops'); + }); + + route.get(function(err, req, res, next){ + req.message = err.message; + next(); + }); + + route.dispatch(req, {}, function (err) { + if (err) return done(err); + assert.strictEqual(req.message, 'oops') + done(); + }); + }); + + it('should handle throw in .all', function(done) { + var req = { method: 'GET', url: '/' }; + var route = new Route(''); + + route.all(function(req, res, next){ + throw new Error('boom!'); + }); + + route.dispatch(req, {}, function(err){ + assert.ok(err) + assert.strictEqual(err.message, 'boom!') + done(); + }); + }); + + it('should handle single error handler', function(done) { + var req = { method: 'GET', url: '/' }; + var route = new Route(''); + + route.all(function(err, req, res, next){ + // this should not execute + throw new Error('should not be called') + }); + + route.dispatch(req, {}, done); + }); + }) +}) diff --git a/apps/api/test/Router.js b/apps/api/test/Router.js new file mode 100644 index 0000000..7bac715 --- /dev/null +++ b/apps/api/test/Router.js @@ -0,0 +1,636 @@ +'use strict' + +var after = require('after'); +var express = require('../') + , Router = express.Router + , methods = require('../lib/utils').methods + , assert = require('node:assert'); + +describe('Router', function () { + it('should return a function with router methods', function () { + var router = new Router(); + assert(typeof router === 'function') + + assert(typeof router.get === 'function') + assert(typeof router.handle === 'function') + assert(typeof router.use === 'function') + }); + + it('should support .use of other routers', function (done) { + var router = new Router(); + var another = new Router(); + + another.get('/bar', function (req, res) { + res.end(); + }); + router.use('/foo', another); + + router.handle({ url: '/foo/bar', method: 'GET' }, { end: done }, function () { }); + }); + + it('should support dynamic routes', function (done) { + var router = new Router(); + var another = new Router(); + + another.get('/:bar', function (req, res) { + assert.strictEqual(req.params.bar, 'route') + res.end(); + }); + router.use('/:foo', another); + + router.handle({ url: '/test/route', method: 'GET' }, { end: done }, function () { }); + }); + + it('should handle blank URL', function (done) { + var router = new Router(); + + router.use(function (req, res) { + throw new Error('should not be called') + }); + + router.handle({ url: '', method: 'GET' }, {}, done); + }); + + it('should handle missing URL', function (done) { + var router = new Router() + + router.use(function (req, res) { + throw new Error('should not be called') + }) + + router.handle({ method: 'GET' }, {}, done) + }) + + it('handle missing method', function (done) { + var all = false + var router = new Router() + var route = router.route('/foo') + var use = false + + route.post(function (req, res, next) { next(new Error('should not run')) }) + route.all(function (req, res, next) { + all = true + next() + }) + route.get(function (req, res, next) { next(new Error('should not run')) }) + + router.get('/foo', function (req, res, next) { next(new Error('should not run')) }) + router.use(function (req, res, next) { + use = true + next() + }) + + router.handle({ url: '/foo' }, {}, function (err) { + if (err) return done(err) + assert.ok(all) + assert.ok(use) + done() + }) + }) + + it('should not stack overflow with many registered routes', function (done) { + this.timeout(5000) // long-running test + + var handler = function (req, res) { res.end(new Error('wrong handler')) }; + var router = new Router(); + + for (var i = 0; i < 6000; i++) { + router.get('/thing' + i, handler) + } + + router.get('/', function (req, res) { + res.end(); + }); + + router.handle({ url: '/', method: 'GET' }, { end: done }, function () { }); + }); + + it('should not stack overflow with a large sync route stack', function (done) { + this.timeout(5000) // long-running test + + var router = new Router() + + router.get('/foo', function (req, res, next) { + req.counter = 0 + next() + }) + + for (var i = 0; i < 6000; i++) { + router.get('/foo', function (req, res, next) { + req.counter++ + next() + }) + } + + router.get('/foo', function (req, res) { + assert.strictEqual(req.counter, 6000) + res.end() + }) + + router.handle({ url: '/foo', method: 'GET' }, { end: done }, function (err) { + assert(!err, err); + }); + }) + + it('should not stack overflow with a large sync middleware stack', function (done) { + this.timeout(5000) // long-running test + + var router = new Router() + + router.use(function (req, res, next) { + req.counter = 0 + next() + }) + + for (var i = 0; i < 6000; i++) { + router.use(function (req, res, next) { + req.counter++ + next() + }) + } + + router.use(function (req, res) { + assert.strictEqual(req.counter, 6000) + res.end() + }) + + router.handle({ url: '/', method: 'GET' }, { end: done }, function (err) { + assert(!err, err); + }) + }) + + describe('.handle', function () { + it('should dispatch', function (done) { + var router = new Router(); + + router.route('/foo').get(function (req, res) { + res.send('foo'); + }); + + var res = { + send: function (val) { + assert.strictEqual(val, 'foo') + done(); + } + } + router.handle({ url: '/foo', method: 'GET' }, res, function () { }); + }) + }) + + describe('.multiple callbacks', function () { + it('should throw if a callback is null', function () { + assert.throws(function () { + var router = new Router(); + router.route('/foo').all(null); + }) + }) + + it('should throw if a callback is undefined', function () { + assert.throws(function () { + var router = new Router(); + router.route('/foo').all(undefined); + }) + }) + + it('should throw if a callback is not a function', function () { + assert.throws(function () { + var router = new Router(); + router.route('/foo').all('not a function'); + }) + }) + + it('should not throw if all callbacks are functions', function () { + var router = new Router(); + router.route('/foo').all(function () { }).all(function () { }); + }) + }) + + describe('error', function () { + it('should skip non error middleware', function (done) { + var router = new Router(); + + router.get('/foo', function (req, res, next) { + next(new Error('foo')); + }); + + router.get('/bar', function (req, res, next) { + next(new Error('bar')); + }); + + router.use(function (req, res, next) { + assert(false); + }); + + router.use(function (err, req, res, next) { + assert.equal(err.message, 'foo'); + done(); + }); + + router.handle({ url: '/foo', method: 'GET' }, {}, done); + }); + + it('should handle throwing inside routes with params', function (done) { + var router = new Router(); + + router.get('/foo/:id', function () { + throw new Error('foo'); + }); + + router.use(function (req, res, next) { + assert(false); + }); + + router.use(function (err, req, res, next) { + assert.equal(err.message, 'foo'); + done(); + }); + + router.handle({ url: '/foo/2', method: 'GET' }, {}, function () { }); + }); + + it('should handle throwing in handler after async param', function (done) { + var router = new Router(); + + router.param('user', function (req, res, next, val) { + process.nextTick(function () { + req.user = val; + next(); + }); + }); + + router.use('/:user', function (req, res, next) { + throw new Error('oh no!'); + }); + + router.use(function (err, req, res, next) { + assert.equal(err.message, 'oh no!'); + done(); + }); + + router.handle({ url: '/bob', method: 'GET' }, {}, function () { }); + }); + + it('should handle throwing inside error handlers', function (done) { + var router = new Router(); + + router.use(function (req, res, next) { + throw new Error('boom!'); + }); + + router.use(function (err, req, res, next) { + throw new Error('oops'); + }); + + router.use(function (err, req, res, next) { + assert.equal(err.message, 'oops'); + done(); + }); + + router.handle({ url: '/', method: 'GET' }, {}, done); + }); + }) + + describe('FQDN', function () { + it('should not obscure FQDNs', function (done) { + var request = { hit: 0, url: 'http://example.com/foo', method: 'GET' }; + var router = new Router(); + + router.use(function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/foo'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should ignore FQDN in search', function (done) { + var request = { hit: 0, url: '/proxy?url=http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/proxy', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, '/?url=http://example.com/blog/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should ignore FQDN in path', function (done) { + var request = { hit: 0, url: '/proxy/http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/proxy', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, '/http://example.com/blog/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should adjust FQDN req.url', function (done) { + var request = { hit: 0, url: 'http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should adjust FQDN req.url with multiple handlers', function (done) { + var request = { hit: 0, url: 'http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use(function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/blog/post/1'); + next(); + }); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 1); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 2); + done(); + }); + }); + + it('should adjust FQDN req.url with multiple routed handlers', function (done) { + var request = { hit: 0, url: 'http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 1); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.use(function (req, res, next) { + assert.equal(req.hit++, 2); + assert.equal(req.url, 'http://example.com/blog/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 3); + done(); + }); + }); + }) + + describe('.all', function () { + it('should support using .all to capture all http verbs', function (done) { + var router = new Router(); + + var count = 0; + router.all('/foo', function () { count++; }); + + var url = '/foo?bar=baz'; + + methods.forEach(function testMethod(method) { + router.handle({ url: url, method: method }, {}, function () { }); + }); + + assert.equal(count, methods.length); + done(); + }) + }) + + describe('.use', function () { + it('should require middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/') }, /argument handler is required/) + }) + + it('should reject string as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', 'foo') }, /argument handler must be a function/) + }) + + it('should reject number as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', 42) }, /argument handler must be a function/) + }) + + it('should reject null as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', null) }, /argument handler must be a function/) + }) + + it('should reject Date as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', new Date()) }, /argument handler must be a function/) + }) + + it('should be called for any URL', function (done) { + var cb = after(4, done) + var router = new Router() + + function no() { + throw new Error('should not be called') + } + + router.use(function (req, res) { + res.end() + }) + + router.handle({ url: '/', method: 'GET' }, { end: cb }, no) + router.handle({ url: '/foo', method: 'GET' }, { end: cb }, no) + router.handle({ url: 'foo', method: 'GET' }, { end: cb }, no) + router.handle({ url: '*', method: 'GET' }, { end: cb }, no) + }) + + it('should accept array of middleware', function (done) { + var count = 0; + var router = new Router(); + + function fn1(req, res, next) { + assert.equal(++count, 1); + next(); + } + + function fn2(req, res, next) { + assert.equal(++count, 2); + next(); + } + + router.use([fn1, fn2], function (req, res) { + assert.equal(++count, 3); + done(); + }); + + router.handle({ url: '/foo', method: 'GET' }, {}, function () { }); + }) + }) + + describe('.param', function () { + it('should require function', function () { + var router = new Router(); + assert.throws(router.param.bind(router, 'id'), /argument fn is required/); + }); + + it('should reject non-function', function () { + var router = new Router(); + assert.throws(router.param.bind(router, 'id', 42), /argument fn must be a function/); + }); + + it('should call param function when routing VERBS', function (done) { + var router = new Router(); + + router.param('id', function (req, res, next, id) { + assert.equal(id, '123'); + next(); + }); + + router.get('/foo/:id/bar', function (req, res, next) { + assert.equal(req.params.id, '123'); + next(); + }); + + router.handle({ url: '/foo/123/bar', method: 'get' }, {}, done); + }); + + it('should call param function when routing middleware', function (done) { + var router = new Router(); + + router.param('id', function (req, res, next, id) { + assert.equal(id, '123'); + next(); + }); + + router.use('/foo/:id/bar', function (req, res, next) { + assert.equal(req.params.id, '123'); + assert.equal(req.url, '/baz'); + next(); + }); + + router.handle({ url: '/foo/123/bar/baz', method: 'get' }, {}, done); + }); + + it('should only call once per request', function (done) { + var count = 0; + var req = { url: '/foo/bob/bar', method: 'get' }; + var router = new Router(); + var sub = new Router(); + + sub.get('/bar', function (req, res, next) { + next(); + }); + + router.param('user', function (req, res, next, user) { + count++; + req.user = user; + next(); + }); + + router.use('/foo/:user/', new Router()); + router.use('/foo/:user/', sub); + + router.handle(req, {}, function (err) { + if (err) return done(err); + assert.equal(count, 1); + assert.equal(req.user, 'bob'); + done(); + }); + }); + + it('should call when values differ', function (done) { + var count = 0; + var req = { url: '/foo/bob/bar', method: 'get' }; + var router = new Router(); + var sub = new Router(); + + sub.get('/bar', function (req, res, next) { + next(); + }); + + router.param('user', function (req, res, next, user) { + count++; + req.user = user; + next(); + }); + + router.use('/foo/:user/', new Router()); + router.use('/:user/bob/', sub); + + router.handle(req, {}, function (err) { + if (err) return done(err); + assert.equal(count, 2); + assert.equal(req.user, 'foo'); + done(); + }); + }); + }); + + describe('parallel requests', function () { + it('should not mix requests', function (done) { + var req1 = { url: '/foo/50/bar', method: 'get' }; + var req2 = { url: '/foo/10/bar', method: 'get' }; + var router = new Router(); + var sub = new Router(); + var cb = after(2, done) + + + sub.get('/bar', function (req, res, next) { + next(); + }); + + router.param('ms', function (req, res, next, ms) { + ms = parseInt(ms, 10); + req.ms = ms; + setTimeout(next, ms); + }); + + router.use('/foo/:ms/', new Router()); + router.use('/foo/:ms/', sub); + + router.handle(req1, {}, function (err) { + assert.ifError(err); + assert.equal(req1.ms, 50); + assert.equal(req1.originalUrl, '/foo/50/bar'); + cb() + }); + + router.handle(req2, {}, function (err) { + assert.ifError(err); + assert.equal(req2.ms, 10); + assert.equal(req2.originalUrl, '/foo/10/bar'); + cb() + }); + }); + }); +}) diff --git a/apps/api/test/acceptance/auth.js b/apps/api/test/acceptance/auth.js new file mode 100644 index 0000000..d783875 --- /dev/null +++ b/apps/api/test/acceptance/auth.js @@ -0,0 +1,117 @@ +var app = require('../../examples/auth') +var request = require('supertest') + +function getCookie(res) { + return res.headers['set-cookie'][0].split(';')[0]; +} + +describe('auth', function(){ + describe('GET /',function(){ + it('should redirect to /login', function(done){ + request(app) + .get('/') + .expect('Location', '/login') + .expect(302, done) + }) + }) + + describe('GET /login',function(){ + it('should render login form', function(done){ + request(app) + .get('/login') + .expect(200, /
  • Tobi
  • Loki
  • Jane
  • ', done) + }) + + it('should accept to text/plain', function(done){ + request(app) + .get('/') + .set('Accept', 'text/plain') + .expect(200, ' - Tobi\n - Loki\n - Jane\n', done) + }) + + it('should accept to application/json', function(done){ + request(app) + .get('/') + .set('Accept', 'application/json') + .expect(200, '[{"name":"Tobi"},{"name":"Loki"},{"name":"Jane"}]', done) + }) + }) + + describe('GET /users', function(){ + it('should default to text/html', function(done){ + request(app) + .get('/users') + .expect(200, '
    • Tobi
    • Loki
    • Jane
    ', done) + }) + + it('should accept to text/plain', function(done){ + request(app) + .get('/users') + .set('Accept', 'text/plain') + .expect(200, ' - Tobi\n - Loki\n - Jane\n', done) + }) + + it('should accept to application/json', function(done){ + request(app) + .get('/users') + .set('Accept', 'application/json') + .expect(200, '[{"name":"Tobi"},{"name":"Loki"},{"name":"Jane"}]', done) + }) + }) +}) diff --git a/apps/api/test/acceptance/cookie-sessions.js b/apps/api/test/acceptance/cookie-sessions.js new file mode 100644 index 0000000..e83c8e4 --- /dev/null +++ b/apps/api/test/acceptance/cookie-sessions.js @@ -0,0 +1,38 @@ + +var app = require('../../examples/cookie-sessions') +var request = require('supertest') + +describe('cookie-sessions', function () { + describe('GET /', function () { + it('should display no views', function (done) { + request(app) + .get('/') + .expect(200, 'viewed 1 times\n', done) + }) + + it('should set a session cookie', function (done) { + request(app) + .get('/') + .expect('Set-Cookie', /session=/) + .expect(200, done) + }) + + it('should display 1 view on revisit', function (done) { + request(app) + .get('/') + .expect(200, 'viewed 1 times\n', function (err, res) { + if (err) return done(err) + request(app) + .get('/') + .set('Cookie', getCookies(res)) + .expect(200, 'viewed 2 times\n', done) + }) + }) + }) +}) + +function getCookies(res) { + return res.headers['set-cookie'].map(function (val) { + return val.split(';')[0] + }).join('; '); +} diff --git a/apps/api/test/acceptance/cookies.js b/apps/api/test/acceptance/cookies.js new file mode 100644 index 0000000..aa9e1fa --- /dev/null +++ b/apps/api/test/acceptance/cookies.js @@ -0,0 +1,71 @@ + +var app = require('../../examples/cookies') + , request = require('supertest'); +var utils = require('../support/utils'); + +describe('cookies', function(){ + describe('GET /', function(){ + it('should have a form', function(done){ + request(app) + .get('/') + .expect(/tobi <tobi@learnboost\.com><\/li>/) + .expect(/
  • loki <loki@learnboost\.com><\/li>/) + .expect(/
  • jane <jane@learnboost\.com><\/li>/) + .expect(200, done) + }) + }) +}) diff --git a/apps/api/test/acceptance/error-pages.js b/apps/api/test/acceptance/error-pages.js new file mode 100644 index 0000000..48feb3f --- /dev/null +++ b/apps/api/test/acceptance/error-pages.js @@ -0,0 +1,99 @@ + +var app = require('../../examples/error-pages') + , request = require('supertest'); + +describe('error-pages', function(){ + describe('GET /', function(){ + it('should respond with page list', function(done){ + request(app) + .get('/') + .expect(/Pages Example/, done) + }) + }) + + describe('Accept: text/html',function(){ + describe('GET /403', function(){ + it('should respond with 403', function(done){ + request(app) + .get('/403') + .expect(403, done) + }) + }) + + describe('GET /404', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/404') + .expect(404, done) + }) + }) + + describe('GET /500', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/500') + .expect(500, done) + }) + }) + }) + + describe('Accept: application/json',function(){ + describe('GET /403', function(){ + it('should respond with 403', function(done){ + request(app) + .get('/403') + .set('Accept','application/json') + .expect(403, done) + }) + }) + + describe('GET /404', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/404') + .set('Accept','application/json') + .expect(404, { error: 'Not found' }, done) + }) + }) + + describe('GET /500', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/500') + .set('Accept', 'application/json') + .expect(500, done) + }) + }) + }) + + + describe('Accept: text/plain',function(){ + describe('GET /403', function(){ + it('should respond with 403', function(done){ + request(app) + .get('/403') + .set('Accept','text/plain') + .expect(403, done) + }) + }) + + describe('GET /404', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/404') + .set('Accept', 'text/plain') + .expect(404) + .expect('Not found', done); + }) + }) + + describe('GET /500', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/500') + .set('Accept','text/plain') + .expect(500, done) + }) + }) + }) +}) diff --git a/apps/api/test/acceptance/error.js b/apps/api/test/acceptance/error.js new file mode 100644 index 0000000..6bdf099 --- /dev/null +++ b/apps/api/test/acceptance/error.js @@ -0,0 +1,29 @@ + +var app = require('../../examples/error') + , request = require('supertest'); + +describe('error', function(){ + describe('GET /', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/') + .expect(500,done) + }) + }) + + describe('GET /next', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/next') + .expect(500,done) + }) + }) + + describe('GET /missing', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/missing') + .expect(404,done) + }) + }) +}) diff --git a/apps/api/test/acceptance/hello-world.js b/apps/api/test/acceptance/hello-world.js new file mode 100644 index 0000000..db90349 --- /dev/null +++ b/apps/api/test/acceptance/hello-world.js @@ -0,0 +1,21 @@ + +var app = require('../../examples/hello-world') +var request = require('supertest') + +describe('hello-world', function () { + describe('GET /', function () { + it('should respond with hello world', function (done) { + request(app) + .get('/') + .expect(200, 'Hello World', done) + }) + }) + + describe('GET /missing', function () { + it('should respond with 404', function (done) { + request(app) + .get('/missing') + .expect(404, done) + }) + }) +}) diff --git a/apps/api/test/acceptance/markdown.js b/apps/api/test/acceptance/markdown.js new file mode 100644 index 0000000..1a7d9e3 --- /dev/null +++ b/apps/api/test/acceptance/markdown.js @@ -0,0 +1,21 @@ + +var app = require('../../examples/markdown') +var request = require('supertest') + +describe('markdown', function(){ + describe('GET /', function(){ + it('should respond with html', function(done){ + request(app) + .get('/') + .expect(/]*>Markdown Example<\/h1>/,done) + }) + }) + + describe('GET /fail',function(){ + it('should respond with an error', function(done){ + request(app) + .get('/fail') + .expect(500,done) + }) + }) +}) diff --git a/apps/api/test/acceptance/multi-router.js b/apps/api/test/acceptance/multi-router.js new file mode 100644 index 0000000..4362a83 --- /dev/null +++ b/apps/api/test/acceptance/multi-router.js @@ -0,0 +1,44 @@ +var app = require('../../examples/multi-router') +var request = require('supertest') + +describe('multi-router', function(){ + describe('GET /',function(){ + it('should respond with root handler', function(done){ + request(app) + .get('/') + .expect(200, 'Hello from root route.', done) + }) + }) + + describe('GET /api/v1/',function(){ + it('should respond with APIv1 root handler', function(done){ + request(app) + .get('/api/v1/') + .expect(200, 'Hello from APIv1 root route.', done) + }) + }) + + describe('GET /api/v1/users',function(){ + it('should respond with users from APIv1', function(done){ + request(app) + .get('/api/v1/users') + .expect(200, 'List of APIv1 users.', done) + }) + }) + + describe('GET /api/v2/',function(){ + it('should respond with APIv2 root handler', function(done){ + request(app) + .get('/api/v2/') + .expect(200, 'Hello from APIv2 root route.', done) + }) + }) + + describe('GET /api/v2/users',function(){ + it('should respond with users from APIv2', function(done){ + request(app) + .get('/api/v2/users') + .expect(200, 'List of APIv2 users.', done) + }) + }) +}) diff --git a/apps/api/test/acceptance/mvc.js b/apps/api/test/acceptance/mvc.js new file mode 100644 index 0000000..35709f6 --- /dev/null +++ b/apps/api/test/acceptance/mvc.js @@ -0,0 +1,132 @@ + +var request = require('supertest') + , app = require('../../examples/mvc'); + +describe('mvc', function(){ + describe('GET /', function(){ + it('should redirect to /users', function(done){ + request(app) + .get('/') + .expect('Location', '/users') + .expect(302, done) + }) + }) + + describe('GET /pet/0', function(){ + it('should get pet', function(done){ + request(app) + .get('/pet/0') + .expect(200, /Tobi/, done) + }) + }) + + describe('GET /pet/0/edit', function(){ + it('should get pet edit page', function(done){ + request(app) + .get('/pet/0/edit') + .expect(/Users<\/h1>/) + .expect(/>TJGuillermoNathanTJ edit/, done) + }) + + it('should display the users pets', function(done){ + request(app) + .get('/user/0') + .expect(/\/pet\/0">Tobi/) + .expect(/\/pet\/1">Loki/) + .expect(/\/pet\/2">Jane/) + .expect(200, done) + }) + }) + + describe('when not present', function(){ + it('should 404', function(done){ + request(app) + .get('/user/123') + .expect(404, done); + }) + }) + }) + + describe('GET /user/:id/edit', function(){ + it('should display the edit form', function(done){ + request(app) + .get('/user/1/edit') + .expect(/Guillermo/) + .expect(200, /Examples:<\/h1>/,done) + }) + }) + + describe('GET /users', function(){ + it('should respond with all users', function(done){ + request(app) + .get('/users') + .expect(/^\[{"name":"tj"},{"name":"ciaran"},{"name":"aaron"},{"name":"guillermo"},{"name":"simon"},{"name":"tobi"}\]/,done) + }) + }) + + describe('GET /users/1', function(){ + it('should respond with user 1', function(done){ + request(app) + .get('/users/1') + .expect(/^{"name":"ciaran"}/,done) + }) + }) + + describe('GET /users/9', function(){ + it('should respond with error', function(done){ + request(app) + .get('/users/9') + .expect('{"error":"Cannot find user"}', done) + }) + }) + + describe('GET /users/1..3', function(){ + it('should respond with users 1 through 3', function(done){ + request(app) + .get('/users/1..3') + .expect(/^
    • ciaran<\/li>\n
    • aaron<\/li>\n
    • guillermo<\/li><\/ul>/,done) + }) + }) + + describe('DELETE /users/1', function(){ + it('should delete user 1', function(done){ + request(app) + .del('/users/1') + .expect(/^destroyed/,done) + }) + }) + + describe('DELETE /users/9', function(){ + it('should fail', function(done){ + request(app) + .del('/users/9') + .expect('Cannot find user', done) + }) + }) + + describe('GET /users/1..3.json', function(){ + it('should respond with users 2 and 3 as json', function(done){ + request(app) + .get('/users/1..3.json') + .expect(/^\[null,{"name":"aaron"},{"name":"guillermo"}\]/,done) + }) + }) +}) diff --git a/apps/api/test/acceptance/route-map.js b/apps/api/test/acceptance/route-map.js new file mode 100644 index 0000000..0bd2a6d --- /dev/null +++ b/apps/api/test/acceptance/route-map.js @@ -0,0 +1,45 @@ + +var request = require('supertest') + , app = require('../../examples/route-map'); + +describe('route-map', function(){ + describe('GET /users', function(){ + it('should respond with users', function(done){ + request(app) + .get('/users') + .expect('user list', done); + }) + }) + + describe('DELETE /users', function(){ + it('should delete users', function(done){ + request(app) + .del('/users') + .expect('delete users', done); + }) + }) + + describe('GET /users/:id', function(){ + it('should get a user', function(done){ + request(app) + .get('/users/12') + .expect('user 12', done); + }) + }) + + describe('GET /users/:id/pets', function(){ + it('should get a users pets', function(done){ + request(app) + .get('/users/12/pets') + .expect('user 12\'s pets', done); + }) + }) + + describe('GET /users/:id/pets/:pid', function(){ + it('should get a users pet', function(done){ + request(app) + .del('/users/12/pets/2') + .expect('delete 12\'s pet 2', done); + }) + }) +}) diff --git a/apps/api/test/acceptance/route-separation.js b/apps/api/test/acceptance/route-separation.js new file mode 100644 index 0000000..867fd29 --- /dev/null +++ b/apps/api/test/acceptance/route-separation.js @@ -0,0 +1,97 @@ + +var app = require('../../examples/route-separation') +var request = require('supertest') + +describe('route-separation', function () { + describe('GET /', function () { + it('should respond with index', function (done) { + request(app) + .get('/') + .expect(200, /Route Separation Example/, done) + }) + }) + + describe('GET /users', function () { + it('should list users', function (done) { + request(app) + .get('/users') + .expect(/TJ/) + .expect(/Tobi/) + .expect(200, done) + }) + }) + + describe('GET /user/:id', function () { + it('should get a user', function (done) { + request(app) + .get('/user/0') + .expect(200, /Viewing user TJ/, done) + }) + + it('should 404 on missing user', function (done) { + request(app) + .get('/user/10') + .expect(404, done) + }) + }) + + describe('GET /user/:id/view', function () { + it('should get a user', function (done) { + request(app) + .get('/user/0/view') + .expect(200, /Viewing user TJ/, done) + }) + + it('should 404 on missing user', function (done) { + request(app) + .get('/user/10/view') + .expect(404, done) + }) + }) + + describe('GET /user/:id/edit', function () { + it('should get a user to edit', function (done) { + request(app) + .get('/user/0/edit') + .expect(200, /Editing user TJ/, done) + }) + }) + + describe('PUT /user/:id/edit', function () { + it('should edit a user', function (done) { + request(app) + .put('/user/0/edit') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ user: { name: 'TJ', email: 'tj-invalid@vision-media.ca' } }) + .expect(302, function (err) { + if (err) return done(err) + request(app) + .get('/user/0') + .expect(200, /tj-invalid@vision-media\.ca/, done) + }) + }) + }) + + describe('POST /user/:id/edit?_method=PUT', function () { + it('should edit a user', function (done) { + request(app) + .post('/user/1/edit?_method=PUT') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ user: { name: 'Tobi', email: 'tobi-invalid@vision-media.ca' } }) + .expect(302, function (err) { + if (err) return done(err) + request(app) + .get('/user/1') + .expect(200, /tobi-invalid@vision-media\.ca/, done) + }) + }) + }) + + describe('GET /posts', function () { + it('should get a list of posts', function (done) { + request(app) + .get('/posts') + .expect(200, /Posts/, done) + }) + }) +}) diff --git a/apps/api/test/acceptance/vhost.js b/apps/api/test/acceptance/vhost.js new file mode 100644 index 0000000..1b633d4 --- /dev/null +++ b/apps/api/test/acceptance/vhost.js @@ -0,0 +1,46 @@ +var app = require('../../examples/vhost') +var request = require('supertest') + +describe('vhost', function(){ + describe('example.com', function(){ + describe('GET /', function(){ + it('should say hello', function(done){ + request(app) + .get('/') + .set('Host', 'example.com') + .expect(200, /hello/i, done) + }) + }) + + describe('GET /foo', function(){ + it('should say foo', function(done){ + request(app) + .get('/foo') + .set('Host', 'example.com') + .expect(200, 'requested foo', done) + }) + }) + }) + + describe('foo.example.com', function(){ + describe('GET /', function(){ + it('should redirect to /foo', function(done){ + request(app) + .get('/') + .set('Host', 'foo.example.com') + .expect(302, /Redirecting to http:\/\/example.com:3000\/foo/, done) + }) + }) + }) + + describe('bar.example.com', function(){ + describe('GET /', function(){ + it('should redirect to /bar', function(done){ + request(app) + .get('/') + .set('Host', 'bar.example.com') + .expect(302, /Redirecting to http:\/\/example.com:3000\/bar/, done) + }) + }) + }) +}) diff --git a/apps/api/test/acceptance/web-service.js b/apps/api/test/acceptance/web-service.js new file mode 100644 index 0000000..2e37b48 --- /dev/null +++ b/apps/api/test/acceptance/web-service.js @@ -0,0 +1,105 @@ + +var request = require('supertest') + , app = require('../../examples/web-service'); + +describe('web-service', function(){ + describe('GET /api/users', function(){ + describe('without an api key', function(){ + it('should respond with 400 bad request', function(done){ + request(app) + .get('/api/users') + .expect(400, done); + }) + }) + + describe('with an invalid api key', function(){ + it('should respond with 401 unauthorized', function(done){ + request(app) + .get('/api/users?api-key=rawr') + .expect(401, done); + }) + }) + + describe('with a valid api key', function(){ + it('should respond users json', function(done){ + request(app) + .get('/api/users?api-key=foo') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '[{"name":"tobi"},{"name":"loki"},{"name":"jane"}]', done) + }) + }) + }) + + describe('GET /api/repos', function(){ + describe('without an api key', function(){ + it('should respond with 400 bad request', function(done){ + request(app) + .get('/api/repos') + .expect(400, done); + }) + }) + + describe('with an invalid api key', function(){ + it('should respond with 401 unauthorized', function(done){ + request(app) + .get('/api/repos?api-key=rawr') + .expect(401, done); + }) + }) + + describe('with a valid api key', function(){ + it('should respond repos json', function(done){ + request(app) + .get('/api/repos?api-key=foo') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(/"name":"express"/) + .expect(/"url":"https:\/\/github.com\/expressjs\/express"/) + .expect(200, done) + }) + }) + }) + + describe('GET /api/user/:name/repos', function(){ + describe('without an api key', function(){ + it('should respond with 400 bad request', function(done){ + request(app) + .get('/api/user/loki/repos') + .expect(400, done); + }) + }) + + describe('with an invalid api key', function(){ + it('should respond with 401 unauthorized', function(done){ + request(app) + .get('/api/user/loki/repos?api-key=rawr') + .expect(401, done); + }) + }) + + describe('with a valid api key', function(){ + it('should respond user repos json', function(done){ + request(app) + .get('/api/user/loki/repos?api-key=foo') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(/"name":"stylus"/) + .expect(/"url":"https:\/\/github.com\/learnboost\/stylus"/) + .expect(200, done) + }) + + it('should 404 with unknown user', function(done){ + request(app) + .get('/api/user/bob/repos?api-key=foo') + .expect(404, done) + }) + }) + }) + + describe('when requesting an invalid route', function(){ + it('should respond with 404 json', function(done){ + request(app) + .get('/api/something?api-key=bar') + .expect('Content-Type', /json/) + .expect(404, '{"error":"Sorry, can\'t find that"}', done) + }) + }) +}) diff --git a/apps/api/test/app.all.js b/apps/api/test/app.all.js new file mode 100644 index 0000000..e4afca7 --- /dev/null +++ b/apps/api/test/app.all.js @@ -0,0 +1,38 @@ +'use strict' + +var after = require('after') +var express = require('../') + , request = require('supertest'); + +describe('app.all()', function(){ + it('should add a router per method', function(done){ + var app = express(); + var cb = after(2, done) + + app.all('/tobi', function(req, res){ + res.end(req.method); + }); + + request(app) + .put('/tobi') + .expect(200, 'PUT', cb) + + request(app) + .get('/tobi') + .expect(200, 'GET', cb) + }) + + it('should run the callback for a method just once', function(done){ + var app = express() + , n = 0; + + app.all('/*splat', function(req, res, next){ + if (n++) return done(new Error('DELETE called several times')); + next(); + }); + + request(app) + .del('/tobi') + .expect(404, done); + }) +}) diff --git a/apps/api/test/app.engine.js b/apps/api/test/app.engine.js new file mode 100644 index 0000000..b0553aa --- /dev/null +++ b/apps/api/test/app.engine.js @@ -0,0 +1,83 @@ +'use strict' + +var assert = require('node:assert') +var express = require('../') + , fs = require('node:fs'); +var path = require('node:path') + +function render(path, options, fn) { + fs.readFile(path, 'utf8', function(err, str){ + if (err) return fn(err); + str = str.replace('{{user.name}}', options.user.name); + fn(null, str); + }); +} + +describe('app', function(){ + describe('.engine(ext, fn)', function(){ + it('should map a template engine', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('.html', render); + app.locals.user = { name: 'tobi' }; + + app.render('user.html', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should throw when the callback is missing', function(){ + var app = express(); + assert.throws(function () { + app.engine('.html', null); + }, /callback function required/) + }) + + it('should work without leading "."', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('html', render); + app.locals.user = { name: 'tobi' }; + + app.render('user.html', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should work "view engine" setting', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('html', render); + app.set('view engine', 'html'); + app.locals.user = { name: 'tobi' }; + + app.render('user', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should work "view engine" with leading "."', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('.html', render); + app.set('view engine', '.html'); + app.locals.user = { name: 'tobi' }; + + app.render('user', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + }) +}) diff --git a/apps/api/test/app.head.js b/apps/api/test/app.head.js new file mode 100644 index 0000000..0207caa --- /dev/null +++ b/apps/api/test/app.head.js @@ -0,0 +1,66 @@ +'use strict' + +var express = require('../'); +var request = require('supertest'); +var assert = require('node:assert'); + +describe('HEAD', function(){ + it('should default to GET', function(done){ + var app = express(); + + app.get('/tobi', function(req, res){ + // send() detects HEAD + res.send('tobi'); + }); + + request(app) + .head('/tobi') + .expect(200, done); + }) + + it('should output the same headers as GET requests', function(done){ + var app = express(); + + app.get('/tobi', function(req, res){ + // send() detects HEAD + res.send('tobi'); + }); + + request(app) + .head('/tobi') + .expect(200, function(err, res){ + if (err) return done(err); + var headers = res.headers; + request(app) + .get('/tobi') + .expect(200, function(err, res){ + if (err) return done(err); + delete headers.date; + delete res.headers.date; + assert.deepEqual(res.headers, headers); + done(); + }); + }); + }) +}) + +describe('app.head()', function(){ + it('should override', function(done){ + var app = express() + + app.head('/tobi', function(req, res){ + res.header('x-method', 'head') + res.end() + }); + + app.get('/tobi', function(req, res){ + res.header('x-method', 'get') + res.send('tobi'); + }); + + request(app) + .head('/tobi') + .expect('x-method', 'head') + .expect(200, done) + }) +}) diff --git a/apps/api/test/app.js b/apps/api/test/app.js new file mode 100644 index 0000000..c1e815a --- /dev/null +++ b/apps/api/test/app.js @@ -0,0 +1,120 @@ +'use strict' + +var assert = require('node:assert') +var express = require('..') +var request = require('supertest') + +describe('app', function(){ + it('should inherit from event emitter', function(done){ + var app = express(); + app.on('foo', done); + app.emit('foo'); + }) + + it('should be callable', function(){ + var app = express(); + assert.equal(typeof app, 'function'); + }) + + it('should 404 without routes', function(done){ + request(express()) + .get('/') + .expect(404, done); + }) +}) + +describe('app.parent', function(){ + it('should return the parent when mounted', function(){ + var app = express() + , blog = express() + , blogAdmin = express(); + + app.use('/blog', blog); + blog.use('/admin', blogAdmin); + + assert(!app.parent, 'app.parent'); + assert.strictEqual(blog.parent, app) + assert.strictEqual(blogAdmin.parent, blog) + }) +}) + +describe('app.mountpath', function(){ + it('should return the mounted path', function(){ + var admin = express(); + var app = express(); + var blog = express(); + var fallback = express(); + + app.use('/blog', blog); + app.use(fallback); + blog.use('/admin', admin); + + assert.strictEqual(admin.mountpath, '/admin') + assert.strictEqual(app.mountpath, '/') + assert.strictEqual(blog.mountpath, '/blog') + assert.strictEqual(fallback.mountpath, '/') + }) +}) + +describe('app.path()', function(){ + it('should return the canonical', function(){ + var app = express() + , blog = express() + , blogAdmin = express(); + + app.use('/blog', blog); + blog.use('/admin', blogAdmin); + + assert.strictEqual(app.path(), '') + assert.strictEqual(blog.path(), '/blog') + assert.strictEqual(blogAdmin.path(), '/blog/admin') + }) +}) + +describe('in development', function(){ + before(function () { + this.env = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + }) + + after(function () { + process.env.NODE_ENV = this.env + }) + + it('should disable "view cache"', function(){ + var app = express(); + assert.ok(!app.enabled('view cache')) + }) +}) + +describe('in production', function(){ + before(function () { + this.env = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + }) + + after(function () { + process.env.NODE_ENV = this.env + }) + + it('should enable "view cache"', function(){ + var app = express(); + assert.ok(app.enabled('view cache')) + }) +}) + +describe('without NODE_ENV', function(){ + before(function () { + this.env = process.env.NODE_ENV + process.env.NODE_ENV = '' + }) + + after(function () { + process.env.NODE_ENV = this.env + }) + + it('should default to development', function(){ + var app = express(); + assert.strictEqual(app.get('env'), 'development') + }) +}) diff --git a/apps/api/test/app.listen.js b/apps/api/test/app.listen.js new file mode 100644 index 0000000..3ef94ff --- /dev/null +++ b/apps/api/test/app.listen.js @@ -0,0 +1,55 @@ +'use strict' + +var express = require('../') +var assert = require('node:assert') + +describe('app.listen()', function(){ + it('should wrap with an HTTP server', function(done){ + var app = express(); + + var server = app.listen(0, function () { + server.close(done) + }); + }) + it('should callback on HTTP server errors', function (done) { + var app1 = express() + var app2 = express() + + var server1 = app1.listen(0, function (err) { + assert(!err) + app2.listen(server1.address().port, function (err) { + assert(err.code === 'EADDRINUSE') + server1.close() + done() + }) + }) + }) + it('accepts port + hostname + backlog + callback', function (done) { + const app = express(); + const server = app.listen(0, '127.0.0.1', 5, function () { + const { address, port } = server.address(); + assert.strictEqual(address, '127.0.0.1'); + assert(Number.isInteger(port) && port > 0); + // backlog isn’t directly inspectable, but if no error was thrown + // we know it was accepted. + server.close(done); + }); + }); + it('accepts just a callback (no args)', function (done) { + const app = express(); + // same as app.listen(0, done) + const server = app.listen(); + server.close(done); + }); + it('server.address() gives a { address, port, family } object', function (done) { + const app = express(); + const server = app.listen(0, () => { + const addr = server.address(); + assert(addr && typeof addr === 'object'); + assert.strictEqual(typeof addr.address, 'string'); + assert(Number.isInteger(addr.port) && addr.port > 0); + assert(typeof addr.family === 'string'); + server.close(done); + }); + }); +}) diff --git a/apps/api/test/app.locals.js b/apps/api/test/app.locals.js new file mode 100644 index 0000000..3963762 --- /dev/null +++ b/apps/api/test/app.locals.js @@ -0,0 +1,26 @@ +'use strict' + +var assert = require('node:assert') +var express = require('../') + +describe('app', function(){ + describe('.locals', function () { + it('should default object with null prototype', function () { + var app = express() + assert.ok(app.locals) + assert.strictEqual(typeof app.locals, 'object') + assert.strictEqual(Object.getPrototypeOf(app.locals), null) + }) + + describe('.settings', function () { + it('should contain app settings ', function () { + var app = express() + app.set('title', 'Express') + assert.ok(app.locals.settings) + assert.strictEqual(typeof app.locals.settings, 'object') + assert.strictEqual(app.locals.settings, app.settings) + assert.strictEqual(app.locals.settings.title, 'Express') + }) + }) + }) +}) diff --git a/apps/api/test/app.options.js b/apps/api/test/app.options.js new file mode 100644 index 0000000..ee4c816 --- /dev/null +++ b/apps/api/test/app.options.js @@ -0,0 +1,116 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('OPTIONS', function(){ + it('should default to the routes defined', function(done){ + var app = express(); + + app.post('/', function(){}); + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + + request(app) + .options('/users') + .expect('Allow', 'GET, HEAD, PUT') + .expect(200, 'GET, HEAD, PUT', done); + }) + + it('should only include each method once', function(done){ + var app = express(); + + app.delete('/', function(){}); + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + app.get('/users', function(req, res){}); + + request(app) + .options('/users') + .expect('Allow', 'GET, HEAD, PUT') + .expect(200, 'GET, HEAD, PUT', done); + }) + + it('should not be affected by app.all', function(done){ + var app = express(); + + app.get('/', function(){}); + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + app.all('/users', function(req, res, next){ + res.setHeader('x-hit', '1'); + next(); + }); + + request(app) + .options('/users') + .expect('x-hit', '1') + .expect('Allow', 'GET, HEAD, PUT') + .expect(200, 'GET, HEAD, PUT', done); + }) + + it('should not respond if the path is not defined', function(done){ + var app = express(); + + app.get('/users', function(req, res){}); + + request(app) + .options('/other') + .expect(404, done); + }) + + it('should forward requests down the middleware chain', function(done){ + var app = express(); + var router = new express.Router(); + + router.get('/users', function(req, res){}); + app.use(router); + app.get('/other', function(req, res){}); + + request(app) + .options('/other') + .expect('Allow', 'GET, HEAD') + .expect(200, 'GET, HEAD', done); + }) + + describe('when error occurs in response handler', function () { + it('should pass error to callback', function (done) { + var app = express(); + var router = express.Router(); + + router.get('/users', function(req, res){}); + + app.use(function (req, res, next) { + res.writeHead(200); + next(); + }); + app.use(router); + app.use(function (err, req, res, next) { + res.end('true'); + }); + + request(app) + .options('/users') + .expect(200, 'true', done) + }) + }) +}) + +describe('app.options()', function(){ + it('should override the default behavior', function(done){ + var app = express(); + + app.options('/users', function(req, res){ + res.set('Allow', 'GET'); + res.send('GET'); + }); + + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + + request(app) + .options('/users') + .expect('GET') + .expect('Allow', 'GET', done); + }) +}) diff --git a/apps/api/test/app.param.js b/apps/api/test/app.param.js new file mode 100644 index 0000000..5c9a563 --- /dev/null +++ b/apps/api/test/app.param.js @@ -0,0 +1,323 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('app', function(){ + describe('.param(names, fn)', function(){ + it('should map the array', function(done){ + var app = express(); + + app.param(['id', 'uid'], function(req, res, next, id){ + id = Number(id); + if (isNaN(id)) return next('route'); + req.params.id = id; + next(); + }); + + app.get('/post/:id', function(req, res){ + var id = req.params.id; + res.send((typeof id) + ':' + id) + }); + + app.get('/user/:uid', function(req, res){ + var id = req.params.id; + res.send((typeof id) + ':' + id) + }); + + request(app) + .get('/user/123') + .expect(200, 'number:123', function (err) { + if (err) return done(err) + request(app) + .get('/post/123') + .expect('number:123', done) + }) + }) + }) + + describe('.param(name, fn)', function(){ + it('should map logic for a single param', function(done){ + var app = express(); + + app.param('id', function(req, res, next, id){ + id = Number(id); + if (isNaN(id)) return next('route'); + req.params.id = id; + next(); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send((typeof id) + ':' + id) + }); + + request(app) + .get('/user/123') + .expect(200, 'number:123', done) + }) + + it('should only call once per request', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + req.user = user; + next(); + }); + + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.use(function(req, res) { + res.end([count, called, req.user].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect('2 1 bob', done); + }) + + it('should call when values differ', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + req.users = (req.users || []).concat(user); + next(); + }); + + app.get('/:user/bob', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.use(function(req, res) { + res.end([count, called, req.users.join(',')].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect('2 2 foo,bob', done); + }) + + it('should support altering req.params across routes', function(done) { + var app = express(); + + app.param('user', function(req, res, next, user) { + req.params.user = 'loki'; + next(); + }); + + app.get('/:user', function(req, res, next) { + next('route'); + }); + app.get('/:user', function (req, res) { + res.send(req.params.user); + }); + + request(app) + .get('/bob') + .expect('loki', done); + }) + + it('should not invoke without route handler', function(done) { + var app = express(); + + app.param('thing', function(req, res, next, thing) { + req.thing = thing; + next(); + }); + + app.param('user', function(req, res, next, user) { + next(new Error('invalid invocation')) + }); + + app.post('/:user', function (req, res) { + res.send(req.params.user); + }); + + app.get('/:thing', function (req, res) { + res.send(req.thing); + }); + + request(app) + .get('/bob') + .expect(200, 'bob', done); + }) + + it('should work with encoded values', function(done){ + var app = express(); + + app.param('name', function(req, res, next, name){ + req.params.name = name; + next(); + }); + + app.get('/user/:name', function(req, res){ + var name = req.params.name; + res.send('' + name); + }); + + request(app) + .get('/user/foo%25bar') + .expect('foo%bar', done); + }) + + it('should catch thrown error', function(done){ + var app = express(); + + app.param('id', function(req, res, next, id){ + throw new Error('err!'); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send('' + id); + }); + + request(app) + .get('/user/123') + .expect(500, done); + }) + + it('should catch thrown secondary error', function(done){ + var app = express(); + + app.param('id', function(req, res, next, val){ + process.nextTick(next); + }); + + app.param('id', function(req, res, next, id){ + throw new Error('err!'); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send('' + id); + }); + + request(app) + .get('/user/123') + .expect(500, done); + }) + + it('should defer to next route', function(done){ + var app = express(); + + app.param('id', function(req, res, next, id){ + next('route'); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send('' + id); + }); + + app.get('/:name/123', function(req, res){ + res.send('name'); + }); + + request(app) + .get('/user/123') + .expect('name', done); + }) + + it('should defer all the param routes', function(done){ + var app = express(); + + app.param('id', function(req, res, next, val){ + if (val === 'new') return next('route'); + return next(); + }); + + app.all('/user/:id', function(req, res){ + res.send('all.id'); + }); + + app.get('/user/:id', function(req, res){ + res.send('get.id'); + }); + + app.get('/user/new', function(req, res){ + res.send('get.new'); + }); + + request(app) + .get('/user/new') + .expect('get.new', done); + }) + + it('should not call when values differ on error', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + if (user === 'foo') throw new Error('err!'); + req.user = user; + next(); + }); + + app.get('/:user/bob', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + + app.use(function(err, req, res, next) { + res.status(500); + res.send([count, called, err.message].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect(500, '0 1 err!', done) + }); + + it('should call when values differ when using "next"', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + if (user === 'foo') return next('route'); + req.user = user; + next(); + }); + + app.get('/:user/bob', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.use(function(req, res) { + res.end([count, called, req.user].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect('1 2 bob', done); + }) + }) +}) diff --git a/apps/api/test/app.render.js b/apps/api/test/app.render.js new file mode 100644 index 0000000..ca15e76 --- /dev/null +++ b/apps/api/test/app.render.js @@ -0,0 +1,374 @@ +'use strict' + +var assert = require('node:assert') +var express = require('..'); +var path = require('node:path') +var tmpl = require('./support/tmpl'); + +describe('app', function(){ + describe('.render(name, fn)', function(){ + it('should support absolute paths', function(done){ + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + + app.render(path.join(__dirname, 'fixtures', 'user.tmpl'), function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should support absolute paths with "view engine"', function(done){ + var app = createApp(); + + app.set('view engine', 'tmpl'); + app.locals.user = { name: 'tobi' }; + + app.render(path.join(__dirname, 'fixtures', 'user'), function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should support index.', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.set('view engine', 'tmpl'); + + app.render('blog/post', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      blog post

      ') + done(); + }) + }) + + it('should handle render error throws', function(done){ + var app = express(); + + function View(name, options){ + this.name = name; + this.path = 'fale'; + } + + View.prototype.render = function(options, fn){ + throw new Error('err!'); + }; + + app.set('view', View); + + app.render('something', function(err, str){ + assert.ok(err) + assert.strictEqual(err.message, 'err!') + done(); + }) + }) + + describe('when the file does not exist', function(){ + it('should provide a helpful error', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.render('rawr.tmpl', function (err) { + assert.ok(err) + assert.equal(err.message, 'Failed to lookup view "rawr.tmpl" in views directory "' + path.join(__dirname, 'fixtures') + '"') + done(); + }); + }) + }) + + describe('when an error occurs', function(){ + it('should invoke the callback', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.render('user.tmpl', function (err) { + assert.ok(err) + assert.equal(err.name, 'RenderError') + done() + }) + }) + }) + + describe('when an extension is given', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.render('email.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      This is an email

      ') + done(); + }) + }) + }) + + describe('when "view engine" is given', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('view engine', 'tmpl'); + app.set('views', path.join(__dirname, 'fixtures')) + + app.render('email', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      This is an email

      ') + done(); + }) + }) + }) + + describe('when "views" is given', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures', 'default_layout')) + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + describe('when array of paths', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, 'tobi') + done(); + }) + }) + + it('should lookup in later paths until found', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + app.locals.name = 'tobi'; + + app.render('name.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should error if file does not exist', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + app.locals.name = 'tobi'; + + app.render('pet.tmpl', function (err, str) { + assert.ok(err) + assert.equal(err.message, 'Failed to lookup view "pet.tmpl" in views directories "' + views[0] + '" or "' + views[1] + '"') + done(); + }) + }) + }) + }) + + describe('when a "view" constructor is given', function(){ + it('should create an instance of it', function(done){ + var app = express(); + + function View(name, options){ + this.name = name; + this.path = 'path is required by application.js as a signal of success even though it is not used there.'; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view', View); + + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + + describe('caching', function(){ + it('should always lookup view without cache', function(done){ + var app = express(); + var count = 0; + + function View(name, options){ + this.name = name; + this.path = 'fake'; + count++; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view cache', false); + app.set('view', View); + + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 2) + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + + it('should cache with "view cache" setting', function(done){ + var app = express(); + var count = 0; + + function View(name, options){ + this.name = name; + this.path = 'fake'; + count++; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view cache', true); + app.set('view', View); + + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + }) + }) + + describe('.render(name, options, fn)', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + var user = { name: 'tobi' }; + + app.render('user.tmpl', { user: user }, function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', {}, function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should give precedence to app.render() locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + var jane = { name: 'jane' }; + + app.render('user.tmpl', { user: jane }, function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      jane

      ') + done(); + }) + }) + + describe('caching', function(){ + it('should cache with cache option', function(done){ + var app = express(); + var count = 0; + + function View(name, options){ + this.name = name; + this.path = 'fake'; + count++; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view cache', false); + app.set('view', View); + + app.render('something', {cache: true}, function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + app.render('something', {cache: true}, function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + }) + }) +}) + +function createApp() { + var app = express(); + + app.engine('.tmpl', tmpl); + + return app; +} diff --git a/apps/api/test/app.request.js b/apps/api/test/app.request.js new file mode 100644 index 0000000..b6c00f5 --- /dev/null +++ b/apps/api/test/app.request.js @@ -0,0 +1,143 @@ +'use strict' + +var after = require('after') +var express = require('../') + , request = require('supertest'); + +describe('app', function(){ + describe('.request', function(){ + it('should extend the request prototype', function(done){ + var app = express(); + + app.request.querystring = function(){ + return require('node:url').parse(this.url).query; + }; + + app.use(function(req, res){ + res.end(req.querystring()); + }); + + request(app) + .get('/foo?name=tobi') + .expect('name=tobi', done); + }) + + it('should only extend for the referenced app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app1.get('/', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/') + .expect(200, 'tobi', cb) + + request(app2) + .get('/') + .expect(500, /(?:not a function|has no method)/, cb) + }) + + it('should inherit to sub apps', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/') + .expect(200, 'tobi', cb) + + request(app1) + .get('/sub') + .expect(200, 'tobi', cb) + }) + + it('should allow sub app to override', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app2.request.foobar = function () { + return 'loki' + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/') + .expect(200, 'tobi', cb) + + request(app1) + .get('/sub') + .expect(200, 'loki', cb) + }) + + it('should not pollute parent app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app2.request.foobar = function () { + return 'loki' + } + + app1.use('/sub', app2) + + app1.get('/sub/foo', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/sub') + .expect(200, 'loki', cb) + + request(app1) + .get('/sub/foo') + .expect(200, 'tobi', cb) + }) + }) +}) diff --git a/apps/api/test/app.response.js b/apps/api/test/app.response.js new file mode 100644 index 0000000..5fb69f6 --- /dev/null +++ b/apps/api/test/app.response.js @@ -0,0 +1,143 @@ +'use strict' + +var after = require('after') +var express = require('../') + , request = require('supertest'); + +describe('app', function(){ + describe('.response', function(){ + it('should extend the response prototype', function(done){ + var app = express(); + + app.response.shout = function(str){ + this.send(str.toUpperCase()); + }; + + app.use(function(req, res){ + res.shout('hey'); + }); + + request(app) + .get('/') + .expect('HEY', done); + }) + + it('should only extend for the referenced app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app1.get('/', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + request(app1) + .get('/') + .expect(200, 'FOO', cb) + + request(app2) + .get('/') + .expect(500, /(?:not a function|has no method)/, cb) + }) + + it('should inherit to sub apps', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + request(app1) + .get('/') + .expect(200, 'FOO', cb) + + request(app1) + .get('/sub') + .expect(200, 'FOO', cb) + }) + + it('should allow sub app to override', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app2.response.shout = function (str) { + this.send(str + '!') + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + request(app1) + .get('/') + .expect(200, 'FOO', cb) + + request(app1) + .get('/sub') + .expect(200, 'foo!', cb) + }) + + it('should not pollute parent app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app2.response.shout = function (str) { + this.send(str + '!') + } + + app1.use('/sub', app2) + + app1.get('/sub/foo', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + request(app1) + .get('/sub') + .expect(200, 'foo!', cb) + + request(app1) + .get('/sub/foo') + .expect(200, 'FOO', cb) + }) + }) +}) diff --git a/apps/api/test/app.route.js b/apps/api/test/app.route.js new file mode 100644 index 0000000..03ae129 --- /dev/null +++ b/apps/api/test/app.route.js @@ -0,0 +1,197 @@ +'use strict' + +var express = require('../'); +var request = require('supertest'); + +describe('app.route', function(){ + it('should return a new route', function(done){ + var app = express(); + + app.route('/foo') + .get(function(req, res) { + res.send('get'); + }) + .post(function(req, res) { + res.send('post'); + }); + + request(app) + .post('/foo') + .expect('post', done); + }); + + it('should all .VERB after .all', function(done){ + var app = express(); + + app.route('/foo') + .all(function(req, res, next) { + next(); + }) + .get(function(req, res) { + res.send('get'); + }) + .post(function(req, res) { + res.send('post'); + }); + + request(app) + .post('/foo') + .expect('post', done); + }); + + it('should support dynamic routes', function(done){ + var app = express(); + + app.route('/:foo') + .get(function(req, res) { + res.send(req.params.foo); + }); + + request(app) + .get('/test') + .expect('test', done); + }); + + it('should not error on empty routes', function(done){ + var app = express(); + + app.route('/:foo'); + + request(app) + .get('/test') + .expect(404, done); + }); + + describe('promise support', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function helloWorld (req, res) { + res.send('hello, world!') + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject() + }) + + route.all(function helloWorld (req, res) { + res.send('hello, world!') + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + res.send('saw GET /foo') + return Promise.resolve('foo') + }) + + route.all(function () { + done(new Error('Unexpected route invoke')) + }) + + request(app) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) + + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught again: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught again: caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + return Promise.reject() + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught again: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught again: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught: ' + err.message) + return Promise.resolve('foo') + }) + + route.all(function () { + done(new Error('Unexpected route invoke')) + }) + + request(app) + .get('/foo') + .expect(500, 'caught: boom!', done) + }) + }) + }) +}); diff --git a/apps/api/test/app.router.js b/apps/api/test/app.router.js new file mode 100644 index 0000000..6e7be68 --- /dev/null +++ b/apps/api/test/app.router.js @@ -0,0 +1,1217 @@ +'use strict' + +var after = require('after'); +var express = require('../') + , request = require('supertest') + , assert = require('node:assert') + , methods = require('../lib/utils').methods; + +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + +describe('app.router', function () { + it('should restore req.params after leaving router', function (done) { + var app = express(); + var router = new express.Router(); + + function handler1(req, res, next) { + res.setHeader('x-user-id', String(req.params.id)); + next() + } + + function handler2(req, res) { + res.send(req.params.id); + } + + router.use(function (req, res, next) { + res.setHeader('x-router', String(req.params.id)); + next(); + }); + + app.get('/user/:id', handler1, router, handler2); + + request(app) + .get('/user/1') + .expect('x-router', 'undefined') + .expect('x-user-id', '1') + .expect(200, '1', done); + }) + + describe('methods', function () { + methods.forEach(function (method) { + if (method === 'connect') return; + + it('should include ' + method.toUpperCase(), function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } + var app = express(); + + app[method]('/foo', function (req, res) { + res.send(method) + }); + + request(app) + [method]('/foo') + .expect(200, done) + }) + + it('should reject numbers for app.' + method, function () { + var app = express(); + assert.throws(app[method].bind(app, '/', 3), /argument handler must be a function/); + }) + }); + + it('should re-route when method is altered', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(function (req, res, next) { + if (req.method !== 'POST') return next(); + req.method = 'DELETE'; + res.setHeader('X-Method-Altered', '1'); + next(); + }); + + app.delete('/', function (req, res) { + res.end('deleted everything'); + }); + + request(app) + .get('/') + .expect(404, cb) + + request(app) + .delete('/') + .expect(200, 'deleted everything', cb); + + request(app) + .post('/') + .expect('X-Method-Altered', '1') + .expect(200, 'deleted everything', cb); + }); + }) + + describe('decode params', function () { + it('should decode correct params', function (done) { + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/foo%2Fbar') + .expect('foo/bar', done); + }) + + it('should not accept params in malformed paths', function (done) { + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/%foobar') + .expect(400, done); + }) + + it('should not decode spaces', function (done) { + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/foo+bar') + .expect('foo+bar', done); + }) + + it('should work with unicode', function (done) { + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/%ce%b1') + .expect('\u03b1', done); + }) + }) + + it('should be .use()able', function (done) { + var app = express(); + + var calls = []; + + app.use(function (req, res, next) { + calls.push('before'); + next(); + }); + + app.get('/', function (req, res, next) { + calls.push('GET /') + next(); + }); + + app.use(function (req, res, next) { + calls.push('after'); + res.json(calls) + }); + + request(app) + .get('/') + .expect(200, ['before', 'GET /', 'after'], done) + }) + + describe('when given a regexp', function () { + it('should match the pathname only', function (done) { + var app = express(); + + app.get(/^\/user\/[0-9]+$/, function (req, res) { + res.end('user'); + }); + + request(app) + .get('/user/12?foo=bar') + .expect('user', done); + }) + + it('should populate req.params with the captures', function (done) { + var app = express(); + + app.get(/^\/user\/([0-9]+)\/(view|edit)?$/, function (req, res) { + var id = req.params[0] + , op = req.params[1]; + res.end(op + 'ing user ' + id); + }); + + request(app) + .get('/user/10/edit') + .expect('editing user 10', done); + }) + + if (supportsRegexp('(?.*)')) { + it('should populate req.params with named captures', function (done) { + var app = express(); + var re = new RegExp('^/user/(?[0-9]+)/(view|edit)?$'); + + app.get(re, function (req, res) { + var id = req.params.userId + , op = req.params[0]; + res.end(op + 'ing user ' + id); + }); + + request(app) + .get('/user/10/edit') + .expect('editing user 10', done); + }) + } + + it('should ensure regexp matches path prefix', function (done) { + var app = express() + var p = [] + + app.use(/\/api.*/, function (req, res, next) { + p.push('a') + next() + }) + app.use(/api/, function (req, res, next) { + p.push('b') + next() + }) + app.use(/\/test/, function (req, res, next) { + p.push('c') + next() + }) + app.use(function (req, res) { + res.end() + }) + + request(app) + .get('/test/api/1234') + .expect(200, function (err) { + if (err) return done(err) + assert.deepEqual(p, ['c']) + done() + }) + }) + }) + + describe('case sensitivity', function () { + it('should be disabled by default', function (done) { + var app = express(); + + app.get('/user', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/USER') + .expect('tj', done); + }) + + describe('when "case sensitive routing" is enabled', function () { + it('should match identical casing', function (done) { + var app = express(); + + app.enable('case sensitive routing'); + + app.get('/uSer', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/uSer') + .expect('tj', done); + }) + + it('should not match otherwise', function (done) { + var app = express(); + + app.enable('case sensitive routing'); + + app.get('/uSer', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(404, done); + }) + }) + }) + + describe('params', function () { + it('should overwrite existing req.params by default', function (done) { + var app = express(); + var router = new express.Router(); + + router.get('/:action', function (req, res) { + res.send(req.params); + }); + + app.use('/user/:user', router); + + request(app) + .get('/user/1/get') + .expect(200, '{"action":"get"}', done); + }) + + it('should allow merging existing req.params', function (done) { + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/:action', function (req, res) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function (k) { return [k, req.params[k]] })); + }); + + app.use('/user/:user', router); + + request(app) + .get('/user/tj/get') + .expect(200, '[["action","get"],["user","tj"]]', done); + }) + + it('should use params from router', function (done) { + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/:thing', function (req, res) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function (k) { return [k, req.params[k]] })); + }); + + app.use('/user/:thing', router); + + request(app) + .get('/user/tj/get') + .expect(200, '[["thing","get"]]', done); + }) + + it('should merge numeric indices req.params', function (done) { + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get(/^\/(.*)\.(.*)/, function (req, res) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function (k) { return [k, req.params[k]] })); + }); + + app.use(/^\/user\/id:(\d+)/, router); + + request(app) + .get('/user/id:10/profile.json') + .expect(200, '[["0","10"],["1","profile"],["2","json"]]', done); + }) + + it('should merge numeric indices req.params when more in parent', function (done) { + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get(/\/(.*)/, function (req, res) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function (k) { return [k, req.params[k]] })); + }); + + app.use(/^\/user\/id:(\d+)\/name:(\w+)/, router); + + request(app) + .get('/user/id:10/name:tj/profile') + .expect(200, '[["0","10"],["1","tj"],["2","profile"]]', done); + }) + + it('should merge numeric indices req.params when parent has same number', function (done) { + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get(/\/name:(\w+)/, function (req, res) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function (k) { return [k, req.params[k]] })); + }); + + app.use(/\/user\/id:(\d+)/, router); + + request(app) + .get('/user/id:10/name:tj') + .expect(200, '[["0","10"],["1","tj"]]', done); + }) + + it('should ignore invalid incoming req.params', function (done) { + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/:name', function (req, res) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function (k) { return [k, req.params[k]] })); + }); + + app.use('/user/', function (req, res, next) { + req.params = 3; // wat? + router(req, res, next); + }); + + request(app) + .get('/user/tj') + .expect(200, '[["name","tj"]]', done); + }) + + it('should restore req.params', function (done) { + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get(/\/user:(\w+)\//, function (req, res, next) { + next(); + }); + + app.use(/\/user\/id:(\d+)/, function (req, res, next) { + router(req, res, function (err) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function (k) { return [k, req.params[k]] })); + }); + }); + + request(app) + .get('/user/id:42/user:tj/profile') + .expect(200, '[["0","42"]]', done); + }) + }) + + describe('trailing slashes', function () { + it('should be optional by default', function (done) { + var app = express(); + + app.get('/user', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect('tj', done); + }) + + describe('when "strict routing" is enabled', function () { + it('should match trailing slashes', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.get('/user/', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect('tj', done); + }) + + it('should pass-though middleware', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.use(function (req, res, next) { + res.setHeader('x-middleware', 'true'); + next(); + }); + + app.get('/user/', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect('x-middleware', 'true') + .expect(200, 'tj', done); + }) + + it('should pass-though mounted middleware', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.use('/user/', function (req, res, next) { + res.setHeader('x-middleware', 'true'); + next(); + }); + + app.get('/user/test/', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user/test/') + .expect('x-middleware', 'true') + .expect(200, 'tj', done); + }) + + it('should match no slashes', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.get('/user', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user') + .expect('tj', done); + }) + + it('should match middleware when omitting the trailing slash', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.use('/user/', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(200, 'tj', done); + }) + + it('should match middleware', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.use('/user', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(200, 'tj', done); + }) + + it('should match middleware when adding the trailing slash', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.use('/user', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect(200, 'tj', done); + }) + + it('should fail when omitting the trailing slash', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.get('/user/', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(404, done); + }) + + it('should fail when adding the trailing slash', function (done) { + var app = express(); + + app.enable('strict routing'); + + app.get('/user', function (req, res) { + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect(404, done); + }) + }) + }) + + it('should allow literal "."', function (done) { + var app = express(); + + app.get('/api/users/:from..:to', function (req, res) { + var from = req.params.from + , to = req.params.to; + + res.end('users from ' + from + ' to ' + to); + }); + + request(app) + .get('/api/users/1..50') + .expect('users from 1 to 50', done); + }) + + describe(':name', function () { + it('should denote a capture group', function (done) { + var app = express(); + + app.get('/user/:user', function (req, res) { + res.end(req.params.user); + }); + + request(app) + .get('/user/tj') + .expect('tj', done); + }) + + it('should match a single segment only', function (done) { + var app = express(); + + app.get('/user/:user', function (req, res) { + res.end(req.params.user); + }); + + request(app) + .get('/user/tj/edit') + .expect(404, done); + }) + + it('should allow several capture groups', function (done) { + var app = express(); + + app.get('/user/:user/:op', function (req, res) { + res.end(req.params.op + 'ing ' + req.params.user); + }); + + request(app) + .get('/user/tj/edit') + .expect('editing tj', done); + }) + + it('should work following a partial capture group', function (done) { + var app = express(); + var cb = after(2, done); + + app.get('/user{s}/:user/:op', function (req, res) { + res.end(req.params.op + 'ing ' + req.params.user + (req.url.startsWith('/users') ? ' (old)' : '')); + }); + + request(app) + .get('/user/tj/edit') + .expect('editing tj', cb); + + request(app) + .get('/users/tj/edit') + .expect('editing tj (old)', cb); + }) + + it('should work inside literal parenthesis', function (done) { + var app = express(); + + app.get('/:user\\(:op\\)', function (req, res) { + res.end(req.params.op + 'ing ' + req.params.user); + }); + + request(app) + .get('/tj(edit)') + .expect('editing tj', done); + }) + + it('should work in array of paths', function (done) { + var app = express(); + var cb = after(2, done); + + app.get(['/user/:user/poke', '/user/:user/pokes'], function (req, res) { + res.end('poking ' + req.params.user); + }); + + request(app) + .get('/user/tj/poke') + .expect('poking tj', cb); + + request(app) + .get('/user/tj/pokes') + .expect('poking tj', cb); + }) + }) + + describe(':name?', function () { + it('should denote an optional capture group', function (done) { + var app = express(); + + app.get('/user/:user{/:op}', function (req, res) { + var op = req.params.op || 'view'; + res.end(op + 'ing ' + req.params.user); + }); + + request(app) + .get('/user/tj') + .expect('viewing tj', done); + }) + + it('should populate the capture group', function (done) { + var app = express(); + + app.get('/user/:user{/:op}', function (req, res) { + var op = req.params.op || 'view'; + res.end(op + 'ing ' + req.params.user); + }); + + request(app) + .get('/user/tj/edit') + .expect('editing tj', done); + }) + }) + + describe(':name*', function () { + it('should match one segment', function (done) { + var app = express() + + app.get('/user/*user', function (req, res) { + res.end(req.params.user[0]) + }) + + request(app) + .get('/user/122') + .expect('122', done) + }) + + it('should match many segments', function (done) { + var app = express() + + app.get('/user/*user', function (req, res) { + res.end(req.params.user.join('/')) + }) + + request(app) + .get('/user/1/2/3/4') + .expect('1/2/3/4', done) + }) + + it('should match zero segments', function (done) { + var app = express() + + app.get('/user{/*user}', function (req, res) { + res.end(req.params.user) + }) + + request(app) + .get('/user') + .expect('', done) + }) + }) + + describe(':name+', function () { + it('should match one segment', function (done) { + var app = express() + + app.get('/user/*user', function (req, res) { + res.end(req.params.user[0]) + }) + + request(app) + .get('/user/122') + .expect(200, '122', done) + }) + + it('should match many segments', function (done) { + var app = express() + + app.get('/user/*user', function (req, res) { + res.end(req.params.user.join('/')) + }) + + request(app) + .get('/user/1/2/3/4') + .expect(200, '1/2/3/4', done) + }) + + it('should not match zero segments', function (done) { + var app = express() + + app.get('/user/*user', function (req, res) { + res.end(req.params.user) + }) + + request(app) + .get('/user') + .expect(404, done) + }) + }) + + describe('.:name', function () { + it('should denote a format', function (done) { + var app = express(); + var cb = after(2, done) + + app.get('/:name.:format', function (req, res) { + res.end(req.params.name + ' as ' + req.params.format); + }); + + request(app) + .get('/foo.json') + .expect(200, 'foo as json', cb) + + request(app) + .get('/foo') + .expect(404, cb) + }) + }) + + describe('.:name?', function () { + it('should denote an optional format', function (done) { + var app = express(); + var cb = after(2, done) + + app.get('/:name{.:format}', function (req, res) { + res.end(req.params.name + ' as ' + (req.params.format || 'html')); + }); + + request(app) + .get('/foo') + .expect(200, 'foo as html', cb) + + request(app) + .get('/foo.json') + .expect(200, 'foo as json', cb) + }) + }) + + describe('when next() is called', function () { + it('should continue lookup', function (done) { + var app = express() + , calls = []; + + app.get('/foo{/:bar}', function (req, res, next) { + calls.push('/foo/:bar?'); + next(); + }); + + app.get('/bar', function () { + assert(0); + }); + + app.get('/foo', function (req, res, next) { + calls.push('/foo'); + next(); + }); + + app.get('/foo', function (req, res) { + calls.push('/foo 2'); + res.json(calls) + }); + + request(app) + .get('/foo') + .expect(200, ['/foo/:bar?', '/foo', '/foo 2'], done) + }) + }) + + describe('when next("route") is called', function () { + it('should jump to next route', function (done) { + var app = express() + + function fn(req, res, next) { + res.set('X-Hit', '1') + next('route') + } + + app.get('/foo', fn, function (req, res) { + res.end('failure') + }); + + app.get('/foo', function (req, res) { + res.end('success') + }) + + request(app) + .get('/foo') + .expect('X-Hit', '1') + .expect(200, 'success', done) + }) + }) + + describe('when next("router") is called', function () { + it('should jump out of router', function (done) { + var app = express() + var router = express.Router() + + function fn(req, res, next) { + res.set('X-Hit', '1') + next('router') + } + + router.get('/foo', fn, function (req, res) { + res.end('failure') + }) + + router.get('/foo', function (req, res) { + res.end('failure') + }) + + app.use(router) + + app.get('/foo', function (req, res) { + res.end('success') + }) + + request(app) + .get('/foo') + .expect('X-Hit', '1') + .expect(200, 'success', done) + }) + }) + + describe('when next(err) is called', function () { + it('should break out of app.router', function (done) { + var app = express() + , calls = []; + + app.get('/foo{/:bar}', function (req, res, next) { + calls.push('/foo/:bar?'); + next(); + }); + + app.get('/bar', function () { + assert(0); + }); + + app.get('/foo', function (req, res, next) { + calls.push('/foo'); + next(new Error('fail')); + }); + + app.get('/foo', function () { + assert(0); + }); + + app.use(function (err, req, res, next) { + res.json({ + calls: calls, + error: err.message + }) + }) + + request(app) + .get('/foo') + .expect(200, { calls: ['/foo/:bar?', '/foo'], error: 'fail' }, done) + }) + + it('should call handler in same route, if exists', function (done) { + var app = express(); + + function fn1(req, res, next) { + next(new Error('boom!')); + } + + function fn2(req, res, next) { + res.send('foo here'); + } + + function fn3(err, req, res, next) { + res.send('route go ' + err.message); + } + + app.get('/foo', fn1, fn2, fn3); + + app.use(function (err, req, res, next) { + res.end('error!'); + }) + + request(app) + .get('/foo') + .expect('route go boom!', done) + }) + }) + + describe('promise support', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject() + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + res.send('saw GET /foo') + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + app.use(router) + + request(app) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) + + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError(err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject() + }) + + router.use(function handleError(err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: caught: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + app.use(router) + + request(app) + .get('/foo') + .expect(200, 'saw Error: boom!', done) + }) + }) + }) + + it('should allow rewriting of the url', function (done) { + var app = express(); + + app.get('/account/edit', function (req, res, next) { + req.user = { id: 12 }; // faux authenticated user + req.url = '/user/' + req.user.id + '/edit'; + next(); + }); + + app.get('/user/:id/edit', function (req, res) { + res.send('editing user ' + req.params.id); + }); + + request(app) + .get('/account/edit') + .expect('editing user 12', done); + }) + + it('should run in order added', function (done) { + var app = express(); + var path = []; + + app.get('/*path', function (req, res, next) { + path.push(0); + next(); + }); + + app.get('/user/:id', function (req, res, next) { + path.push(1); + next(); + }); + + app.use(function (req, res, next) { + path.push(2); + next(); + }); + + app.all('/user/:id', function (req, res, next) { + path.push(3); + next(); + }); + + app.get('/*splat', function (req, res, next) { + path.push(4); + next(); + }); + + app.use(function (req, res, next) { + path.push(5); + res.end(path.join(',')) + }); + + request(app) + .get('/user/1') + .expect(200, '0,1,2,3,4,5', done); + }) + + it('should be chainable', function () { + var app = express(); + assert.strictEqual(app.get('/', function () { }), app) + }) + + it('should not use disposed router/middleware', function (done) { + // more context: https://github.com/expressjs/express/issues/5743#issuecomment-2277148412 + + var app = express(); + var router = new express.Router(); + + router.use(function (req, res, next) { + res.setHeader('old', 'foo'); + next(); + }); + + app.use(function (req, res, next) { + return router.handle(req, res, next); + }); + + app.get('/', function (req, res, next) { + res.send('yee'); + next(); + }); + + request(app) + .get('/') + .expect('old', 'foo') + .expect(function (res) { + if (typeof res.headers['new'] !== 'undefined') { + throw new Error('`new` header should not be present'); + } + }) + .expect(200, 'yee', function (err, res) { + if (err) return done(err); + + router = new express.Router(); + + router.use(function (req, res, next) { + res.setHeader('new', 'bar'); + next(); + }); + + request(app) + .get('/') + .expect('new', 'bar') + .expect(function (res) { + if (typeof res.headers['old'] !== 'undefined') { + throw new Error('`old` header should not be present'); + } + }) + .expect(200, 'yee', done); + }); + }) +}) + +function supportsRegexp(source) { + try { + new RegExp(source) + return true + } catch (e) { + return false + } +} diff --git a/apps/api/test/app.routes.error.js b/apps/api/test/app.routes.error.js new file mode 100644 index 0000000..ed53c78 --- /dev/null +++ b/apps/api/test/app.routes.error.js @@ -0,0 +1,62 @@ +'use strict' + +var assert = require('node:assert') +var express = require('../') + , request = require('supertest'); + +describe('app', function(){ + describe('.VERB()', function(){ + it('should not get invoked without error handler on error', function(done) { + var app = express(); + + app.use(function(req, res, next){ + next(new Error('boom!')) + }); + + app.get('/bar', function(req, res){ + res.send('hello, world!'); + }); + + request(app) + .post('/bar') + .expect(500, /Error: boom!/, done); + }); + + it('should only call an error handling routing callback when an error is propagated', function(done){ + var app = express(); + + var a = false; + var b = false; + var c = false; + var d = false; + + app.get('/', function(req, res, next){ + next(new Error('fabricated error')); + }, function(req, res, next) { + a = true; + next(); + }, function(err, req, res, next){ + b = true; + assert.strictEqual(err.message, 'fabricated error') + next(err); + }, function(err, req, res, next){ + c = true; + assert.strictEqual(err.message, 'fabricated error') + next(); + }, function(err, req, res, next){ + d = true; + next(); + }, function(req, res){ + assert.ok(!a) + assert.ok(b) + assert.ok(c) + assert.ok(!d) + res.sendStatus(204); + }); + + request(app) + .get('/') + .expect(204, done); + }) + }) +}) diff --git a/apps/api/test/app.use.js b/apps/api/test/app.use.js new file mode 100644 index 0000000..1d56aa3 --- /dev/null +++ b/apps/api/test/app.use.js @@ -0,0 +1,542 @@ +'use strict' + +var after = require('after'); +var assert = require('node:assert') +var express = require('..'); +var request = require('supertest'); + +describe('app', function(){ + it('should emit "mount" when mounted', function(done){ + var blog = express() + , app = express(); + + blog.on('mount', function(arg){ + assert.strictEqual(arg, app) + done(); + }); + + app.use(blog); + }) + + describe('.use(app)', function(){ + it('should mount the app', function(done){ + var blog = express() + , app = express(); + + blog.get('/blog', function(req, res){ + res.end('blog'); + }); + + app.use(blog); + + request(app) + .get('/blog') + .expect('blog', done); + }) + + it('should support mount-points', function(done){ + var blog = express() + , forum = express() + , app = express(); + var cb = after(2, done) + + blog.get('/', function(req, res){ + res.end('blog'); + }); + + forum.get('/', function(req, res){ + res.end('forum'); + }); + + app.use('/blog', blog); + app.use('/forum', forum); + + request(app) + .get('/blog') + .expect(200, 'blog', cb) + + request(app) + .get('/forum') + .expect(200, 'forum', cb) + }) + + it('should set the child\'s .parent', function(){ + var blog = express() + , app = express(); + + app.use('/blog', blog); + assert.strictEqual(blog.parent, app) + }) + + it('should support dynamic routes', function(done){ + var blog = express() + , app = express(); + + blog.get('/', function(req, res){ + res.end('success'); + }); + + app.use('/post/:article', blog); + + request(app) + .get('/post/once-upon-a-time') + .expect('success', done); + }) + + it('should support mounted app anywhere', function(done){ + var cb = after(3, done); + var blog = express() + , other = express() + , app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + blog.get('/', function(req, res){ + res.end('success'); + }); + + blog.once('mount', function (parent) { + assert.strictEqual(parent, app) + cb(); + }); + other.once('mount', function (parent) { + assert.strictEqual(parent, app) + cb(); + }); + + app.use('/post/:article', fn1, other, fn2, blog); + + request(app) + .get('/post/once-upon-a-time') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('success', cb); + }) + }) + + describe('.use(middleware)', function(){ + it('should accept multiple arguments', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + app.use(fn1, fn2, function fn3(req, res) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + }); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should invoke middleware for all requests', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/') + .expect(200, 'saw GET /', cb); + + request(app) + .options('/') + .expect(200, 'saw OPTIONS /', cb); + + request(app) + .post('/foo') + .expect(200, 'saw POST /foo', cb); + }) + + it('should accept array of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use([fn1, fn2, fn3]); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept multiple arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use([fn1, fn2], [fn3]); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept nested arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use([[fn1], fn2], [fn3]); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + }) + + describe('.use(path, middleware)', function(){ + it('should require middleware', function () { + var app = express() + assert.throws(function () { app.use('/') }, 'TypeError: app.use() requires a middleware function') + }) + + it('should reject string as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', 'foo') }, /argument handler must be a function/) + }) + + it('should reject number as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', 42) }, /argument handler must be a function/) + }) + + it('should reject null as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', null) }, /argument handler must be a function/) + }) + + it('should reject Date as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', new Date()) }, /argument handler must be a function/) + }) + + it('should strip path from req.url', function (done) { + var app = express(); + + app.use('/foo', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/foo/bar') + .expect(200, 'saw GET /bar', done); + }) + + it('should accept multiple arguments', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + app.use('/foo', fn1, fn2, function fn3(req, res) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + }); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should invoke middleware for all requests starting with path', function (done) { + var app = express(); + var cb = after(3, done); + + app.use('/foo', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .post('/foo') + .expect(200, 'saw POST /', cb); + + request(app) + .post('/foo/bar') + .expect(200, 'saw POST /bar', cb); + }) + + it('should work if path has trailing slash', function (done) { + var app = express(); + var cb = after(3, done); + + app.use('/foo/', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .post('/foo') + .expect(200, 'saw POST /', cb); + + request(app) + .post('/foo/bar') + .expect(200, 'saw POST /bar', cb); + }) + + it('should accept array of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use('/foo', [fn1, fn2, fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept multiple arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use('/foo', [fn1, fn2], [fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept nested arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use('/foo', [fn1, [fn2]], [fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should support array of paths', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(['/foo/', '/bar'], function (req, res) { + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .get('/foo') + .expect(200, 'saw GET / through /foo', cb); + + request(app) + .get('/bar') + .expect(200, 'saw GET / through /bar', cb); + }) + + it('should support array of paths with middleware array', function (done) { + var app = express(); + var cb = after(2, done); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + } + + app.use(['/foo/', '/bar'], [[fn1], fn2], [fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, 'saw GET / through /foo', cb); + + request(app) + .get('/bar') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, 'saw GET / through /bar', cb); + }) + + it('should support regexp path', function (done) { + var app = express(); + var cb = after(4, done); + + app.use(/^\/[a-z]oo/, function (req, res) { + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .get('/foo') + .expect(200, 'saw GET / through /foo', cb); + + request(app) + .get('/zoo/bear') + .expect(200, 'saw GET /bear through /zoo/bear', cb); + + request(app) + .get('/get/zoo') + .expect(404, cb); + }) + + it('should support empty string path', function (done) { + var app = express(); + + app.use('', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + }); + + request(app) + .get('/') + .expect(200, 'saw GET / through /', done); + }) + }) +}) diff --git a/apps/api/test/config.js b/apps/api/test/config.js new file mode 100644 index 0000000..d004de0 --- /dev/null +++ b/apps/api/test/config.js @@ -0,0 +1,207 @@ +'use strict' + +var assert = require('node:assert'); +var express = require('..'); + +describe('config', function () { + describe('.set()', function () { + it('should set a value', function () { + var app = express(); + app.set('foo', 'bar'); + assert.equal(app.get('foo'), 'bar'); + }) + + it('should set prototype values', function () { + var app = express() + app.set('hasOwnProperty', 42) + assert.strictEqual(app.get('hasOwnProperty'), 42) + }) + + it('should return the app', function () { + var app = express(); + assert.equal(app.set('foo', 'bar'), app); + }) + + it('should return the app when undefined', function () { + var app = express(); + assert.equal(app.set('foo', undefined), app); + }) + + it('should return set value', function () { + var app = express() + app.set('foo', 'bar') + assert.strictEqual(app.set('foo'), 'bar') + }) + + it('should return undefined for prototype values', function () { + var app = express() + assert.strictEqual(app.set('hasOwnProperty'), undefined) + }) + + describe('"etag"', function(){ + it('should throw on bad value', function(){ + var app = express(); + assert.throws(app.set.bind(app, 'etag', 42), /unknown value/); + }) + + it('should set "etag fn"', function(){ + var app = express() + var fn = function(){} + app.set('etag', fn) + assert.equal(app.get('etag fn'), fn) + }) + }) + + describe('"trust proxy"', function(){ + it('should set "trust proxy fn"', function(){ + var app = express() + var fn = function(){} + app.set('trust proxy', fn) + assert.equal(app.get('trust proxy fn'), fn) + }) + }) + }) + + describe('.get()', function(){ + it('should return undefined when unset', function(){ + var app = express(); + assert.strictEqual(app.get('foo'), undefined); + }) + + it('should return undefined for prototype values', function () { + var app = express() + assert.strictEqual(app.get('hasOwnProperty'), undefined) + }) + + it('should otherwise return the value', function(){ + var app = express(); + app.set('foo', 'bar'); + assert.equal(app.get('foo'), 'bar'); + }) + + describe('when mounted', function(){ + it('should default to the parent app', function(){ + var app = express(); + var blog = express(); + + app.set('title', 'Express'); + app.use(blog); + assert.equal(blog.get('title'), 'Express'); + }) + + it('should given precedence to the child', function(){ + var app = express(); + var blog = express(); + + app.use(blog); + app.set('title', 'Express'); + blog.set('title', 'Some Blog'); + + assert.equal(blog.get('title'), 'Some Blog'); + }) + + it('should inherit "trust proxy" setting', function () { + var app = express(); + var blog = express(); + + function fn() { return false } + + app.set('trust proxy', fn); + assert.equal(app.get('trust proxy'), fn); + assert.equal(app.get('trust proxy fn'), fn); + + app.use(blog); + + assert.equal(blog.get('trust proxy'), fn); + assert.equal(blog.get('trust proxy fn'), fn); + }) + + it('should prefer child "trust proxy" setting', function () { + var app = express(); + var blog = express(); + + function fn1() { return false } + function fn2() { return true } + + app.set('trust proxy', fn1); + assert.equal(app.get('trust proxy'), fn1); + assert.equal(app.get('trust proxy fn'), fn1); + + blog.set('trust proxy', fn2); + assert.equal(blog.get('trust proxy'), fn2); + assert.equal(blog.get('trust proxy fn'), fn2); + + app.use(blog); + + assert.equal(app.get('trust proxy'), fn1); + assert.equal(app.get('trust proxy fn'), fn1); + assert.equal(blog.get('trust proxy'), fn2); + assert.equal(blog.get('trust proxy fn'), fn2); + }) + }) + }) + + describe('.enable()', function(){ + it('should set the value to true', function(){ + var app = express(); + assert.equal(app.enable('tobi'), app); + assert.strictEqual(app.get('tobi'), true); + }) + + it('should set prototype values', function () { + var app = express() + app.enable('hasOwnProperty') + assert.strictEqual(app.get('hasOwnProperty'), true) + }) + }) + + describe('.disable()', function(){ + it('should set the value to false', function(){ + var app = express(); + assert.equal(app.disable('tobi'), app); + assert.strictEqual(app.get('tobi'), false); + }) + + it('should set prototype values', function () { + var app = express() + app.disable('hasOwnProperty') + assert.strictEqual(app.get('hasOwnProperty'), false) + }) + }) + + describe('.enabled()', function(){ + it('should default to false', function(){ + var app = express(); + assert.strictEqual(app.enabled('foo'), false); + }) + + it('should return true when set', function(){ + var app = express(); + app.set('foo', 'bar'); + assert.strictEqual(app.enabled('foo'), true); + }) + + it('should default to false for prototype values', function () { + var app = express() + assert.strictEqual(app.enabled('hasOwnProperty'), false) + }) + }) + + describe('.disabled()', function(){ + it('should default to true', function(){ + var app = express(); + assert.strictEqual(app.disabled('foo'), true); + }) + + it('should return false when set', function(){ + var app = express(); + app.set('foo', 'bar'); + assert.strictEqual(app.disabled('foo'), false); + }) + + it('should default to true for prototype values', function () { + var app = express() + assert.strictEqual(app.disabled('hasOwnProperty'), true) + }) + }) +}) diff --git a/apps/api/test/exports.js b/apps/api/test/exports.js new file mode 100644 index 0000000..fc7836c --- /dev/null +++ b/apps/api/test/exports.js @@ -0,0 +1,82 @@ +'use strict' + +var assert = require('node:assert') +var express = require('../'); +var request = require('supertest'); + +describe('exports', function(){ + it('should expose Router', function(){ + assert.strictEqual(typeof express.Router, 'function') + }) + + it('should expose json middleware', function () { + assert.equal(typeof express.json, 'function') + assert.equal(express.json.length, 1) + }) + + it('should expose raw middleware', function () { + assert.equal(typeof express.raw, 'function') + assert.equal(express.raw.length, 1) + }) + + it('should expose static middleware', function () { + assert.equal(typeof express.static, 'function') + assert.equal(express.static.length, 2) + }) + + it('should expose text middleware', function () { + assert.equal(typeof express.text, 'function') + assert.equal(express.text.length, 1) + }) + + it('should expose urlencoded middleware', function () { + assert.equal(typeof express.urlencoded, 'function') + assert.equal(express.urlencoded.length, 1) + }) + + it('should expose the application prototype', function(){ + assert.strictEqual(typeof express.application, 'object') + assert.strictEqual(typeof express.application.set, 'function') + }) + + it('should expose the request prototype', function(){ + assert.strictEqual(typeof express.request, 'object') + assert.strictEqual(typeof express.request.accepts, 'function') + }) + + it('should expose the response prototype', function(){ + assert.strictEqual(typeof express.response, 'object') + assert.strictEqual(typeof express.response.send, 'function') + }) + + it('should permit modifying the .application prototype', function(){ + express.application.foo = function(){ return 'bar'; }; + assert.strictEqual(express().foo(), 'bar') + }) + + it('should permit modifying the .request prototype', function(done){ + express.request.foo = function(){ return 'bar'; }; + var app = express(); + + app.use(function(req, res, next){ + res.end(req.foo()); + }); + + request(app) + .get('/') + .expect('bar', done); + }) + + it('should permit modifying the .response prototype', function(done){ + express.response.foo = function(){ this.send('bar'); }; + var app = express(); + + app.use(function(req, res, next){ + res.foo(); + }); + + request(app) + .get('/') + .expect('bar', done); + }) +}) diff --git a/apps/api/test/express.json.js b/apps/api/test/express.json.js new file mode 100644 index 0000000..28746bf --- /dev/null +++ b/apps/api/test/express.json.js @@ -0,0 +1,755 @@ +'use strict' + +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage +const { Buffer } = require('node:buffer'); + +var express = require('..') +var request = require('supertest') + +describe('express.json()', function () { + it('should parse JSON', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should handle Content-Length: 0', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '0') + .expect(200, '{}', done) + }) + + it('should handle empty message-body', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .set('Transfer-Encoding', 'chunked') + .expect(200, '{}', done) + }) + + it('should handle no message-body', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .unset('Transfer-Encoding') + .expect(200, '{}', done) + }) + + // The old node error message modification in body parser is catching this + it('should 400 when only whitespace', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .send(' \n') + .expect(400, '[entity.parse.failed] ' + parseError(' \n'), done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.json()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"str":') + .expect(400, /content length/, done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.json()) + app.use(express.json()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + describe('when JSON is invalid', function () { + before(function () { + this.app = createApp() + }) + + it('should 400 for bad token', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{:') + .expect(400, '[entity.parse.failed] ' + parseError('{:'), done) + }) + + it('should 400 for incomplete', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user"') + .expect(400, '[entity.parse.failed] ' + parseError('{"user"'), done) + }) + + it('should include original body on error object', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'body') + .send(' {"user"') + .expect(400, ' {"user"', done) + }) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '1034') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, '[entity.too.large] request entity too large', done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var app = createApp({ limit: '1kb' }) + var buf = Buffer.alloc(1024, '.') + var test = request(app).post('/') + test.set('Content-Type', 'application/json') + test.set('Transfer-Encoding', 'chunked') + test.write('{"str":') + test.write('"' + buf.toString() + '"}') + test.expect(413, done) + }) + + it('should 413 when inflated body over limit', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a040000', 'hex')) + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: 1024 })) + .post('/') + .set('Content-Type', 'application/json') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1024, '.') + var options = { limit: '1kb' } + var app = createApp(options) + + options.limit = '100kb' + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should not hang response', function (done) { + var buf = Buffer.alloc(10240, '.') + var app = createApp({ limit: '8kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/json') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + + it('should not error when inflating', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a0400', 'hex')) + test.expect(413, done) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(415, '[encoding.unsupported] content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + }) + }) + + describe('with strict option', function () { + describe('when undefined', function () { + before(function () { + this.app = createApp() + }) + + it('should 400 on primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) + }) + }) + + describe('when false', function () { + before(function () { + this.app = createApp({ strict: false }) + }) + + it('should parse primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(200, 'true', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ strict: true }) + }) + + it('should not parse primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) + }) + + it('should not parse primitives with leading whitespaces', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send(' true') + .expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace(/#/g, 't'), done) + }) + + it('should allow leading whitespaces in JSON', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send(' { "user": "tobi" }') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should include correct message in stack trace', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'stack') + .send('true') + .expect(400) + .expect(shouldContainInBody(parseError('#rue').replace(/#/g, 't'))) + .end(done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd.api+json"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd.api+json' }) + }) + + it('should parse JSON for custom type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore standard type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '', done) + }) + }) + + describe('when ["application/json", "application/vnd.api+json"]', function () { + before(function () { + this.app = createApp({ + type: ['application/json', 'application/vnd.api+json'] + }) + }) + + it('should parse JSON for "application/json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should parse JSON for "application/vnd.api+json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore "application/x-json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-json') + .send('{"user":"tobi"}') + .expect(200, '', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.api+json' + } + + request(app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write('{"user":"tobi"}') + test.expect(200, '{"user":"tobi"}', done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value if function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('["tobi"]') + .expect(403, '[entity.verify.failed] no arrays', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.status = 400 + throw err + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('["tobi"]') + .expect(400, '[entity.verify.failed] no arrays', done) + }) + + it('should allow custom type', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.type = 'foo.bar' + throw err + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('["tobi"]') + .expect(403, '[foo.bar] no arrays', done) + }) + + it('should include original body on error object', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'body') + .send('["tobi"]') + .expect(403, '["tobi"]', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work with different charsets', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/json; charset=utf-16') + test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should 415 on unknown charset prior to verify', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } + }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/json; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describe('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.json()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + this.app = app + }) + + it('should persist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{"user":"tobi"}') + .end(done) + }) + + it('should persist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('') + .end(done) + }) + + it('should persist store when inflated', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200) + test.expect('x-store-foo', 'bar') + test.expect('{"name":"论"}') + test.end(done) + }) + + it('should persist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should persist store when parse error', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":') + .expect(400) + .expect('x-store-foo', 'bar') + .end(done) + }) + + it('should persist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"' + Buffer.alloc(1024 * 100, '.').toString() + '"}') + .expect(413) + .expect('x-store-foo', 'bar') + .end(done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should parse utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-8') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse utf-16', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-16') + test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse when content-length != char length', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-8') + test.set('Content-Length', '13') + test.write(Buffer.from('7b2274657374223a22c3a5227d', 'hex')) + test.expect(200, '{"test":"å"}', done) + }) + + it('should default to utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should fail on unknown charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=koi8-r') + test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) + test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '1kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('789cab56ca4bcc4d55b2527ab16e97522d00274505ac', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should 415 on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) + }) + + it('should 400 on malformed encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(400, done) + }) + + it('should 413 when inflated value exceeds limit', function (done) { + // gzip'd data exceeds 1kb, but deflated below 1kb + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bedc1010d000000c2a0f74f6d0f071400000000000000', 'hex')) + test.write(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex')) + test.write(Buffer.from('0000000000000000004f0625b3b71650c30000', 'hex')) + test.expect(413, done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.json(options)) + + app.use(function (err, req, res, next) { + // console.log(err) + res.status(err.status || 500) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} + +function parseError (str) { + try { + JSON.parse(str); throw new SyntaxError('strict violation') + } catch (e) { + return e.message + } +} + +function shouldContainInBody (str) { + return function (res) { + assert.ok(res.text.indexOf(str) !== -1, + 'expected \'' + res.text + '\' to contain \'' + str + '\'') + } +} diff --git a/apps/api/test/express.raw.js b/apps/api/test/express.raw.js new file mode 100644 index 0000000..5576e22 --- /dev/null +++ b/apps/api/test/express.raw.js @@ -0,0 +1,513 @@ +'use strict' + +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage + +var express = require('..') +var request = require('supertest') +const { Buffer } = require('node:buffer'); + +describe('express.raw()', function () { + before(function () { + this.app = createApp() + }) + + it('should parse application/octet-stream', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200, { buf: '746865207573657220697320746f6269' }, done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.raw()) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('stuff') + .expect(400, /content length/, done) + }) + + it('should handle Content-Length: 0', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .set('Content-Length', '0') + .expect(200, { buf: '' }, done) + }) + + it('should handle empty message-body', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .set('Transfer-Encoding', 'chunked') + .send('') + .expect(200, { buf: '' }, done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.raw()) + app.use(express.raw()) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200, { buf: '746865207573657220697320746f6269' }, done) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.set('Content-Length', '1028') + test.write(buf) + test.expect(413, done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.set('Transfer-Encoding', 'chunked') + test.write(buf) + test.expect(413, done) + }) + + it('should 413 when inflated body over limit', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex')) + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: 1024 }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1028, '.') + var options = { limit: '1kb' } + var app = createApp(options) + + options.limit = '100kb' + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.expect(413, done) + }) + + it('should not hang response', function (done) { + var buf = Buffer.alloc(10240, '.') + var app = createApp({ limit: '8kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + + it('should not error when inflating', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a147040400', 'hex')) + test.expect(413, done) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(415, '[encoding.unsupported] content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd+octets"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd+octets' }) + }) + + it('should parse for custom type', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/vnd+octets') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should ignore standard type', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, '', done) + }) + }) + + describe('when ["application/octet-stream", "application/vnd+octets"]', function () { + before(function () { + this.app = createApp({ + type: ['application/octet-stream', 'application/vnd+octets'] + }) + }) + + it('should parse "application/octet-stream"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should parse "application/vnd+octets"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/vnd+octets') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should ignore "application/x-foo"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-foo') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, '', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.octet' + } + + var test = request(app).post('/') + test.set('Content-Type', 'application/vnd.octet') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value is function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } + }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(403, '[entity.verify.failed] no leading null', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x00) return + var err = new Error('no leading null') + err.status = 400 + throw err + } + }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(400, '[entity.verify.failed] no leading null', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } + }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('0102', 'hex')) + test.expect(200, { buf: '0102' }, done) + }) + }) + + describe('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.raw()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + this.app = app + }) + + it('should persist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect({ buf: '746865207573657220697320746f6269' }) + .end(done) + }) + + it('should persist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .end(done) + }) + + it('should persist store when inflated', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200) + test.expect('x-store-foo', 'bar') + test.expect({ buf: '6e616d653de8aeba' }) + test.end(done) + }) + + it('should persist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should persist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is ' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should ignore charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream; charset=utf-8') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, { buf: '6e616d6520697320e8aeba' }, done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '10kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should 415 on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.raw(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) + }) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + return app +} diff --git a/apps/api/test/express.static.js b/apps/api/test/express.static.js new file mode 100644 index 0000000..a203563 --- /dev/null +++ b/apps/api/test/express.static.js @@ -0,0 +1,815 @@ +'use strict' + +var assert = require('node:assert') +var express = require('..') +var path = require('node:path') +const { Buffer } = require('node:buffer'); + +var request = require('supertest') +var utils = require('./support/utils') + +var fixtures = path.join(__dirname, '/fixtures') +var relative = path.relative(process.cwd(), fixtures) + +var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative + +describe('express.static()', function () { + describe('basic operations', function () { + before(function () { + this.app = createApp() + }) + + it('should require root path', function () { + assert.throws(express.static.bind(), /root path required/) + }) + + it('should require root path to be string', function () { + assert.throws(express.static.bind(null, 42), /root path.*string/) + }) + + it('should serve static files', function (done) { + request(this.app) + .get('/todo.txt') + .expect(200, '- groceries', done) + }) + + it('should support nesting', function (done) { + request(this.app) + .get('/users/tobi.txt') + .expect(200, 'ferret', done) + }) + + it('should set Content-Type', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, done) + }) + + it('should set Last-Modified', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Last-Modified', /\d{2} \w{3} \d{4}/) + .expect(200, done) + }) + + it('should default max-age=0', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, done) + }) + + it('should support urlencoded pathnames', function (done) { + request(this.app) + .get('/%25%20of%20dogs.txt') + .expect(200, '20%', done) + }) + + it('should not choke on auth-looking URL', function (done) { + request(this.app) + .get('//todo@txt') + .expect(404, 'Not Found', done) + }) + + it('should support index.html', function (done) { + request(this.app) + .get('/users/') + .expect(200) + .expect('Content-Type', /html/) + .expect('

      tobi, loki, jane

      ', done) + }) + + it('should support ../', function (done) { + request(this.app) + .get('/users/../todo.txt') + .expect(200, '- groceries', done) + }) + + it('should support HEAD', function (done) { + request(this.app) + .head('/todo.txt') + .expect(200) + .expect(utils.shouldNotHaveBody()) + .end(done) + }) + + it('should skip POST requests', function (done) { + request(this.app) + .post('/todo.txt') + .expect(404, 'Not Found', done) + }) + + it('should support conditional requests', function (done) { + var app = this.app + + request(app) + .get('/todo.txt') + .end(function (err, res) { + if (err) throw err + request(app) + .get('/todo.txt') + .set('If-None-Match', res.headers.etag) + .expect(304, done) + }) + }) + + it('should support precondition checks', function (done) { + request(this.app) + .get('/todo.txt') + .set('If-Match', '"foo"') + .expect(412, done) + }) + + it('should serve zero-length files', function (done) { + request(this.app) + .get('/empty.txt') + .expect(200, '', done) + }) + + it('should ignore hidden files', function (done) { + request(this.app) + .get('/.name') + .expect(404, 'Not Found', done) + }) + }); + + (skipRelative ? describe.skip : describe)('current dir', function () { + before(function () { + this.app = createApp('.') + }) + + it('should be served with "."', function (done) { + var dest = relative.split(path.sep).join('/') + request(this.app) + .get('/' + dest + '/todo.txt') + .expect(200, '- groceries', done) + }) + }) + + describe('acceptRanges', function () { + describe('when false', function () { + it('should not include Accept-Ranges', function (done) { + request(createApp(fixtures, { 'acceptRanges': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .expect(200, '123456789', done) + }) + + it('should ignore Rage request header', function (done) { + request(createApp(fixtures, { 'acceptRanges': false })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .expect(utils.shouldNotHaveHeader('Content-Range')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Accept-Ranges', function (done) { + request(createApp(fixtures, { 'acceptRanges': true })) + .get('/nums.txt') + .expect('Accept-Ranges', 'bytes') + .expect(200, '123456789', done) + }) + + it('should obey Rage request header', function (done) { + request(createApp(fixtures, { 'acceptRanges': true })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect('Accept-Ranges', 'bytes') + .expect('Content-Range', 'bytes 0-3/9') + .expect(206, '1234', done) + }) + }) + }) + + describe('cacheControl', function () { + describe('when false', function () { + it('should not include Cache-Control', function (done) { + request(createApp(fixtures, { 'cacheControl': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) + }) + + it('should ignore maxAge', function (done) { + request(createApp(fixtures, { 'cacheControl': false, 'maxAge': 12000 })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Cache-Control', function (done) { + request(createApp(fixtures, { 'cacheControl': true })) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, '123456789', done) + }) + }) + }) + + describe('extensions', function () { + it('should be not be enabled by default', function (done) { + request(createApp(fixtures)) + .get('/todo') + .expect(404, done) + }) + + it('should be configurable', function (done) { + request(createApp(fixtures, { 'extensions': 'txt' })) + .get('/todo') + .expect(200, '- groceries', done) + }) + + it('should support disabling extensions', function (done) { + request(createApp(fixtures, { 'extensions': false })) + .get('/todo') + .expect(404, done) + }) + + it('should support fallbacks', function (done) { + request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] })) + .get('/todo') + .expect(200, '
    • groceries
    • ', done) + }) + + it('should 404 if nothing found', function (done) { + request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] })) + .get('/bob') + .expect(404, done) + }) + }) + + describe('fallthrough', function () { + it('should default to true', function (done) { + request(createApp()) + .get('/does-not-exist') + .expect(404, 'Not Found', done) + }) + + describe('when true', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': true }) + }) + + it('should fall-through when OPTIONS request', function (done) { + request(this.app) + .options('/todo.txt') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when URL malformed', function (done) { + request(this.app) + .get('/%') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when traversing past root', function (done) { + request(this.app) + .get('/users/../../todo.txt') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when URL too long', function (done) { + var app = express() + var root = fixtures + Array(10000).join('/foobar') + + app.use(express.static(root, { 'fallthrough': true })) + app.use(function (req, res, next) { + res.sendStatus(404) + }) + + request(app) + .get('/') + .expect(404, 'Not Found', done) + }) + + describe('with redirect: true', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': true }) + }) + + it('should fall-through when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, 'Not Found', done) + }) + + it('should redirect when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(301, /Redirecting/, done) + }) + }) + + describe('with redirect: false', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': false }) + }) + + it('should fall-through when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(404, 'Not Found', done) + }) + }) + }) + + describe('when false', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': false }) + }) + + it('should 405 when OPTIONS request', function (done) { + request(this.app) + .options('/todo.txt') + .expect('Allow', 'GET, HEAD') + .expect(405, done) + }) + + it('should 400 when URL malformed', function (done) { + request(this.app) + .get('/%') + .expect(400, /BadRequestError/, done) + }) + + it('should 403 when traversing past root', function (done) { + request(this.app) + .get('/users/../../todo.txt') + .expect(403, /ForbiddenError/, done) + }) + + it('should 404 when URL too long', function (done) { + var app = express() + var root = fixtures + Array(10000).join('/foobar') + + app.use(express.static(root, { 'fallthrough': false })) + app.use(function (req, res, next) { + res.sendStatus(404) + }) + + request(app) + .get('/') + .expect(404, /ENAMETOOLONG/, done) + }) + + describe('with redirect: true', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': true }) + }) + + it('should 404 when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, /NotFoundError|ENOENT/, done) + }) + + it('should redirect when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(301, /Redirecting/, done) + }) + }) + + describe('with redirect: false', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': false }) + }) + + it('should 404 when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, /NotFoundError|ENOENT/, done) + }) + + it('should 404 when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(404, /NotFoundError|ENOENT/, done) + }) + }) + }) + }) + + describe('hidden files', function () { + before(function () { + this.app = createApp(fixtures, { 'dotfiles': 'allow' }) + }) + + it('should be served when dotfiles: "allow" is given', function (done) { + request(this.app) + .get('/.name') + .expect(200) + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + }) + + describe('immutable', function () { + it('should default to false', function (done) { + request(createApp(fixtures)) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=0', done) + }) + + it('should set immutable directive in Cache-Control', function (done) { + request(createApp(fixtures, { 'immutable': true, 'maxAge': '1h' })) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=3600, immutable', done) + }) + }) + + describe('lastModified', function () { + describe('when false', function () { + it('should not include Last-Modified', function (done) { + request(createApp(fixtures, { 'lastModified': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Last-Modified')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Last-Modified', function (done) { + request(createApp(fixtures, { 'lastModified': true })) + .get('/nums.txt') + .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/) + .expect(200, '123456789', done) + }) + }) + }) + + describe('maxAge', function () { + it('should accept string', function (done) { + request(createApp(fixtures, { 'maxAge': '30d' })) + .get('/todo.txt') + .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30)) + .expect(200, done) + }) + + it('should be reasonable when infinite', function (done) { + request(createApp(fixtures, { 'maxAge': Infinity })) + .get('/todo.txt') + .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365)) + .expect(200, done) + }) + }) + + describe('redirect', function () { + before(function () { + this.app = express() + this.app.use(function (req, res, next) { + req.originalUrl = req.url = + req.originalUrl.replace(/\/snow(\/|$)/, '/snow \u2603$1') + next() + }) + this.app.use(express.static(fixtures)) + }) + + it('should redirect directories', function (done) { + request(this.app) + .get('/users') + .expect('Location', '/users/') + .expect(301, done) + }) + + it('should include HTML link', function (done) { + request(this.app) + .get('/users') + .expect('Location', '/users/') + .expect(301, /\/users\//, done) + }) + + it('should redirect directories with query string', function (done) { + request(this.app) + .get('/users?name=john') + .expect('Location', '/users/?name=john') + .expect(301, done) + }) + + it('should not redirect to protocol-relative locations', function (done) { + request(this.app) + .get('//users') + .expect('Location', '/users/') + .expect(301, done) + }) + + it('should ensure redirect URL is properly encoded', function (done) { + request(this.app) + .get('/snow') + .expect('Location', '/snow%20%E2%98%83/') + .expect('Content-Type', /html/) + .expect(301, />Redirecting to \/snow%20%E2%98%83\/tobi') + .expect(200, '"tobi"', done) + }) + + it('should ignore standard type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(200, '', done) + }) + }) + + describe('when ["text/html", "text/plain"]', function () { + before(function () { + this.app = createApp({ type: ['text/html', 'text/plain'] }) + }) + + it('should parse "text/html"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/html') + .send('tobi') + .expect(200, '"tobi"', done) + }) + + it('should parse "text/plain"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('tobi') + .expect(200, '"tobi"', done) + }) + + it('should ignore "text/xml"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/xml') + .send('tobi') + .expect(200, '', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'text/vnd.something' + } + + request(app) + .post('/') + .set('Content-Type', 'text/vnd.something') + .send('user is tobi') + .expect(200, '"user is tobi"', done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write('user is tobi') + test.expect(200, '"user is tobi"', done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value is function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } + }) + + request(app) + .post('/') + .set('Content-Type', 'text/plain') + .send(' user is tobi') + .expect(403, '[entity.verify.failed] no leading space', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x20) return + var err = new Error('no leading space') + err.status = 400 + throw err + } + }) + + request(app) + .post('/') + .set('Content-Type', 'text/plain') + .send(' user is tobi') + .expect(400, '[entity.verify.failed] no leading space', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } + }) + + request(app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(200, '"user is tobi"', done) + }) + + it('should 415 on unknown charset prior to verify', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } + }) + + var test = request(app).post('/') + test.set('Content-Type', 'text/plain; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describe('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.text()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + this.app = app + }) + + it('should persist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('"user is tobi"') + .end(done) + }) + + it('should persist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .end(done) + }) + + it('should persist store when inflated', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) + test.expect(200) + test.expect('x-store-foo', 'bar') + test.expect('"name is 论"') + test.end(done) + }) + + it('should persist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b0000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should persist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is ' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should parse utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=utf-8') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should parse codepage charsets', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=koi8-r') + test.write(Buffer.from('6e616d6520697320cec5d4', 'hex')) + test.expect(200, '"name is нет"', done) + }) + + it('should parse when content-length != char length', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=utf-8') + test.set('Content-Length', '11') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should default to utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should 415 on unknown charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '10kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('789ccb4bcc4d55c82c5678b16e17001a6f050e', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should 415 on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.text(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} diff --git a/apps/api/test/express.urlencoded.js b/apps/api/test/express.urlencoded.js new file mode 100644 index 0000000..f4acf23 --- /dev/null +++ b/apps/api/test/express.urlencoded.js @@ -0,0 +1,828 @@ +'use strict' + +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage +const { Buffer } = require('node:buffer'); + +var express = require('..') +var request = require('supertest') + +describe('express.urlencoded()', function () { + before(function () { + this.app = createApp() + }) + + it('should parse x-www-form-urlencoded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.urlencoded()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('str=') + .expect(400, /content length/, done) + }) + + it('should handle Content-Length: 0', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Content-Length', '0') + .send('') + .expect(200, '{}', done) + }) + + it('should handle empty message-body', function (done) { + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Transfer-Encoding', 'chunked') + .send('') + .expect(200, '{}', done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.urlencoded()) + app.use(express.urlencoded()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should not parse extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user[name][first]=Tobi') + .expect(200, '{"user[name][first]":"Tobi"}', done) + }) + + describe('with extended option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ extended: false }) + }) + + it('should not parse extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user[name][first]=Tobi') + .expect(200, '{"user[name][first]":"Tobi"}', done) + }) + + it('should parse multiple key instances', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=Tobi&user=Loki') + .expect(200, '{"user":["Tobi","Loki"]}', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ extended: true }) + }) + + it('should parse multiple key instances', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=Tobi&user=Loki') + .expect(200, '{"user":["Tobi","Loki"]}', done) + }) + + it('should parse extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user[name][first]=Tobi') + .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) + }) + + it('should parse parameters with dots', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user.name=Tobi') + .expect(200, '{"user.name":"Tobi"}', done) + }) + + it('should parse fully-encoded extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user%5Bname%5D%5Bfirst%5D=Tobi') + .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) + }) + + it('should parse array index notation', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('foo[0]=bar&foo[1]=baz') + .expect(200, '{"foo":["bar","baz"]}', done) + }) + + it('should parse array index notation with large array', function (done) { + var str = 'f[0]=0' + + for (var i = 1; i < 500; i++) { + str += '&f[' + i + ']=' + i.toString(16) + } + + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(str) + .expect(function (res) { + var obj = JSON.parse(res.text) + assert.strictEqual(Object.keys(obj).length, 1) + assert.strictEqual(Array.isArray(obj.f), true) + assert.strictEqual(obj.f.length, 500) + }) + .expect(200, done) + }) + + it('should parse array of objects syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!') + .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done) + }) + + it('should parse deep object', function (done) { + var str = 'foo' + + for (var i = 0; i < 32; i++) { + str += '[p]' + } + + str += '=bar' + + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(str) + .expect(function (res) { + var obj = JSON.parse(res.text) + assert.strictEqual(Object.keys(obj).length, 1) + assert.strictEqual(typeof obj.foo, 'object') + + var depth = 0 + var ref = obj.foo + while ((ref = ref.p)) { depth++ } + assert.strictEqual(depth, 32) + }) + .expect(200, done) + }) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(415, '[encoding.unsupported] content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + }) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Content-Length', '1028') + .send('str=' + buf.toString()) + .expect(413, done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var app = createApp({ limit: '1kb' }) + var buf = Buffer.alloc(1024, '.') + var test = request(app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.set('Transfer-Encoding', 'chunked') + test.write('str=') + test.write(buf.toString()) + test.expect(413, done) + }) + + it('should 413 when inflated body over limit', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f9204040000', 'hex')) + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: 1024 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('str=' + buf.toString()) + .expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1024, '.') + var options = { limit: '1kb' } + var app = createApp(options) + + options.limit = '100kb' + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('str=' + buf.toString()) + .expect(413, done) + }) + + it('should not hang response', function (done) { + var app = createApp({ limit: '8kb' }) + var buf = Buffer.alloc(10240, '.') + var test = request(app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + + it('should not error when inflating', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f92040400', 'hex')) + test.expect(413, done) + }) + }) + + describe('with parameterLimit option', function () { + describe('with extended: false', function () { + it('should reject 0', function () { + assert.throws(createApp.bind(null, { extended: false, parameterLimit: 0 }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should reject string', function () { + assert.throws(createApp.bind(null, { extended: false, parameterLimit: 'beep' }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should 413 if over limit', function (done) { + request(createApp({ extended: false, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, '[parameters.too.many] too many parameters', done) + }) + + it('should work when at the limit', function (done) { + request(createApp({ extended: false, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10)) + .expect(expectKeyCount(10)) + .expect(200, done) + }) + + it('should work if number is floating point', function (done) { + request(createApp({ extended: false, parameterLimit: 10.1 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, /too many parameters/, done) + }) + + it('should work with large limit', function (done) { + request(createApp({ extended: false, parameterLimit: 5000 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(5000)) + .expect(expectKeyCount(5000)) + .expect(200, done) + }) + + it('should work with Infinity limit', function (done) { + request(createApp({ extended: false, parameterLimit: Infinity })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10000)) + .expect(expectKeyCount(10000)) + .expect(200, done) + }) + }) + + describe('with extended: true', function () { + it('should reject 0', function () { + assert.throws(createApp.bind(null, { extended: true, parameterLimit: 0 }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should reject string', function () { + assert.throws(createApp.bind(null, { extended: true, parameterLimit: 'beep' }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should 413 if over limit', function (done) { + request(createApp({ extended: true, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, '[parameters.too.many] too many parameters', done) + }) + + it('should work when at the limit', function (done) { + request(createApp({ extended: true, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10)) + .expect(expectKeyCount(10)) + .expect(200, done) + }) + + it('should work if number is floating point', function (done) { + request(createApp({ extended: true, parameterLimit: 10.1 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, /too many parameters/, done) + }) + + it('should work with large limit', function (done) { + request(createApp({ extended: true, parameterLimit: 5000 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(5000)) + .expect(expectKeyCount(5000)) + .expect(200, done) + }) + + it('should work with Infinity limit', function (done) { + request(createApp({ extended: true, parameterLimit: Infinity })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10000)) + .expect(expectKeyCount(10000)) + .expect(200, done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd.x-www-form-urlencoded"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd.x-www-form-urlencoded' }) + }) + + it('should parse for custom type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore standard type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '', done) + }) + }) + + describe('when ["urlencoded", "application/x-pairs"]', function () { + before(function () { + this.app = createApp({ + type: ['urlencoded', 'application/x-pairs'] + }) + }) + + it('should parse "application/x-www-form-urlencoded"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should parse "application/x-pairs"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-pairs') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore application/x-foo', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-foo') + .send('user=tobi') + .expect(200, '', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.something' + } + + request(app) + .post('/') + .set('Content-Type', 'application/vnd.something') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write('user=tobi') + test.expect(200, '{"user":"tobi"}', done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value if function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(' user=tobi') + .expect(403, '[entity.verify.failed] no leading space', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x20) return + var err = new Error('no leading space') + err.status = 400 + throw err + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(' user=tobi') + .expect(400, '[entity.verify.failed] no leading space', done) + }) + + it('should allow custom type', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x20) return + var err = new Error('no leading space') + err.type = 'foo.bar' + throw err + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(' user=tobi') + .expect(403, '[foo.bar] no leading space', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should 415 on unknown charset prior to verify', function (done) { + var app = createApp({ + verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } + }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describe('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.urlencoded()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + this.app = app + }) + + it('should persist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{"user":"tobi"}') + .end(done) + }) + + it('should persist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .end(done) + }) + + it('should persist store when inflated', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200) + test.expect('x-store-foo', 'bar') + test.expect('{"name":"论"}') + test.end(done) + }) + + it('should persist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should persist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should parse utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse when content-length != char length', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') + test.set('Content-Length', '7') + test.write(Buffer.from('746573743dc3a5', 'hex')) + test.expect(200, '{"test":"å"}', done) + }) + + it('should default to utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should fail on unknown charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r') + test.write(Buffer.from('6e616d653dcec5d4', 'hex')) + test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '10kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should 415 on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) + }) + }) +}) + +function createManyParams (count) { + var str = '' + + if (count === 0) { + return str + } + + str += '0=0' + + for (var i = 1; i < count; i++) { + var n = i.toString(36) + str += '&' + n + '=' + n + } + + return str +} + +function createApp (options) { + var app = express() + + app.use(express.urlencoded(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} + +function expectKeyCount (count) { + return function (res) { + assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count) + } +} diff --git a/apps/api/test/fixtures/% of dogs.txt b/apps/api/test/fixtures/% of dogs.txt new file mode 100644 index 0000000..3a4d134 --- /dev/null +++ b/apps/api/test/fixtures/% of dogs.txt @@ -0,0 +1 @@ +20% \ No newline at end of file diff --git a/apps/api/test/fixtures/.name b/apps/api/test/fixtures/.name new file mode 100644 index 0000000..fa66f37 --- /dev/null +++ b/apps/api/test/fixtures/.name @@ -0,0 +1 @@ +tobi \ No newline at end of file diff --git a/apps/api/test/fixtures/blog/index.html b/apps/api/test/fixtures/blog/index.html new file mode 100644 index 0000000..bcc6b18 --- /dev/null +++ b/apps/api/test/fixtures/blog/index.html @@ -0,0 +1 @@ +index \ No newline at end of file diff --git a/apps/api/test/fixtures/blog/post/index.tmpl b/apps/api/test/fixtures/blog/post/index.tmpl new file mode 100644 index 0000000..a9a2a3b --- /dev/null +++ b/apps/api/test/fixtures/blog/post/index.tmpl @@ -0,0 +1 @@ +

      blog post

      \ No newline at end of file diff --git a/apps/api/test/fixtures/broken.send b/apps/api/test/fixtures/broken.send new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/test/fixtures/default_layout/name.tmpl b/apps/api/test/fixtures/default_layout/name.tmpl new file mode 100644 index 0000000..0c49bf6 --- /dev/null +++ b/apps/api/test/fixtures/default_layout/name.tmpl @@ -0,0 +1 @@ +

      $name

      \ No newline at end of file diff --git a/apps/api/test/fixtures/default_layout/user.tmpl b/apps/api/test/fixtures/default_layout/user.tmpl new file mode 100644 index 0000000..67ef510 --- /dev/null +++ b/apps/api/test/fixtures/default_layout/user.tmpl @@ -0,0 +1 @@ +

      $user.name

      \ No newline at end of file diff --git a/apps/api/test/fixtures/email.tmpl b/apps/api/test/fixtures/email.tmpl new file mode 100644 index 0000000..8a2cb77 --- /dev/null +++ b/apps/api/test/fixtures/email.tmpl @@ -0,0 +1 @@ +

      This is an email

      \ No newline at end of file diff --git a/apps/api/test/fixtures/empty.txt b/apps/api/test/fixtures/empty.txt new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/test/fixtures/local_layout/user.tmpl b/apps/api/test/fixtures/local_layout/user.tmpl new file mode 100644 index 0000000..e9c8684 --- /dev/null +++ b/apps/api/test/fixtures/local_layout/user.tmpl @@ -0,0 +1 @@ +$user.name \ No newline at end of file diff --git a/apps/api/test/fixtures/name.tmpl b/apps/api/test/fixtures/name.tmpl new file mode 100644 index 0000000..0c49bf6 --- /dev/null +++ b/apps/api/test/fixtures/name.tmpl @@ -0,0 +1 @@ +

      $name

      \ No newline at end of file diff --git a/apps/api/test/fixtures/name.txt b/apps/api/test/fixtures/name.txt new file mode 100644 index 0000000..fa66f37 --- /dev/null +++ b/apps/api/test/fixtures/name.txt @@ -0,0 +1 @@ +tobi \ No newline at end of file diff --git a/apps/api/test/fixtures/nums.txt b/apps/api/test/fixtures/nums.txt new file mode 100644 index 0000000..e2e107a --- /dev/null +++ b/apps/api/test/fixtures/nums.txt @@ -0,0 +1 @@ +123456789 \ No newline at end of file diff --git a/apps/api/test/fixtures/pets/names.txt b/apps/api/test/fixtures/pets/names.txt new file mode 100644 index 0000000..91407a3 --- /dev/null +++ b/apps/api/test/fixtures/pets/names.txt @@ -0,0 +1 @@ +tobi,loki \ No newline at end of file diff --git a/apps/api/test/fixtures/snow ☃/.gitkeep b/apps/api/test/fixtures/snow ☃/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/test/fixtures/todo.html b/apps/api/test/fixtures/todo.html new file mode 100644 index 0000000..e7af6d7 --- /dev/null +++ b/apps/api/test/fixtures/todo.html @@ -0,0 +1 @@ +
    • groceries
    • \ No newline at end of file diff --git a/apps/api/test/fixtures/todo.txt b/apps/api/test/fixtures/todo.txt new file mode 100644 index 0000000..8c3539d --- /dev/null +++ b/apps/api/test/fixtures/todo.txt @@ -0,0 +1 @@ +- groceries \ No newline at end of file diff --git a/apps/api/test/fixtures/user.html b/apps/api/test/fixtures/user.html new file mode 100644 index 0000000..f5b9962 --- /dev/null +++ b/apps/api/test/fixtures/user.html @@ -0,0 +1 @@ +

      {{user.name}}

      \ No newline at end of file diff --git a/apps/api/test/fixtures/user.tmpl b/apps/api/test/fixtures/user.tmpl new file mode 100644 index 0000000..67ef510 --- /dev/null +++ b/apps/api/test/fixtures/user.tmpl @@ -0,0 +1 @@ +

      $user.name

      \ No newline at end of file diff --git a/apps/api/test/fixtures/users/index.html b/apps/api/test/fixtures/users/index.html new file mode 100644 index 0000000..00a2db4 --- /dev/null +++ b/apps/api/test/fixtures/users/index.html @@ -0,0 +1 @@ +

      tobi, loki, jane

      \ No newline at end of file diff --git a/apps/api/test/fixtures/users/tobi.txt b/apps/api/test/fixtures/users/tobi.txt new file mode 100644 index 0000000..9d9529d --- /dev/null +++ b/apps/api/test/fixtures/users/tobi.txt @@ -0,0 +1 @@ +ferret \ No newline at end of file diff --git a/apps/api/test/middleware.basic.js b/apps/api/test/middleware.basic.js new file mode 100644 index 0000000..1f1ed17 --- /dev/null +++ b/apps/api/test/middleware.basic.js @@ -0,0 +1,42 @@ +'use strict' + +var assert = require('node:assert') +var express = require('../'); +var request = require('supertest'); + +describe('middleware', function(){ + describe('.next()', function(){ + it('should behave like connect', function(done){ + var app = express() + , calls = []; + + app.use(function(req, res, next){ + calls.push('one'); + next(); + }); + + app.use(function(req, res, next){ + calls.push('two'); + next(); + }); + + app.use(function(req, res){ + var buf = ''; + res.setHeader('Content-Type', 'application/json'); + req.setEncoding('utf8'); + req.on('data', function(chunk){ buf += chunk }); + req.on('end', function(){ + res.end(buf); + }); + }); + + request(app) + .get('/') + .set('Content-Type', 'application/json') + .send('{"foo":"bar"}') + .expect('Content-Type', 'application/json') + .expect(function () { assert.deepEqual(calls, ['one', 'two']) }) + .expect(200, '{"foo":"bar"}', done) + }) + }) +}) diff --git a/apps/api/test/regression.js b/apps/api/test/regression.js new file mode 100644 index 0000000..4e99b30 --- /dev/null +++ b/apps/api/test/regression.js @@ -0,0 +1,20 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('throw after .end()', function(){ + it('should fail gracefully', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.end('yay'); + throw new Error('boom'); + }); + + request(app) + .get('/') + .expect('yay') + .expect(200, done); + }) +}) diff --git a/apps/api/test/req.accepts.js b/apps/api/test/req.accepts.js new file mode 100644 index 0000000..2066fb5 --- /dev/null +++ b/apps/api/test/req.accepts.js @@ -0,0 +1,125 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.accepts(type)', function(){ + it('should return true when Accept is not present', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .expect('yes', done); + }) + + it('should return true when present', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept', 'application/json') + .expect('yes', done); + }) + + it('should return false otherwise', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept', 'text/html') + .expect('no', done); + }) + }) + + it('should accept an argument list of type names', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json', 'html')); + }); + + request(app) + .get('/') + .set('Accept', 'application/json') + .expect('json', done); + }) + + describe('.accepts(types)', function(){ + it('should return the first when Accept is not present', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['json', 'html'])); + }); + + request(app) + .get('/') + .expect('json', done); + }) + + it('should return the first acceptable type', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['json', 'html'])); + }); + + request(app) + .get('/') + .set('Accept', 'text/html') + .expect('html', done); + }) + + it('should return false when no match is made', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['text/html', 'application/json']) ? 'yup' : 'nope'); + }); + + request(app) + .get('/') + .set('Accept', 'foo/bar, bar/baz') + .expect('nope', done); + }) + + it('should take quality into account', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['text/html', 'application/json'])); + }); + + request(app) + .get('/') + .set('Accept', '*/html; q=.5, application/json') + .expect('application/json', done); + }) + + it('should return the first acceptable type with canonical mime types', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['application/json', 'text/html'])); + }); + + request(app) + .get('/') + .set('Accept', '*/html') + .expect('text/html', done); + }) + }) +}) diff --git a/apps/api/test/req.acceptsCharsets.js b/apps/api/test/req.acceptsCharsets.js new file mode 100644 index 0000000..360a987 --- /dev/null +++ b/apps/api/test/req.acceptsCharsets.js @@ -0,0 +1,50 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsCharsets(type)', function(){ + describe('when Accept-Charset is not present', function(){ + it('should return true', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharsets('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .expect('yes', done); + }) + }) + + describe('when Accept-Charset is present', function () { + it('should return true', function (done) { + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharsets('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept-Charset', 'foo, bar, utf-8') + .expect('yes', done); + }) + + it('should return false otherwise', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharsets('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept-Charset', 'foo, bar') + .expect('no', done); + }) + }) + }) +}) diff --git a/apps/api/test/req.acceptsEncodings.js b/apps/api/test/req.acceptsEncodings.js new file mode 100644 index 0000000..9f8973c --- /dev/null +++ b/apps/api/test/req.acceptsEncodings.js @@ -0,0 +1,39 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsEncodings', function () { + it('should return encoding if accepted', function (done) { + var app = express(); + + app.get('/', function (req, res) { + res.send({ + gzip: req.acceptsEncodings('gzip'), + deflate: req.acceptsEncodings('deflate') + }) + }) + + request(app) + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { gzip: 'gzip', deflate: 'deflate' }, done) + }) + + it('should be false if encoding not accepted', function(done){ + var app = express(); + + app.get('/', function (req, res) { + res.send({ + bogus: req.acceptsEncodings('bogus') + }) + }) + + request(app) + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { bogus: false }, done) + }) + }) +}) diff --git a/apps/api/test/req.acceptsLanguages.js b/apps/api/test/req.acceptsLanguages.js new file mode 100644 index 0000000..e5629fb --- /dev/null +++ b/apps/api/test/req.acceptsLanguages.js @@ -0,0 +1,57 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsLanguages', function(){ + it('should return language if accepted', function (done) { + var app = express(); + + app.get('/', function (req, res) { + res.send({ + 'en-us': req.acceptsLanguages('en-us'), + en: req.acceptsLanguages('en') + }) + }) + + request(app) + .get('/') + .set('Accept-Language', 'en;q=.5, en-us') + .expect(200, { 'en-us': 'en-us', en: 'en' }, done) + }) + + it('should be false if language not accepted', function(done){ + var app = express(); + + app.get('/', function (req, res) { + res.send({ + es: req.acceptsLanguages('es') + }) + }) + + request(app) + .get('/') + .set('Accept-Language', 'en;q=.5, en-us') + .expect(200, { es: false }, done) + }) + + describe('when Accept-Language is not present', function(){ + it('should always return language', function (done) { + var app = express(); + + app.get('/', function (req, res) { + res.send({ + en: req.acceptsLanguages('en'), + es: req.acceptsLanguages('es'), + jp: req.acceptsLanguages('jp') + }) + }) + + request(app) + .get('/') + .expect(200, { en: 'en', es: 'es', jp: 'jp' }, done) + }) + }) + }) +}) diff --git a/apps/api/test/req.baseUrl.js b/apps/api/test/req.baseUrl.js new file mode 100644 index 0000000..b70803e --- /dev/null +++ b/apps/api/test/req.baseUrl.js @@ -0,0 +1,88 @@ +'use strict' + +var express = require('..') +var request = require('supertest') + +describe('req', function(){ + describe('.baseUrl', function(){ + it('should be empty for top-level route', function(done){ + var app = express() + + app.get('/:a', function(req, res){ + res.end(req.baseUrl) + }) + + request(app) + .get('/foo') + .expect(200, '', done) + }) + + it('should contain lower path', function(done){ + var app = express() + var sub = express.Router() + + sub.get('/:b', function(req, res){ + res.end(req.baseUrl) + }) + app.use('/:a', sub) + + request(app) + .get('/foo/bar') + .expect(200, '/foo', done); + }) + + it('should contain full lower path', function(done){ + var app = express() + var sub1 = express.Router() + var sub2 = express.Router() + var sub3 = express.Router() + + sub3.get('/:d', function(req, res){ + res.end(req.baseUrl) + }) + sub2.use('/:c', sub3) + sub1.use('/:b', sub2) + app.use('/:a', sub1) + + request(app) + .get('/foo/bar/baz/zed') + .expect(200, '/foo/bar/baz', done); + }) + + it('should travel through routers correctly', function(done){ + var urls = [] + var app = express() + var sub1 = express.Router() + var sub2 = express.Router() + var sub3 = express.Router() + + sub3.get('/:d', function(req, res, next){ + urls.push('0@' + req.baseUrl) + next() + }) + sub2.use('/:c', sub3) + sub1.use('/', function(req, res, next){ + urls.push('1@' + req.baseUrl) + next() + }) + sub1.use('/bar', sub2) + sub1.use('/bar', function(req, res, next){ + urls.push('2@' + req.baseUrl) + next() + }) + app.use(function(req, res, next){ + urls.push('3@' + req.baseUrl) + next() + }) + app.use('/:a', sub1) + app.use(function(req, res, next){ + urls.push('4@' + req.baseUrl) + res.end(urls.join(',')) + }) + + request(app) + .get('/foo/bar/baz/zed') + .expect(200, '3@,1@/foo,0@/foo/bar/baz,2@/foo/bar,4@', done); + }) + }) +}) diff --git a/apps/api/test/req.fresh.js b/apps/api/test/req.fresh.js new file mode 100644 index 0000000..3bf6a1f --- /dev/null +++ b/apps/api/test/req.fresh.js @@ -0,0 +1,70 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.fresh', function(){ + it('should return true when the resource is not modified', function(done){ + var app = express(); + var etag = '"12345"'; + + app.use(function(req, res){ + res.set('ETag', etag); + res.send(req.fresh); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should return false when the resource is modified', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('ETag', '"123"'); + res.send(req.fresh); + }); + + request(app) + .get('/') + .set('If-None-Match', '"12345"') + .expect(200, 'false', done); + }) + + it('should return false without response headers', function(done){ + var app = express(); + + app.disable('x-powered-by') + app.use(function(req, res){ + res.send(req.fresh); + }); + + request(app) + .get('/') + .expect(200, 'false', done); + }) + + it('should ignore "If-Modified-Since" when "If-None-Match" is present', function(done) { + var app = express(); + const etag = '"FooBar"' + const now = Date.now() + + app.disable('x-powered-by') + app.use(function(req, res) { + res.set('Etag', etag) + res.set('Last-Modified', new Date(now).toUTCString()) + res.send(req.fresh); + }); + + request(app) + .get('/') + .set('If-Modified-Since', new Date(now - 1000).toUTCString) + .set('If-None-Match', etag) + .expect(304, done); + }) + + }) +}) diff --git a/apps/api/test/req.get.js b/apps/api/test/req.get.js new file mode 100644 index 0000000..e73d109 --- /dev/null +++ b/apps/api/test/req.get.js @@ -0,0 +1,60 @@ +'use strict' + +var express = require('../') + , request = require('supertest') + , assert = require('node:assert'); + +describe('req', function(){ + describe('.get(field)', function(){ + it('should return the header field value', function(done){ + var app = express(); + + app.use(function(req, res){ + assert(req.get('Something-Else') === undefined); + res.end(req.get('Content-Type')); + }); + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .expect('application/json', done); + }) + + it('should special-case Referer', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.get('Referer')); + }); + + request(app) + .post('/') + .set('Referrer', 'http://foobar.com') + .expect('http://foobar.com', done); + }) + + it('should throw missing header name', function (done) { + var app = express() + + app.use(function (req, res) { + res.end(req.get()) + }) + + request(app) + .get('/') + .expect(500, /TypeError: name argument is required to req.get/, done) + }) + + it('should throw for non-string header name', function (done) { + var app = express() + + app.use(function (req, res) { + res.end(req.get(42)) + }) + + request(app) + .get('/') + .expect(500, /TypeError: name must be a string to req.get/, done) + }) + }) +}) diff --git a/apps/api/test/req.host.js b/apps/api/test/req.host.js new file mode 100644 index 0000000..cdda82e --- /dev/null +++ b/apps/api/test/req.host.js @@ -0,0 +1,156 @@ +'use strict' + +var express = require('../') + , request = require('supertest') + +describe('req', function(){ + describe('.host', function(){ + it('should return the Host when present', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', 'example.com') + .expect('example.com', done); + }) + + it('should strip port number', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', 'example.com:3000') + .expect(200, 'example.com:3000', done); + }) + + it('should return undefined otherwise', function(done){ + var app = express(); + + app.use(function(req, res){ + req.headers.host = null; + res.end(String(req.host)); + }); + + request(app) + .post('/') + .expect('undefined', done); + }) + + it('should work with IPv6 Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', '[::1]') + .expect('[::1]', done); + }) + + it('should work with IPv6 Host and port', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', '[::1]:3000') + .expect(200, '[::1]:3000', done); + }) + + describe('when "trust proxy" is enabled', function(){ + it('should respect X-Forwarded-Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('example.com', done); + }) + + it('should ignore X-Forwarded-Host if socket addr not trusted', function(done){ + var app = express(); + + app.set('trust proxy', '10.0.0.1'); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('localhost', done); + }) + + it('should default to Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'example.com') + .expect('example.com', done); + }) + + describe('when trusting hop count', function () { + it('should respect X-Forwarded-Host', function (done) { + var app = express(); + + app.set('trust proxy', 1); + + app.use(function (req, res) { + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('example.com', done); + }) + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should ignore X-Forwarded-Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'evil') + .expect('localhost', done); + }) + }) + }) +}) diff --git a/apps/api/test/req.hostname.js b/apps/api/test/req.hostname.js new file mode 100644 index 0000000..b3716b5 --- /dev/null +++ b/apps/api/test/req.hostname.js @@ -0,0 +1,188 @@ +'use strict' + +var express = require('../') + , request = require('supertest') + +describe('req', function(){ + describe('.hostname', function(){ + it('should return the Host when present', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', 'example.com') + .expect('example.com', done); + }) + + it('should strip port number', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', 'example.com:3000') + .expect('example.com', done); + }) + + it('should return undefined otherwise', function(done){ + var app = express(); + + app.use(function(req, res){ + req.headers.host = null; + res.end(String(req.hostname)); + }); + + request(app) + .post('/') + .expect('undefined', done); + }) + + it('should work with IPv6 Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', '[::1]') + .expect('[::1]', done); + }) + + it('should work with IPv6 Host and port', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', '[::1]:3000') + .expect('[::1]', done); + }) + + describe('when "trust proxy" is enabled', function(){ + it('should respect X-Forwarded-Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com:3000') + .expect('example.com', done); + }) + + it('should ignore X-Forwarded-Host if socket addr not trusted', function(done){ + var app = express(); + + app.set('trust proxy', '10.0.0.1'); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('localhost', done); + }) + + it('should default to Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .set('Host', 'example.com') + .expect('example.com', done); + }) + + describe('when multiple X-Forwarded-Host', function () { + it('should use the first value', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com, foobar.com') + .expect(200, 'example.com', done) + }) + + it('should remove OWS around comma', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com , foobar.com') + .expect(200, 'example.com', done) + }) + + it('should strip port number', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com:8080 , foobar.com:8888') + .expect(200, 'example.com', done) + }) + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should ignore X-Forwarded-Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'evil') + .expect('localhost', done); + }) + }) + }) +}) diff --git a/apps/api/test/req.ip.js b/apps/api/test/req.ip.js new file mode 100644 index 0000000..6bb3c5a --- /dev/null +++ b/apps/api/test/req.ip.js @@ -0,0 +1,113 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.ip', function(){ + describe('when X-Forwarded-For is present', function(){ + describe('when "trust proxy" is enabled', function(){ + it('should return the client addr', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('client', done); + }) + + it('should return the addr after trusted proxy based on count', function (done) { + var app = express(); + + app.set('trust proxy', 2); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('p1', done); + }) + + it('should return the addr after trusted proxy based on list', function (done) { + var app = express() + + app.set('trust proxy', '10.0.0.1, 10.0.0.2, 127.0.0.1, ::1') + + app.get('/', function (req, res) { + res.send(req.ip) + }) + + request(app) + .get('/') + .set('X-Forwarded-For', '10.0.0.2, 10.0.0.3, 10.0.0.1', '10.0.0.4') + .expect('10.0.0.3', done) + }) + + it('should return the addr after trusted proxy, from sub app', function (done) { + var app = express(); + var sub = express(); + + app.set('trust proxy', 2); + app.use(sub); + + sub.use(function (req, res, next) { + res.send(req.ip); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect(200, 'p1', done); + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should return the remote address', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + var test = request(app).get('/') + test.set('X-Forwarded-For', 'client, p1, p2') + test.expect(200, getExpectedClientAddress(test._server), done); + }) + }) + }) + + describe('when X-Forwarded-For is not present', function(){ + it('should return the remote address', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + var test = request(app).get('/') + test.expect(200, getExpectedClientAddress(test._server), done) + }) + }) + }) +}) + +/** + * Get the local client address depending on AF_NET of server + */ + +function getExpectedClientAddress(server) { + return server.address().address === '::' + ? '::ffff:127.0.0.1' + : '127.0.0.1'; +} diff --git a/apps/api/test/req.ips.js b/apps/api/test/req.ips.js new file mode 100644 index 0000000..2f9a073 --- /dev/null +++ b/apps/api/test/req.ips.js @@ -0,0 +1,71 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.ips', function(){ + describe('when X-Forwarded-For is present', function(){ + describe('when "trust proxy" is enabled', function(){ + it('should return an array of the specified addresses', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('["client","p1","p2"]', done); + }) + + it('should stop at first untrusted', function(done){ + var app = express(); + + app.set('trust proxy', 2); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('["p1","p2"]', done); + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should return an empty array', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('[]', done); + }) + }) + }) + + describe('when X-Forwarded-For is not present', function(){ + it('should return []', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .expect('[]', done); + }) + }) + }) +}) diff --git a/apps/api/test/req.is.js b/apps/api/test/req.is.js new file mode 100644 index 0000000..c5904dd --- /dev/null +++ b/apps/api/test/req.is.js @@ -0,0 +1,169 @@ +'use strict' + +var express = require('..') +var request = require('supertest') + +describe('req.is()', function () { + describe('when given a mime type', function () { + it('should return the type when matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/json')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"application/json"', done) + }) + + it('should return false when not matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('image/jpeg')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, 'false', done) + }) + + it('should ignore charset', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/json')) + }) + + request(app) + .post('/') + .type('application/json; charset=UTF-8') + .send('{}') + .expect(200, '"application/json"', done) + }) + }) + + describe('when content-type is not present', function(){ + it('should return false', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/json')) + }) + + request(app) + .post('/') + .send('{}') + .expect(200, 'false', done) + }) + }) + + describe('when given an extension', function(){ + it('should lookup the mime type', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('json')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"json"', done) + }) + }) + + describe('when given */subtype', function(){ + it('should return the full type when matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('*/json')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"application/json"', done) + }) + + it('should return false when not matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('*/html')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, 'false', done) + }) + + it('should ignore charset', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('*/json')) + }) + + request(app) + .post('/') + .type('application/json; charset=UTF-8') + .send('{}') + .expect(200, '"application/json"', done) + }) + }) + + describe('when given type/*', function(){ + it('should return the full type when matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/*')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"application/json"', done) + }) + + it('should return false when not matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('text/*')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, 'false', done) + }) + + it('should ignore charset', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/*')) + }) + + request(app) + .post('/') + .type('application/json; charset=UTF-8') + .send('{}') + .expect(200, '"application/json"', done) + }) + }) +}) diff --git a/apps/api/test/req.path.js b/apps/api/test/req.path.js new file mode 100644 index 0000000..3ff6177 --- /dev/null +++ b/apps/api/test/req.path.js @@ -0,0 +1,20 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.path', function(){ + it('should return the parsed pathname', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.path); + }); + + request(app) + .get('/login?redirect=/post/1/comments') + .expect('/login', done); + }) + }) +}) diff --git a/apps/api/test/req.protocol.js b/apps/api/test/req.protocol.js new file mode 100644 index 0000000..def82ed --- /dev/null +++ b/apps/api/test/req.protocol.js @@ -0,0 +1,113 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.protocol', function(){ + it('should return the protocol string', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .expect('http', done); + }) + + describe('when "trust proxy" is enabled', function(){ + it('should respect X-Forwarded-Proto', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('https', done); + }) + + it('should default to the socket addr if X-Forwarded-Proto not present', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + req.socket.encrypted = true; + res.end(req.protocol); + }); + + request(app) + .get('/') + .expect('https', done); + }) + + it('should ignore X-Forwarded-Proto if socket addr not trusted', function(done){ + var app = express(); + + app.set('trust proxy', '10.0.0.1'); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('http', done); + }) + + it('should default to http', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .expect('http', done); + }) + + describe('when trusting hop count', function () { + it('should respect X-Forwarded-Proto', function (done) { + var app = express(); + + app.set('trust proxy', 1); + + app.use(function (req, res) { + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('https', done); + }) + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should ignore X-Forwarded-Proto', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('http', done); + }) + }) + }) +}) diff --git a/apps/api/test/req.query.js b/apps/api/test/req.query.js new file mode 100644 index 0000000..c0d3c83 --- /dev/null +++ b/apps/api/test/req.query.js @@ -0,0 +1,106 @@ +'use strict' + +var assert = require('node:assert') +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.query', function(){ + it('should default to {}', function(done){ + var app = createApp(); + + request(app) + .get('/') + .expect(200, '{}', done); + }); + + it('should default to parse simple keys', function (done) { + var app = createApp(); + + request(app) + .get('/?user[name]=tj') + .expect(200, '{"user[name]":"tj"}', done); + }); + + describe('when "query parser" is extended', function () { + it('should parse complex keys', function (done) { + var app = createApp('extended'); + + request(app) + .get('/?foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!') + .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done); + }); + + it('should parse parameters with dots', function (done) { + var app = createApp('extended'); + + request(app) + .get('/?user.name=tj') + .expect(200, '{"user.name":"tj"}', done); + }); + }); + + describe('when "query parser" is simple', function () { + it('should not parse complex keys', function (done) { + var app = createApp('simple'); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{"user[name]":"tj"}', done); + }); + }); + + describe('when "query parser" is a function', function () { + it('should parse using function', function (done) { + var app = createApp(function (str) { + return {'length': (str || '').length}; + }); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{"length":17}', done); + }); + }); + + describe('when "query parser" disabled', function () { + it('should not parse query', function (done) { + var app = createApp(false); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{}', done); + }); + }); + + describe('when "query parser" enabled', function () { + it('should not parse complex keys', function (done) { + var app = createApp(true); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{"user[name]":"tj"}', done); + }); + }); + + describe('when "query parser" an unknown value', function () { + it('should throw', function () { + assert.throws(createApp.bind(null, 'bogus'), + /unknown value.*query parser/) + }); + }); + }) +}) + +function createApp(setting) { + var app = express(); + + if (setting !== undefined) { + app.set('query parser', setting); + } + + app.use(function (req, res) { + res.send(req.query); + }); + + return app; +} diff --git a/apps/api/test/req.range.js b/apps/api/test/req.range.js new file mode 100644 index 0000000..1114417 --- /dev/null +++ b/apps/api/test/req.range.js @@ -0,0 +1,104 @@ +'use strict' + +var express = require('..'); +var request = require('supertest') + +describe('req', function(){ + describe('.range(size)', function(){ + it('should return parsed ranges', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120)) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-50,51-100') + .expect(200, '[{"start":0,"end":50},{"start":51,"end":100}]', done) + }) + + it('should cap to the given size', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(75)) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-100') + .expect(200, '[{"start":0,"end":74}]', done) + }) + + it('should cap to the given size when open-ended', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(75)) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-') + .expect(200, '[{"start":0,"end":74}]', done) + }) + + it('should have a .type', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120).type) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-100') + .expect(200, '"bytes"', done) + }) + + it('should accept any type', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120).type) + }) + + request(app) + .get('/') + .set('Range', 'users=0-2') + .expect(200, '"users"', done) + }) + + it('should return undefined if no range', function (done) { + var app = express() + + app.use(function (req, res) { + res.send(String(req.range(120))) + }) + + request(app) + .get('/') + .expect(200, 'undefined', done) + }) + }) + + describe('.range(size, options)', function(){ + describe('with "combine: true" option', function(){ + it('should return combined ranges', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120, { + combine: true + })) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-50,51-100') + .expect(200, '[{"start":0,"end":100}]', done) + }) + }) + }) +}) diff --git a/apps/api/test/req.route.js b/apps/api/test/req.route.js new file mode 100644 index 0000000..9bd7ed9 --- /dev/null +++ b/apps/api/test/req.route.js @@ -0,0 +1,28 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.route', function(){ + it('should be the executed Route', function(done){ + var app = express(); + + app.get('/user/:id{/:op}', function(req, res, next){ + res.header('path-1', req.route.path) + next(); + }); + + app.get('/user/:id/edit', function(req, res){ + res.header('path-2', req.route.path) + res.end(); + }); + + request(app) + .get('/user/12/edit') + .expect('path-1', '/user/:id{/:op}') + .expect('path-2', '/user/:id/edit') + .expect(200, done) + }) + }) +}) diff --git a/apps/api/test/req.secure.js b/apps/api/test/req.secure.js new file mode 100644 index 0000000..0097ed6 --- /dev/null +++ b/apps/api/test/req.secure.js @@ -0,0 +1,101 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.secure', function(){ + describe('when X-Forwarded-Proto is missing', function(){ + it('should return false when http', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .expect('no', done) + }) + }) + }) + + describe('.secure', function(){ + describe('when X-Forwarded-Proto is present', function(){ + it('should return false when http', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('no', done) + }) + + it('should return true when "trust proxy" is enabled', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('yes', done) + }) + + it('should return false when initial proxy is http', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'http, https') + .expect('no', done) + }) + + it('should return true when initial proxy is https', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https, http') + .expect('yes', done) + }) + + describe('when "trust proxy" trusting hop count', function () { + it('should respect X-Forwarded-Proto', function (done) { + var app = express(); + + app.set('trust proxy', 1); + + app.get('/', function (req, res) { + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('yes', done) + }) + }) + }) + }) +}) diff --git a/apps/api/test/req.signedCookies.js b/apps/api/test/req.signedCookies.js new file mode 100644 index 0000000..db56195 --- /dev/null +++ b/apps/api/test/req.signedCookies.js @@ -0,0 +1,37 @@ +'use strict' + +var express = require('../') + , request = require('supertest') + , cookieParser = require('cookie-parser') + +describe('req', function(){ + describe('.signedCookies', function(){ + it('should return a signed JSON cookie', function(done){ + var app = express(); + + app.use(cookieParser('secret')); + + app.use(function(req, res){ + if (req.path === '/set') { + res.cookie('obj', { foo: 'bar' }, { signed: true }); + res.end(); + } else { + res.send(req.signedCookies); + } + }); + + request(app) + .get('/set') + .end(function(err, res){ + if (err) return done(err); + var cookie = res.header['set-cookie']; + + request(app) + .get('/') + .set('Cookie', cookie) + .expect(200, { obj: { foo: 'bar' } }, done) + }); + }) + }) +}) + diff --git a/apps/api/test/req.stale.js b/apps/api/test/req.stale.js new file mode 100644 index 0000000..cda77fa --- /dev/null +++ b/apps/api/test/req.stale.js @@ -0,0 +1,50 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.stale', function(){ + it('should return false when the resource is not modified', function(done){ + var app = express(); + var etag = '"12345"'; + + app.use(function(req, res){ + res.set('ETag', etag); + res.send(req.stale); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should return true when the resource is modified', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('ETag', '"123"'); + res.send(req.stale); + }); + + request(app) + .get('/') + .set('If-None-Match', '"12345"') + .expect(200, 'true', done); + }) + + it('should return true without response headers', function(done){ + var app = express(); + + app.disable('x-powered-by') + app.use(function(req, res){ + res.send(req.stale); + }); + + request(app) + .get('/') + .expect(200, 'true', done); + }) + }) +}) diff --git a/apps/api/test/req.subdomains.js b/apps/api/test/req.subdomains.js new file mode 100644 index 0000000..e5600f2 --- /dev/null +++ b/apps/api/test/req.subdomains.js @@ -0,0 +1,173 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.subdomains', function(){ + describe('when present', function(){ + it('should return an array', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'tobi.ferrets.example.com') + .expect(200, ['ferrets', 'tobi'], done); + }) + + it('should work with IPv4 address', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '127.0.0.1') + .expect(200, [], done); + }) + + it('should work with IPv6 address', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '[::1]') + .expect(200, [], done); + }) + }) + + describe('otherwise', function(){ + it('should return an empty array', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'example.com') + .expect(200, [], done); + }) + }) + + describe('with no host', function(){ + it('should return an empty array', function(done){ + var app = express(); + + app.use(function(req, res){ + req.headers.host = null; + res.send(req.subdomains); + }); + + request(app) + .get('/') + .expect(200, [], done); + }) + }) + + describe('with trusted X-Forwarded-Host', function () { + it('should return an array', function (done) { + var app = express(); + + app.set('trust proxy', true); + app.use(function (req, res) { + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('X-Forwarded-Host', 'tobi.ferrets.example.com') + .expect(200, ['ferrets', 'tobi'], done); + }) + }) + + describe('when subdomain offset is set', function(){ + describe('when subdomain offset is zero', function(){ + it('should return an array with the whole domain', function(done){ + var app = express(); + app.set('subdomain offset', 0); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'tobi.ferrets.sub.example.com') + .expect(200, ['com', 'example', 'sub', 'ferrets', 'tobi'], done); + }) + + it('should return an array with the whole IPv4', function (done) { + var app = express(); + app.set('subdomain offset', 0); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '127.0.0.1') + .expect(200, ['127.0.0.1'], done); + }) + + it('should return an array with the whole IPv6', function (done) { + var app = express(); + app.set('subdomain offset', 0); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '[::1]') + .expect(200, ['[::1]'], done); + }) + }) + + describe('when present', function(){ + it('should return an array', function(done){ + var app = express(); + app.set('subdomain offset', 3); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'tobi.ferrets.sub.example.com') + .expect(200, ['ferrets', 'tobi'], done); + }) + }) + + describe('otherwise', function(){ + it('should return an empty array', function(done){ + var app = express(); + app.set('subdomain offset', 3); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'sub.example.com') + .expect(200, [], done); + }) + }) + }) + }) +}) diff --git a/apps/api/test/req.xhr.js b/apps/api/test/req.xhr.js new file mode 100644 index 0000000..99cf7f1 --- /dev/null +++ b/apps/api/test/req.xhr.js @@ -0,0 +1,42 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('req', function(){ + describe('.xhr', function(){ + before(function () { + this.app = express() + this.app.get('/', function (req, res) { + res.send(req.xhr) + }) + }) + + it('should return true when X-Requested-With is xmlhttprequest', function(done){ + request(this.app) + .get('/') + .set('X-Requested-With', 'xmlhttprequest') + .expect(200, 'true', done) + }) + + it('should case-insensitive', function(done){ + request(this.app) + .get('/') + .set('X-Requested-With', 'XMLHttpRequest') + .expect(200, 'true', done) + }) + + it('should return false otherwise', function(done){ + request(this.app) + .get('/') + .set('X-Requested-With', 'blahblah') + .expect(200, 'false', done) + }) + + it('should return false when not present', function(done){ + request(this.app) + .get('/') + .expect(200, 'false', done) + }) + }) +}) diff --git a/apps/api/test/res.append.js b/apps/api/test/res.append.js new file mode 100644 index 0000000..2dd17a3 --- /dev/null +++ b/apps/api/test/res.append.js @@ -0,0 +1,116 @@ +'use strict' + +var assert = require('node:assert') +var express = require('..') +var request = require('supertest') + +describe('res', function () { + describe('.append(field, val)', function () { + it('should append multiple headers', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.append('Set-Cookie', 'foo=bar') + next() + }) + + app.use(function (req, res) { + res.append('Set-Cookie', 'fizz=buzz') + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz'])) + .end(done) + }) + + it('should accept array of values', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.append('Set-Cookie', ['foo=bar', 'fizz=buzz']) + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz'])) + .end(done) + }) + + it('should get reset by res.set(field, val)', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.append('Set-Cookie', 'foo=bar') + res.append('Set-Cookie', 'fizz=buzz') + next() + }) + + app.use(function (req, res) { + res.set('Set-Cookie', 'pet=tobi') + res.end() + }); + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['pet=tobi'])) + .end(done) + }) + + it('should work with res.set(field, val) first', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.set('Set-Cookie', 'foo=bar') + next() + }) + + app.use(function(req, res){ + res.append('Set-Cookie', 'fizz=buzz') + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz'])) + .end(done) + }) + + it('should work together with res.cookie', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.cookie('foo', 'bar') + next() + }) + + app.use(function (req, res) { + res.append('Set-Cookie', 'fizz=buzz') + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar; Path=/', 'fizz=buzz'])) + .end(done) + }) + }) +}) + +function shouldHaveHeaderValues (key, values) { + return function (res) { + var headers = res.headers[key.toLowerCase()] + assert.ok(headers, 'should have header "' + key + '"') + assert.strictEqual(headers.length, values.length, 'should have ' + values.length + ' occurrences of "' + key + '"') + for (var i = 0; i < values.length; i++) { + assert.strictEqual(headers[i], values[i]) + } + } +} diff --git a/apps/api/test/res.attachment.js b/apps/api/test/res.attachment.js new file mode 100644 index 0000000..8644bab --- /dev/null +++ b/apps/api/test/res.attachment.js @@ -0,0 +1,79 @@ +'use strict' + +const { Buffer } = require('node:buffer'); + +var express = require('../') + , request = require('supertest'); + +describe('res', function(){ + describe('.attachment()', function(){ + it('should Content-Disposition to attachment', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment().send('foo'); + }); + + request(app) + .get('/') + .expect('Content-Disposition', 'attachment', done); + }) + }) + + describe('.attachment(filename)', function(){ + it('should add the filename param', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/path/to/image.png'); + res.send('foo'); + }); + + request(app) + .get('/') + .expect('Content-Disposition', 'attachment; filename="image.png"', done); + }) + + it('should set the Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/path/to/image.png'); + res.send(Buffer.alloc(4, '.')) + }); + + request(app) + .get('/') + .expect('Content-Type', 'image/png', done); + }) + }) + + describe('.attachment(utf8filename)', function(){ + it('should add the filename and filename* params', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/locales/日本語.txt'); + res.send('japanese'); + }); + + request(app) + .get('/') + .expect('Content-Disposition', 'attachment; filename="???.txt"; filename*=UTF-8\'\'%E6%97%A5%E6%9C%AC%E8%AA%9E.txt') + .expect(200, done); + }) + + it('should set the Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/locales/日本語.txt'); + res.send('japanese'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8', done); + }) + }) +}) diff --git a/apps/api/test/res.clearCookie.js b/apps/api/test/res.clearCookie.js new file mode 100644 index 0000000..74a746e --- /dev/null +++ b/apps/api/test/res.clearCookie.js @@ -0,0 +1,62 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('res', function(){ + describe('.clearCookie(name)', function(){ + it('should set a cookie passed expiry', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid').end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) + }) + + describe('.clearCookie(name, options)', function(){ + it('should set the given params', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { path: '/admin' }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) + + it('should ignore maxAge', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { path: '/admin', maxAge: 1000 }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) + + it('should ignore user supplied expires param', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { path: '/admin', expires: new Date() }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) + }) +}) diff --git a/apps/api/test/res.cookie.js b/apps/api/test/res.cookie.js new file mode 100644 index 0000000..180d1be --- /dev/null +++ b/apps/api/test/res.cookie.js @@ -0,0 +1,295 @@ +'use strict' + +var express = require('../') + , request = require('supertest') + , cookieParser = require('cookie-parser') + +describe('res', function(){ + describe('.cookie(name, object)', function(){ + it('should generate a JSON cookie', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('user', { name: 'tobi' }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'user=j%3A%7B%22name%22%3A%22tobi%22%7D; Path=/') + .expect(200, done) + }) + }) + + describe('.cookie(name, string)', function(){ + it('should set a cookie', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi').end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/') + .expect(200, done) + }) + + it('should allow multiple calls', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi'); + res.cookie('age', 1); + res.cookie('gender', '?'); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/,age=1; Path=/,gender=%3F; Path=/') + .expect(200, done) + }) + }) + + describe('.cookie(name, string, options)', function(){ + it('should set params', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { httpOnly: true, secure: true }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/; HttpOnly; Secure') + .expect(200, done) + }) + + describe('expires', function () { + it('should throw on invalid date', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { expires: new Date(NaN) }) + res.end() + }) + + request(app) + .get('/') + .expect(500, /option expires is invalid/, done) + }) + }) + + describe('partitioned', function () { + it('should set partitioned', function (done) { + var app = express(); + + app.use(function (req, res) { + res.cookie('name', 'tobi', { partitioned: true }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/; Partitioned') + .expect(200, done) + }) + }) + + describe('maxAge', function(){ + it('should set relative expires', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { maxAge: 1000 }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', /name=tobi; Max-Age=1; Path=\/; Expires=/) + .expect(200, done) + }) + + it('should set max-age', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { maxAge: 1000 }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', /Max-Age=1/, done) + }) + + it('should not mutate the options object', function(done){ + var app = express(); + + var options = { maxAge: 1000 }; + var optionsCopy = { ...options }; + + app.use(function(req, res){ + res.cookie('name', 'tobi', options) + res.json(options) + }); + + request(app) + .get('/') + .expect(200, optionsCopy, done) + }) + + it('should not throw on null', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { maxAge: null }) + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect('Set-Cookie', 'name=tobi; Path=/') + .end(done) + }) + + it('should not throw on undefined', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { maxAge: undefined }) + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect('Set-Cookie', 'name=tobi; Path=/') + .end(done) + }) + + it('should throw an error with invalid maxAge', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { maxAge: 'foobar' }) + res.end() + }) + + request(app) + .get('/') + .expect(500, /option maxAge is invalid/, done) + }) + }) + + describe('priority', function () { + it('should set low priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'low' }) + res.end() + }) + + request(app) + .get('/') + .expect('Set-Cookie', /Priority=Low/) + .expect(200, done) + }) + + it('should set medium priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'medium' }) + res.end() + }) + + request(app) + .get('/') + .expect('Set-Cookie', /Priority=Medium/) + .expect(200, done) + }) + + it('should set high priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'high' }) + res.end() + }) + + request(app) + .get('/') + .expect('Set-Cookie', /Priority=High/) + .expect(200, done) + }) + + it('should throw with invalid priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'foobar' }) + res.end() + }) + + request(app) + .get('/') + .expect(500, /option priority is invalid/, done) + }) + }) + + describe('signed', function(){ + it('should generate a signed JSON cookie', function(done){ + var app = express(); + + app.use(cookieParser('foo bar baz')); + + app.use(function(req, res){ + res.cookie('user', { name: 'tobi' }, { signed: true }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'user=s%3Aj%3A%7B%22name%22%3A%22tobi%22%7D.K20xcwmDS%2BPb1rsD95o5Jm5SqWs1KteqdnynnB7jkTE; Path=/') + .expect(200, done) + }) + }) + + describe('signed without secret', function(){ + it('should throw an error', function(done){ + var app = express(); + + app.use(cookieParser()); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { signed: true }).end(); + }); + + request(app) + .get('/') + .expect(500, /secret\S+ required for signed cookies/, done); + }) + }) + + describe('.signedCookie(name, string)', function(){ + it('should set a signed cookie', function(done){ + var app = express(); + + app.use(cookieParser('foo bar baz')); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { signed: true }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=s%3Atobi.xJjV2iZ6EI7C8E5kzwbfA9PVLl1ZR07UTnuTgQQ4EnQ; Path=/') + .expect(200, done) + }) + }) + }) +}) diff --git a/apps/api/test/res.download.js b/apps/api/test/res.download.js new file mode 100644 index 0000000..e996600 --- /dev/null +++ b/apps/api/test/res.download.js @@ -0,0 +1,487 @@ +'use strict' + +var after = require('after'); +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage +const { Buffer } = require('node:buffer'); + +var express = require('..'); +var path = require('node:path') +var request = require('supertest'); +var utils = require('./support/utils') + +var FIXTURES_PATH = path.join(__dirname, 'fixtures') + +describe('res', function(){ + describe('.download(path)', function(){ + it('should transfer as an attachment', function(done){ + var app = express(); + + app.use(function(req, res){ + res.download('test/fixtures/user.html'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=utf-8') + .expect('Content-Disposition', 'attachment; filename="user.html"') + .expect(200, '

      {{user.name}}

      ', done) + }) + + it('should accept range requests', function (done) { + var app = express() + + app.get('/', function (req, res) { + res.download('test/fixtures/user.html') + }) + + request(app) + .get('/') + .expect('Accept-Ranges', 'bytes') + .expect(200, '

      {{user.name}}

      ', done) + }) + + it('should respond with requested byte range', function (done) { + var app = express() + + app.get('/', function (req, res) { + res.download('test/fixtures/user.html') + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-2') + .expect('Content-Range', 'bytes 0-2/20') + .expect(206, '

      ', done) + }) + }) + + describe('.download(path, filename)', function(){ + it('should provide an alternate filename', function(done){ + var app = express(); + + app.use(function(req, res){ + res.download('test/fixtures/user.html', 'document'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=utf-8') + .expect('Content-Disposition', 'attachment; filename="document"') + .expect(200, done) + }) + }) + + describe('.download(path, fn)', function(){ + it('should invoke the callback', function(done){ + var app = express(); + var cb = after(2, done); + + app.use(function(req, res){ + res.download('test/fixtures/user.html', cb); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=utf-8') + .expect('Content-Disposition', 'attachment; filename="user.html"') + .expect(200, cb); + }) + + describe('async local storage', function () { + it('should persist store', function (done) { + var app = express() + var cb = after(2, done) + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.download('test/fixtures/name.txt', function (err) { + if (err) return cb(err) + + var local = req.asyncLocalStorage.getStore() + + assert.strictEqual(local.foo, 'bar') + cb() + }) + }) + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Disposition', 'attachment; filename="name.txt"') + .expect(200, 'tobi', cb) + }) + + it('should persist store on error', function (done) { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.download('test/fixtures/does-not-exist', function (err) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.send(err ? 'got ' + err.status + ' error' : 'no error') + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('got 404 error') + .end(done) + }) + }) + }) + + describe('.download(path, options)', function () { + it('should allow options to res.sendFile()', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/.name', { + dotfiles: 'allow', + maxAge: '4h' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename=".name"') + .expect('Cache-Control', 'public, max-age=14400') + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + describe('with "headers" option', function () { + it('should set headers on response', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', { + headers: { + 'X-Foo': 'Bar', + 'X-Bar': 'Foo' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'Bar') + .expect('X-Bar', 'Foo') + .end(done) + }) + + it('should use last header when duplicated', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', { + headers: { + 'X-Foo': 'Bar', + 'x-foo': 'bar' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'bar') + .end(done) + }) + + it('should override Content-Type', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', { + headers: { + 'Content-Type': 'text/x-custom' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .end(done) + }) + + it('should not set headers on 404', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/does-not-exist', { + headers: { + 'X-Foo': 'Bar' + } + }) + }) + + request(app) + .get('/') + .expect(404) + .expect(utils.shouldNotHaveHeader('X-Foo')) + .end(done) + }) + + describe('when headers contains Content-Disposition', function () { + it('should be ignored', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', { + headers: { + 'Content-Disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="user.html"') + .end(done) + }) + + it('should be ignored case-insensitively', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', { + headers: { + 'content-disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="user.html"') + .end(done) + }) + }) + }) + + describe('with "root" option', function () { + it('should allow relative path', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('name.txt', { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="name.txt"') + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + it('should allow up within root', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('fake/../name.txt', { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="name.txt"') + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + it('should reject up outside root', function (done) { + var app = express() + + app.use(function (req, res) { + var p = '..' + path.sep + + path.relative(path.dirname(FIXTURES_PATH), path.join(FIXTURES_PATH, 'name.txt')) + + res.download(p, { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(403) + .expect(utils.shouldNotHaveHeader('Content-Disposition')) + .end(done) + }) + + it('should reject reading outside root', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('../name.txt', { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(403) + .expect(utils.shouldNotHaveHeader('Content-Disposition')) + .end(done) + }) + }) + }) + + describe('.download(path, filename, fn)', function(){ + it('should invoke the callback', function(done){ + var app = express(); + var cb = after(2, done); + + app.use(function(req, res){ + res.download('test/fixtures/user.html', 'document', cb) + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=utf-8') + .expect('Content-Disposition', 'attachment; filename="document"') + .expect(200, cb); + }) + }) + + describe('.download(path, filename, options, fn)', function () { + it('should invoke the callback', function (done) { + var app = express() + var cb = after(2, done) + var options = {} + + app.use(function (req, res) { + res.download('test/fixtures/user.html', 'document', options, cb) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/html; charset=utf-8') + .expect('Content-Disposition', 'attachment; filename="document"') + .end(cb) + }) + + it('should allow options to res.sendFile()', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/.name', 'document', { + dotfiles: 'allow', + maxAge: '4h' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="document"') + .expect('Cache-Control', 'public, max-age=14400') + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + describe('when options.headers contains Content-Disposition', function () { + it('should be ignored', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', 'document', { + headers: { + 'Content-Type': 'text/x-custom', + 'Content-Disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .expect('Content-Disposition', 'attachment; filename="document"') + .end(done) + }) + + it('should be ignored case-insensitively', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', 'document', { + headers: { + 'content-type': 'text/x-custom', + 'content-disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .expect('Content-Disposition', 'attachment; filename="document"') + .end(done) + }) + }) + }) + + describe('on failure', function(){ + it('should invoke the callback', function(done){ + var app = express(); + + app.use(function (req, res, next) { + res.download('test/fixtures/foobar.html', function(err){ + if (!err) return next(new Error('expected error')); + res.send('got ' + err.status + ' ' + err.code); + }); + }); + + request(app) + .get('/') + .expect(200, 'got 404 ENOENT', done); + }) + + it('should remove Content-Disposition', function(done){ + var app = express() + + app.use(function (req, res, next) { + res.download('test/fixtures/foobar.html', function(err){ + if (!err) return next(new Error('expected error')); + res.end('failed'); + }); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Content-Disposition')) + .expect(200, 'failed', done) + }) + }) +}) diff --git a/apps/api/test/res.format.js b/apps/api/test/res.format.js new file mode 100644 index 0000000..0d770d5 --- /dev/null +++ b/apps/api/test/res.format.js @@ -0,0 +1,248 @@ +'use strict' + +var after = require('after') +var express = require('../') + , request = require('supertest') + , assert = require('node:assert'); + +var app1 = express(); + +app1.use(function(req, res, next){ + res.format({ + 'text/plain': function(){ + res.send('hey'); + }, + + 'text/html': function(){ + res.send('

      hey

      '); + }, + + 'application/json': function(a, b, c){ + assert(req === a) + assert(res === b) + assert(next === c) + res.send({ message: 'hey' }); + } + }); +}); + +app1.use(function(err, req, res, next){ + if (!err.types) throw err; + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) +}) + +var app2 = express(); + +app2.use(function(req, res, next){ + res.format({ + text: function(){ res.send('hey') }, + html: function(){ res.send('

      hey

      ') }, + json: function(){ res.send({ message: 'hey' }) } + }); +}); + +app2.use(function(err, req, res, next){ + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) +}) + +var app3 = express(); + +app3.use(function(req, res, next){ + res.format({ + text: function(){ res.send('hey') }, + default: function (a, b, c) { + assert(req === a) + assert(res === b) + assert(next === c) + res.send('default') + } + }) +}); + +var app4 = express(); + +app4.get('/', function (req, res) { + res.format({ + text: function(){ res.send('hey') }, + html: function(){ res.send('

      hey

      ') }, + json: function(){ res.send({ message: 'hey' }) } + }); +}); + +app4.use(function(err, req, res, next){ + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) +}) + +var app5 = express(); + +app5.use(function (req, res, next) { + res.format({ + default: function () { res.send('hey') } + }); +}); + +describe('res', function(){ + describe('.format(obj)', function(){ + describe('with canonicalized mime types', function(){ + test(app1); + }) + + describe('with extnames', function(){ + test(app2); + }) + + describe('with parameters', function(){ + var app = express(); + + app.use(function(req, res, next){ + res.format({ + 'text/plain; charset=utf-8': function(){ res.send('hey') }, + 'text/html; foo=bar; bar=baz': function(){ res.send('

      hey

      ') }, + 'application/json; q=0.5': function(){ res.send({ message: 'hey' }) } + }); + }); + + app.use(function(err, req, res, next){ + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) + }); + + test(app); + }) + + describe('given .default', function(){ + it('should be invoked instead of auto-responding', function(done){ + request(app3) + .get('/') + .set('Accept', 'text/html') + .expect('default', done); + }) + + it('should work when only .default is provided', function (done) { + request(app5) + .get('/') + .set('Accept', '*/*') + .expect('hey', done); + }) + + it('should be able to invoke other formatter', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.format({ + json: function () { res.send('json') }, + default: function () { + res.header('x-default', '1') + this.json() + } + }) + }) + + request(app) + .get('/') + .set('Accept', 'text/plain') + .expect(200) + .expect('x-default', '1') + .expect('json') + .end(done) + }) + }) + + describe('in router', function(){ + test(app4); + }) + + describe('in router', function(){ + var app = express(); + var router = express.Router(); + + router.get('/', function (req, res) { + res.format({ + text: function(){ res.send('hey') }, + html: function(){ res.send('

      hey

      ') }, + json: function(){ res.send({ message: 'hey' }) } + }); + }); + + router.use(function(err, req, res, next){ + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) + }) + + app.use(router) + + test(app) + }) + }) +}) + +function test(app) { + it('should utilize qvalues in negotiation', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, application/json, */*; q=.1') + .expect({"message":"hey"}, done); + }) + + it('should allow wildcard type/subtypes', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, application/*, */*; q=.1') + .expect({"message":"hey"}, done); + }) + + it('should default the Content-Type', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, text/plain') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('hey', done); + }) + + it('should set the correct charset for the Content-Type', function (done) { + var cb = after(3, done) + + request(app) + .get('/') + .set('Accept', 'text/html') + .expect('Content-Type', 'text/html; charset=utf-8', cb) + + request(app) + .get('/') + .set('Accept', 'text/plain') + .expect('Content-Type', 'text/plain; charset=utf-8', cb) + + request(app) + .get('/') + .set('Accept', 'application/json') + .expect('Content-Type', 'application/json; charset=utf-8', cb) + }) + + it('should Vary: Accept', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, text/plain') + .expect('Vary', 'Accept', done); + }) + + describe('when Accept is not present', function(){ + it('should invoke the first callback', function(done){ + request(app) + .get('/') + .expect('hey', done); + }) + }) + + describe('when no match is made', function(){ + it('should respond with 406 not acceptable', function(done){ + request(app) + .get('/') + .set('Accept', 'foo/bar') + .expect('Supports: text/plain, text/html, application/json') + .expect(406, done) + }) + }) +} diff --git a/apps/api/test/res.get.js b/apps/api/test/res.get.js new file mode 100644 index 0000000..a5f12e2 --- /dev/null +++ b/apps/api/test/res.get.js @@ -0,0 +1,21 @@ +'use strict' + +var express = require('..'); +var request = require('supertest'); + +describe('res', function(){ + describe('.get(field)', function(){ + it('should get the response header field', function (done) { + var app = express(); + + app.use(function (req, res) { + res.setHeader('Content-Type', 'text/x-foo'); + res.send(res.get('Content-Type')); + }); + + request(app) + .get('/') + .expect(200, 'text/x-foo', done); + }) + }) +}) diff --git a/apps/api/test/res.json.js b/apps/api/test/res.json.js new file mode 100644 index 0000000..ffd547e --- /dev/null +++ b/apps/api/test/res.json.js @@ -0,0 +1,186 @@ +'use strict' + +var express = require('../') + , request = require('supertest') + , assert = require('node:assert'); + +describe('res', function(){ + describe('.json(object)', function(){ + it('should not support jsonp callbacks', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json({ foo: 'bar' }); + }); + + request(app) + .get('/?callback=foo') + .expect('{"foo":"bar"}', done); + }) + + it('should not override previous Content-Types', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.type('application/vnd.example+json'); + res.json({ hello: 'world' }); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/vnd.example+json; charset=utf-8') + .expect(200, '{"hello":"world"}', done); + }) + + describe('when given primitives', function(){ + it('should respond with json for null', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json(null); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, 'null', done) + }) + + it('should respond with json for Number', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json(300); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '300', done) + }) + + it('should respond with json for String', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json('str'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '"str"', done) + }) + }) + + describe('when given an array', function(){ + it('should respond with json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json(['foo', 'bar', 'baz']); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '["foo","bar","baz"]', done) + }) + }) + + describe('when given an object', function(){ + it('should respond with json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json({ name: 'tobi' }); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '{"name":"tobi"}', done) + }) + }) + + describe('"json escape" setting', function () { + it('should be undefined by default', function () { + var app = express() + assert.strictEqual(app.get('json escape'), undefined) + }) + + it('should unicode escape HTML-sniffing characters', function (done) { + var app = express() + + app.enable('json escape') + + app.use(function (req, res) { + res.json({ '&': ''); + }); + + request(app) + .get('/') + .set('Host', 'http://example.com') + .set('Accept', 'text/plain, */*') + .expect('Content-Type', /plain/) + .expect('Location', 'http://example.com/?param=%3Cscript%3Ealert(%22hax%22);%3C/script%3E') + .expect(302, 'Found. Redirecting to http://example.com/?param=%3Cscript%3Ealert(%22hax%22);%3C/script%3E', done) + }) + + it('should include the redirect type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.redirect(301, 'http://google.com'); + }); + + request(app) + .get('/') + .set('Accept', 'text/plain, */*') + .expect('Content-Type', /plain/) + .expect('Location', 'http://google.com') + .expect(301, 'Moved Permanently. Redirecting to http://google.com', done); + }) + }) + + describe('when accepting neither text or html', function(){ + it('should respond with an empty body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.redirect('http://google.com'); + }); + + request(app) + .get('/') + .set('Accept', 'application/octet-stream') + .expect(302) + .expect('location', 'http://google.com') + .expect('content-length', '0') + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveBody()) + .end(done) + }) + }) +}) diff --git a/apps/api/test/res.render.js b/apps/api/test/res.render.js new file mode 100644 index 0000000..114b398 --- /dev/null +++ b/apps/api/test/res.render.js @@ -0,0 +1,367 @@ +'use strict' + +var express = require('..'); +var path = require('node:path') +var request = require('supertest'); +var tmpl = require('./support/tmpl'); + +describe('res', function(){ + describe('.render(name)', function(){ + it('should support absolute paths', function(done){ + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render(path.join(__dirname, 'fixtures', 'user.tmpl')) + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should support absolute paths with "view engine"', function(done){ + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + app.set('view engine', 'tmpl'); + + app.use(function(req, res){ + res.render(path.join(__dirname, 'fixtures', 'user')) + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should error without "view engine" set and file extension to a non-engine module', function (done) { + var app = createApp() + + app.locals.user = { name: 'tobi' } + + app.use(function (req, res) { + res.render(path.join(__dirname, 'fixtures', 'broken.send')) + }) + + request(app) + .get('/') + .expect(500, /does not provide a view engine/, done) + }) + + it('should error without "view engine" set and no file extension', function (done) { + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render(path.join(__dirname, 'fixtures', 'user')) + }); + + request(app) + .get('/') + .expect(500, /No default engine was specified/, done); + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render('user.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should expose app.locals with `name` property', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.name = 'tobi'; + + app.use(function(req, res){ + res.render('name.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should support index.', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.set('view engine', 'tmpl'); + + app.use(function(req, res){ + res.render('blog/post'); + }); + + request(app) + .get('/') + .expect('

      blog post

      ', done); + }) + + describe('when an error occurs', function(){ + it('should next(err)', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.render('user.tmpl'); + }); + + app.use(function(err, req, res, next){ + res.status(500).send('got error: ' + err.name) + }); + + request(app) + .get('/') + .expect(500, 'got error: RenderError', done) + }) + }) + + describe('when "view engine" is given', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('view engine', 'tmpl'); + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.render('email'); + }); + + request(app) + .get('/') + .expect('

      This is an email

      ', done); + }) + }) + + describe('when "views" is given', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures', 'default_layout')) + + app.use(function(req, res){ + res.render('user.tmpl', { user: { name: 'tobi' } }); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + describe('when array of paths', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + + app.use(function(req, res){ + res.render('user.tmpl', { user: { name: 'tobi' } }); + }); + + request(app) + .get('/') + .expect('tobi', done); + }) + + it('should lookup in later paths until found', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + + app.use(function(req, res){ + res.render('name.tmpl', { name: 'tobi' }); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + }) + }) + }) + + describe('.render(name, option)', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + var user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render('user.tmpl', { user: user }); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render('user.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should expose res.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.locals.user = { name: 'tobi' }; + res.render('user.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should give precedence to res.locals over app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.locals.user = { name: 'jane' }; + res.render('user.tmpl', {}); + }); + + request(app) + .get('/') + .expect('

      jane

      ', done); + }) + + it('should give precedence to res.render() locals over res.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + var jane = { name: 'jane' }; + + app.use(function(req, res){ + res.locals.user = { name: 'tobi' }; + res.render('user.tmpl', { user: jane }); + }); + + request(app) + .get('/') + .expect('

      jane

      ', done); + }) + + it('should give precedence to res.render() locals over app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + var jane = { name: 'jane' }; + + app.use(function(req, res){ + res.render('user.tmpl', { user: jane }); + }); + + request(app) + .get('/') + .expect('

      jane

      ', done); + }) + }) + + describe('.render(name, options, fn)', function(){ + it('should pass the resulting string', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + var tobi = { name: 'tobi' }; + res.render('user.tmpl', { user: tobi }, function (err, html) { + html = html.replace('tobi', 'loki'); + res.end(html); + }); + }); + + request(app) + .get('/') + .expect('

      loki

      ', done); + }) + }) + + describe('.render(name, fn)', function(){ + it('should pass the resulting string', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.locals.user = { name: 'tobi' }; + res.render('user.tmpl', function (err, html) { + html = html.replace('tobi', 'loki'); + res.end(html); + }); + }); + + request(app) + .get('/') + .expect('

      loki

      ', done); + }) + + describe('when an error occurs', function(){ + it('should pass it to the callback', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.render('user.tmpl', function (err) { + if (err) { + res.status(500).send('got error: ' + err.name) + } + }); + }); + + request(app) + .get('/') + .expect(500, 'got error: RenderError', done) + }) + }) + }) +}) + +function createApp() { + var app = express(); + + app.engine('.tmpl', tmpl); + + return app; +} diff --git a/apps/api/test/res.send.js b/apps/api/test/res.send.js new file mode 100644 index 0000000..8547b77 --- /dev/null +++ b/apps/api/test/res.send.js @@ -0,0 +1,569 @@ +'use strict' + +var assert = require('node:assert') +const { Buffer } = require('node:buffer'); +var express = require('..'); +var methods = require('../lib/utils').methods; +var request = require('supertest'); +var utils = require('./support/utils'); + +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + +describe('res', function(){ + describe('.send()', function(){ + it('should set body to ""', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(); + }); + + request(app) + .get('/') + .expect(200, '', done); + }) + }) + + describe('.send(null)', function(){ + it('should set body to ""', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(null); + }); + + request(app) + .get('/') + .expect('Content-Length', '0') + .expect(200, '', done); + }) + }) + + describe('.send(undefined)', function(){ + it('should set body to ""', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(undefined); + }); + + request(app) + .get('/') + .expect(200, '', done); + }) + }) + + describe('.send(Number)', function(){ + it('should send as application/json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(1000); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '1000', done) + }) + }) + + describe('.send(String)', function(){ + it('should send as html', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send('

      hey

      '); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=utf-8') + .expect(200, '

      hey

      ', done); + }) + + it('should set ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + var str = Array(1000).join('-'); + res.send(str); + }); + + request(app) + .get('/') + .expect('ETag', 'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"') + .expect(200, done); + }) + + it('should not override Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain').send('hey'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'hey', done); + }) + + it('should override charset in Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain; charset=iso-8859-1').send('hey'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'hey', done); + }) + + it('should keep charset in Content-Type for Buffers', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain; charset=iso-8859-1').send(Buffer.from('hi')) + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=iso-8859-1') + .expect(200, 'hi', done); + }) + }) + + describe('.send(Buffer)', function(){ + it('should send as octet-stream', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(Buffer.from('hello')) + }); + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'application/octet-stream') + .expect(utils.shouldHaveBody(Buffer.from('hello'))) + .end(done) + }) + + it('should set ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send(Buffer.alloc(999, '-')) + }); + + request(app) + .get('/') + .expect('ETag', 'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"') + .expect(200, done); + }) + + it('should not override Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain').send(Buffer.from('hey')) + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'hey', done); + }) + + it('should accept Uint8Array', function(done){ + var app = express(); + app.use(function(req, res){ + const encodedHey = new TextEncoder().encode("hey"); + res.set("Content-Type", "text/plain").send(encodedHey); + }) + + request(app) + .get("/") + .expect("Content-Type", "text/plain; charset=utf-8") + .expect(200, "hey", done); + }) + + it('should not override ETag', function (done) { + var app = express() + + app.use(function (req, res) { + res.type('text/plain').set('ETag', '"foo"').send(Buffer.from('hey')) + }) + + request(app) + .get('/') + .expect('ETag', '"foo"') + .expect(200, 'hey', done) + }) + }) + + describe('.send(Object)', function(){ + it('should send as application/json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send({ name: 'tobi' }); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '{"name":"tobi"}', done) + }) + }) + + describe('when the request method is HEAD', function(){ + it('should ignore the body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send('yay'); + }); + + request(app) + .head('/') + .expect(200) + .expect(utils.shouldNotHaveBody()) + .end(done) + }) + }) + + describe('when .statusCode is 204', function(){ + it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.status(204).set('Transfer-Encoding', 'chunked').send('foo'); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveHeader('Content-Length')) + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect(204, '', done); + }) + }) + + describe('when .statusCode is 205', function () { + it('should strip Transfer-Encoding field and body, set Content-Length', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(205).set('Transfer-Encoding', 'chunked').send('foo') + }) + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect('Content-Length', '0') + .expect(205, '', done) + }) + }) + + describe('when .statusCode is 304', function(){ + it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.status(304).set('Transfer-Encoding', 'chunked').send('foo'); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveHeader('Content-Length')) + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect(304, '', done); + }) + }) + + it('should always check regardless of length', function(done){ + var app = express(); + var etag = '"asdf"'; + + app.use(function(req, res, next){ + res.set('ETag', etag); + res.send('hey'); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should respond with 304 Not Modified when fresh', function(done){ + var app = express(); + var etag = '"asdf"'; + + app.use(function(req, res){ + var str = Array(1000).join('-'); + res.set('ETag', etag); + res.send(str); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should not perform freshness check unless 2xx or 304', function(done){ + var app = express(); + var etag = '"asdf"'; + + app.use(function(req, res, next){ + res.status(500); + res.set('ETag', etag); + res.send('hey'); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect('hey') + .expect(500, done); + }) + + it('should not support jsonp callbacks', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send({ foo: 'bar' }); + }); + + request(app) + .get('/?callback=foo') + .expect('{"foo":"bar"}', done); + }) + + it('should be chainable', function (done) { + var app = express() + + app.use(function (req, res) { + assert.equal(res.send('hey'), res) + }) + + request(app) + .get('/') + .expect(200, 'hey', done) + }) + + describe('"etag" setting', function () { + describe('when enabled', function () { + it('should send ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send('kajdslfkasdf'); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', 'W/"c-IgR/L5SF7CJQff4wxKGF/vfPuZ0"') + .expect(200, done); + }); + + methods.forEach(function (method) { + if (method === 'connect') return; + + it('should send ETag in response to ' + method.toUpperCase() + ' request', function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } + var app = express(); + + app[method]('/', function (req, res) { + res.send('kajdslfkasdf'); + }); + + request(app) + [method]('/') + .expect('ETag', 'W/"c-IgR/L5SF7CJQff4wxKGF/vfPuZ0"') + .expect(200, done); + }) + }); + + it('should send ETag for empty string response', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send(''); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', 'W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"') + .expect(200, done); + }) + + it('should send ETag for long response', function (done) { + var app = express(); + + app.use(function (req, res) { + var str = Array(1000).join('-'); + res.send(str); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', 'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"') + .expect(200, done); + }); + + it('should not override ETag when manually set', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('etag', '"asdf"'); + res.send('hello!'); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', '"asdf"') + .expect(200, done); + }); + + it('should not send ETag for res.send()', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send(); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('ETag')) + .expect(200, done); + }) + }); + + describe('when disabled', function () { + it('should send no ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + var str = Array(1000).join('-'); + res.send(str); + }); + + app.disable('etag'); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('ETag')) + .expect(200, done); + }); + + it('should send ETag when manually set', function (done) { + var app = express(); + + app.disable('etag'); + + app.use(function (req, res) { + res.set('etag', '"asdf"'); + res.send('hello!'); + }); + + request(app) + .get('/') + .expect('ETag', '"asdf"') + .expect(200, done); + }); + }); + + describe('when "strong"', function () { + it('should send strong ETag', function (done) { + var app = express(); + + app.set('etag', 'strong'); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect('ETag', '"d-HwnTDHB9U/PRbFMN1z1wps51lqk"') + .expect(200, done); + }) + }) + + describe('when "weak"', function () { + it('should send weak ETag', function (done) { + var app = express(); + + app.set('etag', 'weak'); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect('ETag', 'W/"d-HwnTDHB9U/PRbFMN1z1wps51lqk"') + .expect(200, done) + }) + }) + + describe('when a function', function () { + it('should send custom ETag', function (done) { + var app = express(); + + app.set('etag', function (body, encoding) { + var chunk = !Buffer.isBuffer(body) + ? Buffer.from(body, encoding) + : body; + assert.strictEqual(chunk.toString(), 'hello, world!') + return '"custom"'; + }); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect('ETag', '"custom"') + .expect(200, done); + }) + + it('should not send falsy ETag', function (done) { + var app = express(); + + app.set('etag', function (body, encoding) { + return undefined; + }); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('ETag')) + .expect(200, done); + }) + }) + }) +}) diff --git a/apps/api/test/res.sendFile.js b/apps/api/test/res.sendFile.js new file mode 100644 index 0000000..16eea79 --- /dev/null +++ b/apps/api/test/res.sendFile.js @@ -0,0 +1,913 @@ +'use strict' + +var after = require('after'); +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage +const { Buffer } = require('node:buffer'); + +var express = require('../') + , request = require('supertest') +var onFinished = require('on-finished'); +var path = require('node:path'); +var fixtures = path.join(__dirname, 'fixtures'); +var utils = require('./support/utils'); + +describe('res', function(){ + describe('.sendFile(path)', function () { + it('should error missing path', function (done) { + var app = createApp(); + + request(app) + .get('/') + .expect(500, /path.*required/, done); + }); + + it('should error for non-string path', function (done) { + var app = createApp(42) + + request(app) + .get('/') + .expect(500, /TypeError: path must be a string to res.sendFile/, done) + }) + + it('should error for non-absolute path', function (done) { + var app = createApp('name.txt') + + request(app) + .get('/') + .expect(500, /TypeError: path must be absolute/, done) + }) + + it('should transfer a file', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')); + + request(app) + .get('/') + .expect(200, 'tobi', done); + }); + + it('should transfer a file with special characters in string', function (done) { + var app = createApp(path.resolve(fixtures, '% of dogs.txt')); + + request(app) + .get('/') + .expect(200, '20%', done); + }); + + it('should include ETag', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')); + + request(app) + .get('/') + .expect('ETag', /^(?:W\/)?"[^"]+"$/) + .expect(200, 'tobi', done); + }); + + it('should 304 when ETag matches', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')); + + request(app) + .get('/') + .expect('ETag', /^(?:W\/)?"[^"]+"$/) + .expect(200, 'tobi', function (err, res) { + if (err) return done(err); + var etag = res.headers.etag; + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }); + }); + + it('should disable the ETag function if requested', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')).disable('etag'); + + request(app) + .get('/') + .expect(handleHeaders) + .expect(200, done); + + function handleHeaders (res) { + assert(res.headers.etag === undefined); + } + }); + + it('should 404 for directory', function (done) { + var app = createApp(path.resolve(fixtures, 'blog')); + + request(app) + .get('/') + .expect(404, done); + }); + + it('should 404 when not found', function (done) { + var app = createApp(path.resolve(fixtures, 'does-no-exist')); + + app.use(function (req, res) { + res.statusCode = 200; + res.send('no!'); + }); + + request(app) + .get('/') + .expect(404, done); + }); + + it('should send cache-control by default', function (done) { + var app = createApp(path.resolve(__dirname, 'fixtures/name.txt')) + + request(app) + .get('/') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, done) + }) + + it('should not serve dotfiles by default', function (done) { + var app = createApp(path.resolve(__dirname, 'fixtures/.name')) + + request(app) + .get('/') + .expect(404, done) + }) + + it('should not override manual content-types', function (done) { + var app = express(); + + app.use(function (req, res) { + res.contentType('application/x-bogus'); + res.sendFile(path.resolve(fixtures, 'name.txt')); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/x-bogus') + .end(done); + }) + + it('should not error if the client aborts', function (done) { + var app = express(); + var cb = after(2, done) + var error = null + + app.use(function (req, res) { + setImmediate(function () { + res.sendFile(path.resolve(fixtures, 'name.txt')); + setTimeout(function () { + cb(error) + }, 10) + }) + test.req.abort() + }); + + app.use(function (err, req, res, next) { + error = err + next(err) + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + }) + + describe('.sendFile(path, fn)', function () { + it('should invoke the callback when complete', function (done) { + var cb = after(2, done); + var app = createApp(path.resolve(fixtures, 'name.txt'), cb); + + request(app) + .get('/') + .expect(200, cb); + }) + + it('should invoke the callback when client aborts', function (done) { + var cb = after(2, done) + var app = express(); + + app.use(function (req, res) { + setImmediate(function () { + res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { + assert.ok(err) + assert.strictEqual(err.code, 'ECONNABORTED') + cb() + }); + }); + test.req.abort() + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + + it('should invoke the callback when client already aborted', function (done) { + var cb = after(2, done) + var app = express(); + + app.use(function (req, res) { + onFinished(res, function () { + res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { + assert.ok(err) + assert.strictEqual(err.code, 'ECONNABORTED') + cb() + }); + }); + test.req.abort() + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + + it('should invoke the callback without error when HEAD', function (done) { + var app = express(); + var cb = after(2, done); + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), cb); + }); + + request(app) + .head('/') + .expect(200, cb); + }); + + it('should invoke the callback without error when 304', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), cb); + }); + + request(app) + .get('/') + .expect('ETag', /^(?:W\/)?"[^"]+"$/) + .expect(200, 'tobi', function (err, res) { + if (err) return cb(err); + var etag = res.headers.etag; + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, cb); + }); + }); + + it('should invoke the callback on 404', function(done){ + var app = express(); + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) { + res.send(err ? 'got ' + err.status + ' error' : 'no error') + }); + }); + + request(app) + .get('/') + .expect(200, 'got 404 error', done) + }) + + describe('async local storage', function () { + it('should persist store', function (done) { + var app = express() + var cb = after(2, done) + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { + if (err) return cb(err) + + var local = req.asyncLocalStorage.getStore() + + assert.strictEqual(local.foo, 'bar') + cb() + }) + }) + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'tobi', cb) + }) + + it('should persist store on error', function (done) { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.send(err ? 'got ' + err.status + ' error' : 'no error') + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('got 404 error') + .end(done) + }) + }) + }) + + describe('.sendFile(path, options)', function () { + it('should pass options to send module', function (done) { + request(createApp(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 })) + .get('/') + .expect(200, 'to', done) + }) + + describe('with "acceptRanges" option', function () { + describe('when true', function () { + it('should advertise byte range accepted', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Accept-Ranges', 'bytes') + .expect('123456789') + .end(done) + }) + + it('should respond to range request', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: true + }) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-4') + .expect(206, '12345', done) + }) + }) + + describe('when false', function () { + it('should not advertise accept-ranges', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .end(done) + }) + + it('should not honor range requests', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: false + }) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-4') + .expect(200, '123456789', done) + }) + }) + }) + + describe('with "cacheControl" option', function () { + describe('when true', function () { + it('should send cache-control header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + cacheControl: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .end(done) + }) + }) + + describe('when false', function () { + it('should not send cache-control header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + cacheControl: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .end(done) + }) + }) + }) + + describe('with "dotfiles" option', function () { + describe('when "allow"', function () { + it('should allow dotfiles', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, '.name'), { + dotfiles: 'allow' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + }) + + describe('when "deny"', function () { + it('should deny dotfiles', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, '.name'), { + dotfiles: 'deny' + }) + }) + + request(app) + .get('/') + .expect(403) + .expect(/Forbidden/) + .end(done) + }) + }) + + describe('when "ignore"', function () { + it('should ignore dotfiles', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, '.name'), { + dotfiles: 'ignore' + }) + }) + + request(app) + .get('/') + .expect(404) + .expect(/Not Found/) + .end(done) + }) + }) + }) + + describe('with "headers" option', function () { + it('should set headers on response', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + headers: { + 'X-Foo': 'Bar', + 'X-Bar': 'Foo' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'Bar') + .expect('X-Bar', 'Foo') + .end(done) + }) + + it('should use last header when duplicated', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + headers: { + 'X-Foo': 'Bar', + 'x-foo': 'bar' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'bar') + .end(done) + }) + + it('should override Content-Type', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + headers: { + 'Content-Type': 'text/x-custom' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .end(done) + }) + + it('should not set headers on 404', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'does-not-exist'), { + headers: { + 'X-Foo': 'Bar' + } + }) + }) + + request(app) + .get('/') + .expect(404) + .expect(utils.shouldNotHaveHeader('X-Foo')) + .end(done) + }) + }) + + describe('with "immutable" option', function () { + describe('when true', function () { + it('should send cache-control header with immutable', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + immutable: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0, immutable') + .end(done) + }) + }) + + describe('when false', function () { + it('should not send cache-control header with immutable', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + immutable: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .end(done) + }) + }) + }) + + describe('with "lastModified" option', function () { + describe('when true', function () { + it('should send last-modified header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldHaveHeader('Last-Modified')) + .end(done) + }) + + it('should conditionally respond with if-modified-since', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: true + }) + }) + + request(app) + .get('/') + .set('If-Modified-Since', (new Date(Date.now() + 99999).toUTCString())) + .expect(304, done) + }) + }) + + describe('when false', function () { + it('should not have last-modified header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Last-Modified')) + .end(done) + }) + + it('should not honor if-modified-since', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: false + }) + }) + + request(app) + .get('/') + .set('If-Modified-Since', (new Date(Date.now() + 99999).toUTCString())) + .expect(200) + .expect(utils.shouldNotHaveHeader('Last-Modified')) + .end(done) + }) + }) + }) + + describe('with "maxAge" option', function () { + it('should set cache-control max-age to milliseconds', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: 20000 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=20') + .end(done) + }) + + it('should cap cache-control max-age to 1 year', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: 99999999999 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=31536000') + .end(done) + }) + + it('should min cache-control max-age to 0', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: -20000 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .end(done) + }) + + it('should floor cache-control max-age', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: 21911.23 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=21') + .end(done) + }) + + describe('when cacheControl: false', function () { + it('should not send cache-control', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + cacheControl: false, + maxAge: 20000 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .end(done) + }) + }) + + describe('when string', function () { + it('should accept plain number as milliseconds', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20000' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=20') + .end(done) + }) + + it('should accept suffix "s" for seconds', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20s' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=20') + .end(done) + }) + + it('should accept suffix "m" for minutes', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20m' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=1200') + .end(done) + }) + + it('should accept suffix "d" for days', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20d' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=1728000') + .end(done) + }) + }) + }) + + describe('with "root" option', function () { + it('should allow relative path', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('name.txt', { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(200, 'tobi', done) + }) + + it('should allow up within root', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('fake/../name.txt', { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(200, 'tobi', done) + }) + + it('should reject up outside root', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('..' + path.sep + path.relative(path.dirname(fixtures), path.join(fixtures, 'name.txt')), { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(403, done) + }) + + it('should reject reading outside root', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('../name.txt', { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(403, done) + }) + }) + }) +}) + +function createApp(path, options, fn) { + var app = express(); + + app.use(function (req, res) { + res.sendFile(path, options, fn); + }); + + return app; +} diff --git a/apps/api/test/res.sendStatus.js b/apps/api/test/res.sendStatus.js new file mode 100644 index 0000000..b244cf9 --- /dev/null +++ b/apps/api/test/res.sendStatus.js @@ -0,0 +1,44 @@ +'use strict' + +var express = require('..') +var request = require('supertest') + +describe('res', function () { + describe('.sendStatus(statusCode)', function () { + it('should send the status code and message as body', function (done) { + var app = express(); + + app.use(function(req, res){ + res.sendStatus(201); + }); + + request(app) + .get('/') + .expect(201, 'Created', done); + }) + + it('should work with unknown code', function (done) { + var app = express(); + + app.use(function(req, res){ + res.sendStatus(599); + }); + + request(app) + .get('/') + .expect(599, '599', done); + }) + + it('should raise error for invalid status code', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendStatus(undefined).end() + }) + + request(app) + .get('/') + .expect(500, /TypeError: Invalid status code/, done) + }) + }) +}) diff --git a/apps/api/test/res.set.js b/apps/api/test/res.set.js new file mode 100644 index 0000000..04511c1 --- /dev/null +++ b/apps/api/test/res.set.js @@ -0,0 +1,124 @@ +'use strict' + +var express = require('..'); +var request = require('supertest'); + +describe('res', function(){ + describe('.set(field, value)', function(){ + it('should set the response header field', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/x-foo; charset=utf-8').end(); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/x-foo; charset=utf-8') + .end(done); + }) + + it('should coerce to a string', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('X-Number', 123); + res.end(typeof res.get('X-Number')); + }); + + request(app) + .get('/') + .expect('X-Number', '123') + .expect(200, 'string', done); + }) + }) + + describe('.set(field, values)', function(){ + it('should set multiple response header fields', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Set-Cookie', ["type=ninja", "language=javascript"]); + res.send(res.get('Set-Cookie')); + }); + + request(app) + .get('/') + .expect('["type=ninja","language=javascript"]', done); + }) + + it('should coerce to an array of strings', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('X-Numbers', [123, 456]); + res.end(JSON.stringify(res.get('X-Numbers'))); + }); + + request(app) + .get('/') + .expect('X-Numbers', '123, 456') + .expect(200, '["123","456"]', done); + }) + + it('should not set a charset of one is already set', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('Content-Type', 'text/html; charset=lol'); + res.end(); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=lol') + .expect(200, done); + }) + + it('should throw when Content-Type is an array', function (done) { + var app = express() + + app.use(function (req, res) { + res.set('Content-Type', ['text/html']) + res.end() + }); + + request(app) + .get('/') + .expect(500, /TypeError: Content-Type cannot be set to an Array/, done) + }) + }) + + describe('.set(object)', function(){ + it('should set multiple fields', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set({ + 'X-Foo': 'bar', + 'X-Bar': 'baz' + }).end(); + }); + + request(app) + .get('/') + .expect('X-Foo', 'bar') + .expect('X-Bar', 'baz') + .end(done); + }) + + it('should coerce to a string', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set({ 'X-Number': 123 }); + res.end(typeof res.get('X-Number')); + }); + + request(app) + .get('/') + .expect('X-Number', '123') + .expect(200, 'string', done); + }) + }) +}) diff --git a/apps/api/test/res.status.js b/apps/api/test/res.status.js new file mode 100644 index 0000000..59c8a57 --- /dev/null +++ b/apps/api/test/res.status.js @@ -0,0 +1,206 @@ +'use strict' +const express = require('../.'); +const request = require('supertest'); + +describe('res', function () { + describe('.status(code)', function () { + + it('should set the status code when valid', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(200).end(); + }); + + request(app) + .get('/') + .expect(200, done); + }); + + describe('accept valid ranges', function() { + // not testing w/ 100, because that has specific meaning and behavior in Node as Expect: 100-continue + it('should set the response status code to 101', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(101).end() + }) + + request(app) + .get('/') + .expect(101, done) + }) + + it('should set the response status code to 201', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(201).end() + }) + + request(app) + .get('/') + .expect(201, done) + }) + + it('should set the response status code to 302', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(302).end() + }) + + request(app) + .get('/') + .expect(302, done) + }) + + it('should set the response status code to 403', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(403).end() + }) + + request(app) + .get('/') + .expect(403, done) + }) + + it('should set the response status code to 501', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(501).end() + }) + + request(app) + .get('/') + .expect(501, done) + }) + + it('should set the response status code to 700', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(700).end() + }) + + request(app) + .get('/') + .expect(700, done) + }) + + it('should set the response status code to 800', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(800).end() + }) + + request(app) + .get('/') + .expect(800, done) + }) + + it('should set the response status code to 900', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(900).end() + }) + + request(app) + .get('/') + .expect(900, done) + }) + }) + + describe('invalid status codes', function () { + it('should raise error for status code below 100', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(99).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for status code above 999', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(1000).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for non-integer status codes', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(200.1).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for undefined status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(undefined).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for null status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(null).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for string status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status("200").end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for NaN status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(NaN).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + }); + }); +}); + diff --git a/apps/api/test/res.type.js b/apps/api/test/res.type.js new file mode 100644 index 0000000..09285af --- /dev/null +++ b/apps/api/test/res.type.js @@ -0,0 +1,46 @@ +'use strict' + +var express = require('../') + , request = require('supertest'); + +describe('res', function(){ + describe('.type(str)', function(){ + it('should set the Content-Type based on a filename', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('foo.js').end('var name = "tj";'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/javascript; charset=utf-8') + .end(done) + }) + + it('should default to application/octet-stream', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('rawr').end('var name = "tj";'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/octet-stream', done); + }) + + it('should set the Content-Type with type/subtype', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('application/vnd.amazon.ebook') + .end('var name = "tj";'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/vnd.amazon.ebook', done); + }) + }) +}) diff --git a/apps/api/test/res.vary.js b/apps/api/test/res.vary.js new file mode 100644 index 0000000..ff3c971 --- /dev/null +++ b/apps/api/test/res.vary.js @@ -0,0 +1,90 @@ +'use strict' + +var express = require('..'); +var request = require('supertest'); +var utils = require('./support/utils'); + +describe('res.vary()', function(){ + describe('with no arguments', function(){ + it('should throw error', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary(); + res.end(); + }); + + request(app) + .get('/') + .expect(500, /field.*required/, done) + }) + }) + + describe('with an empty array', function(){ + it('should not set Vary', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary([]); + res.end(); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Vary')) + .expect(200, done); + }) + }) + + describe('with an array', function(){ + it('should set the values', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary(['Accept', 'Accept-Language', 'Accept-Encoding']); + res.end(); + }); + + request(app) + .get('/') + .expect('Vary', 'Accept, Accept-Language, Accept-Encoding') + .expect(200, done); + }) + }) + + describe('with a string', function(){ + it('should set the value', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary('Accept'); + res.end(); + }); + + request(app) + .get('/') + .expect('Vary', 'Accept') + .expect(200, done); + }) + }) + + describe('when the value is present', function(){ + it('should not add it again', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary('Accept'); + res.vary('Accept-Encoding'); + res.vary('Accept-Encoding'); + res.vary('Accept-Encoding'); + res.vary('Accept'); + res.end(); + }); + + request(app) + .get('/') + .expect('Vary', 'Accept, Accept-Encoding') + .expect(200, done); + }) + }) +}) diff --git a/apps/api/test/support/env.js b/apps/api/test/support/env.js new file mode 100644 index 0000000..000638c --- /dev/null +++ b/apps/api/test/support/env.js @@ -0,0 +1,3 @@ + +process.env.NODE_ENV = 'test'; +process.env.NO_DEPRECATION = 'body-parser,express'; diff --git a/apps/api/test/support/tmpl.js b/apps/api/test/support/tmpl.js new file mode 100644 index 0000000..e24b6fe --- /dev/null +++ b/apps/api/test/support/tmpl.js @@ -0,0 +1,36 @@ +var fs = require('node:fs'); + +var variableRegExp = /\$([0-9a-zA-Z\.]+)/g; + +module.exports = function renderFile(fileName, options, callback) { + function onReadFile(err, str) { + if (err) { + callback(err); + return; + } + + try { + str = str.replace(variableRegExp, generateVariableLookup(options)); + } catch (e) { + err = e; + err.name = 'RenderError' + } + + callback(err, str); + } + + fs.readFile(fileName, 'utf8', onReadFile); +}; + +function generateVariableLookup(data) { + return function variableLookup(str, path) { + var parts = path.split('.'); + var value = data; + + for (var i = 0; i < parts.length; i++) { + value = value[parts[i]]; + } + + return value; + }; +} diff --git a/apps/api/test/support/utils.js b/apps/api/test/support/utils.js new file mode 100644 index 0000000..fb816b0 --- /dev/null +++ b/apps/api/test/support/utils.js @@ -0,0 +1,86 @@ + +/** + * Module dependencies. + * @private + */ + +var assert = require('node:assert'); +const { Buffer } = require('node:buffer'); + +/** + * Module exports. + * @public + */ + +exports.shouldHaveBody = shouldHaveBody +exports.shouldHaveHeader = shouldHaveHeader +exports.shouldNotHaveBody = shouldNotHaveBody +exports.shouldNotHaveHeader = shouldNotHaveHeader; +exports.shouldSkipQuery = shouldSkipQuery + +/** + * Assert that a supertest response has a specific body. + * + * @param {Buffer} buf + * @returns {function} + */ + +function shouldHaveBody (buf) { + return function (res) { + var body = !Buffer.isBuffer(res.body) + ? Buffer.from(res.text) + : res.body + assert.ok(body, 'response has body') + assert.strictEqual(body.toString('hex'), buf.toString('hex')) + } +} + +/** + * Assert that a supertest response does have a header. + * + * @param {string} header Header name to check + * @returns {function} + */ + +function shouldHaveHeader (header) { + return function (res) { + assert.ok((header.toLowerCase() in res.headers), 'should have header ' + header) + } +} + +/** + * Assert that a supertest response does not have a body. + * + * @returns {function} + */ + +function shouldNotHaveBody () { + return function (res) { + assert.ok(res.text === '' || res.text === undefined) + } +} + +/** + * Assert that a supertest response does not have a header. + * + * @param {string} header Header name to check + * @returns {function} + */ +function shouldNotHaveHeader(header) { + return function (res) { + assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); + }; +} + +function getMajorVersion(versionString) { + return versionString.split('.')[0]; +} + +function shouldSkipQuery(versionString) { + // Skipping HTTP QUERY tests below Node 22, QUERY wasn't fully supported by Node until 22 + // we could update this implementation to run on supported versions of 21 once they exist + // upstream tracking https://github.com/nodejs/node/issues/51562 + // express tracking issue: https://github.com/expressjs/express/issues/5615 + return Number(getMajorVersion(versionString)) < 22 +} + diff --git a/apps/api/test/utils.js b/apps/api/test/utils.js new file mode 100644 index 0000000..1c06036 --- /dev/null +++ b/apps/api/test/utils.js @@ -0,0 +1,83 @@ +'use strict' + +var assert = require('node:assert'); +const { Buffer } = require('node:buffer'); +var utils = require('../lib/utils'); + +describe('utils.etag(body, encoding)', function(){ + it('should support strings', function(){ + assert.strictEqual(utils.etag('express!'), + '"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') + }) + + it('should support utf8 strings', function(){ + assert.strictEqual(utils.etag('express❤', 'utf8'), + '"a-JBiXf7GyzxwcrxY4hVXUwa7tmks"') + }) + + it('should support buffer', function(){ + assert.strictEqual(utils.etag(Buffer.from('express!')), + '"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') + }) + + it('should support empty string', function(){ + assert.strictEqual(utils.etag(''), + '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"') + }) +}) + +describe('utils.normalizeType acceptParams method', () => { + it('should handle a type with a malformed parameter and break the loop in acceptParams', () => { + const result = utils.normalizeType('text/plain;invalid'); + assert.deepEqual(result,{ + value: 'text/plain', + quality: 1, + params: {} // No parameters are added since "invalid" has no "=" + }); + }); +}); + + +describe('utils.setCharset(type, charset)', function () { + it('should do anything without type', function () { + assert.strictEqual(utils.setCharset(), undefined); + }); + + it('should return type if not given charset', function () { + assert.strictEqual(utils.setCharset('text/html'), 'text/html'); + }); + + it('should keep charset if not given charset', function () { + assert.strictEqual(utils.setCharset('text/html; charset=utf-8'), 'text/html; charset=utf-8'); + }); + + it('should set charset', function () { + assert.strictEqual(utils.setCharset('text/html', 'utf-8'), 'text/html; charset=utf-8'); + }); + + it('should override charset', function () { + assert.strictEqual(utils.setCharset('text/html; charset=iso-8859-1', 'utf-8'), 'text/html; charset=utf-8'); + }); +}); + +describe('utils.wetag(body, encoding)', function(){ + it('should support strings', function(){ + assert.strictEqual(utils.wetag('express!'), + 'W/"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') + }) + + it('should support utf8 strings', function(){ + assert.strictEqual(utils.wetag('express❤', 'utf8'), + 'W/"a-JBiXf7GyzxwcrxY4hVXUwa7tmks"') + }) + + it('should support buffer', function(){ + assert.strictEqual(utils.wetag(Buffer.from('express!')), + 'W/"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') + }) + + it('should support empty string', function(){ + assert.strictEqual(utils.wetag(''), + 'W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"') + }) +}) diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..19b8bf4 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,16 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..97c58ea --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "voxblog", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/config-ts/package.json b/packages/config-ts/package.json new file mode 100644 index 0000000..97c58ea --- /dev/null +++ b/packages/config-ts/package.json @@ -0,0 +1,12 @@ +{ + "name": "voxblog", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/config-ts/tsconfig.json b/packages/config-ts/tsconfig.json new file mode 100644 index 0000000..9c7d374 --- /dev/null +++ b/packages/config-ts/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..24a85d1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5229 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + apps/admin: + dependencies: + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.2.2)(react@19.2.0) + '@emotion/styled': + specifier: ^11.14.1 + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@mui/icons-material': + specifier: ^7.3.4 + version: 7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@mui/material': + specifier: ^7.3.4 + version: 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: + specifier: ^19.1.1 + version: 19.2.0 + react-dom: + specifier: ^19.1.1 + version: 19.2.0(react@19.2.0) + devDependencies: + '@eslint/js': + specifier: ^9.36.0 + version: 9.38.0 + '@types/node': + specifier: ^24.6.0 + version: 24.9.1 + '@types/react': + specifier: ^19.1.16 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.1.9 + version: 19.2.2(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^5.0.4 + version: 5.0.4(vite@7.1.11(@types/node@24.9.1)) + eslint: + specifier: ^9.36.0 + version: 9.38.0 + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.38.0) + eslint-plugin-react-refresh: + specifier: ^0.4.22 + version: 0.4.24(eslint@9.38.0) + globals: + specifier: ^16.4.0 + version: 16.4.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.45.0 + version: 8.46.2(eslint@9.38.0)(typescript@5.9.3) + vite: + specifier: ^7.1.7 + version: 7.1.11(@types/node@24.9.1) + + apps/api: + dependencies: + accepts: + specifier: ^2.0.0 + version: 2.0.0 + body-parser: + specifier: ^2.2.0 + version: 2.2.0 + content-disposition: + specifier: ^1.0.0 + version: 1.0.0 + content-type: + specifier: ^1.0.5 + version: 1.0.5 + cookie: + specifier: ^0.7.1 + version: 0.7.2 + cookie-signature: + specifier: ^1.2.1 + version: 1.2.2 + cors: + specifier: ^2.8.5 + version: 2.8.5 + debug: + specifier: ^4.4.0 + version: 4.4.3(supports-color@8.1.1) + depd: + specifier: ^2.0.0 + version: 2.0.0 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + encodeurl: + specifier: ^2.0.0 + version: 2.0.0 + escape-html: + specifier: ^1.0.3 + version: 1.0.3 + etag: + specifier: ^1.8.1 + version: 1.8.1 + express: + specifier: ^5.1.0 + version: 5.1.0 + finalhandler: + specifier: ^2.1.0 + version: 2.1.0 + fresh: + specifier: ^2.0.0 + version: 2.0.0 + http-errors: + specifier: ^2.0.0 + version: 2.0.0 + merge-descriptors: + specifier: ^2.0.0 + version: 2.0.0 + mime-types: + specifier: ^3.0.0 + version: 3.0.1 + on-finished: + specifier: ^2.4.1 + version: 2.4.1 + once: + specifier: ^1.4.0 + version: 1.4.0 + parseurl: + specifier: ^1.3.3 + version: 1.3.3 + proxy-addr: + specifier: ^2.0.7 + version: 2.0.7 + qs: + specifier: ^6.14.0 + version: 6.14.0 + range-parser: + specifier: ^1.2.1 + version: 1.2.1 + router: + specifier: ^2.2.0 + version: 2.2.0 + send: + specifier: ^1.1.0 + version: 1.2.0 + serve-static: + specifier: ^2.2.0 + version: 2.2.0 + statuses: + specifier: ^2.0.1 + version: 2.0.2 + type-is: + specifier: ^2.0.1 + version: 2.0.1 + vary: + specifier: ^1.1.2 + version: 1.1.2 + devDependencies: + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.3 + version: 5.0.3 + '@types/node': + specifier: ^24.6.0 + version: 24.9.1 + after: + specifier: 0.8.2 + version: 0.8.2 + connect-redis: + specifier: ^8.0.1 + version: 8.1.0(express-session@1.18.2) + cookie-parser: + specifier: 1.4.7 + version: 1.4.7 + cookie-session: + specifier: 2.1.1 + version: 2.1.1 + ejs: + specifier: ^3.1.10 + version: 3.1.10 + eslint: + specifier: 8.47.0 + version: 8.47.0 + express-session: + specifier: ^1.18.1 + version: 1.18.2 + hbs: + specifier: 4.2.0 + version: 4.2.0 + marked: + specifier: ^15.0.3 + version: 15.0.12 + method-override: + specifier: 3.0.0 + version: 3.0.0 + mocha: + specifier: ^10.7.3 + version: 10.8.2 + morgan: + specifier: 1.10.1 + version: 1.10.1 + nyc: + specifier: ^17.1.0 + version: 17.1.0 + pbkdf2-password: + specifier: 1.2.1 + version: 1.2.1 + supertest: + specifier: ^6.3.0 + version: 6.3.4 + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@24.9.1)(typescript@5.9.3) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vhost: + specifier: ~3.0.2 + version: 3.0.2 + + packages/config-ts: {} + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.25.11': + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.11': + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.11': + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.11': + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.11': + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.11': + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.11': + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.11': + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.11': + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.11': + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.11': + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.11': + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.11': + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.11': + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.11': + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.11': + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.11': + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.11': + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.11': + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.11': + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.11': + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.11': + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.11': + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.11': + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.11': + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.11': + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.1': + resolution: {integrity: sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.16.0': + resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@9.38.0': + resolution: {integrity: sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.0': + resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@mui/core-downloads-tracker@7.3.4': + resolution: {integrity: sha512-BIktMapG3r4iXwIhYNpvk97ZfYWTreBBQTWjQKbNbzI64+ULHfYavQEX2w99aSWHS58DvXESWIgbD9adKcUOBw==} + + '@mui/icons-material@7.3.4': + resolution: {integrity: sha512-9n6Xcq7molXWYb680N2Qx+FRW8oT6j/LXF5PZFH3ph9X/Rct0B/BlLAsFI7iL9ySI6LVLuQIVtrLiPT82R7OZw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^7.3.4 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/material@7.3.4': + resolution: {integrity: sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^7.3.3 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@7.3.3': + resolution: {integrity: sha512-OJM+9nj5JIyPUvsZ5ZjaeC9PfktmK+W5YaVLToLR8L0lB/DGmv1gcKE43ssNLSvpoW71Hct0necfade6+kW3zQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@7.3.3': + resolution: {integrity: sha512-CmFxvRJIBCEaWdilhXMw/5wFJ1+FT9f3xt+m2pPXhHPeVIbBg9MnMvNSJjdALvnQJMPw8jLhrUtXmN7QAZV2fw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@7.3.3': + resolution: {integrity: sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.4.7': + resolution: {integrity: sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@7.3.3': + resolution: {integrity: sha512-kwNAUh7bLZ7mRz9JZ+6qfRnnxbE4Zuc+RzXnhSpRSxjTlSTj7b4JxRLXpG+MVtPVtqks5k/XC8No1Vs3x4Z2gg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rolldown/pluginutils@1.0.0-beta.38': + resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} + + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + cpu: [x64] + os: [win32] + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} + + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@24.9.1': + resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.2.2': + resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.2.2': + resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/send@1.2.0': + resolution: {integrity: sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==} + + '@types/serve-static@1.15.9': + resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==} + + '@types/strip-bom@3.0.0': + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + + '@types/strip-json-comments@0.0.30': + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + + '@typescript-eslint/eslint-plugin@8.46.2': + resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.46.2 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.46.2': + resolution: {integrity: sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.46.2': + resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.46.2': + resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.46.2': + resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.46.2': + resolution: {integrity: sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.46.2': + resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.46.2': + resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.46.2': + resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.46.2': + resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@5.0.4': + resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + after@0.8.2: + resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-transform@2.0.0: + resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==} + engines: {node: '>=8'} + + archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.8.19: + resolution: {integrity: sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==} + hasBin: true + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + caching-transform@4.0.0: + resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001751: + resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + connect-redis@8.1.0: + resolution: {integrity: sha512-Km0EYLDlmExF52UCss5gLGTtrukGC57G6WCC2aqEMft5Vr4xNWuM4tL+T97kWrw+vp40SXFteb6Xk/7MxgpwdA==} + engines: {node: '>=18'} + peerDependencies: + express-session: '>=1' + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-session@2.1.1: + resolution: {integrity: sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==} + engines: {node: '>= 0.10'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-require-extensions@3.0.1: + resolution: {integrity: sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.237: + resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.25.11: + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.24: + resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.47.0: + resolution: {integrity: sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + eslint@9.38.0: + resolution: {integrity: sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express-session@1.18.2: + resolution: {integrity: sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==} + engines: {node: '>= 0.8.0'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + foreachasync@3.0.0: + resolution: {integrity: sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==} + + foreground-child@2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + formidable@2.1.5: + resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fromentries@1.3.2: + resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hbs@4.2.0: + resolution: {integrity: sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-hook@3.0.0: + resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-processinfo@2.0.3: + resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.flattendeep@4.4.0: + resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + method-override@3.0.0: + resolution: {integrity: sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==} + engines: {node: '>= 0.10'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mocha@10.8.2: + resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} + engines: {node: '>= 14.0.0'} + hasBin: true + + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-preload@0.2.1: + resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==} + engines: {node: '>=8'} + + node-releases@2.0.26: + resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nyc@17.1.0: + resolution: {integrity: sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-hash@4.0.0: + resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==} + engines: {node: '>=8'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pbkdf2-password@1.2.1: + resolution: {integrity: sha512-I6ZiUi82uhYN47lOzi8P7O69e70LRopgS4TIkq0ecDAPHrlxABWOHxLEsTzCuogkxTofFJDj0eEbOALgrrxhKg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + process-on-spawn@1.1.0: + resolution: {integrity: sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==} + engines: {node: '>=8'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@19.2.0: + resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + release-zalgo@1.0.0: + resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==} + engines: {node: '>=4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawn-wrap@2.0.0: + resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} + engines: {node: '>=8'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + superagent@8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + + supertest@6.3.4: + resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} + engines: {node: '>=6.4.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-node-dev@2.0.0: + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript-eslint@8.46.2: + resolution: {integrity: sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vhost@3.0.2: + resolution: {integrity: sha512-S3pJdWrpFWrKMboRU4dLYgMrTgoPALsmYwOvyebK2M6X95b9kQrjZy5rwl3uzzpfpENe/XrNYu/2U+e7/bmT5g==} + engines: {node: '>= 0.8.0'} + + vite@7.1.11: + resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + walk@2.3.15: + resolution: {integrity: sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.28.4 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) + '@emotion/utils': 1.4.2 + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.0)': + dependencies: + react: 19.2.0 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.25.11': + optional: true + + '@esbuild/android-arm64@0.25.11': + optional: true + + '@esbuild/android-arm@0.25.11': + optional: true + + '@esbuild/android-x64@0.25.11': + optional: true + + '@esbuild/darwin-arm64@0.25.11': + optional: true + + '@esbuild/darwin-x64@0.25.11': + optional: true + + '@esbuild/freebsd-arm64@0.25.11': + optional: true + + '@esbuild/freebsd-x64@0.25.11': + optional: true + + '@esbuild/linux-arm64@0.25.11': + optional: true + + '@esbuild/linux-arm@0.25.11': + optional: true + + '@esbuild/linux-ia32@0.25.11': + optional: true + + '@esbuild/linux-loong64@0.25.11': + optional: true + + '@esbuild/linux-mips64el@0.25.11': + optional: true + + '@esbuild/linux-ppc64@0.25.11': + optional: true + + '@esbuild/linux-riscv64@0.25.11': + optional: true + + '@esbuild/linux-s390x@0.25.11': + optional: true + + '@esbuild/linux-x64@0.25.11': + optional: true + + '@esbuild/netbsd-arm64@0.25.11': + optional: true + + '@esbuild/netbsd-x64@0.25.11': + optional: true + + '@esbuild/openbsd-arm64@0.25.11': + optional: true + + '@esbuild/openbsd-x64@0.25.11': + optional: true + + '@esbuild/openharmony-arm64@0.25.11': + optional: true + + '@esbuild/sunos-x64@0.25.11': + optional: true + + '@esbuild/win32-arm64@0.25.11': + optional: true + + '@esbuild/win32-ia32@0.25.11': + optional: true + + '@esbuild/win32-x64@0.25.11': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@8.47.0)': + dependencies: + eslint: 8.47.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.0(eslint@9.38.0)': + dependencies: + eslint: 9.38.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.1': + dependencies: + '@eslint/core': 0.16.0 + + '@eslint/core@0.16.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@8.1.1) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@8.1.1) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@eslint/js@9.38.0': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.0': + dependencies: + '@eslint/core': 0.16.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mui/core-downloads-tracker@7.3.4': {} + + '@mui/icons-material@7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/core-downloads-tracker': 7.3.4 + '@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@mui/types': 7.4.7(@types/react@19.2.2) + '@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@19.2.2) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-is: 19.2.0 + react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@types/react': 19.2.2 + + '@mui/private-theming@7.3.3(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0) + prop-types: 15.8.1 + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@mui/styled-engine@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.2.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + + '@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/private-theming': 7.3.3(@types/react@19.2.2)(react@19.2.0) + '@mui/styled-engine': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0) + '@mui/types': 7.4.7(@types/react@19.2.2) + '@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.2.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@types/react': 19.2.2 + + '@mui/types@7.4.7(@types/react@19.2.2)': + dependencies: + '@babel/runtime': 7.28.4 + optionalDependencies: + '@types/react': 19.2.2 + + '@mui/utils@7.3.3(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/types': 7.4.7(@types/react@19.2.2) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.2.0 + react-is: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.8.0 + + '@popperjs/core@2.11.8': {} + + '@rolldown/pluginutils@1.0.0-beta.38': {} + + '@rollup/rollup-android-arm-eabi@4.52.5': + optional: true + + '@rollup/rollup-android-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-x64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.5': + optional: true + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.9.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.9.1 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 24.9.1 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.1.0': + dependencies: + '@types/node': 24.9.1 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.0 + + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.0 + '@types/serve-static': 1.15.9 + + '@types/http-errors@2.0.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/mime@1.3.5': {} + + '@types/node@24.9.1': + dependencies: + undici-types: 7.16.0 + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.2.2(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + + '@types/react-transition-group@4.4.12(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + + '@types/react@19.2.2': + dependencies: + csstype: 3.1.3 + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 24.9.1 + + '@types/send@1.2.0': + dependencies: + '@types/node': 24.9.1 + + '@types/serve-static@1.15.9': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.9.1 + '@types/send': 0.17.5 + + '@types/strip-bom@3.0.0': {} + + '@types/strip-json-comments@0.0.30': {} + + '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0)(typescript@5.9.3))(eslint@9.38.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.46.2(eslint@9.38.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.38.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.2 + eslint: 9.38.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.46.2(eslint@9.38.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.2 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.38.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) + '@typescript-eslint/types': 8.46.2 + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.46.2': + dependencies: + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/visitor-keys': 8.46.2 + + '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.46.2(eslint@9.38.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.38.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.38.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.46.2': {} + + '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/visitor-keys': 8.46.2 + debug: 4.4.3(supports-color@8.1.1) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.46.2(eslint@9.38.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0) + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + eslint: 9.38.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.46.2': + dependencies: + '@typescript-eslint/types': 8.46.2 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react@5.0.4(vite@7.1.11(@types/node@24.9.1))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.38 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.1.11(@types/node@24.9.1) + transitivePeerDependencies: + - supports-color + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + after@0.8.2: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + append-transform@2.0.0: + dependencies: + default-require-extensions: 3.0.1 + + archy@1.0.0: {} + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + asap@2.0.6: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.4 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.8.19: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + binary-extensions@2.3.0: {} + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@8.1.1) + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-stdout@1.3.1: {} + + browserslist@4.26.3: + dependencies: + baseline-browser-mapping: 2.8.19 + caniuse-lite: 1.0.30001751 + electron-to-chromium: 1.5.237 + node-releases: 2.0.26 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + + buffer-from@1.1.2: {} + + bytes@3.1.2: {} + + caching-transform@4.0.0: + dependencies: + hasha: 5.2.2 + make-dir: 3.1.0 + package-hash: 4.0.0 + write-file-atomic: 3.0.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001751: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clean-stack@2.2.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commondir@1.0.1: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + connect-redis@8.1.0(express-session@1.18.2): + dependencies: + express-session: 1.18.2 + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-session@2.1.1: + dependencies: + cookies: 0.9.1 + debug: 3.2.7 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + transitivePeerDependencies: + - supports-color + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookiejar@2.1.4: {} + + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.1.0: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@1.2.0: {} + + decamelize@4.0.0: {} + + deep-is@0.1.4: {} + + default-require-extensions@3.0.1: + dependencies: + strip-bom: 4.0.0 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff@4.0.2: {} + + diff@5.2.0: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.1.3 + + dotenv@17.2.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + dynamic-dedupe@0.3.0: + dependencies: + xtend: 4.0.2 + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-to-chromium@1.5.237: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es6-error@4.1.1: {} + + esbuild@0.25.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.11 + '@esbuild/android-arm': 0.25.11 + '@esbuild/android-arm64': 0.25.11 + '@esbuild/android-x64': 0.25.11 + '@esbuild/darwin-arm64': 0.25.11 + '@esbuild/darwin-x64': 0.25.11 + '@esbuild/freebsd-arm64': 0.25.11 + '@esbuild/freebsd-x64': 0.25.11 + '@esbuild/linux-arm': 0.25.11 + '@esbuild/linux-arm64': 0.25.11 + '@esbuild/linux-ia32': 0.25.11 + '@esbuild/linux-loong64': 0.25.11 + '@esbuild/linux-mips64el': 0.25.11 + '@esbuild/linux-ppc64': 0.25.11 + '@esbuild/linux-riscv64': 0.25.11 + '@esbuild/linux-s390x': 0.25.11 + '@esbuild/linux-x64': 0.25.11 + '@esbuild/netbsd-arm64': 0.25.11 + '@esbuild/netbsd-x64': 0.25.11 + '@esbuild/openbsd-arm64': 0.25.11 + '@esbuild/openbsd-x64': 0.25.11 + '@esbuild/openharmony-arm64': 0.25.11 + '@esbuild/sunos-x64': 0.25.11 + '@esbuild/win32-arm64': 0.25.11 + '@esbuild/win32-ia32': 0.25.11 + '@esbuild/win32-x64': 0.25.11 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@5.2.0(eslint@9.38.0): + dependencies: + eslint: 9.38.0 + + eslint-plugin-react-refresh@0.4.24(eslint@9.38.0): + dependencies: + eslint: 9.38.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.47.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.47.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + eslint@9.38.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.1 + '@eslint/core': 0.16.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.38.0 + '@eslint/plugin-kit': 0.4.0 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + express-session@1.18.2: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.1.0 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fastfall@1.5.1: + dependencies: + reusify: 1.1.0 + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.0: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-root@1.1.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flat@5.0.2: {} + + flatted@3.3.3: {} + + foreachasync@3.0.0: {} + + foreground-child@2.0.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 3.0.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@2.1.5: + dependencies: + '@paralleldrive/cuid2': 2.2.2 + dezalgo: 1.0.4 + once: 1.4.0 + qs: 6.14.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fromentries@1.3.2: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@16.4.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + handlebars@4.7.7: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasha@5.2.2: + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hbs@4.2.0: + dependencies: + handlebars: 4.7.7 + walk: 2.3.15 + + he@1.2.0: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-escaper@2.0.2: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@2.1.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-hook@3.0.0: + dependencies: + append-transform: 2.0.0 + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + istanbul-lib-processinfo@2.0.3: + dependencies: + archy: 1.0.0 + cross-spawn: 7.0.6 + istanbul-lib-coverage: 3.2.2 + p-map: 3.0.0 + rimraf: 3.0.2 + uuid: 8.3.2 + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.4 + picocolors: 1.1.1 + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.flattendeep@4.4.0: {} + + lodash.merge@4.6.2: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + + make-error@1.3.6: {} + + marked@15.0.12: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge2@1.4.1: {} + + method-override@3.0.0: + dependencies: + debug: 3.1.0 + methods: 1.1.2 + parseurl: 1.3.3 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mkdirp@1.0.4: {} + + mocha@10.8.2: + dependencies: + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.6.0 + debug: 4.4.3(supports-color@8.1.1) + diff: 5.2.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.1.6 + ms: 2.1.3 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + yargs-unparser: 2.0.0 + + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + node-preload@0.2.1: + dependencies: + process-on-spawn: 1.1.0 + + node-releases@2.0.26: {} + + normalize-path@3.0.0: {} + + nyc@17.1.0: + dependencies: + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + caching-transform: 4.0.0 + convert-source-map: 1.9.0 + decamelize: 1.2.0 + find-cache-dir: 3.3.2 + find-up: 4.1.0 + foreground-child: 3.3.1 + get-package-type: 0.1.0 + glob: 7.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-hook: 3.0.0 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-processinfo: 2.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + make-dir: 3.1.0 + node-preload: 0.2.1 + p-map: 3.0.0 + process-on-spawn: 1.1.0 + resolve-from: 5.0.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + spawn-wrap: 2.0.0 + test-exclude: 6.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - supports-color + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@3.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@2.2.0: {} + + package-hash@4.0.0: + dependencies: + graceful-fs: 4.2.11 + hasha: 5.2.2 + lodash.flattendeep: 4.4.0 + release-zalgo: 1.0.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.3.0: {} + + path-type@4.0.0: {} + + pbkdf2-password@1.2.1: + dependencies: + fastfall: 1.5.1 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + process-on-spawn@1.1.0: + dependencies: + fromentries: 1.3.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + random-bytes@1.0.0: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-is@19.2.0: {} + + react-refresh@0.17.0: {} + + react-transition-group@4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + react@19.2.0: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + release-zalgo@1.0.0: + dependencies: + es6-error: 4.1.1 + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.52.5: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + send@1.2.0: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + spawn-wrap@2.0.0: + dependencies: + foreground-child: 2.0.0 + is-windows: 1.0.2 + make-dir: 3.1.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + which: 2.0.2 + + sprintf-js@1.0.3: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + stylis@4.2.0: {} + + superagent@8.1.2: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@8.1.1) + fast-safe-stringify: 2.1.1 + form-data: 4.0.4 + formidable: 2.1.5 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + supertest@6.3.4: + dependencies: + methods: 1.1.2 + superagent: 8.1.2 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-table@0.2.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tree-kill@1.2.2: {} + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-node-dev@2.0.0(@types/node@24.9.1)(typescript@5.9.3): + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.11 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@types/node@24.9.1)(typescript@5.9.3) + tsconfig: 7.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + + ts-node@10.9.2(@types/node@24.9.1)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.9.1 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig@7.0.0: + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + + tsscmp@1.0.6: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.8.1: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typescript-eslint@8.46.2(eslint@9.38.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0)(typescript@5.9.3))(eslint@9.38.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.2(eslint@9.38.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.38.0)(typescript@5.9.3) + eslint: 9.38.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true + + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + undici-types@7.16.0: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@8.3.2: {} + + v8-compile-cache-lib@3.0.1: {} + + vary@1.1.2: {} + + vhost@3.0.2: {} + + vite@7.1.11(@types/node@24.9.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.9.1 + fsevents: 2.3.3 + + walk@2.3.15: + dependencies: + foreachasync: 3.0.0 + + which-module@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + workerpool@6.5.1: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.2: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@20.2.9: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..e9b0dad --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'apps/*' + - 'packages/*'