import js from '@eslint/js' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['**/dist/**', '**/node_modules/**', '**/*.tsbuildinfo'] }, js.configs.recommended, tseslint.configs.recommended, { // Plain-JS tooling runs on Node. no-undef is off for .ts files by default, // because the compiler answers that question better. // Build and CI tooling, wherever it lives — the repository's scripts/, the // per-package ones a build step invokes, and the ones a workflow runs. files: [ 'scripts/**/*.{js,mjs,cjs}', 'packages/*/scripts/**/*.{js,mjs,cjs}', '.github/scripts/**/*.{js,mjs,cjs}', ], languageOptions: { // Listed rather than pulled from a `globals` package: the list being // short is the point. A script here reaching for something not on it is // worth noticing, and `fetch` and `setTimeout` earned their place by the // release plan asking the registry what it already holds. globals: { console: 'readonly', process: 'readonly', URL: 'readonly', fetch: 'readonly', setTimeout: 'readonly', // Beside `setTimeout`, and it earned its place the same way: the seed // harness arms a deadline for the stub API's port and disarms it, and a // timer nobody clears keeps a check's process alive after it has // answered. clearTimeout: 'readonly', // Earned its place the same way: the WebAuthn end-to-end signs a token // with a secret it builds here, exactly as the suites do. TextEncoder: 'readonly', // For the *browser* side of a `page.evaluate`, which is lexically in // these files and runs somewhere else entirely. `sessionStorage` and // friends are reached through this rather than being listed, because // listing them would tell the linter a Node script may use them — and // one that did would fail at run time with the check green. globalThis: 'readonly', }, }, }, { rules: { // The permission model reads better with explicit, narrow types than // with inferred ones; these two catch the drift that hides a widening. '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'error', }, }, { // A bare `decodeURIComponent` over a captured path segment throws // `URIError` on a malformed escape (`/v1/documents/%ZZ` — WHATWG `URL` // leaves the invalid escape in `pathname` verbatim), which the error // boundary turns into a `500` and a spurious `error` audit row where the // answer is the `404` every unknown id gets. Twelve routes decoded one by // hand; they all go through `pathMatch` in `server.ts` now, and this is // what stops the thirteenth being written the old way. The one legitimate // reader outside it is `core/redis.ts`, decoding credentials out of a URL // an operator wrote — a URIError there is a configuration error at // startup, not a caller-reachable throw. files: ['packages/api/src/**/*.ts', 'packages/mcp/src/**/*.ts'], ignores: ['**/__tests__/**'], rules: { 'no-restricted-syntax': [ 'error', { selector: "CallExpression[callee.name='decodeURIComponent']:not(FunctionDeclaration[id.name='pathMatch'] CallExpression)", message: 'A malformed percent-escape throws URIError and becomes a 500. Route segments go through pathMatch (server.ts), which answers the 404 an unknown path gets.', }, ], }, }, )