diff --git a/.github/workflows/sentry-pii-guard.yml b/.github/workflows/sentry-pii-guard.yml new file mode 100644 index 0000000..70dc683 --- /dev/null +++ b/.github/workflows/sentry-pii-guard.yml @@ -0,0 +1,28 @@ +# R31 — block sendDefaultPii: true from ever landing. +# +# This is a defense-in-depth gate: the privacy posture is also enforced by +# the centralized init helpers in core-shared/instrumentation/sentry/, but +# this grep makes any drift impossible to merge. + +name: Sentry PII guard (R31) + +on: + pull_request: + push: + branches: [main] + +jobs: + pii-guard: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify sendDefaultPii is never true + run: | + if grep -RIn --include='*.ts' --include='*.tsx' --include='*.mjs' --include='*.cjs' --include='*.js' \ + --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=.turbo \ + -E 'sendDefaultPii\s*:\s*true' \ + packages/ apps/; then + echo "::error::R31 violation — sendDefaultPii: true is forbidden anywhere in the repo." + exit 1 + fi + echo "OK — no sendDefaultPii: true detected." diff --git a/packages/core-eslint/base.js b/packages/core-eslint/base.js index f839153..e1607f2 100644 --- a/packages/core-eslint/base.js +++ b/packages/core-eslint/base.js @@ -3,10 +3,17 @@ import eslintConfigPrettier from "eslint-config-prettier"; import tseslint from "typescript-eslint"; import turboPlugin from "eslint-plugin-turbo"; import boundaries from "eslint-plugin-boundaries"; +import globals from "globals"; export default [ { ignores: ["dist/**", "node_modules/**", ".next/**", ".turbo/**", "storybook-static/**"] }, js.configs.recommended, + { + files: ["**/*.{mjs,cjs,js}", "**/*.config.{ts,tsx}"], + languageOptions: { + globals: { ...globals.node }, + }, + }, ...tseslint.configs.recommended, eslintConfigPrettier, { @@ -15,6 +22,19 @@ export default [ "turbo/no-undeclared-env-vars": "warn", }, }, + { + rules: { + // Honour the leading-underscore convention for intentionally-unused params/vars. + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, { plugins: { boundaries }, settings: { @@ -45,4 +65,42 @@ export default [ ], }, }, + // R40 — block direct @sentry/* imports outside the allowlisted instrumentation paths + { + files: ["**/*.{ts,tsx,mjs,cjs,js}"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["@sentry/*"], + message: + "Import from @repo/core-shared/instrumentation instead — feature packages must not depend on Sentry directly (R40).", + }, + ], + }, + ], + }, + }, + // R40 allowlist — the only paths permitted to import @sentry/*. + // Patterns are double-star prefixed so they match whether eslint runs from + // the repo root or from inside a sub-package. + { + files: [ + "**/instrumentation/sentry/**", + "**/instrumentation/di/bind-sentry-instrumentation.{ts,js}", + "**/instrumentation/di/bind-sentry-instrumentation.test.{ts,js}", + "**/setup/no-sentry.{ts,js}", + "**/setup/no-sentry.test.{ts,js}", + "**/instrumentation.{ts,js,mjs}", + "**/instrumentation-client.{ts,js,mjs}", + "**/next.config.{mjs,ts,js}", + "**/vite.config.{ts,mjs,js}", + "**/sentry.*.config.{ts,mjs,js}", + ], + rules: { + "no-restricted-imports": "off", + }, + }, ]; diff --git a/packages/core-eslint/package.json b/packages/core-eslint/package.json index 17659fc..7e5a4a7 100644 --- a/packages/core-eslint/package.json +++ b/packages/core-eslint/package.json @@ -17,5 +17,8 @@ "eslint-plugin-boundaries": "^4.2.2", "eslint-plugin-turbo": "^2.4.0", "typescript-eslint": "^8.25.0" + }, + "dependencies": { + "globals": "^17.6.0" } } diff --git a/packages/core-testing/src/contract/define-contract-suite.test.ts b/packages/core-testing/src/contract/define-contract-suite.test.ts index 7273193..223bc3e 100644 --- a/packages/core-testing/src/contract/define-contract-suite.test.ts +++ b/packages/core-testing/src/contract/define-contract-suite.test.ts @@ -42,6 +42,7 @@ describe("defineContractSuite — getTracer plumbing (R50)", () => { // Vitest defers actual assertion to the `it`; we verify the wiring by re-reading after. // (This is a meta-test of plumbing only — the inner it() runs as a child describe.) expect(typeof tracer.startSpan).toBe("function"); + void receivedTracer; }); it("getTracer is undefined when opts.tracer not provided (backward compat)", () => { diff --git a/packages/marketing-pages/src/application/use-cases/get-site-settings.use-case.ts b/packages/marketing-pages/src/application/use-cases/get-site-settings.use-case.ts index 0b7bf59..e5d8e42 100644 --- a/packages/marketing-pages/src/application/use-cases/get-site-settings.use-case.ts +++ b/packages/marketing-pages/src/application/use-cases/get-site-settings.use-case.ts @@ -16,7 +16,6 @@ export type IGetSiteSettingsUseCase = ReturnType; export const getSiteSettingsUseCase = (siteSettingsRepository: ISiteSettingsRepository) => - // eslint-disable-next-line @typescript-eslint/no-unused-vars async (_input: GetSiteSettingsInput): Promise => { const result = await siteSettingsRepository.getSiteSettings(); return getSiteSettingsOutputSchema.parse(result); diff --git a/packages/marketing-pages/src/di/bind-dev-seed.ts b/packages/marketing-pages/src/di/bind-dev-seed.ts index c8d52b1..b7a87cd 100644 --- a/packages/marketing-pages/src/di/bind-dev-seed.ts +++ b/packages/marketing-pages/src/di/bind-dev-seed.ts @@ -7,7 +7,6 @@ import { import { marketingPagesContainer } from "./container.js"; import { MARKETING_PAGES_SYMBOLS } from "./symbols.js"; import { MockPagesRepository } from "../infrastructure/repositories/pages.repository.mock.js"; -import { MockSiteSettingsRepository } from "../infrastructure/repositories/site-settings.repository.mock.js"; import { buildDevPages, buildDevSiteSettings } from "../__seeds__/dev.js"; import { getSiteSettingsUseCase } from "../application/use-cases/get-site-settings.use-case.js"; import { getPageBySlugUseCase } from "../application/use-cases/get-page-by-slug.use-case.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cadcdd0..7216b6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,6 +480,10 @@ importers: version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0) packages/core-eslint: + dependencies: + globals: + specifier: ^17.6.0 + version: 17.6.0 devDependencies: '@eslint/js': specifier: ^9.20.0 @@ -4956,6 +4960,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -11864,6 +11872,8 @@ snapshots: globals@14.0.0: {} + globals@17.6.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {}