Skip to main content

Directory Web Template

The Directory Web Template is a modern, full-stack directory website solution built with Next.js 16 and organized as a Turborepo monorepo. It's designed to help you create professional directory websites for tools, services, products, or any other type of listing platform.

Key Features

  • Modern Tech Stack: Next.js 16, React 19, TypeScript, Tailwind CSS, HeroUI React
  • Turborepo Monorepo: pnpm workspaces with shared configs, web app, e2e tests, and docs
  • Flexible Authentication: NextAuth.js v5, Supabase Auth, OAuth providers (Google, GitHub, Facebook, Twitter, Microsoft)
  • Payment Integration: Stripe, LemonSqueezy, Polar, subscription management
  • Internationalization: Multiple languages supported with full RTL support via next-intl
  • Git-based CMS: Content synchronization from Git repositories with YAML-based structure
  • Theming System: Built-in themes with dynamic color generation
  • Maps & Location: Mapbox / Google Maps abstraction with marker clustering, auto-geocoding from YAML addresses, and an opt-in Map view for listings (sidebar + map, Zillow / Airbnb style)
  • Analytics & Monitoring: PostHog, Sentry, performance monitoring
  • Admin Dashboard: Content management, user management, and analytics
  • SEO Optimized: Sitemap generation, structured data (JSON-LD), meta tags

Quick Start

# Clone the monorepo
git clone https://github.com/ever-works/directory-web-template.git
cd directory-web-template

# Install dependencies (pnpm required)
pnpm install

# Set up environment for the web app
cp apps/web/.env.example apps/web/.env.local
# Edit apps/web/.env.local with your configuration

# Start development server
pnpm run dev

Visit http://localhost:3000 to see your site!

Next Steps

For Contributors & AI Agents

The template uses spec-driven development following the GitHub Spec Kit convention. Every non-trivial change goes through a spec → plan → tasks trio.

  • AGENTS.md -- Cross-cutting rules for any AI agent operating in this monorepo.
  • .specify/README.md -- Spec Kit workflow.
  • .specify/memory/constitution.md -- Ten durable principles every plan must respect.
  • Spec Index -- Per-feature spec/plan/tasks documents under docs/spec/.
  • Change Log -- Running log of doc and spec changes.
  • Open Questions -- Open questions with chosen defaults.
  • Plugin System (Architecture) -- The pluggable, modular core (see Spec 002).
  • Authoring a Plugin -- Walk-through for authoring your first plugin.
  • Plugin Lifecycle -- Boot, validation, enable/disable, and teardown semantics every plugin must cooperate with.
  • Testing a Plugin -- Unit-test the manifest, register through createTestRegistry, render slots in isolation, and add a Playwright smoke spec.
  • Plugin Capabilities Reference -- Complete auth / payment / analytics / search / content-source / maps / newsletter / notifications / ai / ui-slot capability surface, with lookup-style and provider-resolution rules.
  • Plugin Slots Reference -- Per-slot contract for every canonical slot id (header.left, home.before-listing, admin.dashboard.widgets, …) with composition rules and the checklist for adding a new slot.
  • Plugin Loader Reference -- Per-API reference for loadPlugins / mergeConfigSources paired with packages/plugin-runtime/src/loader.ts: env / DB / override precedence, the failure matrix, and how to test the loader directly.
  • Plugin Registry Reference -- Per-API reference for the PluginRegistry class paired with packages/plugin-runtime/src/registry.ts: register / enable / disable / get / list / slotsFor / list_all semantics, the read-vs-write surface summary, and the duplicate-name / unregistered / throwing-teardown failure matrix.
  • Plugin SlotHost Reference -- Per-component reference for <SlotHost slotId registry fallback? /> paired with packages/plugin-runtime/src/SlotHost.tsx: the slotId / registry / fallback props, the empty-vs-non-empty rules, server-friendliness, the composition rules that follow from slotsFor, and the failure matrix that anchors stable React keys on the registry's duplicate-name guarantee.
  • Plugin Testing Reference -- Per-helper reference for createTestRegistry({ plugins }) paired with packages/plugin-runtime/src/testing.ts: the four-step internal flow over new PluginRegistry() + loadPlugins, the read / write surface summary, the failure matrix that distinguishes silent Zod drops from propagated duplicate-name throws, the dual import surface (@ever-works/plugin-runtime barrel and @ever-works/plugin-runtime/testing sub-path), the three worked Vitest examples (happy path, config-required, disable round-trip), the five anti-patterns, and the explicit non-goals that point at loadPlugins and new PluginRegistry() for non-default config, persistence-callback assertions, and rejection inspection.
  • Plugin Manifest Reference -- Per-field reference for PluginManifest<C> paired with packages/plugin-sdk/src/manifest.ts: every manifest field (name, version, description, templateRange, capabilities, config, defaultEnabled, adminToggleable, homepage) with its type, default, semantics, and authoring guidance; the PluginConfig<C> type alias; the failure matrix that maps templateRange and config failures onto LoadPluginsResult.rejected[name].reason, distinguishes the duplicate-name throw as the only manifest-level propagated failure, and clarifies that adminToggleable is a UI hint (not an authorization check); plus the checklist for adding a new manifest field that pairs the SDK source change with the docs/log.md entry.
  • Plugin Definition Reference -- Per-export reference for defineDirectoryPlugin paired with packages/plugin-sdk/src/plugin.ts: the factory's role in inferring C extends z.ZodTypeAny from manifest.config; the DirectoryPlugin<C> shape (manifest, optional setup / teardown hooks, slots, providers); the PluginContext<TConfig> runtime context handed to setup and every slot component (config, name, enabled, optional logger); the SlotComponentProps<TConfig> slot-component contract that limits the props surface to a single ctx field; the PluginProviders map keyed on Capability (with 'ui-slot' typed as never) and the PluginSlots<TConfig> map keyed on SlotId; the read / write surface summary that maps every caller (plugin author, loadPlugins, PluginRegistry.register, PluginRegistry.disable, <SlotHost />, createTestRegistry, slot components) to the fields they touch; and the failure matrix that surfaces every observable failure in the loader / registry / <SlotHost /> layers the plugin returns into (hand-rolled plugin loses C inference, duplicate name throws via register, manifest.config / templateRange rejections route through LoadPluginsResult.rejected, throwing setup is plugin-local, throwing teardown is swallowed by disable, slot components throw through React, and TypeScript catches 'ui-slot' provider attempts and unknown slot ids at compile time).
  • Plugin Providers Reference -- Per-export reference for the nine concrete capability-provider interfaces paired with packages/plugin-sdk/src/providers.ts: one section per AuthProvider, PaymentProvider, AnalyticsProvider, SearchProvider, ContentSource, MapsProvider, NewsletterProvider, NotificationsProvider, AIProvider, with every member's type, nullability, and per-member type-system notes (the (string & {}) literal-with-fallback trick on PaymentProvider.id that keeps the union open without giving up autocomplete, the Promise<unknown[]> widening contract on SearchProvider.search that defers Item-shape assertion to the host, the Promise<unknown | undefined> absent-vs-error distinction on ContentSource.getItem, the void | Promise<void> sync-or-async pattern on optional hooks, the { ok; reason? } result envelope on NewsletterProvider that surfaces provider-specific failures as data); the CapabilityProviderMap mapped type that binds each Capability member to its interface and types PluginRegistry.get<C> / list<C> and PluginProviders generically; the 'ui-slot' = never lockout that turns providers: { 'ui-slot': anything } into a TypeScript compile error; the read / write surface that maps every caller (plugin author, defineDirectoryPlugin, PluginRegistry.register, get<C>, list<C>, <SlotHost />, host code under apps/web/lib/<capability>/**) to the fields they touch; and the failure matrix that maps every observable failure (compile-time mis-typing, throwing setupLoadPluginsResult.rejected[name].reason: 'setup', fan-out swallow vs. single-lookup propagation, runtime malformed shape, two enabled plugins on the same single-lookup capability) onto the layer that surfaces it.
  • Plugin Packages -- SDK, runtime, and demo packages overview.
  • Reference Plugin (@ever-works/plugin-demo) -- Per-source-file reference for the bundled reference / demo plugin paired with packages/plugin-demo/src/index.tsx, config.ts, and Header.tsx: the at-a-glance manifest summary (name 'demo', templateRange '>=0.1 <1.0', 'ui-slot' capability, 'header.right' slot, defaultEnabled: true, adminToggleable: true); the file map (config → manifest schema, Header → slot component, index → defineDirectoryPlugin composition); the per-line walk-through of ConfigSchema and DemoConfig (Zod defaults that make the inferred type non-optional, the enabled / greeting two-key surface); the DemoHeaderBadge props / render contract / disabled-config short-circuit and the stable data-plugin="demo" / data-testid="demo-plugin-badge" test hooks; the defineDirectoryPlugin invocation broken down by manifest field and slot binding with the type-inference path that ties ConfigSchema to SlotComponentProps<DemoConfig>; the three call sites (loader Zod parse + register, registry key, slot host render); the failure matrix that maps demo-plugin manifestations onto the loader / registry / slot-host failure surfaces (Zod-rejected config, templateRange mismatch, admin override flipping enabled, duplicate-name throw); the replace-the-demo-plugin recipe that exercises the slot ordering guarantee + admin toggle + defaultEnabled: false lever without removing the reference package from tree; and the evolution checklist that pairs every source-file change with the matching SDK reference page and docs/log.md entry.
  • Plugin SDK Public Surface Reference -- Per-source-file reference for the SDK barrel paired with packages/plugin-sdk/src/index.ts: every public name the barrel re-exports (CAPABILITIES, isCapability, Capability, SLOT_IDS, isSlotId, SlotId, PluginManifest<C>, PluginConfig<C>, the nine capability-provider interfaces and CapabilityProviderMap, defineDirectoryPlugin, and the five plugin-shape types) split by kind (value vs. type-only) with one row per export and a link to the per-source-file reference that owns its shape; the package.json#exports sub-path map (., ./capabilities, ./slots) and the rationale for keeping manifest, providers, plugin, loader, registry, and SlotHost reachable only through the barrel; the per-line walkthrough of index.ts that pins each line to a documentation impact (the JSDoc preamble's framework-agnostic / React-as-peer / architecture-cross-link invariants, the capability re-exports, the slot re-exports, the manifest type re-exports, the provider type re-exports, the defineDirectoryPlugin value re-export, and the plugin-shape type re-exports); the value-vs-type contract that calls out moving a name across the boundary as a breaking change and points at @typescript-eslint/consistent-type-exports as the lint rule; the failure matrix that maps barrel-level mistakes (non-public sub-path import, value-vs-type mis-import, lost C inference when authors skip defineDirectoryPlugin, capability not added to CAPABILITIES, dropped sideEffects flag) onto the layer that surfaces them; and the public-surface change checklist that ties any addition / removal back to Spec Kit, the docs/log.md entry, and the pnpm tsc --noEmit / Playwright verification step.
  • Plugin Runtime Public Surface Reference -- Per-source-file reference for the runtime barrel paired with packages/plugin-runtime/src/index.ts: every public name the barrel re-exports (PluginRegistry, loadPlugins, mergeConfigSources, PluginConfigSources, LoadPluginsResult, SlotHost, SlotHostProps, createTestRegistry) split by kind (value vs. type-only) and grouped by the three concerns the runtime owns -- the host-app boot pipeline (PluginRegistry + loadPlugins + mergeConfigSources + PluginConfigSources + LoadPluginsResult), the React render surface (SlotHost + SlotHostProps), and the unit-test helper (createTestRegistry); the package.json#exports sub-path map (., ./registry, ./SlotHost, ./loader, ./testing) and the rationale for keeping the four narrowed sub-paths so a server action can import PluginRegistry from @ever-works/plugin-runtime/registry without dragging React into the server bundle, a test file can import createTestRegistry from @ever-works/plugin-runtime/testing without spinning up a JSDOM environment, and a host layout can import <SlotHost /> from @ever-works/plugin-runtime/SlotHost to keep the React boundary explicit in bundle reports; the per-line walkthrough of index.ts that pins each line to a documentation impact (the JSDoc preamble's React-aware-only-in-SlotHost / owns-registry-loader-host / cross-link invariants, the registry value re-export, the loader value and type re-exports, the slot-host value and props re-exports, and the testing-helper value re-export with the explicit no-export type companion line because the helper's options object is an inline anonymous type); the value-vs-type contract that locks moving a name across the boundary as a breaking change and points at @typescript-eslint/consistent-type-exports as the lint rule; the failure matrix that maps barrel-level mistakes (non-public sub-path import, value-vs-type mis-import on LoadPluginsResult, treeshake-stripped PluginRegistry constructor, registry-instance mismatch between loadPlugins and <SlotHost />, dropped sideEffects flag pulling the entire runtime into a mergeConfigSources-only consumer, React leaking into a server bundle when a host action imports from the barrel instead of the narrowed ./registry sub-path) onto the layer that surfaces them; and the public-surface change checklist that ties any addition / removal back to Spec Kit, the docs/log.md entry, and the pnpm tsc --noEmit / Playwright verification step.
  • Plugin SDK Package Manifest Reference -- Per-source-file reference for the SDK package manifest paired with packages/plugin-sdk/package.json: every load-bearing field (name, version, license, private, type: "module", sideEffects: false, main, types, exports."." / ./capabilities / ./slots, files, scripts.typecheck / scripts.lint, dependencies.zod, peerDependencies.react with peerDependenciesMeta.react.optional, and the devDependencies set) with its purpose, why-it-matters note, and the change-event-class it implies for plugin authors; the sub-path map that locks the barrel-vs-narrowed contract (the two narrowed sub-paths are a strict subset of the barrel and resolve to the same module instance via Node's path-keyed module cache, and manifest.ts / providers.ts / plugin.ts / index.ts deliberately have no narrowed sub-path because their exports are types-only or single-author-facing-factory); the failure matrix that maps each manifest-level mistake (non-public sub-path import, CJS-without-import(), dropped sideEffects flag, non-workspace:* specifier, React-18-typings, Zod-3-schema, public-name-without-exports-entry, file-without-barrel-re-export, breaking version bump) onto the layer that surfaces them; and the public-surface change checklist that ties any field change to a SDK Public Surface cross-check, a packages.md cross-check, an apps/web/package.json peer-range / Zod-major propagation check, a docs/log.md entry, an open-questions register entry, the pnpm tsc --noEmit and Playwright smoke-spec verification step, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability).
  • Plugin Runtime Package Manifest Reference -- Per-source-file reference for the runtime package manifest paired with packages/plugin-runtime/package.json: every load-bearing field (name, version, description, license, private, type: "module", sideEffects: false, main, types, exports."." / ./registry / ./SlotHost / ./loader / ./testing, files, scripts.typecheck / scripts.lint, dependencies.@ever-works/plugin-sdk with workspace:*, dependencies.zod, peerDependencies.react without peerDependenciesMeta.react.optional (unlike the SDK because <SlotHost /> is a React function component), and the devDependencies set) with its purpose, why-it-matters note, and the change-event-class it implies for host-app authors; the sub-path map that locks the barrel-vs-narrowed contract (the four narrowed sub-paths are a strict subset of the barrel and resolve to the same module instance via Node's path-keyed module cache, and each one isolates a different concern -- ./registry keeps React out of server-only callers, ./loader is the boot pipeline, ./SlotHost makes the React boundary explicit, ./testing keeps JSDOM out of server-side unit tests); the failure matrix that maps each manifest-level mistake (non-public sub-path import, server action importing PluginRegistry from the barrel instead of ./registry, lowercased slothost, CJS-without-import(), dropped sideEffects flag, non-workspace:* specifier, runtime-version-diverges-from-SDK-version, host-installs-no-React, Zod-3-schema, public-name-without-exports-entry, file-without-barrel-re-export) onto the layer that surfaces them; and the public-surface change checklist that ties any field change to a Runtime Public Surface cross-check, a SDK Package Manifest cross-check, a packages.md cross-check, an apps/web/package.json peer-range / Zod-major propagation check, a docs/log.md entry, an open-questions register entry, the pnpm tsc --noEmit and Playwright smoke-spec verification step, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability).
  • Plugin Demo Package Manifest Reference -- Per-source-file reference for the demo plugin package manifest paired with packages/plugin-demo/package.json: every load-bearing field (name, version, description, license, private, type: "module", sideEffects: false, main, types, exports."." (single entry pointing at ./src/index.tsx because the entry composes JSX), files, scripts.typecheck / scripts.lint, dependencies.@ever-works/plugin-sdk with workspace:*, dependencies.zod, peerDependencies.react without peerDependenciesMeta.react.optional (because Header.tsx ships a slot component that always needs React), and the devDependencies set) with its purpose, why-it-matters note, and the change-event-class it implies for downstream plugin authors who copy this manifest as a starting point; the deliberately-empty sub-path map (no narrowed sub-paths because the demo is a leaf consumer with a single default export — narrowing would imply public structure inside Header.tsx / config.ts which the demo intentionally hides); the manifest.version vs. package.json#version drift contract (the manifest version gates templateRange; the package version is workspace-graph metadata only); the .tsx-vs-.ts-extension-on-the-entry rationale (the entry composes JSX through Header.tsx, so .tsx opens the JSX scope under jsx: "preserve"); the failure matrix that maps demo-level manifestations onto the resolution / type-check / install layer (non-public sub-path import like @ever-works/plugin-demo/Header, CJS-without-import(), dropped sideEffects flag, non-workspace:* SDK specifier, .tsx-flipped-to-.ts, React-18-typings, Zod-3-schemas, manifest.version/package.json#version drift, templateRange widened beyond SDK version, downstream-author-keeps-@ever-works-scope, downstream-author-keeps-private: true-while-publishing, downstream-author-keeps-required-React-peer-on-non-React-plugin); and the public-surface change checklist that ties any field change to a Reference Plugin cross-check, an SDK Package Manifest and Runtime Package Manifest cross-check (the three manifests move in lock-step on version, Zod range, React peer range, and sideEffects flag), a packages.md cross-check, an apps/web/package.json lockfile cross-check, a docs/log.md entry, an open-questions register entry, the pnpm tsc --noEmit and Playwright smoke-spec verification step, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability).
  • Plugin Package TypeScript Configurations -- Per-source-file reference for the three byte-identical tsconfig.json files at packages/plugin-sdk/tsconfig.json, packages/plugin-runtime/tsconfig.json, and packages/plugin-demo/tsconfig.json: the shared five-line shape (extends: "@ever-works/tsconfig/base.json", compilerOptions.jsx: "react-jsx", compilerOptions.types: ["react"], include: ["src/**/*"], exclude: ["node_modules", "dist"]); the field-by-field walkthrough that pins each line to a documentation impact (the inherited base config's target: "ES2017" lowering floor, lib: ["dom", "dom.iterable", "esnext"] DOM types, allowJs: true escape hatch, skipLibCheck: true transitive-typings opt-out, strict: true load-bearing flag, noEmit: true no-build-step posture, esModuleInterop: true Zod CJS-shim compatibility, module: "esnext" + moduleResolution: "bundler" ESM-with-bundler resolution, resolveJsonModule: true JSON import support, isolatedModules: true swc/esbuild compatibility, incremental: true .tsbuildinfo cache); the react-jsx automatic-runtime rationale (no import React from 'react' needed in .tsx files; the SDK's plugin.ts references React.ComponentType types so the JSX scope must be open even where no JSX is authored); the types: ["react"] whitelist semantics (transitive @types/node / @types/jest / DOM-polyfill packages cannot leak ambient types into the plugin's compilation); the include-and-exclude rationale that locks the package boundary at src/ and forward-guards against a future dist/ build step; the "How the three packages diverge from this baseline" matrix that lists every hypothetical override (types: ["react", "node"], declaration: true, outDir, composite: true, lib: ["esnext"], widened include for scripts, narrowed exclude for tests) with the reason it is and is not warranted today; the failure matrix that maps each tsconfig.json mistake (JSX element implicitly has type 'any' from a dropped React-types entry, Cannot use JSX unless the '--jsx' flag is provided from a removed JSX flag, 'process' is not defined from a missing Node-types entry, slow pnpm tsc --noEmit from an incremental: false regression, stray @types/jest symbols from a removed types whitelist, dist/index.js has not been built from source from an accidental noEmit: false, 'isolatedModules' may not be used with 'composite' from a composite: true override, the demo's Cannot find module 'react/jsx-runtime' symptom of a React-18 lockfile downgrade, and a downstream plugin's silent-strict-mode regression from a missed extends directive) onto the layer that surfaces them; and the public-surface change checklist that ties any option change to a matching package-manifest cross-check, an Authoring a Plugin cross-check, a packages.md cross-check, the dual pnpm tsc --noEmit runs (workspace-root + per-package), a docs/log.md entry, an open-questions register entry, and the Constitution-Check note in the PR description for Article II (TypeScript-Only) and Article III (Public-Surface Stability).
  • Shared ESLint Config (@ever-works/eslint-config) -- Per-source-file reference for the workspace's shared ESLint flat config paired with packages/eslint-config/nextjs.mjs and packages/eslint-config/package.json: the at-a-glance summary (single sub-path ./nextjs, default factory nextjsConfig(tsconfigPath = './tsconfig.json'), ESLint v9 flat config format, eslint@^9 peer-dep, four direct deps @typescript-eslint/eslint-plugin, @typescript-eslint/parser, eslint-plugin-react, eslint-plugin-react-hooks); the file map (nextjs.mjs ships the factory, package.json declares the sub-path and deps); the per-block walk-through of the three flat-config blocks the factory returns (block 1 ignores for **/node_modules/**, **/.next/**, **/out/**, **/build/**, **/dist/**, **/*.config.{js,ts,mjs} with the rationale for each; block 2 JS/TS shared rules for *.{js,jsx,ts,tsx} with react-hooks/rules-of-hooks: 'error', react-hooks/exhaustive-deps: 'warn', the deliberate 'no-unused-vars': 'off' to defer to the TS-aware variant, 'no-console': 'off' to allow the structured-logging convention; block 3 TS-only typed rules for *.{ts,tsx} with the typed parser threading parserOptions.project: tsconfigPath, @typescript-eslint/no-unused-vars: 'warn' with the ^_ prefix convention for _request, catch (_) { ... }, and head-discarded destructuring); the package.json field-by-field walkthrough (name, version: '0.0.0', private: true, license: AGPL-3.0, exports."./nextjs", the four direct deps with their workspace-floor ranges, the eslint@^9 peer-dep that pins the flat-config format); the consumer table that maps the four current consumers (apps/web, apps/docs, apps/web-e2e, plugin packages) onto how each calls nextjsConfig(...) and the Phase-D plan to wire the per-package lint gate; the failure matrix that maps each configuration mistake (Cannot find module '@ever-works/eslint-config/nextjs' from a lost sub-path entry, Parsing error: Cannot find module '@typescript-eslint/parser' from a stale ESLint-8 lockfile, Configuration for rule "react-hooks/rules-of-hooks" is invalid from a stale plugin pin, '_request' is defined but never used from a re-enabled JS no-unused-vars, 'console' is not defined from a flipped no-console, raised-to-error react/jsx-key from a consumer override, invalid tsconfigPath, .next/-build-output linting from a removed ignore, tooling-config linting from a removed *.config.* ignore, eslintrc-syntax-mixed-with-flat-config from a regression, eslint@9.x not found peer-dep refusal) onto the layer that surfaces them; and the public-surface change checklist that ties any rule or field change to a Plugin Package TypeScript Configurations cross-check, an Authoring a Plugin cross-check, an apps/web/eslint.config.mjs propagation check, the workspace-root pnpm lint run, the pnpm install lockfile run, a docs/log.md entry, an open-questions register entry, and the Constitution-Check note in the PR description for Article II (TypeScript-Only) and Article IX (Test Coverage Bar).
  • Shared TypeScript Presets (@ever-works/tsconfig) -- Per-source-file reference for the workspace's shared TypeScript preset package paired with packages/tsconfig/base.json, packages/tsconfig/nextjs.json, packages/tsconfig/playwright.json, and packages/tsconfig/package.json: the at-a-glance summary (package name @ever-works/tsconfig, private: true, version: '0.0.0' pinned because consumed via workspace:* only, three preset files declared in package.json#files, six current consumers across apps/web, apps/web-e2e, and the three plugin packages, no dependencies / devDependencies / peerDependencies / scripts); the file map (base.json is the workspace's TypeScript posture, nextjs.json is the Next.js leaf, playwright.json is the Playwright leaf, package.json declares the package and files array); the per-field walk-through of base.json (target: 'ES2017' lowering floor, lib: ['dom', 'dom.iterable', 'esnext'] ambient types, allowJs: true for .mjs tooling configs, skipLibCheck: true transitive-typings escape hatch, strict: true and the seven sub-flags it enables, noEmit: true no-build-step posture, esModuleInterop: true Zod CJS-shim compatibility, module: 'esnext' + moduleResolution: 'bundler' ESM-with-bundler resolution, resolveJsonModule: true JSON import support, isolatedModules: true swc/esbuild compatibility, incremental: true .tsbuildinfo cache that cuts CI lint times from ~45s to ~12s, exclude: ['node_modules']); the per-field walk-through of nextjs.json (relative extends: './base.json' for inheritance lock, jsx: 'react-jsx' automatic React-17+ runtime, plugins: [{ name: 'next' }] editor-only LSP plugin); the per-field walk-through of playwright.json (relative extends: './base.json', types: ['node'] whitelist, redundant noEmit: true re-pin); the per-field walk-through of package.json (name, version: '0.0.0', private: true, license: AGPL-3.0, files whitelist) plus the matrix of deliberately-absent fields (type, main, types, exports, dependencies, devDependencies, peerDependencies, scripts) and what each absence implies; the inheritance ASCII diagram showing the two leaves fanning out from base.json and the three plugin packages bypassing the leaves to extend base.json directly; the consumer table that maps each of the six current consumers to its extends target with the rationale for the choice (Next.js wants react-jsx and the LSP plugin, Playwright wants @types/node and no DOM globals, plugins want react-jsx without the LSP plugin); the deliberate apps/docs out-of-scope note (extends @docusaurus/tsconfig directly because Docusaurus ships its own preset for .mdx ambient typings, tracked in docs/questions.md); the cross-cutting concerns walkthrough (target: 'ES2017' and what it covers / doesn't cover, module + moduleResolution pair semantics, strict sub-flags, incremental cache mechanics); the "How the leaves diverge from the base" matrix that lists every option each leaf adds or overrides plus the per-consumer concerns (paths, baseUrl, outDir, declaration, composite, target override, lib override) deliberately excluded from the leaves; the failure matrix that maps each preset-level mistake (dropped files entry, flipped private, added dependencies, bumped version, removed target / strict / moduleResolution from the base, switched a leaf's extends from relative to package-rooted self-reference, dropped jsx / plugins from the Next.js leaf, dropped types: ['node'] from the Playwright leaf or stuffed it with ['jest'], added composite: true without project references, flipped incremental: false) onto the layer that surfaces them (pnpm install mirror, per-consumer tsc --noEmit, reviewer's eye, CI runtime regression); and the public-surface change checklist that ties any preset change to a Plugin Package TypeScript Configurations cross-check, a Shared ESLint Config cross-check, a packages.md cross-check, the dual pnpm tsc --noEmit runs (workspace-root + per-package), a docs/log.md entry, an open-questions register entry, the Playwright smoke run, and the Constitution-Check note in the PR description for Article II (TypeScript-Only) and Article III (Public-Surface Stability).
  • pnpm Workspace Manifest (pnpm-workspace.yaml) -- Per-source-file reference for the monorepo's pnpm workspace declaration paired with pnpm-workspace.yaml at the repo root, the same way tsconfig-presets.md pairs with the four files inside packages/tsconfig/, eslint-config.md pairs with the two files inside packages/eslint-config/, and the per-package manifest references each pair with one packages/*/package.json. Documents the at-a-glance summary (path at the repo root, YAML 1.2 format, single packages top-level key, two globs "apps/*" and "packages/*", eight resolved members across apps/web, apps/docs, apps/web-e2e, and the five packages/*, micromatch glob engine, pinned to pnpm@10.31.0 via package.json#packageManager, Prettier *.yml override that pins YAML to spaces with tabWidth: 2); the file-contents walk-through (the three-line file with one row per field — packages array and the two globs); the "Why a glob, not an explicit list" rationale (new packages auto-register, removed packages auto-unregister, the convention scales to two roots without over-matching apps/web/.content/**); the resolved-members table that maps each of the eight current members to its path, glob, and matching per-package reference page; the glob-semantics matrix that pins what "apps/*" and "packages/*" match versus do not match (one level deep, no ** recursion); the workspace:* resolution walk-through that traces the four-step chain pnpm performs at install time; the "Deliberately absent fields" matrix covering catalog / catalogs, linkWorkspacePackages, preferWorkspacePackages, sharedWorkspaceLockfile, saveWorkspaceProtocol, injectWorkspacePackages, overrides, peerDependencyRules, packageExtensions, and onlyBuiltDependencies with the default behaviour we accept and why each one is not set today; the "Why this file lives at the repo root" rationale (pnpm walks up the directory tree and uses the first pnpm-workspace.yaml it finds as the workspace anchor; same property as turbo.json); the consumer table that maps each reader (pnpm install, pnpm -r, pnpm --filter, turbo run, the script aliases like pnpm dev:web, and tooling that imports @pnpm/find-workspace-packages) to how it consumes this file; the failure matrix that maps each workspace-level mistake (file deleted, file renamed .yml, file moved out of root, globs narrowed, globs broadened to **, two members declared with the same name, YAML indentation mistake, packages key renamed to Yarn's workspaces:, package added without a package.json, package's name changed without updating consumers, glob uses Windows-style backslashes, apps/web/.content/ accidental inclusion) onto the layer that surfaces them; and the public-surface change checklist that ties any glob change to a pnpm install round-trip, a turbo run --dry-run discovery check, a Packages Overview cross-check, an apps/web/package.json lockfile cross-check, a docs/log.md entry, an open-questions register entry, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability).
  • Turborepo Pipeline Configuration (turbo.json) -- Per-source-file reference for the monorepo's Turborepo task pipeline paired with turbo.json at the repo root, the second of the two root-level config references (the first is pnpm-workspace.md). Where pnpm-workspace.md documents which folders become workspace members, this page documents what tasks those members can run, in what order, with what inputs. Documents the at-a-glance summary (path at the repo root, JSON-with-comments format, $schema: "https://turbo.build/schema.json", two top-level keys $schema and tasks, six task entries build, build:en, lint, dev, test:e2e, clean, four cached tasks plus two uncached, one persistent task dev, local .turbo/ cache backend with no remote-cache wired today); the file-contents walk-through (the full JSON file); the per-task field-by-field walkthrough — build with dependsOn: ["^build"] upstream-first ordering, the outputs: [".next/**", "!.next/cache/**", "build/**", "dist/**"] artefact whitelist with the Next.js cache exclusion rationale, and the 19-entry env allow-list (ANALYZE, AUTH_*, COOKIE_SECRET, CRON_SECRET, DATA_REPOSITORY, DATABASE_*, EMAIL_*, GH_*, GITHUB_*, NEXT_PUBLIC_*, PLATFORM_API_*, POLAR_*, POSTHOG_*, RESEND_*, SENTRY_*, SMTP_*, STRIPE_*, TRIGGER_DEV_*, VERCEL_*) with the per-family rationale for why each must be in the cache key; build:en with the narrowed outputs: ["build/**"] Docusaurus-only artefact set; lint with dependsOn: ["^build"] so generated typings compile first; dev with cache: false + persistent: true for the long-running watch process; test:e2e with dependsOn: ["build"] (no ^ prefix so the local-package build runs first) and cache: false because Playwright runs are not deterministic functions of source content; clean with cache: false because a delete operation is meaningless to cache; the workspace-and-task-graph composition walk-through showing how pnpm-workspace.yaml expands → workspace members → package.json#dependencies DAG → Turborepo's ^build rule → the four-stage build chain plugin-sdk → plugin-runtime + plugin-demo (parallel) → web; the "Why some tasks declare outputs and others don't" matrix; the env allow-list family-by-family table (Bundle analysis / Auth / Cookies / Cron / Content repo / Database / Email transport / GitHub integration / Public client vars / Platform API / Polar / PostHog / Resend / Sentry / SMTP / Stripe / Trigger.dev / Vercel) with the consumer file path and why each family must contribute to the cache key; the "Deliberately absent fields" matrix covering top-level globalDependencies, globalEnv, globalDotEnv, remoteCache, ui, daemon, concurrency, and per-task inputs, passThroughEnv, dotEnv, cache: false on build, interactive, extends with the default behaviour we accept and why each one is not set today; the "Why this file lives at the repo root" rationale (Turborepo walks up the directory tree and uses the first turbo.json it finds as the workspace anchor; same property as pnpm-workspace.yaml); the consumer table that maps each reader (pnpm run build, pnpm run lint, pnpm run dev, pnpm run test:e2e, the dev:web / dev:docs script aliases using --filter, CI's turbo run build lint --filter, editor tooling that reads $schema) to how each consumes this file; the failure matrix that maps each pipeline-level mistake (file deleted, file renamed, file moved out of root, dropping the ^ from dependsOn on build, removing dependsOn from build, widening outputs to ["**"], narrowing outputs to drop .next/**, removing an env family entry like NEXT_PUBLIC_*, adding a literal MY_VAR=value non-pattern entry, removing cache: false from dev or test:e2e, removing persistent: true from dev, adding a new task without cache / outputs / env decisions, changing the $schema URL, JSON syntax errors, task-name collisions with a future per-package turbo.json) onto the layer that surfaces each one; and the public-surface change checklist that ties any pipeline change to a turbo run --dry-run round-trip, a --summarize cache-key cross-check, a pnpm Workspace Manifest cross-check, an apps/web/.env.example propagation check, a docs/log.md entry, an open-questions register entry, and the Constitution-Check note in the PR description for Article I (Plugin-First), Article III (Public-Surface Stability), and Article IX (Test Coverage Bar).
  • Workspace Hoisting Posture (.npmrc) -- Per-source-file reference for the monorepo's pnpm install-time hoisting posture paired with .npmrc at the repo root, the fourth root-level config reference after pnpm-workspace.md, turbo-config.md, and workspace-root-manifest.md. Where pnpm-workspace.md documents which folders become workspace members, turbo-config.md documents what tasks those members can run, in what order, with what inputs, and workspace-root-manifest.md documents the version-pinning, native-build allow-list, and Prettier baseline for the entire repo, this page documents the install-time hoisting posture itself — the two load-bearing settings (shamefully-hoist=true that flattens every transitive dependency into the workspace root's node_modules/, and public-hoist-pattern[]=*@heroui/* that forces every @heroui/* package into the workspace-root public-hoist directory so HeroUI's internal-peer model resolves identically from every workspace member) plus the rationale for each (Next.js / ESLint / Tailwind / PostCSS plugin discovery from the project root for the first; @heroui/system / @heroui/shared-utils / per-component peer resolution for the second), the .npmrc precedence chain (system → user → project → env-vars → CLI flags) that explains why a personal ~/.npmrc cannot weaken the workspace's posture but a one-off pnpm install --shamefully-hoist=false flag can produce the expected resolution-failure smoke signal, the failure matrix that pins each setting flip back to a concrete user-visible failure (Next.js font / MDX / built-in-provider resolver Cannot find module errors, ESLint plugin resolution failures, Tailwind plugin loader breakage, HeroUI internal-peer resolution failures showing up as useTheme is not a function or two-versions-of-React errors, equivalent-but-slower node-linker=hoisted aliasing, auto-install-peers=false peer-dependency install drift, and CRLF / encoding parse errors on POSIX CI runners), the per-line walkthrough for the file's two lines (the shamefully-hoist=true rationale plus the two compile-and-lint-time safety nets in @ever-works/tsconfig and @ever-works/eslint-config that catch the legitimate "imported a hoisted-but-not-declared package" cost; the public-hoist-pattern[]=*@heroui/* rationale and the [] array-syntax accumulation rule plus the leading-* glob safety choice), and the .npmrc-change checklist that ties any flip back to the four cross-cutting rules — keep the doc and the file in lock-step in the same PR, add a one-line entry in docs/log.md under the current date heading, cross-reference the Spec 002 — Plugin Architecture public-surface contract, and route the change through reviewer eyes since .npmrc flips affect every contributor's pnpm install outcome and every CI runner's build environment.
  • Web App TypeScript Configuration (apps/web/tsconfig.json) -- Per-source-file reference for the host web application's TypeScript configuration paired with apps/web/tsconfig.json, sitting one directory below the shared @ever-works/tsconfig presets the same way plugin-tsconfigs.md sits one directory below those presets for the three plugin packages. Documents the at-a-glance summary table of every load-bearing field (the extends: "@ever-works/tsconfig/nextjs.json" chain that locks the workspace-wide TypeScript posture in one place; the single compilerOptions.paths entry { "@/*": ["./*"] } that is the Next.js convention for an internal-import alias rooted at the application's top-level directory and powers every internal import in the App Router; the six-entry include array — next-env.d.ts for Next-generated process.env.* and next/image typings, **/*.ts and **/*.tsx for every TypeScript and JSX source file because **/*.ts does not match .tsx, .next/types/**/*.ts for the next build route-typed-link declarations, scripts/generate-openapi.ts for the OpenAPI-generation script that lives outside the App Router tree, and .next/dev/types/**/*.ts for the Next 16 dev-server variant of typed routes; the single exclude: ["node_modules"] entry); the full file annotated line-by-line with the extends chain explained (nextjs.jsonbase.json, locking target: ES2017, module: esnext, moduleResolution: bundler, strict: true, noEmit: true, esModuleInterop: true, resolveJsonModule: true, isolatedModules: true, incremental: true, jsx: react-jsx, the next LSP plugin, allowJs: true, skipLibCheck: true, and the dom/dom.iterable/esnext lib set); the "Why the @/* path alias" walkthrough with a four-row consumer table that traces each alias use back to the resolved file; the "Why the six-entry include array" walkthrough that pins each entry to a concrete failure if dropped (typed routes regress when .next/types/**/*.ts is dropped, dev-server typed routes regress when .next/dev/types/**/*.ts is dropped, the OpenAPI generator's type errors escape detection when scripts/generate-openapi.ts is dropped, every .tsx source file falls out of scope when **/*.tsx is dropped); the "Why the exclude entry" rationale (resilience against a future preset change that might drop the node_modules exclude); the failure matrix that maps each tsconfig.json mistake (extends dropped → mass type errors, extends switched to a non-Next preset → JSX transform breaks, @/* alias dropped → every internal import that uses the alias fails to resolve, **/*.tsx dropped → JSX source files fall out of scope, .next/types/**/*.ts dropped → typed routes regress, node_modules exclude dropped → orders of magnitude slower type-check) onto the layer that surfaces each one; the per-line walkthrough table; and the tsconfig.json-change checklist that ties any field change to an tsconfig-presets.md cross-check, a docs/log.md entry, a Spec 002 — Plugin Architecture cross-link, the dual pnpm tsc --noEmit runs (workspace-root + per-app), and a reviewer pass.
  • E2E TypeScript Configuration (apps/web-e2e/tsconfig.json) -- Per-source-file reference for the Playwright e2e suite's TypeScript configuration paired with apps/web-e2e/tsconfig.json, sitting one directory below the shared @ever-works/tsconfig presets the same way web-app-tsconfig.md sits one directory below those presets for the host web app and plugin-tsconfigs.md sits one directory below them for the three plugin packages. Documents the at-a-glance summary table of every load-bearing field (the extends: "@ever-works/tsconfig/playwright.json" chain that inherits the workspace's TypeScript posture — target: ES2017, module: esnext, moduleResolution: bundler, strict: true, noEmit: true, esModuleInterop, resolveJsonModule, isolatedModules, incremental, the dom/dom.iterable/esnext lib set — plus the Playwright leaf's types: ["node"] whitelist that opens up process.env.*, URL, Buffer, and the rest of the Node ambient surface; the single-entry include array ["./**/*.ts"] that scopes the type-checker to the suite's own source tree picking up the entire tests/ tree under tests/api/, tests/admin/, tests/auth/, tests/client/, tests/i18n/, tests/public/, tests/smoke/, plus fixtures/, helpers/, page-objects/, and the four top-level globals global-setup.ts, global-teardown.ts, and playwright.config.ts; the single exclude: ["node_modules"] entry); the full file annotated line-by-line with the extends chain explained (playwright.jsonbase.json, locking the workspace-wide compiler-options posture and adding the Node-ambient whitelist plus the redundant-but-deliberate noEmit: true re-pin); the "Why the extends chain matters" walkthrough that lists every inherited compiler option and which preset layer contributes it; the "Why the single include glob" walkthrough that maps each path under apps/web-e2e/ (tests/api/**/*.ts, tests/admin/**/*.ts, tests/auth/**/*.ts, tests/client/**/*.ts, tests/i18n/**/*.ts, tests/public/**/*.ts, tests/smoke/**/*.ts, fixtures/**/*.ts, helpers/**/*.ts, page-objects/**/*.ts, global-setup.ts, global-teardown.ts, playwright.config.ts) onto whether the glob picks it up plus the deliberately-absent matches (no **/*.tsx for the JSX-free suite, no node_modules/**, no playwright-report/**, no tsconfig.tsbuildinfo, no adjacent workspace members); the "Why the exclude entry" rationale (defensive re-statement against a future preset change that might drop the line); the "How the e2e suite diverges from the host app" matrix that pins every divergence between apps/web/tsconfig.json and apps/web-e2e/tsconfig.json (different leaf preset nextjs.json vs playwright.json, no @/* alias for the e2e suite, the inherited types: ["node"] whitelist swap for the host app's Next ambient layer, no **/*.tsx glob in the e2e suite, no .next/types/**/*.ts or .next/dev/types/**/*.ts entries, no scripts/generate-openapi.ts entry, the leading-./ anchor on the e2e include glob); the failure matrix that maps each tsconfig.json mistake (extends dropped → mass type errors, extends switched to nextjs.json → Node-ambient regression for process.env.*, extends switched to base.json directly → loses both the Node whitelist and the noEmit re-pin, ./**/*.ts glob narrowed to tests/**/*.tsglobal-setup.ts/global-teardown.ts/playwright.config.ts/fixtures//helpers//page-objects/ fall out of scope, **/*.tsx added → drift away from the suite's TS-only posture, node_modules exclude dropped → orders of magnitude slower type-check, composite: true added → 'isolatedModules' may not be used with 'composite' panic, noEmit: false flipped → .js contamination next to every .ts file) onto the layer that surfaces each one; the per-line walkthrough table; and the tsconfig.json-change checklist that ties any flip back to a tsconfig-presets.md cross-check, a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link, the dual pnpm tsc --noEmit runs (e2e + workspace-root), the Playwright smoke run, and a reviewer pass.
  • E2E Package Manifest (apps/web-e2e/package.json) -- Per-source-file reference for the Playwright e2e suite's package manifest paired with apps/web-e2e/package.json, the test-only manifest companion to the four runtime manifest references (workspace-root-manifest.md, runtime-package-manifest.md, sdk-package-manifest.md, plugin-demo-package-manifest.md). Where those four documents document the manifest of a host-app or library workspace member, this one documents the manifest of a test-only workspace member — a member that ships no runtime exports, no main / types / exports map, declares everything in devDependencies because the package is consumed only by the workspace itself, and deliberately omits a dependencies block. Documents the at-a-glance summary table of every load-bearing field (the name: '@ever-works/web-e2e' workspace identifier that pnpm --filter @ever-works/web-e2e and Turborepo's test:e2e task resolve through; the version: '0.0.0' symbolic-only pin justified by private: true; the private: true hard-block on pnpm publish; the license: 'AGPL-3.0' workspace-wide license inheritance; the five Playwright scripts.* entries test:e2e / test:e2e:ui / test:e2e:chromium / test:e2e:headed / test:e2e:debug and the no-op scripts.lint echo that lets the workspace-wide pnpm -r lint walk this member without a per-package opt-out; the four devDependencies @ever-works/tsconfig with workspace:* for the in-tree TypeScript preset chain extended by e2e-tsconfig.md, @playwright/test ^1.58.2 for the runner this manifest gates, @faker-js/faker ^10.1.0 for synthetic data when e2e-test-data.md's TEST_DATA.generate*() is too coarse, dotenv ^16.4.7 for the cross-app .env.local load playwright-config.md performs at boot, typescript ^5 matching the workspace's pin); the file-contents walk-through (the full JSON file); the per-field walkthrough that pins each field to a concrete responsibility — the three load-bearing properties of name (the @ever-works/ scope match for the pnpm --filter '@ever-works/*' glob, the web-e2e suffix mirroring the directory name so pnpm-workspace.yaml's apps/* glob auto-registers it, and the presence of a name at all being what makes the directory a workspace member), the three rejected scripts.lint alternatives (drop the script → pnpm -r lint exits non-zero with missing script "lint", wire up a real lint → duplicates the pnpm tsc --noEmit safety net and requires a nextjsConfig invocation the suite doesn't use, switch to true → works but says nothing), the three rationales for the devDependencies.@ever-works/tsconfig workspace:* specifier (always tracks the workspace's TypeScript posture, lets pnpm install resolve from in-tree source, matches every other @ever-works/* workspace member's convention), the three required cross-checks for a @playwright/test major bump (a playwright-config.md cross-check, an auth-fixture.md cross-check, a pnpm install round-trip), and the rationale for @faker-js/faker and dotenv being in devDependencies rather than runtime helpers (they are consumed only at test-runner time and at config-boot time respectively); the deliberately-absent fields matrix (no description / homepage / repository / bugs / author / keywords because those surface only on published packages, no engines / packageManager / pnpm.* / prettier because those are inherited from workspace-root-manifest.md, no type because Playwright's CLI handles both ESM and CJS spec files and the suite's tsconfig.json sets module: 'esnext' directly, no main / types / exports / bin / peerDependencies / peerDependenciesMeta / files because the package is a leaf consumer with no public surface, no dependencies because every reach for a third-party library is at test-runner time, no scripts.dev / scripts.build / scripts.start because the suite has no dev or build step); the consumer table that maps each reader (pnpm install resolving the apps/* workspace glob, pnpm --filter @ever-works/web-e2e <script>, Turborepo's test:e2e task, CI workflows, the Playwright runner's CLI walk-up, TypeScript's tsc --noEmit gate, Renovate / Dependabot for upgrade-PR generation, editors for workspace-treeing) to the fields it consumes; the failure matrix that maps each manifest-level mistake (drop the name field → workspace member becomes unaddressable, rename to a non-@ever-works/* scope → CI's pnpm --filter '@ever-works/*' glob silently skips the package, drop private: true → next pnpm publish -r pushes @ever-works/web-e2e@0.0.0 to the npm registry, drop the license field → AGPL-3.0 inheritance breaks on root drift, drop scripts.test:e2e → Turborepo's test:e2e task fails with Couldn't find script, drop scripts.lint → workspace-wide pnpm -r lint exits non-zero, switch the no-op scripts.lint to a real lint without wiring eslint.config.mjsCannot find module 'eslint', drop devDependencies.@playwright/testpnpm test:e2e exits with module-not-found, drop or change devDependencies.@ever-works/tsconfig from workspace:*Cannot find base config errors at TS gate, drop devDependencies.dotenvplaywright.config.ts's import dotenv fails at runner-boot and host-app env-driven branches flap, drop devDependencies.@faker-js/faker → faker-using specs fail at runner-boot while simpler specs mask the breakage, drop devDependencies.typescript → workspace-wide TS gate fails, tighten the Playwright range to 1.58.2 → Renovate stops opening minor-bump PRs, loosen to * → next 2.x major silently lands and breaks the suite en masse, move the file → workspace member silently no-ops, add a dependencies block → implies a runtime contract the suite does not have, add "type": "module" → risks regression on future Playwright CJS-only utility, bump version away from 0.0.0 → drift between field and private: true constraint) onto the layer that surfaces each one; the per-line walkthrough table that pins each line of the 21-line file to its purpose; and the package.json-change checklist that ties any field change to the appropriate cross-check (pnpm-workspace.md on name change, playwright-config.md on Playwright or dotenv change, e2e-tsconfig.md on tsconfig or typescript change, auth-fixture.md on Playwright major bump, e2e-test-data.md on Faker major bump, turbo-config.md on new workspace-spanning script, workspace-root-manifest.md on engines/packageManager/prettier/pnpm.* posture divergence), a pnpm install round-trip, a dual pnpm tsc --noEmit run (workspace-root + e2e), a smoke-subset Playwright run (pnpm --filter @ever-works/web-e2e test:e2e:chromium), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Fixtures Barrel (apps/web-e2e/fixtures/index.ts) -- Per-source-file reference for the Playwright e2e suite's fixtures-directory barrel module paired with apps/web-e2e/fixtures/index.ts, the directory-level public-surface companion to auth-fixture.md (which the barrel re-exports from). Where auth-fixture.md documents the authenticated-fixture boundary (how the persisted storage states minted at pre-flight become per-test isolated browser contexts), this page documents the directory-level fixture-export boundary — what the fixtures/ directory exposes as a single import target, what the import-from-the-file versus import-from-the-directory shapes guarantee, and how future fixture modules compose through this barrel. Documents the at-a-glance summary table of every load-bearing element (the single export { test, expect } from './auth.fixture' re-export statement that forwards both test and expect so the canonical import { test, expect } from '../../fixtures' shape resolves through the barrel; the .ts file extension matching the suite's TS-only posture documented in e2e-tsconfig.md and the include: ["./**/*.ts"] glob; the deliberately single-line file body that keeps the barrel tightly scoped to re-exports only — adding logic here defeats the "directory-level public surface" intent and invites drift from auth-fixture.md; the trailing newline matching the workspace's Prettier endOfLine: lf posture inherited from the workspace-root-manifest.md prettier block); the full file annotated line-by-line with the three load-bearing properties of the re-export (forwarding both test AND expect to prevent the "imported test from one place but expect from another" anti-pattern that breaks Playwright's test soft-failure aggregation, the relative ./auth.fixture source path that resolves through moduleResolution: "bundler" without going through any paths mapping, the bare test and expect names without renaming because Playwright's runner contract requires the test function be named test); the "Why a barrel module instead of importing the file directly" walkthrough that pins the three failure modes of file-direct imports (a new fixture module's exports cannot be composed into the same test runner without touching every consumer, a future move of auth.fixture.ts would force every consumer to update its import, the "import from the directory" shape is the JavaScript ecosystem's lingua franca that the barrel teaches the suite to speak) against the lowest-cost composable barrel shape; the "Why a single re-export and not export *" walkthrough that pins the three failure modes of export * (type-only exports are dropped without an additional export type * companion, the barrel's intent becomes opaque, implicit re-exports surface accidental additions of internal-only helpers like getAuthState() / requireAuthState() / AUTH_FIXTURES) against the lowest-coupling named-re-export shape; the failure matrix that maps each barrel-level mistake (drop the re-export of expect → consumer imports fail to resolve, drop the re-export of test → suite cannot author authenticated specs through the barrel, switch to export * → type-only exports drop silently and future internal helpers leak, rename test to e2eTest on the way out → spec authors' editors flag every test(...) call and the suite drifts away from Playwright convention, switch the source path to a package-rooted self-reference → Cannot find module because the suite has no paths mapping, add helper code inside the file → the barrel's "directory-level public surface" intent breaks, move the file to apps/web-e2e/fixtures.ts → directory shape goes away and future fixture modules cannot live alongside the barrel, ship the file with a CRLF line ending → Prettier diff on POSIX CI runners, drop the trailing newline → Prettier diff and CI lint failure, switch the file extension to .tsxinclude: ["./**/*.ts"] glob does not match and the file falls out of the type-checker's scope, author a parallel barrel in helpers/index.ts → two directory-level public surfaces compete and specs drift between import paths) onto the layer that surfaces each one; the per-line walkthrough table; and the index.ts-change checklist that ties any re-export change to an auth-fixture.md cross-check (every symbol re-exported here originates there today; a new auth fixture export must land in auth.fixture.ts first, then flow through this barrel), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob already picks up fixtures/index.ts; if the glob narrows or if the file moves, the type-checker stops walking the file), an e2e-package-manifest.md cross-check (the package's devDependencies.@playwright/test underwrites the test and expect types this barrel forwards; a Playwright major bump may change the type signatures), every consumer (today's authenticated specs under apps/web-e2e/tests/admin/ and apps/web-e2e/tests/client/ import from '../../fixtures/auth.fixture'; new specs should import from '../../fixtures' to use the barrel; consumer audit on every PR keeps the convention consistent), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run that confirms the runner discovers the same specs as before, the authenticated specs see the same fixtures, and trace recordings remain isolated per spec, a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new fixture pair that affects test authoring conventions, and a reviewer pass.
  • Playwright Auth Fixture (apps/web-e2e/fixtures/auth.fixture.ts) -- Per-source-file reference for the Playwright e2e suite's authenticated-context fixture paired with apps/web-e2e/fixtures/auth.fixture.ts, the authenticated-fixture companion to global-setup.md (which mints the persisted authentication storage states), global-teardown.md (today a no-op placeholder), and e2e-test-data.md (which exports ADMIN_STATE_FILE and CLIENT_STATE_FILE). Where global-setup.md documents the suite's pre-flight boundary, global-teardown.md documents the suite's post-flight boundary, and e2e-test-data.md documents the suite's shared-data boundary, this page documents the suite's authenticated-fixture boundary — how the persisted storage states minted at pre-flight become per-test isolated browser contexts. Documents the at-a-glance summary table of every load-bearing element (/* eslint-disable react-hooks/rules-of-hooks */ file-scoped directive that suppresses the false-positive flag on Playwright's use parameter name; import { test as base, type Page, type BrowserContext } from '@playwright/test' with the mandatory as base rename to free up test for the local export and the type-only imports that stay out of the runtime bundle; import fs from 'fs' + import path from 'path' for the requireAuthState() existsSync check and the path.resolve(__dirname, '..', ...) absolute-path computation; import { ADMIN_STATE_FILE, CLIENT_STATE_FILE } from '../helpers/test-data' so the fixture never types the literal 'auth-states/admin.json'; ADMIN_STATE_PATH / CLIENT_STATE_PATH resolved-once-at-module-load absolute paths with the __dirname-anchored shape that survives webServer.cwd: '../..'; requireAuthState(filePath) fail-fast guard that throws with a contributor-actionable message naming the file path AND the most likely cause SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD; the AuthFixtures type with the four fixture names (adminContext: BrowserContext, adminPage: Page, clientContext: BrowserContext, clientPage: Page); the four base.extend<AuthFixtures>(...) factories with the adminContext depending on browser and adminPage depending on adminContext — the only shapes that load the storage state at context creation rather than after-the-fact; the per-test await context.close() / await page.close() teardown that prevents memory leaks under high-parallelism workers; and the export { expect } from '@playwright/test' re-export that saves every spec one import line and prevents the "imported test from one place but expect from another" anti-pattern that breaks Playwright's test-soft-failure aggregation); the full file annotated chunk-by-chunk; the "How a spec uses the fixture" walkthrough showing the canonical import { test, expect } from '../../fixtures/auth.fixture' shape and the async ({ adminPage }) => { ... } parameter destructure pattern; the "Why a fixture instead of a test.beforeEach() hook" walkthrough that pins the three failure modes of the hook approach (testInfo.context is not a public stash, teardown drift across beforeEach / afterEach, every test pays the cost regardless of whether it destructures the fixture) against the lazy-composition fixture model; the "Why BrowserContext per fixture, not a shared one" walkthrough that pins the three failure modes of the shared-context optimisation (cross-test cookie / localStorage pollution, parallel-page races on shared localStorage / IndexedDB / service-worker scope, per-test trace recordings contain operations from every test) against the fresh-context-per-test isolation; the failure matrix that maps each auth.fixture.ts mistake (drop the requireAuthState guard → cryptic 30-s timeout instead of fail-fast, switch to fs.statSync → low-level ENOENT instead of contributor-actionable message, drop the path.resolve(__dirname, '..', ...) shape → relative paths resolve against the wrong webServer.cwd, hard-code the literal 'auth-states/admin.json' → drift on a future directory rename, drop the as base rename → TypeScript redeclaration error, drop the AuthFixtures type parameter → loss of IntelliSense and typo-driven runtime errors, reuse a shared BrowserContext → cross-test pollution and races, drop the await close() teardown → OOM under high parallelism, drop the re-export { expect } → spec drift back to dual imports breaking soft-failure aggregation, switch the adminContext factory's dependency from browser to context → silent breakage of the "pre-loaded with admin auth" guarantee, switch the adminPage factory's dependency from adminContext to page → silent breakage of the "authenticated page" guarantee, move the file from apps/web-e2e/fixtures/Cannot find module on every consumer import, add a third fixture pair without updating the AuthFixtures type → new fixture not destructurable from spec, remove the eslint-disable directive → CI lint failure on the false-positive flag) onto the layer that surfaces each one; the per-line walkthrough table; and the auth.fixture.ts-change checklist that ties any fixture change to a global-setup.md cross-check (the storage-state files this fixture reads are the files global-setup.ts writes), a global-teardown.md cross-check (the future cleanup bucket will use the same AUTH_STATE_DIR constant), an e2e-test-data.md cross-check (ADMIN_STATE_FILE / CLIENT_STATE_FILE and any future role constants live there), a playwright-config.md cross-check (the webServer.cwd resolves the relative paths), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob picks up this file), every authenticated spec under apps/web-e2e/tests/admin/ and apps/web-e2e/tests/client/ (they all import { test, expect } from this file via the relative path), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run of the admin / client spec set that confirms the fixture loads both storage-state files (no requireAuthState throw), the admin specs see admin-only routes, the client specs see client-only routes, and each spec's trace contains operations from only that spec, a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if a new auth role or fixture pair is added, and a reviewer pass.
  • Workspace .gitignore (.gitignore) -- Per-source-file reference for the monorepo's workspace-root git-ignore manifest paired with .gitignore, the single git-ignore boundary that gates every file the workspace-root git status, every pnpm install, every pnpm tsc --noEmit, every pnpm build, every pnpm test:e2e run, and every CI actions/checkout step decide whether to track. Sits at the workspace root the same way pnpm-workspace.md sits at the root for workspace membership and turbo-config.md sits at the root for task orchestration. Where pnpm-workspace.md documents the workspace-membership boundary and turbo-config.md documents the task-graph boundary, this page documents the tracked-file boundary — which artefacts the workspace deliberately keeps out of source control, why each pattern is in the file, what the consumers expect, and what the failure modes look like when an entry drifts. Documents the at-a-glance summary table of every section (# dependencies covering node_modules / .pnp / .pnp.* / .yarn/* / !.yarn/patches / !.yarn/plugins / !.yarn/releases / !.yarn/versions / .pnp.js for npm + Yarn PnP + Yarn Berry; # turbo covering Turborepo's per-task .turbo cache directory documented in turbo-config.md; # testing covering coverage / **/auth-states/ / **/test-results/ / **/playwright-report/ / **/.playwright/ for the Playwright suite's runtime artefacts and the auth-state cache documented in auth-fixture.md and global-setup.md; # next.js covering .next/ build output and the legacy out/ static-export target; # docusaurus covering **/build/ and **/.docusaurus/ for the docs workspace member at apps/docs/; # production covering generic dist; # misc covering .DS_Store and *.pem; # debug covering all four package-manager debug logs; # env files covering the security-critical .env* glob plus !.env.example re-include — the single most important block for the workspace's secret posture; # vercel covering .vercel; # typescript covering *.tsbuildinfo and next-env.d.ts; # content covering .content (the Git-CMS content directory cloned at runtime by apps/web/scripts/clone.cjs from DATA_REPOSITORY) and analyze/; # vscode AI rules covering the single-file .github/instructions/codacy.instructions.md exclusion; # cache covering .cache and the duplicate .pnpm-debug.log pattern; # OpenAPI backups covering the three public/openapi.backup.json / **/*.backup.openapi.json / **/openapi.backup.json patterns that exhaustively cover the apps/web/scripts/generate-openapi.ts script's backup output; # claude covering the .claude per-checkout state directory); the full file annotated section-by-section; the "Why a single workspace-root .gitignore and not per-package files" walkthrough that pins the three rejected alternatives (per-package .gitignore for each workspace member multiplying maintenance burden, .gitignore only at directories that need extra exclusions creating redundancy with the **/-anchored root patterns, workspace-root .gitignore plus per-developer .gitignore_global forcing every contributor to configure core.excludesFile) against the single-file posture; the "Why **/auth-states/ and not just auth-states/" walkthrough that pins the three reasons for the **/ anchor (resolves at any depth so the actual apps/web-e2e/auth-states/ location is matched, future-proofs against a refactor that moves the e2e suite, symmetric with the other test patterns) plus the security posture that the persisted-storage-state files contain NextAuth session cookies and a leaked file is a leaked admin / client account; the "Why .env* plus !.env.example and not a positive include list" walkthrough that pins the three reasons for the glob-then-re-include shape (defence in depth against future Next env-file conventions like a hypothetical .env.preview, defence against typos like .env.locall, .env.example being the documentation surface contributors should commit while every other variant stays out); the "Why .content is gitignored" walkthrough that pins the three reasons (lives in a separate repo at DATA_REPOSITORY, regenerated on every dev / build by the idempotent apps/web/scripts/clone.cjs, per-deployment customisation requires the gitignore as the single chokepoint that prevents customer content leaking back into the template); the "Why .claude is gitignored" walkthrough that pins the three reasons (per-developer state, cache freshness, the workspace ships its rules through CLAUDE.md / AGENTS.md / .specify/ rather than per-checkout .claude/); the "Why the OpenAPI backup patterns repeat across **/" rationale that pins the three patterns to their distinct coverage shapes (canonical-path public/openapi.backup.json, suffix-pattern **/*.backup.openapi.json, bare-name **/openapi.backup.json); the failure matrix that maps each gitignore-level mistake (drop node_modulespnpm install outputs hundreds of thousands of files, drop .next/ → ~10k generated files per next dev, drop **/auth-states/security regression with persisted NextAuth cookies committed, narrow **/auth-states/ to auth-states/ → security regression with the actual nested location becoming trackable, drop **/test-results/ → trace / video / screenshot pollution, drop **/playwright-report/ → HTML report pollution, drop .env*security regression with production secrets leaking on the first commit, drop !.env.example → the canonical env-var list becomes un-trackable on certain creation orders, switch .env* to a positive list → future Next env conventions silently leak, drop .content → Git-CMS content gets committed and diverges from DATA_REPOSITORY, drop .vercel → Vercel CLI per-project link gets committed, drop *.tsbuildinfo → cross-developer cache contamination, drop next-env.d.ts → contributors with different Next versions diverge on the file, drop .turbo → cache invalidation drifts across developers, drop **/build/ → Docusaurus output gets committed, drop **/.docusaurus/ → Docusaurus internal cache gets committed, drop *.pemsecurity regression with TLS / SSH keys committed, drop .DS_Store → cross-platform contributors see noise diffs, drop the *-debug.log* patterns → per-package-manager debug logs get committed, drop .cache → generic-library caches get committed, drop analyze/ → bundle-analyzer HTML output gets committed, drop .claude → per-developer Claude Code session state gets committed, drop the OpenAPI backup patterns → backup files get committed every time the OpenAPI script runs, drop the Codacy line → editor-specific Codacy ruleset gets shipped to every contributor, add .idea/ / .vscode/ / .cursor/ to the file → per-developer editor configs leak across the team, switch a **/-anchored pattern to a bare pattern → the actual nested location becomes trackable, move the file from .gitignore → git stops reading any pattern in this file, switch the file extension to .gitignore.txt → same as above, ship the file with a CRLF line ending → older git versions misparse the patterns and the file appears empty) onto the layer that surfaces each one; the per-section walkthrough table that maps every section of the file to its purpose; and the .gitignore-change checklist that ties any change to a git status cleanliness verification after each of the four primary workflows (pnpm install / pnpm dev / pnpm build / pnpm test:e2e), an auth-fixture.md cross-check (the **/auth-states/ security boundary), a global-setup.md cross-check (the mkdirSync('auth-states/') call writes into the gitignored directory), an e2e-test-data.md cross-check (the AUTH_STATE_DIR literal must match the gitignore pattern), a playwright-config.md cross-check (the Playwright config's outputDir / reporter / webServer.cwd / trace / video / screenshot settings determine what appears in the worktree), a workspace-root-manifest.md cross-check (a new scripts.* entry that writes new files to the worktree may require a new entry), a turbo-config.md cross-check (the .turbo cache directory boundary), the .env.example cross-check (any change to the # env files block must verify the canonical env-var list remains trackable), the CI actions/checkout cross-check (CI steps that run git status to verify worktree cleanliness would catch a regression), a reviewer pass against the failure matrix, a docs/log.md entry, and a Spec 010 — E2E Test Coverage cross-link if the change introduces a new test-related exclusion.
  • E2E Language-Switcher Page Object (apps/web-e2e/page-objects/public/language-switcher.page.ts) -- Per-source-file reference for the Playwright e2e suite's header locale-switcher driver paired with apps/web-e2e/page-objects/public/language-switcher.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and star-rating-page-object.md documents the suite's per-item rating-picker driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's locale-switching driver boundary — the smallest possible page object that lets a spec drive the global header language-switcher dropdown end-to-end (open the dropdown by clicking the aria-label="Select language" trigger button, select any locale by its full localized native display name like "Français" / "Español" / "Deutsch" / "العربية" / "中文" via the aria-label="Switch to ${fullName}" per-locale option button, read the trigger button's current text content upper-cased to expose the active locale code, and read the aria-expanded attribute to assert that the dropdown is open). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class LanguageSwitcher single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage AND is also consumed inside selectLanguage(fullName) to construct the per-locale option Locator at call-time against page-level scope because the dropdown may be portal-rendered; the readonly button: Locator page.locator('button[aria-label="Select language"]').first() exact-match selector pinned to the deliberate English literal "Select language" (not localized) for strict-mode-correctness AND so a user landing on a page in a language they cannot read can still find the switcher; the constructor(page: Page) that stores the page and pre-binds the trigger Locator in a single pass without a super(page) call; the open() minimal "open the dropdown" primitive every other action method composes against; the selectLanguage(fullName: string) composite "open the dropdown then click the per-locale option button" primitive with the load-bearing full localized native display name parameter ("Français" not "French" and not "fr") reflecting the canonical "language picker shows each language in its own language" UX convention so a non-English speaker can find their language, and the page-level Locator construction (not container-scoped) because portal-rendered dropdowns are not DOM descendants of the trigger; the getCurrentLocaleCode(): Promise<string> accessor that reads the trigger button's text content via textContent()?.trim().toUpperCase() ?? '' with the load-bearing .toUpperCase() casing-fold tolerating future production-source casing drift, the ?.trim() whitespace-tolerance, and the ?? '' nullish-coalesce that pins the public return type to Promise<string>; the isOpen(): Promise<boolean> accessor that reads the aria-expanded attribute and returns the strict-equality comparison expanded === 'true' collapsing both the missing-attribute case and the aria-expanded="false" case into a definitive false return that pins the boolean result type to Promise<boolean> without a bespoke null-narrowing branch); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/language-switcher.spec.ts (trigger visibility on /, dropdown opens via aria-expanded === 'true', French selection navigates to /fr, Spanish selection navigates to /es); the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the global header surface, reusability across all role trees, constructor parity with non-page widgets); the "Why the trigger pins the English aria-label="Select language"" walkthrough that pins the three reasons (the host app deliberately does not translate this label so a user landing on a page in a language they cannot read can still find the switcher, strict-equality survives a future aria-label="Choose region" / aria-label="Switch currency" related-control regression, no production-source change required); the "Why per-locale options pin aria-label="Switch to ${fullName}"" walkthrough that pins the three reasons for the localized-display-name match (the language-picker UX convention, the consuming-spec mental model that selectLanguage("French") will fail-loudly to teach the convention, no production-source change required); the "Why the option Locator does not carry .first()" walkthrough that pins the three reasons for the asymmetry (per-locale aria-label values are unique by design, strict-mode signal preservation against future duplicate-button regressions, symmetric posture with sibling driver per-mode buttons); the "Why .first() on the trigger button" walkthrough that pins the three failure modes of dropping it (strict-mode collision against future second switcher, against an already-rendered duplicate, against an admin-shell mirror); the "Why the constructor uses this.page.locator(…) and not the inherited header scope" walkthrough; the "Why getCurrentLocaleCode() upper-cases the result" walkthrough that pins the three reasons (casing-drift tolerance, ISO-639-1 UI convention, defensive symmetry with sibling drivers); the "Why isOpen() checks aria-expanded === 'true'" walkthrough that pins the three reasons (the ARIA attribute is the screen-reader contract, resilience to portal-rendered dropdowns, string-literal equality narrows the boolean return); the failure matrix covering every language-switcher-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop on page or button, aria-label*="language" substring swap that breaks against "Choose region" siblings, i18n-wiring of the trigger label that breaks under non-English baselines, data-testid swap, .first() drop on button, visible-text match on selectLanguage that breaks under flag-emoji-prefixed labels, locale-code parameter swap on selectLanguage that breaks the convention, container-scoped per-locale Locator that breaks portal-rendered dropdowns, .first() add on per-locale Locator that masks duplicate-button regressions, ?.trim() drop on getCurrentLocaleCode, .toUpperCase() drop, ?? '' drop, sibling-selector swap on isOpen that breaks portal-rendered dropdowns, file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec, future smoke / a11y specs that drive remaining locale flows like selectLanguage('Deutsch') / selectLanguage('العربية') / selectLanguage('中文'), the header production-source component for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto trigger-aria-label-localization, per-locale-aria-label-rename, locale-name-rename, casing-drift (invariant-by-design), portal-dropdown-render-change (invariant-by-design), aria-expanded-removal, locale-disable, middleware-prefix, and baseURL-change failures; and the language-switcher.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/language-switcher.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (the trigger's aria-label, the per-locale option aria-label, the aria-expanded attribute, the trigger's text-content shape), a next-intl configuration cross-check (the locale set the production source surfaces in the dropdown is the set every consuming spec can pass to selectLanguage()), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future authenticated variant would surface there), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the language-switcher spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Language Switcher"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Map Page Object (apps/web-e2e/page-objects/public/map.page.ts) -- Per-source-file reference for the Playwright e2e suite's Map View page driver paired with apps/web-e2e/page-objects/public/map.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and item-detail-page-object.md documents the suite's per-item detail-page driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's Map View driver boundary — the smallest possible page object that lets a spec drive the Map View feature (Spec 017) end-to-end (navigate to the dedicated /map route via BasePage.goto(), query the data-testid="map-view" markers container or the data-testid="map-empty-state" empty placeholder to detect whether the feature rendered successfully on environments without provider keys / coordinates, query the data-testid="map-sidebar" rail and the data-testid="map-sidebar-card" per-item cards inside it for sidebar interaction flows, locate the header Map navigation link via the inherited header Locator scoped to role="link" and the case-insensitive exact-match /^Map$/ regex anchored on the translation HEADER_MAP rendering as "Map" in en, locate the listing-page view-toggle Map button via the substring-matched button[aria-label*="map" i] selector with .first() strict-mode-correctness append, and surface the mobile-responsive Show map / Show list accessible-name-matched buttons via the case-insensitive /show map/i / /show list/i regexes that survive the host theme's casing drift). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the export class MapPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the eight readonly Locator fields covering mapView / mapEmptyState / mapSidebar / sidebarCards / mapHeaderLink / viewToggleMapButton / showMapButton / showListButton; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via four getByTestId selectors / one inherited-header-scoped getByRole('link', …) exact-match / one aria-label*="map" i substring-with-case-insensitive-flag plus .first() / two case-insensitive accessible-name-matched buttons; the navigate() dedicated /map route navigation primitive via inherited goto(); the isPageRendered(): Promise<boolean> graceful-degradation accessor with the OR-of-two-paths over mapView and mapEmptyState and the .catch(() => false) error shields on both isVisible() calls); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 017 — Map View for Listings and the consuming spec at apps/web-e2e/tests/public/map.spec.ts (the dedicated /map route returns 200 when location features are enabled or 404 when they are gated off, the listing-page view-toggle Map button is visible when the feature is available, the header Map navigation link tracks the header.map_enabled config gate, and the sidebar card highlights itself with aria-current="true" on click when markers are present); the "Why the class extends BasePage" walkthrough that pins the three load-bearing reasons (page-route navigation via inherited goto(), global header / footer / navLinks chrome surfaced through inherited composite getters and consumed by mapHeaderLink's this.header.getByRole(...) scope reduction, waitForPageReady() post-navigation stabiliser); the "Why the view-toggle uses aria-label*="map" i" walkthrough that pins the three reasons (host-theme i18n drift across aria-label="Map view" / "Show as map" / "Map layout", casing drift across "Map" / "map" / "MAP" handled by the i flag, .first() strict-mode-correctness against future "Map settings" / "Open in map" siblings); the "Why isPageRendered() accepts the empty-state path" walkthrough that pins the three reasons (dev environments without provider keys must succeed, the empty-state is a real production-source render path on tenants with location feature enabled but no items with coordinates, .catch(() => false) shields against transient Locator-resolution failures during DOM reflow); the failure matrix covering every map-page-level mistake (type-only import drop, extends BasePage removal, readonly drop on any Locator, CSS-class / element-tag re-bind on any of the four data-testid Locators, mapEmptyState field drop, .first() drop on viewToggleMapButton, i flag drop on the substring selector, exact-match re-bind to aria-label="Map view", non-anchored /Map/ regex on mapHeaderLink, case-insensitive /^Map$/i re-bind on mapHeaderLink, top-level page.getByRole(...) re-bind that drops the this.header scope, super(page) drop, view AND empty AND-conversion of the OR-paths in isPageRendered, view-only conversion that breaks dev / CI runners without provider keys, .catch(() => false) drop on either isVisible() call, .first() drop on mapView / mapEmptyState inside isPageRendered, exact-match re-bind on showMapButton / showListButton, data-test / data-cy / data-qa re-bind on any getByTestId selector, file move, .tsx rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/map.spec.ts, the indirect apps/web-e2e/tests/public/seo-manifests.spec.ts reference to /map via the inherited goto(), the production-source DOM contract under apps/web/components/map/*, base-page-object.md for the inherited surface, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, fixtures-index.md for a future authenticated variant) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto data-testid-rename, HEADER_MAP-translation-rename, view-toggle-aria-label-substring-drop, Show map / Show list-accessible-name-rename, header.map_enabled-config-flip, settings.location.enabled-config-flip, NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN / NEXT_PUBLIC_GOOGLE_MAPS_API_KEY-absence (gracefully handled), middleware-prefix-change, and baseURL-change failures; and the map.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/map.spec.ts), a base-page-object.md cross-check, a production-source cross-check (the four data-testids, the header Map link via HEADER_MAP, the view-toggle aria-label*="map" substring, the mobile Show map / Show list button accessible names), an e2e-tsconfig.md cross-check, a playwright-config.md cross-check, a fixtures-index.md cross-check, dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the map spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Map View"), a docs/log.md entry, a Spec 017 — Map View for Listings cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Newsletter Page Object (apps/web-e2e/page-objects/public/newsletter.page.ts) -- Per-source-file reference for the Playwright e2e suite's footer newsletter signup form driver paired with apps/web-e2e/page-objects/public/newsletter.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and map-page-object.md documents the suite's Map View page-route driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's footer newsletter-signup widget driver boundary — the smallest possible page object that lets a spec drive the footer newsletter signup form end-to-end (locate the first input[type="email"][name="email"] form field on the page, locate the button[type="submit"] that lives one DOM level up from the email input via the .. parent-traversal step, locate the inline red-tinted validation message Tailwind utility classes emit (p.text-red-600 / p.text-red-400) for the dark- and light-theme casing of the same paragraph, fill the email and click submit in a single subscribe(email) action, and detect whether a [data-sonner-toast] success toast surfaced after submission via hasSuccessToast() with the .first() strict-mode-correctness append and the .catch(() => false) graceful-degradation collapse). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import with no BasePage value import — the standalone-class widget posture; the export class Newsletter single named export with no extends clause — the load-bearing standalone-class widget convention; the four readonly fields covering page / emailInput / submitButton / errorMessage; the synchronous constructor that pre-binds every per-page Locator in a single pass via the compound input[type="email"][name="email"] + .first() selector for the email input, the .. parent-traversal step + button[type="submit"] selector for the submit button, and the comma-separated p.text-red-600, p.text-red-400 selector + .first() for the inline error paragraph; the subscribe(email) two-step composite that fills the email then clicks the submit button via two sequential awaits; the hasSuccessToast(): Promise<boolean> graceful-degradation accessor with the [data-sonner-toast] Sonner-canonical data-attribute selector + .first() + .catch(() => false) error collapse); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and Spec 012 — Newsletter Providers; the "Why the class does not extend BasePage" three-reason analysis (footer-rendered widget vs. page-route distinction, local-to-the-footer surface scope, symmetry with sibling widget drivers); the "Why the email input uses .first()" three-reason analysis (single-form-on-the-page invariant, strict-mode-collision-protection posture, future-proofing against host-theme refactors); the "Why the submit button uses .. traversal" three-reason analysis (sibling-scoped-not-page-scoped invariant, .. step as the canonical CSS-equivalent of XPath's parent::* axis, resilience to nested-wrapper drift); the "Why the error message uses text-red-600, text-red-400" three-reason analysis (light-vs-dark-theme utility-class variance, Tailwind-utility-classes-as-canonical-inline-error-styling-primitive posture, <p> element-tag prefix load-bearing constraint); the "Why hasSuccessToast() collapses errors to false" three-reason analysis (Sonner-toast-lifecycle mid-fade transient errors, expect.poll-pollability primitive, stacked-toast strict-mode-collision protection); the failure matrix of 21 mistakes (drop the import type modifier, add an extends BasePage clause, drop readonly, re-bind emailInput to input[type="email"] only, drop the .first() chain on emailInput, re-bind emailInput to getByRole('textbox', { name: 'email' }), re-bind submitButton to page.locator('button[type="submit"]'), replace .. with ... .. multi-step traversal, re-bind submitButton to getByRole('button', { name: 'Subscribe' }), re-bind errorMessage to p.text-red-500, drop the <p> element-tag prefix, drop the .first() chain on errorMessage, replace the comma-separated selector with a single text-red-600, convert subscribe() to a Promise.all([fill, click]) race, drop the await on either step inside subscribe(), drop the .catch(() => false) on hasSuccessToast()'s isVisible(), drop the .first() on hasSuccessToast()'s [data-sonner-toast] Locator, re-bind [data-sonner-toast] to a CSS-class selector like .sonner-toast, move the file outside apps/web-e2e/page-objects/public/, rename the file to newsletter.page.tsx, rename the class to NewsletterPage, commit the file with CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every consuming spec (a future apps/web-e2e/tests/public/newsletter.spec.ts), the production-source DOM contract under apps/web/components/newsletter/* and the global footer under apps/web/components/layouts/footer/*, the Sonner integration under apps/web/lib/notifications/*, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, and fixtures-index.md for a future authenticated variant; the read / write surface failure modes table covering production-source / library / config drift cases (form-field name="email" rename, form-field type="email" change to type="text", submit button type="submit" change to type="button", submit button moved outside the email input's parent, inline-error-paragraph utility-class rename, Sonner library upgrade that changes the data-sonner-toast attribute, Sonner library replacement, newsletter feature config flip, locale change with translated submit-button label, baseURL change in playwright-config.md); and the 12-step newsletter.page.ts-change checklist (audit consuming specs, cross-check base-page-object.md, cross-check the production source under apps/web/components/newsletter/* and the global footer, cross-check the Sonner integration, cross-check e2e-tsconfig.md, cross-check playwright-config.md, cross-check fixtures-index.md, run dual pnpm tsc --noEmit (e2e package + workspace root), run a smoke-subset Playwright run targeting --grep "Newsletter", a docs/log.md entry, a Spec 012 — Newsletter Providers cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Profile-Dropdown Page Object (apps/web-e2e/page-objects/public/profile-dropdown.page.ts) -- Per-source-file reference for the Playwright e2e suite's header profile-dropdown menu driver paired with apps/web-e2e/page-objects/public/profile-dropdown.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and newsletter-page-object.md documents the suite's footer newsletter-signup widget driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's header profile-dropdown menu driver boundary — the smallest possible page object that lets a spec drive the global header avatar / profile button dropdown end-to-end (locate the trigger button by its canonical production-source id="user-menu-button" identifier, locate the dropdown-menu container by its canonical production-source id="profile-menu" identifier, locate every [role="menuitem"] child of the menu as a collection scoped to this.menu for clickMenuItem filtering, locate the last menu item as the canonical "Logout" / "Sign out" button by positional convention, open the menu via a click on the trigger button, read the menu's open state via the trigger button's aria-expanded attribute strict-compared to the literal 'true', click any menu item by a case-insensitive RegExp hasText filter on its visible label with .first() strict-mode-correctness append, and click the last menu item directly via the dedicated logout() shortcut). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import with no BasePage value import; the export class ProfileDropdown single named export with no extends clause — the standalone-class widget convention; the five readonly fields covering page / triggerButton / menu / menuItems / logoutButton; the synchronous constructor that pre-binds every per-page Locator in a single pass via the HTML-id-based #user-menu-button / #profile-menu selectors and the this.menu-scoped [role="menuitem"] collection plus .last(); the open() single-step click primitive; the isOpen(): Promise<boolean> strict-equality aria-expanded === 'true' accessor; the clickMenuItem(name: RegExp) arbitrary-menu-item composite with hasText filter + .first(); the logout() shortcut bound to the last menu item); the spec context cross-links to Spec 010 — E2E Test Coverage and Spec 003 — Auth Providers; the "Why the class does not extend BasePage" three-reason analysis (header-rendered widget vs. page-route, local-to-the-header surface scope, sibling widget-driver symmetry); the "Why the trigger button uses #user-menu-button" three-reason analysis (HTML id as canonical accessibility-wiring primitive cross-referenced by aria-controls / aria-labelledby, locale-stable selector, theme-stable selector); the "Why the logout button uses .last()" three-reason analysis (production-source positional convention, accessibility convention alignment, locale-fragility avoidance); the "Why isOpen() checks the exact 'true' string" three-reason analysis (getAttribute() returns string-or-null, Boolean('false') is true footgun, null-coerces-to-false for unrendered states); the "Why clickMenuItem takes a RegExp not a string" three-reason analysis (locale-sensitive label flexibility, opt-in case-insensitivity, mirroring Playwright's hasText upstream API); the failure matrix of 22 mistakes; the per-line walkthrough table; the read / write surface tables covering future apps/web-e2e/tests/auth/* consuming specs and the production-source DOM contract under the header profile-dropdown component (id="user-menu-button", id="profile-menu", [role="menuitem"], aria-expanded, the bottom-most-logout positional convention); the read / write surface failure modes table covering trigger-id-rename, menu-container-id-rename, aria-expanded-removal, role-menuitemcheckbox-substitution, logout-positional-drift, authentication-state-regression, locale change with translated logout label, and baseURL change failures; and the 12-step profile-dropdown.page.ts-change checklist (audit consuming specs under apps/web-e2e/tests/auth/ and apps/web-e2e/tests/public/, cross-check base-page-object.md, cross-check the production-source header-component DOM contract, cross-check the auth-provider integration, cross-check e2e-tsconfig.md, cross-check playwright-config.md, cross-check fixtures-index.md for a future authenticatedPage fixture, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting --grep "Profile Dropdown", a docs/log.md entry, a Spec 003 — Auth Providers cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Public-Pages Page Object (apps/web-e2e/page-objects/public/public-pages.page.ts) -- Per-source-file reference for the Playwright e2e suite's generic public content-page + error-page drivers paired with apps/web-e2e/page-objects/public/public-pages.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and profile-dropdown-page-object.md documents the suite's header profile-dropdown menu driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's generic public content-page + error-page driver boundary — the smallest possible page objects that let a spec drive any of the six static / nearly-static public content routes (/collections, /categories, /tags, /cookies, /pricing, /sponsor) and the two error surfaces (404, 403) end-to-end (navigate to a chosen route via the dedicated navigateToCollections() / navigateToCategories() / navigateToTags() / navigateToCookies() / navigateToPricing() / navigateToSponsor() shortcut methods that close over the inherited goto(), locate the first <h1> / role="heading" element on the page as the page title anchor, locate the <main> element as the per-route content container for visibility assertions, locate any nav[aria-label*="breadcrumb" i] or fallback <nav><ol> element as the breadcrumb trail, locate the page heading on an error page, locate the 404|403 literal text anywhere in the document for status-code assertions, locate the first role="link" matching the case-insensitive /home/i regex as the canonical "go home" recovery link, and locate the first role="button" matching the case-insensitive /go back/i regex as the canonical browser-history-pop button). Documents the at-a-glance summary table of every load-bearing element across both classes (the type-only Playwright import; the runtime BasePage value import; the export class PublicPagesPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the three readonly Locator fields covering heading / mainContent / breadcrumb; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via the getByRole('heading').first() accessibility-tree-canonical selector for the heading, the page.locator('main').first() element-tag selector for the main content, and the OR-of-two-paths comma-separated nav[aria-label*="breadcrumb" i], nav ol selector with .first() strict-mode-correctness append for the breadcrumb; the six route-shortcut methods (navigateToCollections / navigateToCategories / navigateToTags / navigateToCookies / navigateToPricing / navigateToSponsor) that close over the inherited goto; the export class ErrorPage extends BasePage second named export with the extends BasePage clause — the error-page driver posture; the four readonly Locator fields covering heading / errorCode / goHomeButton / goBackButton; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via the same getByRole('heading').first() selector for the heading, the page.getByText(/404|403/) regex-alternation text-match selector for the error code, the page.getByRole('link', { name: /home/i }).first() substring-regex accessibility selector for the go-home recovery link, and the page.getByRole('button', { name: /go back/i }).first() two-word-substring-regex accessibility selector for the browser-history-pop button); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming specs at apps/web-e2e/tests/public/collections.spec.ts (three flows over /collections — loads successfully, has a heading, has a breadcrumb), the indirect consumer at apps/web-e2e/tests/public/sponsor.spec.ts (the onErrorPage branch of the OR-of-three-statuses assertion onSignIn || onErrorPage || stayedOnSponsor), and the indirect consumer at apps/web-e2e/tests/public/error-pages.spec.ts; the "Why PublicPagesPage extends BasePage" three-reason analysis (page-route navigation via the inherited goto method, global header / footer / navLinks chrome surfaced for free, waitForPageReady post-navigation stabiliser); the "Why PublicPagesPage and ErrorPage co-habit a single file" three-reason analysis (an error page is a content page that happens to be a 404 / 403 — the structural primitives are the same role="heading" first element / <main> content container / recovery role="link" to home, the two classes share the same BasePage import and the same Page, Locator type-only import, the two classes are consumed together by every spec that drives a feature-flag-gated route like /sponsor); the "Why heading uses getByRole('heading').first()" three-reason analysis (accessibility-tree-canonical posture matches both <h1><h6> element tags AND any [role="heading"] ARIA attribute override indistinguishably, strict-mode-correctness against the <h2> / <h3> siblings inside <main>, locale-stable selector independent of heading text); the "Why breadcrumb uses an OR-of-two-paths" three-reason analysis (canonical accessibility primitive is aria-label="breadcrumb" with case-insensitive i flag for "Breadcrumb" capitalisation, structural fallback <nav><ol> matches host themes without the aria-label, .first() strict-mode-correctness against multiple breadcrumb trails); the "Why errorCode uses getByText(/404|403/)" three-reason analysis (error code as primary user-facing discriminator, regex-form preferred over string-form to prevent future "It's a 4040 error" typo matches, no .first() required because the error code is emitted exactly once on the canonical error template); the "Why goHomeButton uses role="link"" three-reason analysis (recovery link is canonically an <a href="/"> with role="link" not a <button> with role="button", .first() strict-mode-correctness against header / footer "Home" links, case-insensitive substring regex /home/i for locale / casing drift); the "Why goBackButton uses role="button"" three-reason analysis (browser-history-pop button is canonically a <button onClick={() => history.back()}> with role="button" not an <a href> with role="link", two-word /go back/i regex for safety against bare "Back" button collisions in unrelated nav chrome, .first() strict-mode-correctness against unrelated "Go back to top" / "Go back to listing" siblings); the failure matrix covering every public-pages-page-level mistake (type-only import drop, extends BasePage clause drop on either class, readonly drop on any of the seven Locator fields, super(page) drop in either constructor, .first() drop on heading / mainContent / breadcrumb, OR-of-two-paths alternation drop in breadcrumb, i flag drop on breadcrumb, getByRole('navigation') re-bind that hides the structural-fallback path, page.locator('h1') re-bind on heading that breaks <div role="heading"> overrides, getByRole('main') re-bind on mainContent that breaks the host theme today, route-literal change in any of the six route shortcuts, inlined page.goto(path) that drops waitForPageReady, regex alternation drop on errorCode, role="button" re-bind on goHomeButton, i flag drop on goHomeButton, exact-string name: 'Home' re-bind on goHomeButton, .first() drop on goHomeButton, role="link" re-bind on goBackButton, i flag drop on goBackButton, single-word name: /back/i re-bind on goBackButton that collides with bare "Back" siblings, .first() drop on goBackButton, file split into separate public-pages.page.ts and error.page.ts, file move, .tsx rename, class rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming specs at collections.spec.ts / sponsor.spec.ts / error-pages.spec.ts, future consumers at categories.spec.ts / tags.spec.ts / cookies.spec.ts / pricing.spec.ts, the production-source DOM contract under apps/web/app/[lang]/collections/ / categories/ / tags/ / cookies/ / pricing/ / sponsor/, the production-source error-page DOM contract under apps/web/app/not-found.tsx / apps/web/app/error.tsx, base-page-object.md for the inherited surface, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, fixtures-index.md for a future authenticated-route variant) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto route-literal-rename, <h1>-to-<div role="heading">-survival, <main>-to-<div role="main">-breakage, breadcrumb structural-refactor failures, payment.enabled-flag-flip 404 escalation through ErrorPage, sponsor-feature-flag-flip 404/redirect tolerance, 404-text-rename, "Home" / "Go back" translation drift, middleware-prefix-change, and baseURL-change handling; and the 13-step public-pages.page.ts-change checklist (audit consuming specs under apps/web-e2e/tests/public/, cross-check base-page-object.md, cross-check the production-source DOM contract on each of the six routes, cross-check the production-source error-page contract, cross-check the route literals against the host-theme middleware, cross-check e2e-tsconfig.md, cross-check playwright-config.md, cross-check fixtures-index.md for a future authenticatedPage fixture, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting --grep "Collections|Categories|Tags|Cookies|Pricing|Sponsor|Error", a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Bulk-Actions Page Object (apps/web-e2e/page-objects/admin/bulk-actions.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin items bulk-operations driver paired with apps/web-e2e/page-objects/admin/bulk-actions.page.ts, the first per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the template the remaining sixteen admin-tree page-object docs (one per source file) will mirror — sitting inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (clients.page.ts, collections.page.ts, comments.page.ts, companies.page.ts, dashboard.page.ts, data-export.page.ts, featured-items.page.ts, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root and signin-page-object.md documents the suite's auth-form driver boundary under apps/web-e2e/page-objects/auth/, this page documents the suite's admin items bulk-operations driver boundary — the smallest possible page object that lets a spec drive the full admin-shell items-listing bulk surface end-to-end (navigate to /admin/items via the inherited goto(), locate the page heading, locate the first select-all checkbox via the bilingual aria-label*="Select all" i, aria-label*="SELECT_ALL" i substring OR-of-two-paths, locate the bulk action toolbar via the canonical [role="toolbar"] selector once a row selection has triggered it, locate each of the four action buttons — approve, reject, delete, clear-selection — by their case-insensitive name regex, locate the modal confirmation dialog via the canonical [role="dialog"][aria-modal="true"] selector once a destructive action has been clicked, and expose every individual non-select-all checkbox via the filtered itemCheckboxes getter for per-row selection flows). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtime BasePage value import; the export class AdminBulkActionsPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the eight readonly Locator fields covering heading / selectAllCheckbox / bulkActionBar / approveButton / rejectButton / deleteButton / clearSelectionButton / confirmDialog; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via the getByRole('heading').first() accessibility-tree-canonical selector for the heading, the bilingual [aria-label*="Select all" i], [aria-label*="SELECT_ALL" i] OR-of-two-paths substring with .first() strict-mode-correctness for the select-all checkbox, the [role="toolbar"] ARIA role with .first() for the bulk-action bar, the getByRole('button', { name: /approve|reject|delete/i }).first() accessibility-tree-canonical posture for the three workflow-state action buttons, the getByRole('button', { name: /deselect|clear/i }).first() alternation regex tolerating "Clear selection" / "Deselect all" / "Clear" phrasings for the clear-selection button, and the [role="dialog"][aria-modal="true"] two-attribute selector with .first() for the modal confirmation dialog; the single navigate() shortcut method that closes over the inherited goto; the get itemCheckboxes(): Locator late-bound getter with the [aria-label*="Select" i] substring-OR'd selector and the hasNotText: /all/i filter to exclude the global select-all checkbox from the per-row Locator collection); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/admin/bulk-actions.spec.ts (five flows over the admin-shell items-listing bulk surface — select-all checkbox is visible, clicking select-all shows the bulk action bar, bulk action bar has approve / reject / delete buttons, clicking bulk delete opens the confirmation dialog, clear selection removes the bulk action bar); the "Why AdminBulkActionsPage extends BasePage" three-reason analysis (page-route navigation via the inherited goto method, global header / footer / nav-link chrome surfaced for free, post-navigation waitForPageReady stabiliser); the "Why the bilingual aria-label OR-of-two-paths on selectAllCheckbox" three-reason analysis (production source bilingualism between the canonical "Select all" English-locale phrase and the i18n-key fallback "SELECT_ALL" for catalogue-incomplete tenants, substring tolerance via the i flag for casing variants, .first() pin against per-row select-all duplicates); the "Why [role="toolbar"] for bulkActionBar" three-reason analysis (accessibility-tree-canonical posture, production-source-first selector posture, strict-mode safety from the per-route single-instance shape with .first() against future shared admin-shell command-palette role="toolbar" peers); the "Why getByRole('button', { name: /…/i }) for the four action buttons" three-reason analysis (accessibility-tree-canonical posture against the computed accessible name, locale-tolerant via the case-insensitive regex, strict-mode safety from the .first() pin against multi-button surfaces); the "Why [role="dialog"][aria-modal="true"] for confirmDialog" three-reason analysis (modal vs non-modal disambiguation, strict-mode safety against tooltip / toast libraries that mount with role="dialog", .first() pin against potential parallel modals); the "Why itemCheckboxes is a getter and not a readonly field" three-reason analysis (late-binding against pagination state, symmetric with the per-call filter() invocation, documentation-by-convention with other admin-tree page objects' getter posture for "collection of matching elements" surfaces); the "Why aria-label*="Select" i and not getByRole('checkbox', { name: /select/i }) for itemCheckboxes" three-reason analysis (defends against role-attribute drift between native <input type="checkbox"> / role="checkbox"-shaped <button> / <button>-with-icon "select chip" variants, direct symmetry with selectAllCheckbox's CSS-attribute substring shape, no collision risk with non-checkbox role elements); the failure matrix covering every bulk-actions-page-level mistake (type-only import drop, extends BasePage clause drop, super(page) drop in the constructor, readonly drop on any field, selectAllCheckbox switched to a single exact-match selector that silently test.skip()s on catalogue-incomplete tenants, i flag drop on any substring locator, .first() drop on selectAllCheckbox / bulkActionBar / any action button, bulkActionBar switched to [data-testid="bulk-actions"] violating production-source-first, confirmDialog switched to drop the [aria-modal="true"] filter, itemCheckboxes switched to getByRole('checkbox', { name: /select/i }) losing per-row <button>-with-icon coverage, hasNotText: /all/i filter drop that bleeds the select-all checkbox into the per-row collection, navigate() method drop that forces every consuming spec to restate goto, file move, class rename, .tsx rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/admin/bulk-actions.spec.ts, future per-row selection / bulk-approve / bulk-reject specs, the admin items production-source components for the DOM contract, base-page-object.md for the inherited surface, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL + adminPage fixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto bilingual-aria-label-rename, role="toolbar"-to-role="region"-switch, role="dialog"-to-custom-element-modal-switch, text-content-button-to-icon-only-button-switch, single-toolbar-to-per-row-action-menu-switch, empty-items-listing seeding regression that auto-test.skip()s every spec, adminPage fixture authentication-regression failures, /admin/items middleware-disabling failures, playwright.config.ts baseURL-change failures, and next-intl-message-catalogue drops of the canonical English aria-label; and the 11-step bulk-actions.page.ts-change checklist (audit consuming specs under apps/web-e2e/tests/admin/bulk-actions.spec.ts, cross-check base-page-object.md for the BasePage posture and the standalone-class precedent set by scroll-to-top-page-object.md, cross-check the production source for the canonical aria-label="Select all" / SELECT_ALL t-key on the select-all checkbox, the role="toolbar" on the bulk-action bar, the role="dialog" + aria-modal="true" on the confirmation modal, and the four action button accessible names, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound bulk-actions driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the bulk-actions spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Bulk Operations"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Clients Page Object (apps/web-e2e/page-objects/admin/clients.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin clients-management driver paired with apps/web-e2e/page-objects/admin/clients.page.ts, the second per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the continuation of the rollout the admin-bulk-actions-page-object.md template established — sitting inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts, collections.page.ts, comments.page.ts, companies.page.ts, dashboard.page.ts, data-export.page.ts, featured-items.page.ts, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root and admin-bulk-actions-page-object.md documents the suite's admin items bulk-operations driver boundary at /admin/items, this page documents the suite's admin clients-management driver boundary at /admin/clients — the smallest possible page object that lets a spec drive the admin clients listing end-to-end (navigate to /admin/clients via the inherited goto(), locate the page heading, locate the first "Add client" trigger button by its case-insensitive /add client/i accessible-name regex, locate the multi-step add-client form modal via the .fixed.inset-0.z-50 Tailwind-overlay positional selector once the trigger has been clicked, locate the per-row delete confirmation modal via the same .fixed.inset-0.z-50 positional selector filtered down to the hasText: /delete client/i substring once a delete button has been clicked, and expose the confirm-delete / cancel-delete button pair as role="button" Locators nested inside the delete confirmation modal). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtime BasePage value import; the export class AdminClientsPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the two readonly Locator fields covering heading / addClientButton; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via the getByRole('heading').first() accessibility-tree-canonical selector for the heading and the getByRole('button', { name: /add client/i }).first() accessibility-tree-canonical posture for the add-client trigger; the single navigate() shortcut method that closes over the inherited goto; the four per-page modal getters (get clientFormModal(): Locator with the .fixed.inset-0.z-50 Tailwind-utility positional selector and .first() strict-mode-correctness append for the multi-step add-client form modal overlay; get deleteConfirmModal(): Locator with the same .fixed.inset-0.z-50 positional selector filtered down to the hasText: /delete client/i substring for the delete confirmation modal overlay; get confirmDeleteButton(): Locator with the anchored ^delete$ button regex scoped under deleteConfirmModal; get cancelDeleteButton(): Locator with the substring /cancel/i button regex scoped under deleteConfirmModal); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/admin/clients.spec.ts (four flows over the admin clients-management surface — admin can access clients management page, clients page displays client list, admin can open create client modal, admin can open delete client confirmation); the "Why AdminClientsPage extends BasePage" three-reason analysis (page-route navigation via the inherited goto method, global header / footer / nav-link chrome surfaced for free, post-navigation waitForPageReady stabiliser); the "Why getByRole('button', { name: /add client/i }) for addClientButton" three-reason analysis (accessibility-tree-canonical posture against the computed accessible name, locale-tolerant via the case-insensitive regex, strict-mode safety from the .first() pin against multi-button surfaces); the "Why clientFormModal and deleteConfirmModal are getters and not readonly fields" three-reason analysis (late-binding against modal mount/unmount lifecycle, symmetric with itemCheckboxes on the bulk-actions driver, per-call filter() invocation on deleteConfirmModal); the "Why .fixed.inset-0.z-50 for both modal surfaces" three-reason analysis (production-source posture without role="dialog", substring filter disambiguates between the two modals, reuses the host app's CSS-utility convention); the "Why ^delete$ anchored regex for confirmDeleteButton" three-reason analysis (defends against per-row "Delete client" heading collision, defends against per-row "Cannot delete" warning button collision, symmetric with bulk-actions driver but smaller-blast-radius for the more crowded modal scope); the "Why nested deleteConfirmModal.getByRole(...) and not page.getByRole(...)" three-reason analysis (defends against per-row "Delete" / "Cancel" button collision on the underlying clients table, re-uses the late-binding lifecycle of the deleteConfirmModal getter, symmetric with public-tree modal drivers); the failure matrix covering every clients-page-level mistake (type-only import drop, extends BasePage clause drop, super(page) drop in the constructor, readonly drop on any field, i flag drop on the addClientButton regex, .first() drop on addClientButton, addClientButton switched to [data-testid="add-client"] violating production-source-first, .first() drop on clientFormModal, hasText: /delete client/i filter drop on deleteConfirmModal, ^…$ anchors drop on confirmDeleteButton, confirmDeleteButton / cancelDeleteButton switched to page.getByRole(...) peers, modal getters switched to [role="dialog"][aria-modal="true"], navigate() method drop that forces every consuming spec to restate goto, file move, class rename, .tsx rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/admin/clients.spec.ts, future per-row clients-table specs, future add-client form-submit specs, the admin clients production-source components for the DOM contract, base-page-object.md for the inherited surface, admin-bulk-actions-page-object.md for the per-source-file template, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL + adminPage fixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto Add-Client-button-rename, .fixed.inset-0.z-50-overlay-primitive-switch, "Delete client"-heading-rename, confirm-button-rename, cancel-button-rename, text-content-button-to-icon-only-button-switch, empty-clients-listing seeding regression that auto-test.skip()s every spec, adminPage fixture authentication-regression failures, /admin/clients middleware-disabling failures, and playwright.config.ts baseURL-change failures; and the 11-step clients.page.ts-change checklist (audit consuming specs under apps/web-e2e/tests/admin/clients.spec.ts, cross-check base-page-object.md for the BasePage posture, cross-check admin-bulk-actions-page-object.md for the admin-tree page-object template, cross-check the production source for the canonical "Add Client" button accessible name, the .fixed.inset-0.z-50 Tailwind utility chain on both modal overlays, the "Delete client" heading text inside the delete confirmation modal, and the accessible names of the confirm-delete / cancel-delete buttons, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound clients driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the clients spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Clients Management"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Collections Page Object (apps/web-e2e/page-objects/admin/collections.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin collections-management driver paired with apps/web-e2e/page-objects/admin/collections.page.ts, the third per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents a named-row helper API (getCollectionByName(name), editCollection(name), deleteCollection(name)) on top of the per-page Locator fields, plus a per-form fill helper (fillCollectionForm({ id?, name, description? })) that encodes the multi-input form-fill convention every future admin-form driver in the suite mirrors. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts — see admin-bulk-actions-page-object.md, clients.page.ts — see admin-clients-page-object.md, comments.page.ts, companies.page.ts, dashboard.page.ts, data-export.page.ts, featured-items.page.ts, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root and admin-clients-page-object.md documents the suite's admin clients-management driver boundary at /admin/clients, this page documents the suite's admin collections-management driver boundary at /admin/collections — the smallest possible page object that lets a spec drive the admin collections listing end-to-end (navigate to /admin/collections via the inherited goto(), locate the page heading, locate the first "Add collection" trigger button by its case-insensitive /add collection/i accessible-name regex, locate the collection-form modal via the .fixed.inset-0.z-50 Tailwind-overlay positional selector once the trigger has been clicked, locate every form-input field by its production-source placeholder (/frontend-frameworks/i for the ID input, /collection name/i for the name input, 🤖 exact-match for the icon input, /short description/i for the description textarea), locate the active toggle by its [role="switch"] ARIA role, locate the cancel / create / save buttons by their case-insensitive accessible names, and expose a per-row collection finder + per-row edit / delete trigger pair via the getCollectionByName(name) / editCollection(name) / deleteCollection(name) helper APIs). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtime BasePage value import; the export class AdminCollectionsPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the two readonly Locator fields covering heading / addCollectionButton; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via the getByRole('heading').first() accessibility-tree-canonical selector for the heading and the getByRole('button', { name: /add collection/i }).first() accessibility-tree-canonical posture for the add-collection trigger; the single navigate() shortcut method that closes over the inherited goto; the three named-row helpers (getCollectionByName(name): Locator with the div-tag Locator + case-insensitive substring filter + .first() pin posture, editCollection(name) and deleteCollection(name) async wrappers that resolve the row via getCollectionByName and click the scoped per-row edit / delete button); the nine per-form-element getters covering collectionFormModal (with the .fixed.inset-0.z-50 Tailwind-utility positional selector and .first() strict-mode-correctness append for the form modal overlay), collectionIdInput / collectionNameInput / collectionIconInput / collectionDescriptionInput (with modal-scoped getByPlaceholder(...) matches against the production-source-emitted placeholders), activeToggle (with the modal-scoped [role="switch"] ARIA role + .first() pin), and cancelButton / createButton / saveButton (with modal-scoped getByRole('button', { name: /…/i }) matches); the per-form fill helper fillCollectionForm(data: { id?: string; name: string; description?: string }) with the named-arg shape and the if (data.id) / if (data.description) guards for the optional fields); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/admin/collections.spec.ts (five flows over the admin collections-management surface — admin can access collections management page, admin can create a new collection, admin can edit an existing collection, admin can delete a collection using native confirm dialog, collections page displays stats cards); the "Why AdminCollectionsPage extends BasePage" three-reason analysis (page-route navigation via the inherited goto method, global header / footer / nav-link chrome surfaced for free, post-navigation waitForPageReady stabiliser); the "Why getByPlaceholder(...) for every form-input field" three-reason analysis (HeroUI's <Input> component does not pair with a visible <label> element, getByRole('textbox', { name: … }) would resolve via the same accessible-name computation but with an extra hop, the data-testid posture would force a production-source change purely for the e2e suite); the "Why collectionFormModal is a getter and not a readonly field" three-reason analysis (late-binding against modal mount/unmount lifecycle, symmetric with clientFormModal / deleteConfirmModal on the clients driver, used as the scope-anchor for nine downstream per-form-element getters); the "Why three named-row helpers" three-reason analysis (the collections page is the first admin-tree surface with per-row edit/delete buttons in the rollout, the helpers compose with the underlying Locator API rather than replacing it, the helpers are documentation-by-default for new contributors); the "Why fillCollectionForm accepts an object and not positional args" three-reason analysis (optional fields with TypeScript-required name, self-documenting at the call site, forward-compatible with new fields); the "Why placeholder-only inputs (no per-input aria-label or data-testid)" three-reason analysis (production-source posture, substring-regex tolerance, locale-stability via the production-source's English-only placeholders); the failure matrix covering every collections-page-level mistake (type-only import drop, extends BasePage clause drop, super(page) drop in the constructor, readonly drop on any field, i flag drop on any name-regex, .first() drop on addCollectionButton / heading / collectionFormModal, addCollectionButton switched to [data-testid="add-collection"] violating production-source-first, collectionFormModal switched to [role="dialog"][aria-modal="true"], any input getter switched from getByPlaceholder(...) to getByLabel(...), modal-scope drop on any input getter or button getter, createButton regex switched from /create collection/i to /create/i, saveButton regex switched from /save changes/i to /save/i, activeToggle switched from [role="switch"] to input[type="checkbox"], getCollectionByName helper drop, getCollectionByName switched from div-tag to [role="row"], editCollection / deleteCollection switched to page.getByRole(...) peers, if (data.id) guard drop in fillCollectionForm, navigate() method drop, file move, class rename, .tsx rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/admin/collections.spec.ts, future per-row collections-table specs, future create-flow validation specs, the admin collections production-source components for the DOM contract, base-page-object.md for the inherited surface, admin-bulk-actions-page-object.md for the per-source-file template, admin-clients-page-object.md for the modal-overlay precedent, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL + adminPage fixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto Add-Collection-button-rename, .fixed.inset-0.z-50-overlay-primitive-switch, form-input-placeholder-rename, HeroUI-<Input>-to-<input>-with-<label> switch, HeroUI-<Switch>-to-native-<input type="checkbox"> switch, create-mode-submit-button-rename, edit-mode-submit-button-rename, per-row-delete-confirm()-to-custom-React-modal switch, per-row-edit-/-delete-button-to-kebab-menu-opener switch, empty-collections-listing seeding regression, adminPage fixture authentication-regression failures, /admin/collections middleware-disabling failures, and playwright.config.ts baseURL-change failures; and the 11-step collections.page.ts-change checklist (audit consuming specs, cross-check base-page-object.md for the BasePage posture, cross-check admin-bulk-actions-page-object.md and admin-clients-page-object.md for the admin-tree page-object template, cross-check the production source for the canonical "Add Collection" button accessible name and the form-input placeholders, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound collections driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the collections spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Collections Management"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Comments Page Object (apps/web-e2e/page-objects/admin/comments.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin comments-management driver paired with apps/web-e2e/page-objects/admin/comments.page.ts, the fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents a HeroUI-Modal-based delete confirmation surface (a [role="dialog"] overlay rather than the browser-native confirm() dialog the collections driver documents, and rather than the custom-React deleteConfirmModal overlay the clients driver documents). Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts — see admin-bulk-actions-page-object.md, clients.page.ts — see admin-clients-page-object.md, collections.page.ts — see admin-collections-page-object.md, companies.page.ts, dashboard.page.ts, data-export.page.ts, featured-items.page.ts, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root and admin-collections-page-object.md documents the suite's admin collections-management driver boundary at /admin/collections, this page documents the suite's admin comments-management driver boundary at /admin/comments — the smallest possible page object that lets a spec drive the admin comments listing end-to-end (navigate to /admin/comments via the inherited goto(), locate the page heading via the inherited accessibility-tree-canonical heading Locator, locate the search input via the [role="searchbox"] ARIA role, drive the search input via searchComments(term) / clearSearch() helpers, and locate both the deletion-confirmation HeroUI Modal and every per-row delete trigger via the deleteCommentDialog / deleteButtons getters). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtime BasePage value import; the export class AdminCommentsPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the two readonly Locator fields covering heading / searchInput; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via the getByRole('heading').first() accessibility-tree-canonical selector for the heading and the getByRole('searchbox').first() accessibility-tree-canonical posture for the search input; the single navigate() shortcut method that closes over the inherited goto; the two per-action methods searchComments(term) (with the await this.searchInput.fill(term) posture) and clearSearch() (with the await this.searchInput.clear() posture); the two per-element getters deleteCommentDialog (with the [role="dialog"] ARIA role + case-insensitive /delete/i text filter for the HeroUI Modal pin) and deleteButtons (with the dual-selector button[color="danger"], button.text-red-600 for the per-row delete-trigger collection covering both the HeroUI color="danger" prop variant and the Tailwind utility-class fallback)); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/admin/comments.spec.ts (four flows over the admin comments-management surface — admin can access comments management page, comments page displays comment list, admin can search comments, admin can open delete comment dialog); the "Why AdminCommentsPage extends BasePage" three-reason analysis (page-route navigation via the inherited goto method, global header / footer / nav-link chrome surfaced for free, post-navigation waitForPageReady stabiliser); the "Why getByRole('searchbox') for the search input" three-reason analysis (HeroUI's <Input type="search"> lights up the canonical role automatically, independent of placeholder text, the data-testid posture would force a production-source change purely for the e2e suite); the "Why searchComments / clearSearch and not direct Locator drives" three-reason analysis (documentation-by-default, forward-compatible with debouncing / IME composition / multi-step interactions, symmetric with future per-input drivers); the "Why deleteCommentDialog and deleteButtons are getters and not readonly fields" three-reason analysis (late-binding against modal mount/unmount lifecycle, late-binding against per-row pagination, symmetric with the modal-getter posture across the admin-tree page-object directory); the "Why a HeroUI Modal (and not confirm() or a custom-React overlay) for the delete confirmation" three-reason analysis (production-source consistency with HeroUI Modal use elsewhere in the admin shell, per-page contract divergence from collections / clients postures, [role="dialog"] lights up the screen-reader path); the "Why deleteButtons uses a dual-selector (button[color="danger"], button.text-red-600)" three-reason analysis (HeroUI color="danger" prop and Tailwind utility-class fallback are both valid production-source shapes, future-proof against HeroUI prop reshuffling, the consuming spec uses an inline svg-children selector for the delete trigger today); the failure matrix covering every comments-page-level mistake (type-only import drop, extends BasePage clause drop, super(page) drop in the constructor, readonly drop on any field, .first() drop on heading / searchInput, searchInput switched from getByRole('searchbox') to getByPlaceholder(...), searchInput switched to [data-testid="search"] violating production-source-first, searchComments(term) method drop, clearSearch() method drop, searchComments switched from fill to type, deleteCommentDialog switched from [role="dialog"] to .fixed.inset-0.z-50, /delete/i text-filter drop, deleteButtons switched from dual-selector to a single selector, navigate() method drop, file move, class rename, .tsx rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/admin/comments.spec.ts, future per-row comment-table specs, future search-flow validation specs, the admin comments production-source components for the DOM contract, base-page-object.md for the inherited surface, admin-bulk-actions-page-object.md for the per-source-file template, admin-clients-page-object.md for the custom-React modal-overlay precedent, admin-collections-page-object.md for the named-row helper API + per-form fill helper conventions, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL + adminPage fixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto HeroUI-<Input type="search">-to-non-search-<input>-switch, HeroUI-<Modal>-to-custom-React-overlay-switch, HeroUI-<Button color="danger">-to-non-color-prop-button-switch, Tailwind-text-red-600-to-semantic-token-switch, modal-text-rename, empty-comments-listing seeding regression, adminPage fixture authentication-regression failures, /admin/comments middleware-disabling failures, and playwright.config.ts baseURL-change failures; and the 11-step comments.page.ts-change checklist (audit consuming specs, cross-check base-page-object.md for the BasePage posture, cross-check admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md for the admin-tree page-object template, cross-check the production source for the canonical [role="searchbox"] ARIA role and the [role="dialog"] ARIA role on the modal, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound comments driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the comments spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Comments Management"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Companies Page Object (apps/web-e2e/page-objects/admin/companies.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin companies-management driver paired with apps/web-e2e/page-objects/admin/companies.page.ts, the fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents both a bare .fixed.inset-0.z-50 Tailwind-overlay form modal (matching the clients driver's posture for the create / edit form) and a separate text-filtered Tailwind-overlay delete-confirmation modal (.fixed.inset-0.z-50 overlay primitive scoped by a hasText: /delete company/i filter — distinct from the clients driver's named-class deleteConfirmModal selector and from the comments driver's [role="dialog"] selector). Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts — see admin-bulk-actions-page-object.md, clients.page.ts — see admin-clients-page-object.md, collections.page.ts — see admin-collections-page-object.md, comments.page.ts — see admin-comments-page-object.md, dashboard.page.ts, data-export.page.ts, featured-items.page.ts, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root and admin-comments-page-object.md documents the suite's admin comments-management driver boundary at /admin/comments, this page documents the suite's admin companies-management driver boundary at /admin/companies — the smallest possible page object that lets a spec drive the admin companies listing end-to-end (navigate to /admin/companies via the inherited goto(), locate the page heading, locate the first "Add Company" trigger button by its case-insensitive /add company/i accessible-name regex, locate the company-form modal via the .fixed.inset-0.z-50 Tailwind-overlay positional selector once the trigger has been clicked, locate the company-name input as the first <input> inside the modal, locate the cancel / create / update buttons via their case-insensitive accessible names, and locate the deletion-confirmation modal via the .fixed.inset-0.z-50 overlay scoped by a hasText: /delete company/i text filter plus the ^delete$ exact-match confirm-button regex). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtime BasePage value import; the export class AdminCompaniesPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the two readonly Locator fields covering heading / addCompanyButton; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass via the getByRole('heading').first() accessibility-tree-canonical selector for the heading and the getByRole('button', { name: /add company/i }).first() accessibility-tree-canonical posture for the add-company trigger; the single navigate() shortcut method that closes over the inherited goto; the seven per-element getters covering companyFormModal (with the .fixed.inset-0.z-50 Tailwind-utility positional selector and .first() strict-mode-correctness append for the form modal overlay), companyNameInput (with the modal-scoped locator('input').first() positional first-input selector), cancelButton / createCompanyButton / updateCompanyButton (with modal-scoped getByRole('button', { name: /…/i }) matches against the per-mode submit button accessible names), deleteConfirmModal (with the .fixed.inset-0.z-50 overlay primitive scoped by a hasText: /delete company/i text filter), and confirmDeleteButton (with the modal-scoped getByRole('button', { name: /^delete$/i }) exact-match accessible-name regex)); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/admin/companies.spec.ts (four flows over the admin companies-management surface — admin can access companies management page, admin can open create company modal, admin can create a new company, admin can open delete company confirmation); the "Why AdminCompaniesPage extends BasePage" three-reason analysis (page-route navigation via the inherited goto method, global header / footer / nav-link chrome surfaced for free, post-navigation waitForPageReady stabiliser); the "Why .fixed.inset-0.z-50 for the form modal" three-reason analysis (production-source consistency with the clients / collections form-modal posture, no [role="dialog"] on the production source today, the data-testid posture would force a production-source change purely for the e2e suite); the "Why .first() on companyFormModal (and not on deleteConfirmModal)" three-reason analysis (.fixed.inset-0.z-50 is a multi-instance selector, deleteConfirmModal uses a text filter to disambiguate, modal-mount lifecycle differences); the "Why companyNameInput uses locator('input').first() (and not getByPlaceholder / getByRole('textbox'))" three-reason analysis (no production-source-stable placeholder, no accessible-name binding via a <label> element, single-input form contract); the "Why confirmDeleteButton uses an exact-match /^delete$/i regex" three-reason analysis (the HeroUI Modal emits the title as the modal's accessible name, the case-insensitive /i flag tolerates capitalisation drift, the modal-scope is the second-line defence); the "Why two distinct submit-button getters (createCompanyButton / updateCompanyButton)" three-reason analysis (the form modal mounts in two distinct modes with two distinct accessible names, per-mode test assertions, future-proof against per-mode-specific submit behaviours); the "Why companyFormModal is a getter and not a readonly field" three-reason analysis (late-binding against modal mount/unmount lifecycle, symmetric with the modal-getter posture across the admin-tree page-object directory, used as the scope-anchor for downstream getters); the failure matrix covering every companies-page-level mistake (type-only import drop, extends BasePage clause drop, super(page) drop in the constructor, readonly drop on any field, .first() drop on heading / addCompanyButton / companyFormModal, /i flag drop on addCompanyButton regex, companyFormModal switched to [role="dialog"][aria-modal="true"], companyNameInput switched from locator('input').first() to getByPlaceholder(...), modal-scope drop on any input getter or button getter, createCompanyButton regex switched from /create company/i to /create/i, updateCompanyButton regex switched from /update company/i to /update/i, ^…$ anchors drop on confirmDeleteButton regex, deleteConfirmModal switched from .fixed.inset-0.z-50 to [role="dialog"], /delete company/i text-filter drop, navigate() method drop, file move, class rename, .tsx rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/admin/companies.spec.ts, future per-row companies-table specs, future status-filter / search-flow validation specs, the admin companies production-source components for the DOM contract, base-page-object.md for the inherited surface, admin-bulk-actions-page-object.md for the per-source-file template, admin-clients-page-object.md for the .fixed.inset-0.z-50 form-modal precedent, admin-collections-page-object.md for the named-row helper API + per-form fill helper conventions, admin-comments-page-object.md for the [role="dialog"] HeroUI Modal-based delete-confirmation modal precedent, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL + adminPage fixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto Add-Company-button-rename, .fixed.inset-0.z-50-overlay-primitive-switch, Create Company / Update Company submit-button-rename, delete-confirmation-modal-title-rename, confirm-delete-button-rename, additional-<input>-before-name-input, empty-companies-listing seeding regression, adminPage fixture authentication-regression failures, /admin/companies middleware-disabling failures, and playwright.config.ts baseURL-change failures; and the 11-step companies.page.ts-change checklist (audit consuming specs, cross-check base-page-object.md for the BasePage posture, cross-check admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md for the admin-tree page-object template, cross-check the production source for the canonical Add Company button accessible name and the .fixed.inset-0.z-50 Tailwind-overlay primitive on the form modal and the delete-confirmation modal, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound companies driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the companies spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Companies Management"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Dashboard Page Object (apps/web-e2e/page-objects/admin/dashboard.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin dashboard-landing driver paired with apps/web-e2e/page-objects/admin/dashboard.page.ts, the sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents a getByRole('tablist')-anchored multi-tab navigation surface with a per-tab selectTab(tabName) helper that closes over a case-insensitive substring-match accessible-name regex (distinct from the form-modal / row-action postures the five prior admin-tree drivers document, and distinct from every public-tree driver in the suite which has no tab-based navigation surface today). Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, data-export.page.ts, featured-items.page.ts, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin dashboard-landing driver boundary at /admin -- the smallest possible page object that lets a spec drive the admin dashboard's landing page end-to-end (navigate to /admin, locate the main content region via the #main-content id-selector, locate the tab navigation via the accessibility-tree-canonical getByRole('tablist') locator, locate the refresh trigger via the case-insensitive /refresh/i accessible-name regex, and select a per-tab navigation target by case-insensitive substring-match accessible-name filter via the selectTab(tabName) helper). Anchored by apps/web-e2e/tests/admin/dashboard.spec.ts (four flows over the admin-shell dashboard-landing surface -- authenticated admin can access admin panel, admin dashboard displays tab navigation, non-admin client is redirected from admin, unauthenticated user cannot access admin); cross-references to base-page-object.md for the inheritance root, admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md / admin-companies-page-object.md for the prior admin-tree page-object boundaries, and to the consuming spec at apps/web-e2e/tests/admin/dashboard.spec.ts for the four flows the driver enables. Any future change to the dashboard driver -- adding a per-stat Locator getter, a clickRefresh() flow helper, an assertTabSelected(tabName) invariant assertion, a per-tab Locator getter on top of the selectTab(tabName) helper, or a data-testid migration of the existing #main-content / getByRole('tablist') postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-dashboard-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/dashboard.spec.ts for the four-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound dashboard driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the dashboard spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Admin: Dashboard"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Featured Items Page Object (apps/web-e2e/page-objects/admin/featured-items.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin featured-items management driver paired with apps/web-e2e/page-objects/admin/featured-items.page.ts, the seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents an #active-only id-selector toggle (a positional <input id="active-only"> checkbox surface, distinct from every other admin-tree driver's getByRole('button') or getByRole('heading') posture) plus a pair of search-input helpers (search(term) / clearSearch() -- composable mutators on the same underlying getByRole('textbox').first() Locator, distinct from the form-modal-bound input mutators every other admin-tree driver documents) plus a statsCards Locator getter that pins to the positional .grid selector (a CSS-utility-class anchor, distinct from the [role="dialog"] / .fixed.inset-0.z-50 overlay primitives every other admin-tree driver's modal-Locator getters use). Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin featured-items-management driver boundary at /admin/featured-items -- the smallest possible page object that lets a spec drive the admin featured-items page end-to-end (navigate to /admin/featured-items, locate the page heading via the inherited-default getByRole('heading').first() posture, locate the first "Add Featured Item" trigger button by its case-insensitive /add featured item/i accessible-name regex, locate the search input as the first <textbox> on the page, locate the active-only filter toggle by its #active-only id-selector, locate the per-row featured-item modal via the [role="dialog"] accessibility-tree-canonical posture, locate the per-stats grid via the positional .grid CSS-utility selector, and run the search / clear-search mutators that close over fill(term) / .clear() on the shared search-input Locator). Anchored by apps/web-e2e/tests/admin/featured-items.spec.ts (five flows over the admin featured-items management surface -- admin can access featured items page, featured items page displays stats cards, admin can open add featured item modal, search input filters featured items, active-only toggle filters items); cross-references to base-page-object.md for the inheritance root, admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md / admin-companies-page-object.md / admin-dashboard-page-object.md for the prior admin-tree page-object boundaries, and to the consuming spec at apps/web-e2e/tests/admin/featured-items.spec.ts for the five flows the driver enables. Any future change to the featured-items driver -- adding a per-row Locator getter, an addFeaturedItem(...) / editFeaturedItem(...) / deleteFeaturedItem(...) flow helper, an assertActiveOnly invariant assertion, a getStatsValue(label) helper, or a data-testid migration of the existing getByRole('textbox').first() / #active-only / .grid / [role="dialog"] postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-featured-items-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/featured-items.spec.ts for the five-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound featured-items driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the featured-items spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Featured Items"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Data Export Page Object (apps/web-e2e/page-objects/admin/data-export.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin data-export widget driver paired with apps/web-e2e/page-objects/admin/data-export.page.ts, the eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a /admin co-tenant widget posture (the data-export widget is composed into the admin dashboard landing page rather than mounted at a dedicated route -- distinct from every prior admin-tree driver's per-feature route posture), (b) a format-button pair (csvButton / jsonButton) pinned to case-insensitive ^CSV$ / ^JSON$ exact-match accessible-name regexes, (c) a #include-metadata id-selector checkbox symmetric with the featured-items driver's #active-only posture, (d) a broad-name exportButtons Locator that intentionally resolves to a multi-element match via the case-insensitive /export|download/i alternation regex, and (e) a progressBar Locator with composite-or selectors ([role="progressbar"], .bg-blue-600.rounded-full) -- the first admin-tree driver to document a fallback chain between an accessibility-tree-canonical posture and a positional Tailwind-utility posture. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin data-export widget driver boundary at /admin (composed into the dashboard landing page rather than mounted at a dedicated route) -- the smallest possible page object that lets a spec drive the data-export widget end-to-end (navigate to /admin, locate the widget heading via the inherited-default getByRole('heading').first() posture, locate the first "CSV" format trigger button by its case-insensitive ^CSV$ exact-match accessible-name regex, locate the first "JSON" format trigger button by its case-insensitive ^JSON$ exact-match accessible-name regex, locate the include-metadata toggle by its #include-metadata id-selector, locate every export / download trigger button by the case-insensitive /export|download/i alternation regex, and locate the progress indicator via the composite-or [role="progressbar"], .bg-blue-600.rounded-full selector chain). Anchored by apps/web-e2e/tests/admin/data-export.spec.ts (three flows over the admin data-export widget surface -- admin dashboard has export format buttons, include metadata checkbox is available, export/download buttons are available -- each guarded by a test.skip(true, …) defensive posture so the test remains green when the widget is hidden behind a feature-flag); cross-references to base-page-object.md for the inheritance root, admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md / admin-companies-page-object.md / admin-dashboard-page-object.md / admin-featured-items-page-object.md for the prior admin-tree page-object boundaries, and to the consuming spec at apps/web-e2e/tests/admin/data-export.spec.ts for the three flows the driver enables. Any future change to the data-export driver -- adding a per-format download flow helper, an enableMetadata() / disableMetadata() setter helper pair, an assertProgress(percent) invariant assertion, a format-equivalence helper that switches between CSV and JSON, or a data-testid migration of the existing ^CSV$ / ^JSON$ / #include-metadata / [role="progressbar"], .bg-blue-600.rounded-full postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-data-export-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/data-export.spec.ts for the three-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound data-export driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the data-export spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Data Export"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Item Form Page Object (apps/web-e2e/page-objects/admin/item-form.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin multi-step item creation / edit form modal driver paired with apps/web-e2e/page-objects/admin/item-form.page.ts, the ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a standalone (no BasePage extension) modal driver posture (the AdminItemFormPage class is the first admin-tree driver that does not extend BasePage because the modal owns no route — it is composed into whichever admin route opens it, conventionally /admin/items), (b) a multi-step wizard surface with four documented steps (Basic Info, Media & Links, Classification, Review & Submit) driven by goToNextStep() / goToPreviousStep() mutator helpers and three submit buttons (createButton, updateButton, cancelButton), (c) a [role="dialog"][aria-modal="true"] accessibility-tree-canonical modal selector scoped via this.modal.locator(...) for every per-step input field — the first admin-tree driver to document the explicit aria-modal="true" focus-trapping selector pair (distinct from the comments driver's bare [role="dialog"] posture and the companies driver's positional .fixed.inset-0.z-50 Tailwind-overlay posture), (d) a per-step id-selector input field posture (#id, #name, #slug, #description, #icon_url, #source_url) for every step that emits HeroUI form inputs with a stable id, (e) a placeholder-regex input field posture (getByPlaceholder(/enter categories/i) / getByPlaceholder(/enter tags/i)) for the Classification step's autocomplete inputs that the production source does not bind to a stable id, (f) a bare select HTML-element selector for the status field plus a [role="switch"] accessibility-tree-canonical selector for the featured toggle (distinct from the [role="checkbox"] posture the data-export / featured-items drivers' toggles use because HeroUI's Switch is a binary on/off toggle without an indeterminate state), (g) a stratified helper API across three categories (per-step fill helpers fillBasicInfo({...}) / fillMediaLinks({...}) / addCategory(name) / addTag(name), per-step navigation helpers goToNextStep() / goToPreviousStep(), per-submit helpers submitCreate() / submitUpdate() / cancel()), and (h) a per-modal lifecycle helper API (waitForOpen() / waitForClosed()) that wraps the this.modal.waitFor(...) Playwright primitives in named, intent-revealing methods. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, items.page.ts, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin item-form modal driver boundary — a route-less, modal-bound driver composed into whichever admin route opens the modal (the conventional caller is /admin/items). Anchored by apps/web-e2e/tests/admin/items-crud.spec.ts (a full create-then-edit-then-delete flow over the admin items management surface — nameInput / descriptionInput fill on Step 1, nextButton validity gate, sourceUrlInput fill on Step 2, addCategory(name) / addTag(name) autocomplete commits on Step 3, submitCreate() on the Last Step, then a second flow that opens the modal in edit mode and submits via submitUpdate()); cross-references to base-page-object.md for the inheritance root the standalone posture diverges from, signin-page-object.md / item-detail-page-object.md / discover-page-object.md for the precedent standalone postures, admin-bulk-actions-page-object.md for the /admin/items parent route the modal is conventionally composed into, admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md / admin-companies-page-object.md / admin-dashboard-page-object.md / admin-data-export-page-object.md / admin-featured-items-page-object.md for the prior admin-tree page-object boundaries this driver pairs with, and to the consuming spec at apps/web-e2e/tests/admin/items-crud.spec.ts for the create / edit / delete flows the driver enables. Any future change to the item-form driver -- adding a per-step assertStep(name) invariant assertion, a submitAndWaitClosed() composite helper, a per-modal getValidationError(field) helper, a setStatus(status) / toggleFeatured() setter pair on the Last Step's controls, or a data-testid migration of the existing [role="dialog"][aria-modal="true"] / #name / #description / #source_url / getByPlaceholder(/enter categories/i) / [role="switch"] postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-item-form-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/items-crud.spec.ts for the create / edit / delete flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound item-form driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the items-crud spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Item CRUD"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Items Page Object (apps/web-e2e/page-objects/admin/items.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-management driver paired with apps/web-e2e/page-objects/admin/items.page.ts, the tenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) an eleven-readonly-Locator-field surface -- the largest per-page Locator inventory of any admin-tree driver to date covering page chrome, per-row selection, and the bulk-action toolbar; (b) a nine-method helper API -- the largest per-driver method count of any admin-tree driver to date covering navigation, per-status-tab filtering with a five-element TypeScript union, search-flow mutators, per-row resolution, per-row action-menu interactions, and per-row selection; (c) a two-modal-getter posture (rejectModal and bulkConfirmDialog both pinned to [role="dialog"][aria-modal="true"] with hasText filters); (d) a <input>-id-bound modal-scoped input getter (rejectionReasonInput resolves via this.rejectModal.locator('#rejectionReason')); (e) a <h4>-tag-anchored named-row resolver (getItemByName(name)) that uses a double-.. parent walk to lift the resolution from the per-item heading up to the row container -- the first admin-tree driver posture to document a multi-level DOM-traversal resolution; (f) a multi-attribute composite OR-selector for the pagination Locator (nav[aria-label*="pagination"], nav[aria-label*="Pagination"]); (g) a partial-aria-label-substring-anchored toolbar selector ([role="toolbar"][aria-label*="ulk"]); and (h) exact-match ^approve$ / ^reject$ / ^delete$ regexes for the per-action bulk triggers (distinct from the bulkDeselectButton's substring /deselect/i posture). Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, notifications.page.ts, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin items-management driver boundary at /admin/items -- the smallest possible page object that lets a spec drive the admin items page end-to-end. Anchored by four consuming spec files -- the largest spec-fan-out of any admin-tree driver to date: apps/web-e2e/tests/admin/items.spec.ts (three baseline flows), apps/web-e2e/tests/admin/items-crud.spec.ts (per-item CRUD lifecycle), apps/web-e2e/tests/admin/items-filter.spec.ts (status-tab + search filter flows), and apps/web-e2e/tests/admin/items-review.spec.ts (per-row review + bulk-action flows); cross-references to base-page-object.md for the inheritance root, admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md / admin-companies-page-object.md / admin-dashboard-page-object.md / admin-data-export-page-object.md / admin-featured-items-page-object.md / admin-item-form-page-object.md for the prior admin-tree page-object boundaries, and to the four consuming specs above for the flows the driver enables. Any future change to the items driver -- adding a per-row Locator-factory beyond getItemByName(name), a clickReject(itemName, reason) composite flow helper, an assertItemPresent(name) / assertItemAbsent(name) invariant assertion, a clickPaginationPage(page) / nextPage() / prevPage() pagination helper, or a data-testid migration of the existing getByRole('searchbox') / <h4> / [role="toolbar"][aria-label*="ulk"] / [role="dialog"][aria-modal="true"] postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-items-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the four consuming specs above for the flow envelopes, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound items driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the items spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Items"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Notifications Page Object (apps/web-e2e/page-objects/admin/notifications.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin notifications dropdown driver paired with apps/web-e2e/page-objects/admin/notifications.page.ts, the eleventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a non-extends-BasePage posture (the AdminNotifications class is the first admin-tree driver that does NOT extend BasePage, by design — header-chrome dropdowns do not need the page-navigation helpers BasePage provides); (b) a plain-class constructor binding all four core Locator fields directly in the constructor body (the BasePage subclasses pass super(page) first and bind their per-page Locators afterwards); (c) a four-readonly-Locator-field core surface (bellButton, dropdown, refreshButton, closeButton) plus a five-getter dropdown-content surface (markAllReadButton, unreadBadge, notificationItems, viewAllButton, emptyState); (d) a page.locator(…).first()-anchored bell-button field (page.locator('button[aria-label*="Notifications"]').first()) — the first admin-tree driver field that documents an explicit collection-narrowing call; (e) a #admin-notifications-dropdown id-selector posture for the dropdown panel that scopes every dropdown-content getter via this.dropdown.getByRole(…) / this.dropdown.locator(…); (f) a two-action surfaceopen() and close() — distinct from every prior admin-tree driver's larger method surfaces; (g) a regex-based getByRole('button', { name: … }) resolution for the markAllReadButton and viewAllButton getters that defends against bilingual capitalisation drift; (h) a .animate-pulse-Tailwind-utility-class-anchored unread-badge getter — the first admin-tree driver getter that documents a Tailwind-utility-class-anchored resolution; (i) a [role="button"]-anchored notificationItems getter (vs. getByRole('button')) that disambiguates the per-row count from the per-action button count because the production-source notification items are rendered as <div role="button">; and (j) a getByText(/no notifications/i) empty-state getter — the first admin-tree driver getter that documents a text-content-anchored resolution. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, items.page.ts -- see admin-items-page-object.md, reports.page.ts, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root every other admin-tree page object extends, this page documents the suite's admin notifications dropdown driver boundary -- the smallest possible page object that lets a spec drive the admin shell's bell-button + dropdown + per-content surface end-to-end. Anchored by the single consuming spec file apps/web-e2e/tests/admin/notifications.spec.ts (five test cases covering the bell-button visibility, dropdown open / close, content-or-empty state, and the refresh button); cross-references to base-page-object.md for the inheritance root the standalone posture diverges from, auth-fixture.md for the adminPage fixture the consuming spec uses, admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md / admin-companies-page-object.md / admin-dashboard-page-object.md / admin-data-export-page-object.md / admin-featured-items-page-object.md / admin-item-form-page-object.md / admin-items-page-object.md for the prior admin-tree page-object boundaries, and to the consuming spec at apps/web-e2e/tests/admin/notifications.spec.ts for the dropdown lifecycle flows the driver enables. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-notifications-query.spec.ts which covers the route-side two-step gate (session?.user?.id → 401, then session.user.isAdmin → 403). Any future change to the notifications driver -- adding a per-notification markAsRead(text) helper, a refresh() composite that wraps the refresh-button click + reload settle, a getUnreadCount(): Promise<number> accessor, a getNotificationByText(text) named-row resolver, or a data-testid migration of the existing aria-label*="Notifications" / #admin-notifications-dropdown / aria-label="Refresh notifications" / aria-label="Close notifications panel" postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-notifications-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/notifications.spec.ts for the dropdown flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound notifications driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the notifications spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Admin: Notifications"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Reports Page Object (apps/web-e2e/page-objects/admin/reports.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin reports-management driver paired with apps/web-e2e/page-objects/admin/reports.page.ts, the twelfth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a <button>-anchored status-tab navigation surface rather than the getByRole('tab')-anchored navigation surface every prior status-tab driver uses (the reports page emits the status filter as <button> elements, not [role="tab"] / [role="tablist"]); (b) a five-element status-tab TypeScript union ('All' | 'Pending' | 'Reviewed' | 'Resolved' | 'Dismissed') reflecting the report lifecycle's distinct state machine (Pending → Reviewed → Resolved / Dismissed) rather than the items lifecycle (Draft → Pending → Approved / Rejected); (c) a .border-l-4 Tailwind-utility-anchored card-list selector (reportCards) -- the first admin-tree driver to pin per-row resolution to a Tailwind border-utility class rather than a semantic role; (d) a broad /review/i substring reviewButtons Locator that intentionally resolves to a multi-element match (symmetric with the data-export driver's exportButtons posture); and (e) a bare [role="dialog"] review-dialog getter without the [aria-modal="true"] composite attribute the items driver's rejectModal getter uses (the bare role posture tolerates HeroUI's per-version aria-modal drift). Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, items.page.ts -- see admin-items-page-object.md, notifications.page.ts -- see admin-notifications-page-object.md, roles.page.ts, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin reports-management driver boundary at /admin/reports -- the smallest possible page object that lets a spec drive the admin reports page end-to-end. Anchored by apps/web-e2e/tests/admin/reports.spec.ts (five flows over the admin reports management surface -- admin can access reports management page, reports page displays stats cards, status tabs filter reports, admin can open review dialog for a report, reports page shows empty state for non-matching search); cross-references to base-page-object.md for the inheritance root, admin-bulk-actions-page-object.md / admin-clients-page-object.md / admin-collections-page-object.md / admin-comments-page-object.md / admin-companies-page-object.md / admin-dashboard-page-object.md / admin-data-export-page-object.md / admin-featured-items-page-object.md / admin-item-form-page-object.md / admin-items-page-object.md / admin-notifications-page-object.md for the prior admin-tree page-object boundaries, and to the consuming spec at apps/web-e2e/tests/admin/reports.spec.ts for the five flows the driver enables. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-reports-query.spec.ts which covers the route-side single-step gate (!session?.user?.isAdmin → 403 'Forbidden', distinct from the notifications route's two-step 401/403 gate). Any future change to the reports driver -- adding a per-card Locator-factory beyond the reviewButtons multi-resolution Locator, a clickReview(reportId) / dismissReport(reportId) / resolveReport(reportId, notes) flow helper, an assertCardCount(n) / assertEmptyState() invariant assertion, a clearSearch() reset helper, or a data-testid migration of the existing getByRole('button') / getByRole('searchbox') / [role="dialog"] / .border-l-4 postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-reports-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/reports.spec.ts for the five-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound reports driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the reports spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Reports"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Roles Page Object (apps/web-e2e/page-objects/admin/roles.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin roles-management driver paired with apps/web-e2e/page-objects/admin/roles.page.ts, the thirteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a <select>-anchored dual-filter surface for status and role-type — distinct from every prior admin-tree driver in the rollout which pin status filters via either getByRole('tab') (items, clients, comments, companies, collections) or getByRole('button') (reports). The roles page emits each filter as a native HTML <select> element, and the driver locates them positionally via page.locator('select').first() and page.locator('select').nth(1); (b) a modal-overlay-getter triplet (roleFormModal, deleteRoleDialog, permissionsModal) pinned to the .fixed.inset-0.z-50 Tailwind-utility-stack selector rather than the [role="dialog"] / [aria-modal="true"] accessibility-tree-canonical selectors every prior admin-tree driver uses (the roles page renders modals as bare Tailwind-utility-stacked <div> elements, NOT [role="dialog"] / [aria-modal="true"] ARIA-tree-canonical surfaces); (c) a Locator.filter({ hasText }) chained Locator posture for the two specialised modal getters — the first admin-tree driver in the rollout to use Locator.filter({ hasText }) for modal disambiguation; (d) a searchRoles(term) flow helper that does NOT trigger search submission (consumer must wait the debounce window explicitly); (e) a bare getByRole('heading').first() heading resolver and a bare getByRole('button', { name: /add role/i }).first() add-button resolver; and (f) a <input type="text"> first-element search resolver (the roles page emits the search input as a bare <input type="text">, NOT a <input type="search"> resolvable via getByRole('searchbox')). Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, items.page.ts -- see admin-items-page-object.md, notifications.page.ts -- see admin-notifications-page-object.md, reports.page.ts -- see admin-reports-page-object.md, settings.page.ts, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin roles-management driver boundary at /admin/roles -- the smallest possible page object that lets a spec drive the admin roles page end-to-end. Anchored by apps/web-e2e/tests/admin/roles.spec.ts (four flows over the admin roles management surface -- admin can access roles management page, roles page displays stats cards, admin can search roles, admin can open add role form modal); cross-references to base-page-object.md for the inheritance root, all twelve prior admin-tree page-object docs for the prior admin-tree page-object boundaries, and to the consuming spec at apps/web-e2e/tests/admin/roles.spec.ts for the four flows the driver enables. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-roles-stats-query.spec.ts which covers the route-side two-step gate (session?.user → 401 'Unauthorized', then session.user.isAdmin → 403 'Forbidden') for /api/admin/roles/stats — distinct from the reports route's single-step !session?.user?.isAdmin → 403 'Forbidden' gate AND from the clients / comments / companies / users routes' single-step !session?.user?.isAdmin → 401 'Unauthorized' gate. Any future change to the roles driver -- adding a per-row Locator-factory beyond the positional <select> resolvers, a clickAddRole() / clickDeleteRole(name) / editRolePermissions(name) flow helper, an assertRolePresent(name) / assertRoleAbsent(name) invariant assertion, a debounce-wait helper for the searchRoles(term) posture, or a data-testid migration of the existing positional-<select> / .fixed.inset-0.z-50 Tailwind-utility-stack postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-roles-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/roles.spec.ts for the four-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound roles driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the roles spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Roles"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Settings Page Object (apps/web-e2e/page-objects/admin/settings.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin settings-management driver paired with apps/web-e2e/page-objects/admin/settings.page.ts, the fourteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a minimal-fields, accordion-section-driven driver posture -- the smallest admin-tree page object surface to date with only ONE readonly Locator field (heading), one method (navigate()), one helper (openSection(sectionName)), and two getters (switches, selects) -- justified by the page's content shape (a vertical accordion of collapsed sections, each opened on demand); (b) a getByRole('button', { name: ... }).first() accordion trigger resolver that uses a runtime-built RegExp (new RegExp(sectionName, 'i')) rather than a static regex literal -- letting the consuming spec drive any accordion section by name via a string parameter; (c) a broad multi-resolution switches Locator (page.locator('[role="switch"]')) that exposes every toggle switch on the page -- the first admin-tree driver to do so; (d) a broad multi-resolution selects Locator (page.locator('select')) using the bare HTML element-selector to pin to native <select> elements (distinct from the WAI-ARIA [role="listbox"] posture HeroUI's React Select component emits); and (e) a per-section accordion lifecycle posture with seven canonical sections (General, Homepage, Header, Footer, Monetization, Location, Navigation) -- distinct from every prior admin-tree driver where the page is a flat surface or a single-modal composite. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, items.page.ts -- see admin-items-page-object.md, notifications.page.ts -- see admin-notifications-page-object.md, reports.page.ts -- see admin-reports-page-object.md, roles.page.ts -- see admin-roles-page-object.md, sponsorships.page.ts, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin settings-management driver boundary at /admin/settings -- the smallest possible page object that lets a spec drive the admin settings page end-to-end. Anchored by apps/web-e2e/tests/admin/settings.spec.ts (six flows over the admin settings management surface -- admin can access settings page, settings page has accordion sections, admin can expand General Settings section, admin can expand Homepage Settings section, admin can expand Header Settings section, admin can expand Monetization Settings section); cross-references to base-page-object.md for the inheritance root, all thirteen prior admin-tree page-object docs, and to the consuming spec at apps/web-e2e/tests/admin/settings.spec.ts for the six flows the driver enables. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-settings-query.spec.ts which covers the route-side getCachedApiSession(req) cached-session helper (a custom variant of auth() that caches the session lookup per-request) and the bare { error: 'Unauthorized' } envelope (NOT { success: false, error } shape every other admin-tree route emits). Any future change to the settings driver -- adding a closeSection(sectionName) helper, an isOpen(sectionName): Promise<boolean> accessor, per-section convenience methods (openGeneral() / openHomepage() / etc.), a toggleSwitch(switchName) / selectOption(selectName, value) form-control helper, a submit() / save() form-submission helper, or a data-testid migration of the existing [role="switch"] / select / getByRole('button') postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-settings-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/settings.spec.ts for the six-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound settings driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the settings spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Settings"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Sponsorships Page Object (apps/web-e2e/page-objects/admin/sponsorships.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsorships-management driver paired with apps/web-e2e/page-objects/admin/sponsorships.page.ts, the fifteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a dual-modal-getter posture that uses two different selector strategies for two semantically distinct modals on the same page — rejectModal pinned to the WAI-ARIA-canonical [role="dialog"][aria-modal="true"] selector with .first() (cheapest resolver because the rejection modal is positionally first), forceApproveModal pinned to the less-strict [role="dialog"] selector chained with Locator.filter({ hasText: /force approve/i }) (no positional guarantee because confirmation, error, or info modals may mount between them); (b) a fire-and-forget searchSponsorships(term) flow helper that does NOT trigger search submission — symmetric with the roles driver's searchRoles(term) posture (consumer must wait the debounce window explicitly via page.waitForTimeout(…)); (c) a <input>-id-bound modal-scoped input getter (rejectionReasonInput) that resolves at the page-scope via this.page.locator('#rejectionReason') rather than the modal-scope (this.rejectModal.locator('#rejectionReason')) — defensive against future portal-render refactors that mount the textarea outside the modal subtree; (d) a getByRole('searchbox').first() search input resolver symmetric with the items / clients / comments / companies / collections drivers' search posture (the sponsorships page emits the search input as a native <input type="search"> resolvable via getByRole('searchbox') — distinct from the roles driver's bare <input type="text"> first-element posture); and (e) a bare getByRole('heading').first() heading resolver. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, items.page.ts -- see admin-items-page-object.md, notifications.page.ts -- see admin-notifications-page-object.md, reports.page.ts -- see admin-reports-page-object.md, roles.page.ts -- see admin-roles-page-object.md, settings.page.ts -- see admin-settings-page-object.md, surveys.page.ts, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin sponsorships-management driver boundary at /admin/sponsorships -- the smallest possible page object that lets a spec drive the admin sponsorships page end-to-end. Anchored by apps/web-e2e/tests/admin/sponsorships.spec.ts (three flows over the admin sponsorships management surface -- admin can access sponsorships management page, sponsorships page displays stats and content, admin can search sponsorships); cross-references to base-page-object.md for the inheritance root, all fourteen prior admin-tree page-object docs, and to the consuming spec at apps/web-e2e/tests/admin/sponsorships.spec.ts for the three flows the driver enables. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-sponsor-ads-query.spec.ts which covers the route-side single-step !session?.user?.isAdmin → 401 'Unauthorized. Admin access required.' gate (the longer-message variant — distinct from the bare 'Unauthorized' message every other admin-tree route emits and distinct from the bare 'Forbidden' message the reports route's single-step gate emits) for /api/admin/sponsor-ads and pins the AFTER-the-auth-gate ordering of validatePaginationParams(searchParams) and querySponsorAdsSchema.safeParse(queryParams) (a regression that swaps the order would surface as 400 instead of 401 on the unauth branch). Any future change to the sponsorships driver -- adding a clickReject(itemName, reason) / clickForceApprove(itemName) composite flow helper, an assertSponsorshipPresent(name) / assertSponsorshipAbsent(name) invariant assertion, a selectStatusTab(status) / selectStatusFilter(status) status-filter helper, a clickPaginationPage(page) / nextPage() / prevPage() pagination helper, a getSponsorshipByName(name) / getSponsorshipById(id) per-row Locator-factory, or a data-testid migration of the existing getByRole('searchbox') / [role="dialog"][aria-modal="true"] / [role="dialog"] + hasText / #rejectionReason postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-sponsorships-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/sponsorships.spec.ts for the three-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound sponsorships driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the sponsorships spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Sponsorships"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Surveys Page Object (apps/web-e2e/page-objects/admin/surveys.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin surveys-management driver paired with apps/web-e2e/page-objects/admin/surveys.page.ts, the sixteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/ and the first admin-tree driver in the rollout that documents (a) a bare page.locator('h1').first() heading resolver -- distinct from every other admin-tree driver's page.getByRole('heading').first() posture (the surveys page emits its top-level heading as a literal <h1> element today, so the bare-tag-name selector is the production-source-stable hook); (b) a literal-union-typed selectFilter(filter) flow helper that takes a 'all' | 'global' | 'item' literal-union argument and dispatches on a Record<string, RegExp> filterMap to a getByRole('button', { name: filterMap[filter] }).first().click() call -- with three case-insensitive regexes (/all surveys/i, /global/i, /items/i) -- the literal-union typing is load-bearing because it enforces the filterMap domain at compile-time (a regression that drops the union type would let consumers pass an arbitrary filter name resolving to undefined via the filterMap index, surfacing as a runtime Cannot read properties of undefined failure rather than a compile-time type error); (c) a dual index-based per-row Locator-factory posture (getEditButton(index) / getDeleteButton(index)) that returns this.page.locator('button[title*="Edit"]').nth(index) and this.page.locator('button[title*="Delete"]').nth(index) -- the first title-attribute substring posture in the admin-tree page-object subtree (the per-row buttons are icon-only buttons with no visible text label, so the title attribute substring-match is the next-best production-source-stable hook); and (d) a getByRole('button', { name: /create survey/i }).first() CTA-button resolver that pins to the case-insensitive accessible-name regex match for the page's primary CTA with .first() defence against the empty-state illustration's duplicate CTA. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, items.page.ts -- see admin-items-page-object.md, notifications.page.ts -- see admin-notifications-page-object.md, reports.page.ts -- see admin-reports-page-object.md, roles.page.ts -- see admin-roles-page-object.md, settings.page.ts -- see admin-settings-page-object.md, sponsorships.page.ts -- see admin-sponsorships-page-object.md, tags.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin surveys-management driver boundary at /admin/surveys -- the smallest possible page object that lets a spec drive the admin surveys page end-to-end. Anchored by apps/web-e2e/tests/admin/surveys.spec.ts (four flows over the admin surveys management surface -- admin can access surveys management page, surveys page shows create survey button (with a fallback warning-banner check when the surveys feature is flag-disabled), filter buttons switch between All / Global / Item surveys, survey list shows edit and delete actions); cross-references to base-page-object.md for the inheritance root, all fifteen prior admin-tree page-object docs, and to the consuming spec at apps/web-e2e/tests/admin/surveys.spec.ts for the four flows the driver enables. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-tags-query.spec.ts which covers the route-side single-step !session?.user?.isAdmin → 401 { success: false, error: 'Unauthorized' } gate (the bare-message-with-success-key envelope variant -- the only admin-tree route that combines both the bare 'Unauthorized' message AND the success: false discriminator key, distinct from the longer-message variant 'Unauthorized. Admin access required.' that the admin/categories / admin/sponsor-ads routes emit and distinct from the bare-key envelope { error: 'Unauthorized' } (no success: false discriminator) that the admin/clients / admin/comments / admin/companies / admin/users routes emit) for /api/admin/tags and pins the AFTER-the-auth-gate ordering of validatePaginationParams(searchParams) (a regression that swaps the order would surface as a 400 'Invalid page parameter. …' instead of a 401 on the unauth branch when the query is malformed). Any future change to the surveys driver -- adding a clickCreateSurvey() flow helper, a clickEditSurvey(index) / clickDeleteSurvey(index) composite flow helper, an assertSurveyPresent(name) / assertSurveyAbsent(name) invariant assertion, a waitForListReady() post-load wait helper to replace the consuming spec's inline waitForTimeout(2_000) calls, a surveyForm / surveyEditModal modal Locator, a clickPaginationPage(page) / nextPage() / prevPage() pagination helper, an additional filter value beyond 'all' / 'global' / 'item' (e.g. 'archived', 'draft') that must extend BOTH the literal-union type AND the filterMap dispatch, or a data-testid migration of the existing h1 / getByRole('button', { name: /create survey/i }) / getByRole('button', { name: filterMap[filter] }) / button[title*="Edit"] / button[title*="Delete"] postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-surveys-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/surveys.spec.ts for the four-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound surveys driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the surveys spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Surveys"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Admin Tags Page Object (apps/web-e2e/page-objects/admin/tags.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin tags-management driver paired with apps/web-e2e/page-objects/admin/tags.page.ts, the seventeenth and final per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/admin/, completing the admin-tree page-object docs rollout (17-of-17). With this page landed, every concrete page-object source file under apps/web-e2e/page-objects/admin/ has a paired per-source-file docs anchor that explains the load-bearing reasons each Locator pins to its current selector and the cross-references that any new helper must respect. The tags driver is the first admin-tree driver in the rollout that documents (a) a named-row-resolved CRUD helper trio (getTagByName(name), editTag(name), deleteTag(name)) -- the most direct named-row-driven CRUD posture in the admin tree (distinct from the items driver's resolver-only posture and from the collections driver's single-parent-walk resolver); (b) a <div>-anchored named-row resolver with a ^${name} start-anchor regex -- the broadest possible row-anchor in the admin tree plus the first admin-tree driver posture to document a regex-based hasText filter that pins to the row's text content STARTING with the tag name; (c) a #tag-id and #tag-name hyphenated-kebab-case id-selector input field pair -- production-source-stable hooks following the production source's HTML form convention rather than HeroUI's camelCase default; (d) a modal-scoped [role="switch"] status toggle getter scoped through the tagFormModal (distinct from the settings driver's page-level switches multi-resolution Locator); (e) a .fixed.inset-0.z-50 Tailwind-overlay form modal (matching the companies and roles drivers); (f) a per-mode submit-button-pair posture (createTagButton / updateTagButton) mirroring the companies driver; and (g) a two-key data: { id?: string; name: string } optional-id form-fill helper -- the first admin-tree driver helper to document a conditional-fill posture driven by an optional TypeScript object key. Sits inside the admin/ page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts -- see admin-bulk-actions-page-object.md, clients.page.ts -- see admin-clients-page-object.md, collections.page.ts -- see admin-collections-page-object.md, comments.page.ts -- see admin-comments-page-object.md, companies.page.ts -- see admin-companies-page-object.md, dashboard.page.ts -- see admin-dashboard-page-object.md, data-export.page.ts -- see admin-data-export-page-object.md, featured-items.page.ts -- see admin-featured-items-page-object.md, item-form.page.ts -- see admin-item-form-page-object.md, items.page.ts -- see admin-items-page-object.md, notifications.page.ts -- see admin-notifications-page-object.md, reports.page.ts -- see admin-reports-page-object.md, roles.page.ts -- see admin-roles-page-object.md, settings.page.ts -- see admin-settings-page-object.md, sponsorships.page.ts -- see admin-sponsorships-page-object.md, surveys.page.ts -- see admin-surveys-page-object.md). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's admin tags-management driver boundary at /admin/tags -- the smallest possible page object that lets a spec drive the admin tags page end-to-end. Anchored by apps/web-e2e/tests/admin/tags.spec.ts (five flows over the admin tags management surface -- admin can access tags management page, admin can create a new tag, admin can edit an existing tag, admin can delete a tag using native confirm dialog, tags page shows tag count in stats); cross-references to base-page-object.md for the inheritance root and all sixteen prior admin-tree page-object docs. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-tags-all-query.spec.ts which covers the route-side getCachedItems({ lang }) Git-CMS reader (distinct from every other admin-tree route's database-backed posture) and the ?locale= query param with type-coercion validation. Any future change to the tags driver -- adding a createTag(data) / submitCreate(data) / submitEdit(data) composite flow helper, a confirmDelete() helper for the native confirm() dialog, an assertTagPresent(name) / assertTagAbsent(name) invariant assertion, a getTagsCount(): Promise<number> accessor, or a data-testid migration of the existing <div>-anchored row resolver / #tag-id / #tag-name / [role="switch"] / .fixed.inset-0.z-50 postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update admin-tags-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/admin/tags.spec.ts for the five-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and adminPage fixture binding, cross-check fixtures-index.md for a future fixture-bound tags driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the tags spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Tags"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the admin-tree page-object docs rollout is complete (17-of-17); subsequent rollouts should turn to the apps/web-e2e/page-objects/auth/ and remaining apps/web-e2e/page-objects/client/ subtrees.
  • E2E Client Dashboard Page Object (apps/web-e2e/page-objects/client/dashboard.page.ts) -- Per-source-file reference for the Playwright e2e suite's client dashboard driver paired with apps/web-e2e/page-objects/client/dashboard.page.ts, the first per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/client/, opening the client-tree page-object docs rollout (1-of-6) that mirrors the seventeen-file admin-tree rollout completed at admin-tags-page-object.md. The five sibling client page objects this rollout will publish references for in subsequent runs are profile.page.ts, settings.page.ts, submissions.page.ts, submit.page.ts, and trash.page.ts (six total under apps/web-e2e/page-objects/client/). The client dashboard driver is the first client-tree driver in the rollout that documents (a) a smallest-possible-surface posture with only a navigate() method and three pre-bound Locator fields (heading / statsGrid / welcomeText) -- no composite primitives today; consuming specs drive the page directly via inline locators on top of the auth-fixture.md clientPage authenticated-page fixture (a posture symmetric with discover-page-object.md and signin-page-object.md); (b) a getByRole('heading', { name: /dashboard/i }) locale-tolerant case-insensitive substring resolver for the dashboard heading -- the highest-stability locator per Playwright's locator-priority guidance and the most resilient against future English-string changes ("My Dashboard" / "Welcome to your Dashboard") and locale switches across EN / FR / ES / DE / AR / ZH; (c) a .grid.grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-4 Tailwind responsive class chain anchor for the stats grid -- a brittle-by-design structural anchor that pins the responsive 1-column-mobile / 2-column-tablet / 4-column-desktop grid contract that the dashboard's visual hierarchy depends on (no data-testid is wired up today, and the change checklist anticipates a future data-testid="stats-grid" migration); (d) a getByText(/welcome back/i) greeting-string-tolerant resolver for the per-user welcome message -- substring-anchored to survive a future "Welcome back, {name}!" or "Welcome back to {feature}" change without masking a real greeting-rename regression; and (e) a .first() strict-mode-correctness append on every Locator field -- preventing future strict-mode multi-match errors from a second /dashboard/i heading, a secondary stats grid, or a "Welcome back to {feature}" widget. Sits inside the client/ page-object subtree alongside the five sibling client-surface page objects pending docs (profile.page.ts, settings.page.ts, submissions.page.ts, submit.page.ts, trash.page.ts) and is paired one-to-one with routes under apps/web/app/[locale]/client/**, the authenticated client area of the public-facing app (distinct from the admin area at /admin/** which the seventeen admin-* references cover, and distinct from the public-facing pages at /, /discover, /items/[slug], etc. that the public-pages-page-object.md through item-detail-page-object.md fourteen public-tree references cover). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's authenticated-client dashboard driver boundary at /client/dashboard -- the smallest possible page object that lets a spec drive the client dashboard end-to-end. Anchored by apps/web-e2e/tests/client/dashboard.spec.ts (three flows over the client dashboard surface -- authenticated client can access dashboard at /client/dashboard, unauthenticated user is redirected to /auth/signin by the [locale]/client/** middleware gate, dashboard displays a /dashboard/i heading); cross-references to base-page-object.md for the inheritance root, auth-fixture.md for the clientPage authenticated-page fixture, signin-page-object.md for the auth-tree driver consuming specs depend on for the authenticated clientPage fixture's setup precondition, admin-dashboard-page-object.md for the admin-area dashboard sibling concept, discover-page-object.md for another smallest-possible-surface page-object posture this driver mirrors, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL posture this file's navigation resolves against, and fixtures-index.md for the fixture barrel that exposes the clientPage authenticated-page fixture. Pinned to the co-tenant API smoke spec at apps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts which covers the route-side getCachedApiSession() cached-session helper and pins the unauthenticated GET branch's 401 envelope contract. Any future change to the client dashboard driver -- adding a getStat(name) per-stat-card resolver, a clickFirstActivity() activity-feed primitive, an assertWelcomeMessage(name) invariant assertion, a data-testid="stats-grid" migration of the existing Tailwind class chain, or a fixture-bound clientDashboardPage accessor -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update client-dashboard-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/client/dashboard.spec.ts for the three-flow envelope, cross-check base-page-object.md for the inheritance root, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and clientPage fixture binding, cross-check fixtures-index.md for a future fixture-bound client dashboard driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the client dashboard subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client: Dashboard"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring across the client/ subtree, and a reviewer pass. With this entry the client-tree page-object docs rollout opens (1-of-6); subsequent rollouts in this subtree will turn to profile.page.ts, settings.page.ts, submissions.page.ts, submit.page.ts, and trash.page.ts.
  • E2E Client Profile Page Object (apps/web-e2e/page-objects/client/profile.page.ts) -- Per-source-file reference for the Playwright e2e suite's client profile / settings driver paired with apps/web-e2e/page-objects/client/profile.page.ts, the second per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/client/ (continuing the client-tree page-object docs rollout, 2-of-6) and the first client-tree driver in the rollout that documents (a) a multi-route navigation pair (navigateToSettings() / navigateToBasicInfo()) — the first client-tree driver to expose more than one navigation shortcut, distinct from every prior page-object driver in the suite which exposes a single navigate() method; (b) an eight-input form-field cluster (displayNameInput, usernameInput, bioInput, locationInput, companyInput, jobTitleInput, websiteInput, saveButton) -- the largest per-page form-field inventory of any non-modal page-object driver in the suite; (c) a camelCase id-selector input field cluster (#displayName, #bio, #jobTitle) matching the HeroUI <Input> component's default id emission for camelCase name props (distinct from the tags driver's hyphenated kebab-case #tag-id posture and from the item-form driver's snake_case #icon_url posture); (d) a .grid Tailwind-utility-anchored settings-cards getter (settingsCards); and (e) a page-level form posture -- distinct from every admin-tree driver's modal-bound form posture (the basic-info form is rendered page-level on a dedicated route at /client/settings/profile/basic-info, not inside a modal overlay). Sits inside the client/ page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts -- see client-dashboard-page-object.md, settings.page.ts, submissions.page.ts, submit.page.ts, trash.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's client profile / settings driver boundary at /client/settings and /client/settings/profile/basic-info. Anchored by apps/web-e2e/tests/client/profile.spec.ts (five flows over the client profile / settings surface -- client can access settings page, settings page shows settings cards grid, client can access basic info form, basic info form has save button, display name field accepts input); cross-references to base-page-object.md for the inheritance root, client-dashboard-page-object.md for the client-tree rollout-template precedent, signin-page-object.md for the auth-tree driver consuming specs depend on for the authenticated clientPage fixture's setup precondition, admin-tags-page-object.md and admin-item-form-page-object.md for the admin-tree id-selector-posture variants this driver's camelCase posture intentionally diverges from. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-clients-dashboard-query.spec.ts which covers the first admin-tree route the smoke layer covers that documents the checkAdminAuth() three-step guard (from @/lib/auth/admin-guard.ts) — distinct from every other admin-tree route's inline gate posture. Any future change to the profile driver -- adding a per-field setDisplayName(name) / setBio(text) setter helper, a fillBasicInfo(data) composite form-fill helper, a submitBasicInfo() / saveAndAssertSuccess() composite flow helper, a getDisplayName(): Promise<string> / getBio(): Promise<string> accessor, per-tab navigation methods beyond navigateToBasicInfo() (e.g. navigateToLocation() / navigateToSecurity()), or a data-testid migration of the existing camelCase id-selector / getByRole('button', { name: /save/i }) postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update client-profile-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/client/profile.spec.ts for the five-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and clientPage fixture binding, cross-check fixtures-index.md for a future fixture-bound profile driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the profile spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Profile"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 2-of-6; subsequent rollouts in this subtree will turn to settings.page.ts, submissions.page.ts, submit.page.ts, and trash.page.ts.
  • E2E Client Settings Page Object (apps/web-e2e/page-objects/client/settings.page.ts) -- Per-source-file reference for the Playwright e2e suite's client settings index driver paired with apps/web-e2e/page-objects/client/settings.page.ts, the third per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/client/ (continuing the client-tree page-object docs rollout, 3-of-6) and the first client-tree driver in the rollout that documents (a) a three-link navigation-shelf cluster (basicInfoLink, securityLink, billingLink) -- the first client-tree driver to expose pre-bound Locators for in-page navigation links, anchored to getByRole('link', { name: /…/i }) substring resolvers, distinct from the profile driver's multi-route navigation method posture which routes via goto(); (b) a .grid.grid-cols-1.md\\:grid-cols-2 Tailwind-class-chain settings-grid getter (settingsGrid) -- pinned to the responsive 1-column-mobile / 2-column-tablet+ Tailwind class chain, distinct from the profile driver's bare .grid posture and from the client-dashboard driver's wider four-column-desktop chain; (c) a single navigate() method -- goto('/client/settings') -- symmetric with every prior page-object driver in the suite that exposes a single navigation shortcut, distinct from the profile driver's multi-route navigateToSettings() / navigateToBasicInfo() pair because the settings index is a single route whose only purpose is to render the navigation shelf of cards; (d) a level: 1 heading getter -- getByRole('heading', { level: 1 }).first() -- the first client-tree driver that pins the heading Locator to the per-page H1 specifically, distinct from the dashboard driver's name: /dashboard/i substring pin and from the profile driver's bare getByRole('heading').first() pin; and (e) a navigation-shelf-only posture -- the driver exposes Locators for the shelf of navigation cards (heading + grid + three links) but no form-field Locators because the /client/settings index is the per-tenant navigation shelf for the per-tab forms (the forms themselves render under /client/settings/profile/basic-info, /client/settings/security/*, etc., scoped through the profile driver and any future per-tab driver, not through this index driver). Sits inside the client/ page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts -- see client-dashboard-page-object.md, profile.page.ts -- see client-profile-page-object.md, submissions.page.ts, submit.page.ts, trash.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's client settings index driver boundary at /client/settings -- the smallest possible page object that lets a spec drive the client settings shelf end-to-end. Anchored by apps/web-e2e/tests/client/settings.spec.ts (three flows over the client settings index surface -- authenticated client can access settings page, settings page displays settings cards via a getByRole('link') count assertion, unauthenticated user is redirected from settings via the [locale]/client/** middleware redirect to /auth/signin); cross-references to base-page-object.md for the inheritance root, client-dashboard-page-object.md for the rollout-template precedent, client-profile-page-object.md for the multi-route navigation pair posture this driver's single-method posture intentionally diverges from, signin-page-object.md for the auth-tree driver consuming specs depend on for the authenticated clientPage fixture's setup precondition, auth-fixture.md for the clientPage fixture used in the two authenticated flows. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-location-index-query.spec.ts which covers the second admin-tree route the smoke layer covers that documents the checkAdminAuth() three-step guard from @/lib/auth/admin-guard.ts AND is the first admin-tree route covered by the smoke layer that exposes BOTH a GET AND a POST handler. Any future change to the settings driver -- adding per-tab clickBasicInfoLink() / navigateToBasicInfoTab() shortcuts, a getCardCount(): Promise<number> accessor, a clickFirstCard() / assertCardOrder(labels) composite flow helper, a fixture-bound clientSettingsPage accessor, a navigateAndWait() composite navigation helper, or a data-testid migration of the existing .grid.grid-cols-1.md\\:grid-cols-2 Tailwind class chain -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update client-settings-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/client/settings.spec.ts for the three-flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and clientPage fixture binding, cross-check fixtures-index.md for a future fixture-bound settings driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the settings spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client: Settings"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 3-of-6; subsequent rollouts in this subtree will turn to submissions.page.ts, submit.page.ts, and trash.page.ts.
  • E2E Client Submissions Page Object (apps/web-e2e/page-objects/client/submissions.page.ts) -- Per-source-file reference for the Playwright e2e suite's client submissions management driver paired with apps/web-e2e/page-objects/client/submissions.page.ts, the fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/client/ (continuing the client-tree page-object docs rollout, 4-of-6) and the first client-tree driver in the rollout that documents (a) a named-row-resolved CRUD helper trio (viewSubmission(title), editSubmission(title), deleteSubmission(title)) -- the first client-tree driver to expose per-row imperative helpers, mirroring the admin-tree admin-tags-page-object.md driver's editTag(name) / deleteTag(name) posture and the admin-collections-page-object.md driver's editCollection(name) / deleteCollection(name) posture but adapted for the client surface; (b) a named-row resolver via two-parent-walk (getSubmissionByTitle(title)) -- the helper resolves a submission row by walking page.locator('h3').filter({ hasText: title }).first().locator('..').locator('..'), pinning to the production source's two-deep <h3> → row card grandparent shape (the deepest parent-walk in the page-object suite, encoding the production source's card-with-header-and-actions layout pattern); (c) a button[title*="…"] substring-attribute-selector triplet (button[title*="iew"] / button[title*="dit"] / button[title*="elete"]) -- the row-action buttons resolve via the HTML title attribute's substring (intentionally dropping the leading capital so that "View" / "view" / "VIEW" all match), the first client-tree driver to document an HTML-attribute-substring selector posture distinct from the admin-tree drivers' aria-label / getByRole postures; (d) a status-filter tab navigator (selectStatusFilter(status: 'all' | 'pending' | 'approved' | 'rejected')) -- a literal-union TypeScript parameter that drives status-tab clicks via getByRole('button', { name: new RegExp('^${status}', 'i') }).first().click() with start-anchor regex defending against future button-text drift like "All submissions" / "Pending review" / "Approved items" / "Rejected by moderator"; (e) a three-modal getter triplet (detailModal, editModal, deleteDialog) -- the first client-tree driver to document multiple [role="dialog"] re-evaluating Locator getters with distinct scoping strategies (detailModal uses bare .first(), editModal uses .filter({ has: this.page.locator('#name') }) form-field-presence scope, deleteDialog uses .filter({ hasText: /delete/i }) body-text scope); (f) a navigation-shelf header pair (heading, newSubmissionLink, trashLink); and (g) a search-input field (searchInput) pinned via input[type="text"][placeholder*="earch"] -- the substring-on-placeholder selector drops the leading capital so that "Search" / "search" / "Search submissions" / "search items" all match. Sits inside the client/ page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts -- see client-dashboard-page-object.md, profile.page.ts -- see client-profile-page-object.md, settings.page.ts -- see client-settings-page-object.md, submit.page.ts, trash.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's client submissions management driver boundary at /client/submissions -- the smallest possible page object that lets a spec drive the client submissions list end-to-end (search, filter by status, view / edit / delete a per-row submission, navigate to the new-submission flow or the trash bin). Anchored by apps/web-e2e/tests/client/submissions.spec.ts (three flows -- submit page loads for authenticated client, submissions list page loads for authenticated client, unauthenticated user is redirected from submissions to /auth/signin) AND apps/web-e2e/tests/client/submit-and-manage.spec.ts (the only client-tree driver consumed by a P0 critical-business-flow spec from PR #621, used for the submit-and-manage flow with getSubmissionByTitle(title) / viewSubmission(title) / editSubmission(title) / deleteSubmission(title) per-row CRUD helpers); cross-references to base-page-object.md for the inheritance root, client-dashboard-page-object.md / client-profile-page-object.md / client-settings-page-object.md for the rollout-template precedents, signin-page-object.md for the auth-tree driver consuming specs depend on for the authenticated clientPage fixture's setup precondition, auth-fixture.md for the clientPage fixture used in all authenticated flows, admin-tags-page-object.md and admin-collections-page-object.md for the admin-tree drivers with the per-row CRUD helper posture this driver mirrors, and admin-comments-page-object.md for the admin-tree driver with the [role="dialog"] delete-confirmation modal posture. Pinned to the co-tenant API smoke spec at apps/web-e2e/tests/api/admin-clients-stats-query.spec.ts which covers the admin-only enhanced-client-statistics endpoint at apps/web/app/api/admin/clients/stats/route.ts and pins the route's inline two-step auth() chain with the uniquely shaped if (!session) first-step gate (checking the whole session object rather than the more common if (!session?.user) pattern the sibling admin/roles/stats route uses) -- the unauthenticated branch returns 401 with the bare 'Unauthorized' envelope, distinct from the catch's 'Failed to fetch client stats' route-specific message and from every other admin-tree stats route's catch envelope. Any future change to the submissions driver -- adding a createSubmission(data) composite flow helper, a confirmDelete() helper for the delete-dialog, an assertSubmissionPresent(title) / assertSubmissionAbsent(title) invariant assertion, a getSubmissionsCount(): Promise<number> accessor, per-tab status-filter shortcuts beyond the literal-union typed selectStatusFilter(status), or a data-testid migration of the existing <h3>-anchored two-parent-walk row resolver / [title*=] row-action triplet / [role="dialog"] modal triplet -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update client-submissions-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming specs at apps/web-e2e/tests/client/submissions.spec.ts AND apps/web-e2e/tests/client/submit-and-manage.spec.ts for the three-flow envelope plus per-row CRUD helper consumers, cross-check base-page-object.md for the inheritance root, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and clientPage fixture binding, cross-check fixtures-index.md for a future fixture-bound submissions driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the submissions spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client: Submissions") AND the submit-and-manage subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client Submit"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 4-of-6; subsequent rollouts in this subtree will turn to submit.page.ts and trash.page.ts.
  • E2E Client Submit Page Object (apps/web-e2e/page-objects/client/submit.page.ts) -- Per-source-file reference for the Playwright e2e suite's client item-submission three-step form driver paired with apps/web-e2e/page-objects/client/submit.page.ts, the fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/client/ (continuing the client-tree page-object docs rollout, 5-of-6) and the first client-tree driver in the rollout that documents (a) a multi-step wizard surface with three documented steps (Basic Info → Payment / Plan Selection → Review & Submit) driven by the nextStepButton / previousButton / submitButton triplet -- distinct from every prior client-tree driver (all single-page surfaces) and distinct from the admin item-form driver's modal-bound four-step wizard; (b) a /submit public-tree route boundary -- the only client-tree page object the docs rollout covers that targets a route OUTSIDE /client/**; (c) a mixed selector-anchor posture combining id-selectors (#name, #description, #categories) for production-source-stable form fields, bare HTML element type-selectors (input[type="url"]) for the LinkInput component which has no stable id today, and accessible-name regex-anchored buttons for wizard navigation triggers; (d) a per-step fillBasicInfo({ name, url, description }) composite helper with a load-bearing fill order (URL first, then name, then description -- to let the LinkInput's OG-metadata fetch fire before the explicit name / description fills); (e) a per-step selectCategory(categoryName) / selectTag(tagName) autocomplete commit helper pair -- the first client-tree driver to document combobox / tag-selection autocomplete helpers; and (f) a selectFreePlan() plan-selection helper with an OR-of-two-substring regex matching either Get Started Free or Select Free button labels. Sits inside the client/ page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts -- see client-dashboard-page-object.md, profile.page.ts -- see client-profile-page-object.md, settings.page.ts -- see client-settings-page-object.md, submissions.page.ts -- see client-submissions-page-object.md, trash.page.ts). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's client item-submission three-step form driver boundary at /submit. Anchored by apps/web-e2e/tests/client/submit-and-manage.spec.ts (the full three-step submit flow runs in serial mode because the subsequent flows depend on the just-submitted item being visible in the submissions list); cross-references to base-page-object.md for the inheritance root, all four prior client-tree page-object docs for the rollout-template precedents, and admin-item-form-page-object.md for the modal-bound counterpart that this driver's per-route three-step form is the public-facing client equivalent of. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-categories-git-query.spec.ts which covers the admin-only Git-repository-status endpoint at apps/web/app/api/admin/categories/git/route.ts -- the first admin-tree route the smoke layer covers that documents a unique combination of FOUR distinct contracts (zero-argument GET() handler signature, the BARE { error: '...' } envelope WITH the role-context-specific 'Unauthorized. Admin access required.' message, GitHub-API-backed service via createCategoryGitService(gitConfig), and three distinct configuration-error 500 envelopes after the gate). Any future change to the submit driver -- adding a submitFullFlow(data) composite that drives all three steps, paid-plan selection helpers (selectProPlan() / selectEnterprisePlan()), an assertStep(step) invariant assertion, a getCurrentStep(): Promise<number> accessor, or a data-testid migration of the existing camelCase id-selector / input[type="url"] / getByRole('button', { name: /next step/i }) postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update client-submit-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check the consuming spec at apps/web-e2e/tests/client/submit-and-manage.spec.ts for the three-step flow envelope, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and clientPage fixture binding, cross-check fixtures-index.md for a future fixture-bound submit driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the submit subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Submit"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 5-of-6; the final entry will turn to trash.page.ts to close the rollout.
  • E2E Client Trash Page Object (apps/web-e2e/page-objects/client/trash.page.ts) -- Per-source-file reference for the Playwright e2e suite's client submissions trash bin driver paired with apps/web-e2e/page-objects/client/trash.page.ts, the sixth and final per-source-file reference the docs tree publishes for any file under apps/web-e2e/page-objects/client/ -- closing the client-tree page-object docs rollout at 6-of-6 -- and the first client-tree driver in the rollout that documents (a) a soft-deleted-row recovery surface at /client/submissions/trash -- the only client-tree page object the docs rollout covers that targets a derived sub-route of an existing client-tree page (the /client/submissions/trash route is a child of the /client/submissions route the client-submissions-page-object.md driver covers); (b) a breadcrumb back-navigation Locator (backLink) pinned via the a[href*="/client/submissions"] substring-attribute selector -- the first client-tree driver to document an href*= substring-attribute selector for back-link navigation, where the *= substring posture defends against future production-source href drift between /client/submissions (the bare-ancestor route) and /client/submissions?status=… (a query-param-augmented variant the production source might add for "go back to the filtered view") and a future locale-prefixed /en/client/submissions shape that Next.js's middleware-based i18n posture sometimes emits server-side; (c) a filter-by-text-content row collection Locator (trashItems) pinned via page.locator('button').filter({ hasText: /restore/i }) -- the first client-tree driver to document a text-content filter on a bare HTML element-type Locator that resolves to every restore button on the trash page (one per soft-deleted row), letting a consuming spec count restorable items via await trashPage.trashItems.count() and act on the first via restoreFirst(); (d) an empty-state-affordance Locator (emptyState) pinned via page.getByText(/trash.*empty|no.*deleted/i).first() -- the first client-tree driver to document an OR-of-two-substring regex on a getByText Locator, where the OR-regex matches either the Your trash is empty / Trash is empty / Trash bin empty rendering OR the No deleted items / No items deleted rendering and the .* between the two substrings allows arbitrary intermediate words; and (e) a bare imperative restoreFirst() mutator -- the first client-tree driver to document a named-action helper that does NOT take a row-key parameter (in contrast to the submissions driver's viewSubmission(title) / editSubmission(title) / deleteSubmission(title) trio) acting on the first matching restore button in DOM order, reflecting the trash bin's intentionally minimal surface where the consuming spec only needs to prove that at least one soft-deleted item can be restored, not that a specific named item can be restored. Sits inside the client/ page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts -- see client-dashboard-page-object.md, profile.page.ts -- see client-profile-page-object.md, settings.page.ts -- see client-settings-page-object.md, submissions.page.ts -- see client-submissions-page-object.md, submit.page.ts -- see client-submit-page-object.md). Where base-page-object.md documents the page-object inheritance root, this page documents the suite's client submissions trash bin driver boundary at /client/submissions/trash. Documents the at-a-glance summary table of every load-bearing element (the type-only Page, Locator import; the import { BasePage } from '../base.page' runtime import; the export class ClientTrashPage extends BasePage single named export; the four readonly Locator fields covering heading / backLink / trashItems / emptyState; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass; the navigate() method that calls the inherited goto('/client/submissions/trash'); the restoreFirst() minimal-surface restore mutator); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 -- E2E Test Coverage; cross-references to base-page-object.md for the inheritance root, all five prior client-tree page-object docs for the rollout-template precedents, and signin-page-object.md for the auth-tree driver consuming specs depend on for the authenticated clientPage fixture's setup precondition. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-categories-all-query.spec.ts which covers the admin-only Git-CMS categories-listing endpoint at apps/web/app/api/admin/categories/all/route.ts -- the first admin-tree route the smoke layer covers that documents the unique combination of THREE distinct contracts (getCachedItems({ lang }) Git-based CMS reader for categories, a ?locale= query param read AFTER the gate WITHOUT any defensive typeof locale !== 'string' narrowing distinct from the sibling tags-all route's dead-branch narrow, and the paired categories-data-route posture as the read-only Git-CMS variant of the database-backed /api/admin/categories listing route distinct from both the database-backed listing posture and the /api/admin/categories/git GitHub-API-backed sibling route). Any future change to the trash driver -- adding a restoreByTitle(title) per-row helper, a permanentlyDelete(title) mutator, an assertEmpty() invariant assertion, a getRestorableCount(): Promise<number> accessor, bulk mutators (restoreAll() / permanentlyDeleteAll() / emptyTrash()), or a data-testid migration of the existing getByRole('heading') / a[href*="/client/submissions"] / button + hasText filter / getByText OR-regex postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update client-trash-page-object.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the baseURL and clientPage fixture binding, cross-check fixtures-index.md for a future fixture-bound trash driver, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the trash subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Trash"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 6-of-6 -- closing the rollout; subsequent rollouts will turn to the public-tree page objects (e.g. home.page.ts, browse.page.ts) or to per-spec docs covering the client / admin / api / public / smoke / i18n / auth test trees.
  • E2E Smoke Health Spec (apps/web-e2e/tests/smoke/health.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public-pages health-check spec paired with apps/web-e2e/tests/smoke/health.spec.ts, the first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/, opening the per-spec-file docs rollout that complements the now-closed page-object docs rollout (the admin-tree at 17-of-17, the public-tree at 14-of-14, the client-tree at 6-of-6, plus the auth/signin and base.page.ts roots). Where the page-object docs rollout documented the driver layer (the *.page.ts files that encapsulate per-page Locator and helper APIs), this page opens the consumer layer rollout -- the *.spec.ts files that import drivers / fixtures / helpers and turn them into assertion-bearing scenarios. The smoke health spec is the first consumer-layer reference in the rollout that documents (a) a session-agnostic posture -- the spec imports the runtime test directly from @playwright/test rather than the project's auth-aware fixture from fixtures-index.md, with the load-bearing reasons being session agnosticism (the smoke layer must prove the public surface is reachable to a fresh, unauthenticated browser context), independence from global-setup.md (the smoke layer must run successfully even before global-setup runs), and a smaller import graph (the runtime test has zero transitive imports beyond Playwright); (b) a data-driven test generation posture -- a single for (const route of PUBLIC_ROUTES) loop generates one Playwright test() per route in the shared PUBLIC_ROUTES constant from e2e-test-data.md, giving a single source of truth for the public-route surface, stable per-route test IDs that survive PUBLIC_ROUTES reordering, and isolated failure where a regression on one route does not cascade to others; (c) a waitUntil: 'domcontentloaded' trade-off -- the second-earliest of Playwright's four wait conditions ('commit' / 'domcontentloaded' / 'load' / 'networkidle'), trading full-page-load wait time for smoke-suite speed while still letting the body-visibility assertion succeed; (d) a < 400 HTTP status threshold that deliberately includes the 3xx redirect class to accept locale-prefix injection 307s (the apps/web/middleware.ts middleware), trailing-slash normalisation 308s, auth-redirect 302s for already-authenticated visitors hitting /auth/signin, and Cache-Control: max-age 304s -- distinct from a === 200 strict pin which would emit spurious failures on every locale-redirect; and (e) a most-universal body Locator pin for the rendered-DOM assertion -- distinct from a main / [role="main"] pin (which would fail on routes that wrap content in non-<main> semantic roots), a header pin (which would fail on auth routes that opt out of the global header), or a page.title() non-empty pin (which would fail on routes that emit metadata-driven empty titles). Sits inside the tests/smoke/ test subtree alongside the sibling smoke spec at tests/smoke/navigation.spec.ts (pending docs). Where base-page-object.md documents the page-object inheritance root and fixtures-index.md documents the fixture-export boundary, this page documents the suite's smoke layer health-check entry point -- the smallest possible "did the page render" assertion-bearing spec the e2e suite publishes. Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-clients-advanced-search-query.spec.ts which covers the admin-only advanced-client-search endpoint at apps/web/app/api/admin/clients/advanced-search/route.ts -- the first admin-tree route the smoke layer covers that documents the unique combination of FOUR distinct contracts (the bare { error: 'Unauthorized' } envelope on the unauth 401 branch with NO success key, the largest documented query-param surface in the admin tree at 13+ keys plus pagination, the inline Number() / Number.isFinite() / Math.floor() / Math.min(Math.max(…, 1), 100) pagination clamp distinct from the shared validatePaginationParams() helper, and four distinct date-range filters via the shared parseDate(v) helper that silently ignores NaN-valued Date objects). Documents the at-a-glance summary table of every load-bearing element (the import { test, expect } runtime import; the single import { PUBLIC_ROUTES } from '../../helpers/test-data' shared-data import; the single test.describe('Smoke: Public pages health check', …) block; the for (const route of PUBLIC_ROUTES) data-driven generator; the per-test title combining name and path; the page.goto() with waitUntil: 'domcontentloaded'; the expect(response, …).not.toBeNull() defensive non-null pin; the expect(response!.status()).toBeLessThan(400) HTTP-status threshold; the expect(page.locator('body')).toBeVisible() rendered-DOM pin); the why waitUntil: 'domcontentloaded' four-condition comparison; the why < 400 four-branch redirect-class enumeration; the why body universal-pin rationale comparing against main / [role="main"] / header / page.title() alternatives; a "What it does not contain" five-bullet enumeration of the deliberate omissions (no data-testid selectors, no accessibility assertions, no screenshot / visual-regression assertions, no per-route content assertions, no locale enumeration, no authenticated-route entries); and the why '@playwright/test' three-reason rationale (session agnosticism, independence from global-setup, smaller import graph). Cross-references to base-page-object.md for the inheritance root the smoke spec deliberately does NOT use, e2e-test-data.md for the shared-data boundary, fixtures-index.md for the fixture-export boundary the smoke spec deliberately does NOT use, playwright-config.md for the project-level config, global-setup.md for the per-suite global setup the smoke spec runs successfully without, and to Spec 010 -- E2E Test Coverage for the spec governing the e2e suite's coverage goals. Any future change to the smoke health spec -- adding per-locale PUBLIC_ROUTES enumeration, adding data-testid selectors, adding accessibility assertions, adding screenshot / visual-regression assertions, adding per-route content assertions, adding authenticated-route entries, switching to waitUntil: 'load' / 'networkidle', switching to a === 200 strict status pin, switching from body to main / [role="main"], switching from '@playwright/test' to the project's auth-aware fixture, or generalising the for (const route of PUBLIC_ROUTES) posture to a forEach / .test.each posture -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each design choice pins to its current shape, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update smoke-health-spec.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the smoke-project filter, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the smoke spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Smoke"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the per-spec-file docs rollout opens (1-of-N); subsequent rollouts in this rollout will turn to tests/smoke/navigation.spec.ts, then to per-tree spec rollouts under tests/admin/ / tests/client/ / tests/public/ / tests/api/ / tests/auth/ / tests/i18n/.
  • E2E Smoke Navigation Spec (apps/web-e2e/tests/smoke/navigation.spec.ts) -- Per-source-file reference for the Playwright e2e suite's core navigation smoke spec paired with apps/web-e2e/tests/smoke/navigation.spec.ts, the second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/, continuing the per-spec-file docs rollout opened by smoke-health-spec.md. Where the sibling smoke-health-spec.md documents a data-driven, breadth-first smoke posture (one test() per route in a shared PUBLIC_ROUTES constant from e2e-test-data.md, body-visibility-only assertions), this page documents the hand-crafted, depth-first smoke posture -- four hand-written test() blocks that exercise specific user-flow primitives (home → item-detail click-through, home → sign-in click-through, an item-grid count assertion, a categories-page heading pin) the per-route health spec cannot exercise. With this entry the per-spec-file docs rollout closes its coverage of the smoke tree (the tests/smoke/ directory has exactly two *.spec.ts files; both now have docs anchors). Documents (a) the four hand-written test() blocks that pin distinct user-flow primitives -- 'home page displays directory items' (item-grid populated invariant via a[href*="/items/"] count > 0), 'can navigate from home to an item detail page' (click-through navigation via locator + click + waitForURL(/\/items\//) + h1 visibility pin), 'can navigate to categories page' (categories-page heading invariant via goto('/categories') + h1 visibility), 'can navigate to sign in from home' (sign-in CTA discoverability via getByRole('link', { name: /sign in/i }) + URL pin to /auth/signin); (b) the why hand-written test() blocks (not a data-driven loop) three-reason rationale -- per-scenario assertion divergence (each scenario pins a structurally different invariant: count > 0 / h1 visibility / URL match / link-click), per-scenario navigation primitive divergence (goto, goto + locator + click + waitForURL, goto + locator, goto + role-locator + click + URL pin), per-scenario test-ID granularity (hand-written, descriptive titles that survive reordering); (c) the why a[href*="/items/"] attribute-substring CSS selector rationale (resilience to the page-object refactor surface, cross-route coverage matching every list / grid / card variant, selector simplicity self-documenting "any anchor whose href contains /items/"); (d) the why getByRole('link', { name: /sign in/i }) accessibility-first locator rationale (tests the user-visible primitive resolved via the accessibility tree exactly the way an assistive-technology user reaches the link, resilience to URL refactor where renaming /auth/signin/login would only require updating the URL pin, cross-locale coverage with the locale: 'en-US' use-flag from playwright-config.md); (e) the why 30-second expect.toBeVisible({ timeout: 30_000 }) override rationale (the explicit override is a deliberate self-documenting pin against future contributors who lower the global default, cold-cache resilience for the home-page render that involves fetching every Git CMS item / locale resource bundle / theme CSS / SSR-hydrated React tree, distinct from the navigation timeout governing goto / waitForURL waits); (f) a "What it does not contain" six-bullet enumeration of the deliberate omissions (no authenticated flows, no form interactions beyond CTA discoverability, no screenshot / visual-regression assertions, no per-locale enumeration, no per-content-warning enumeration, no accessibility-tree assertions beyond the sign-in CTA). Pinned to the co-tenant smoke spec at apps/web-e2e/tests/api/admin-reports-stats-query.spec.ts which covers the admin-only report-statistics endpoint at apps/web/app/api/admin/reports/stats/route.ts -- the first admin-tree route the smoke layer covers that documents the single-step 403 'Forbidden' gate combined with the bare GET() handler signature, an intersection no other admin-tree route documents (the sibling admin-reports-query.spec.ts documents the 403 gate with a GET(request: Request) signature; the admin-roles-stats-query.spec.ts documents the bare GET() signature with a two-step 401/403 gate; this spec documents the intersection -- the single-step 403 gate with no request parameter at all). Cross-references to smoke-health-spec.md (the sibling smoke spec, paired with tests/smoke/health.spec.ts, documenting the data-driven posture this spec deliberately contrasts with), base-page-object.md (the page-object inheritance root the smoke spec deliberately does NOT use), e2e-test-data.md (the shared-data boundary the smoke spec deliberately does NOT import from -- distinct from the sibling health spec which imports PUBLIC_ROUTES from helpers/test-data.ts), fixtures-index.md (the fixture-export boundary the smoke spec intentionally does NOT use, same session-agnostic posture as the sibling health spec), playwright-config.md (the project-level config from which the smoke spec inherits fullyParallel: true / retries: process.env.CI ? 2 : 0 / expect.timeout: 30_000 / navigationTimeout: 60_000 / reporter settings), global-setup.md (the per-suite global setup the smoke spec runs successfully without -- deliberate independence, same posture as the sibling health spec), signin-page-object.md (the sign-in page-object driver the smoke spec does NOT use because the smoke layer must run before any driver is exercised), and to Spec 010 -- E2E Test Coverage for the spec governing the e2e suite's coverage goals. Any future change to the smoke navigation spec -- adding a fifth scenario (e.g. 'can navigate to about page'), generalising the four hand-written blocks into a data-driven loop, replacing the substring-CSS a[href*="/items/"] selector with a data-testid locator, replacing the role-based sign-in locator with a CSS attribute selector, replacing the h1 heading pin with a body-visibility pin (the sibling-spec's posture), adding a per-locale enumeration, adding form-interaction coverage beyond the CTA discoverability, or switching from '@playwright/test' to the project's auth-aware fixture from fixtures-index.md -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each design choice pins to its current shape, and the cross-references that any new helper must respect. The expected change protocol for a refactor: update smoke-navigation-spec.md in the same PR that touches the source file, update docs/log.md with a one-line summary, cross-check e2e-tsconfig.md for the include glob, cross-check playwright-config.md for the smoke-project filter, run dual pnpm tsc --noEmit, run a smoke-subset Playwright run targeting the smoke spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Smoke"), a docs/log.md entry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the per-spec-file docs rollout extends to 2-of-N and the smoke tree closes at 2-of-2; subsequent rollouts will turn to per-tree spec rollouts under tests/admin/ / tests/client/ / tests/public/ / tests/api/ / tests/auth/ / tests/i18n/.
  • E2E Admin Map-Status Query Spec (apps/web-e2e/tests/api/admin-settings-map-status-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin map-provider configuration-status query-param smoke spec paired with apps/web-e2e/tests/api/admin-settings-map-status-query.spec.ts, the third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/, opening the per-admin-API-query-smoke-spec docs sub-rollout that follows the now-closed (2-of-2) tests/smoke/ rollout opened by smoke-health-spec.md and smoke-navigation-spec.md. The route under test (apps/web/app/api/admin/settings/map-status/route.ts) is the first admin-tree route the smoke layer covers that documents three distinct postures the prior sibling specs do not -- (a) the getCachedApiSession(req) request-scoped session resolver (the only admin-tree route the smoke layer covers that uses the wrapper from apps/web/lib/auth/cached-session.ts rather than the bare auth() call every other admin-tree route uses, with the request-bearing GET(req: NextRequest) handler signature necessary because the wrapper requires the NextRequest to key its per-request cache); (b) the single-step gate-collapse posture with the bare { error } envelope (if (!session?.user?.isAdmin) collapses both unauth and auth-non-admin into the same 401 envelope, deliberately lacking the success: false key every other admin-tree route smoke-covered to date emits -- a regression that adopts the canonical envelope would change the client-side error-handling contract for every consumer of the map-status admin dashboard widget); (c) the per-environment publishable-key disclosure surface (the success branch returns booleans derived from Boolean(process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN) and Boolean(process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY), never the actual values; the spec includes a deliberate negative-string assertion that the unauth response body does NOT contain a Mapbox public access token (pk.*) or Google Maps API key (AIza*) substring or either env-var name -- the first admin-tree query smoke that pins a per-env-disclosure contract on the unauth branch). Documents the at-a-glance scenario tree (a 49-path bulk-loop walk of every plausible query-param shape asserting < 500; a bare-{ error } envelope assertion with body.success undefined check; eight per-key isolation walks for ?provider= / ?include= / ?isAdmin= / ?userId= / ?token= / ?bypass= / ?reveal= / Accept header; a per-env-key non-disclosure assertion). Cross-references to smoke-health-spec.md and smoke-navigation-spec.md (the sibling per-spec-file references the rollout opened with), admin-settings-page-object.md (the admin settings page-object driver paired with the same route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 011 -- Maps Providers for the governing specs. With this entry the per-spec-file docs rollout extends to 3-of-N and opens the tests/api/ per-spec-file sub-rollout at 1-of-many.
  • E2E Admin Twenty CRM Config Query Spec (apps/web-e2e/tests/api/admin-twenty-crm-config-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Twenty CRM configuration query-param smoke spec paired with apps/web-e2e/tests/api/admin-twenty-crm-config-query.spec.ts, the fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the second under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/twenty-crm/config/route.ts -- the first admin-tree route the smoke layer covers that documents three distinct postures the prior sibling specs do not (or do partially): (a) the route-specific 'Unauthorized. Admin access required.' error string (distinct from the bare 'Unauthorized' string every other admin-tree route smoke-covered to date emits, and distinct from the 'Forbidden' string the admin/reports and admin/clients/stats routes' second-step gates emit; the only sibling that uses the same purpose-built string is admin/sponsor-ads); (b) the bare GET() handler signature combined with single-step canonical-envelope gate (the handler signature is GET() with no request parameter, so there is no searchParams surface inside the handler at all; combined with the single-step if (!session?.user?.isAdmin) gate and the canonical { success: false, error } envelope, this is the first admin-tree route the smoke layer covers that documents this exact intersection -- distinct from the immediately-preceding admin-settings-map-status-query-spec.md which uses the request-bearing GET(req: NextRequest) signature with the bare { error } envelope); (c) the per-tenant credential-disclosure contract (the success branch returns a Twenty CRM config object whose apiKey field is masked by the TwentyCrmConfigRepository.getConfig() helper to ****<last4>; the spec includes a deliberate negative-string assertion that the unauth response body does NOT contain the masked form via the /\*{4}[A-Za-z0-9]{4}/ regex, the TWENTY_CRM_API_KEY / TWENTY_CRM_BASE_URL env-var names, or any of the config sub-field names apiKey / baseUrl / syncMode -- the first admin-tree query smoke that pins a per-tenant CRM-credential-disclosure contract on the unauth branch). Documents the at-a-glance scenario tree (a 52-path bulk-loop walk asserting < 500; a route-specific Unauthorized-envelope assertion; seven per-key isolation walks for ?syncMode= / ?reveal= / ?isAdmin= / ?userId= / ?token= / ?bypass= / Accept header; a per-tenant CRM-credential non-disclosure assertion; a cross-route error-string isolation assertion that the GET response error does NOT echo any of 'Unauthorized' / 'Forbidden' / 'Failed to retrieve configuration' / 'User ID not found' / 'Insufficient permissions'). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md (the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 4-of-N and the tests/api/ per-spec-file sub-rollout extends to 2-of-many.
  • E2E Admin Sponsor-Ads Query Spec (apps/web-e2e/tests/api/admin-sponsor-ads-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ads listing query-param smoke spec paired with apps/web-e2e/tests/api/admin-sponsor-ads-query.spec.ts, the fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the third under apps/web-e2e/tests/api/. Pairs with the existing spec covering apps/web/app/api/admin/sponsor-ads/route.ts -- the first admin-tree route the smoke layer covers that documents three distinct postures the prior sibling specs do not (or do partially): (a) the route-specific 'Unauthorized. Admin access required.' error string with a request-bearing GET(request: NextRequest) handler signature (distinct from the immediately-preceding admin-twenty-crm-config-query-spec.md which uses the same purpose-built error string but pairs it with a bare GET() handler signature -- the sponsor-ads route is the first admin-tree route the smoke layer covers that documents the longer-message variant combined with a wide query-param surface read via new URL(request.url).searchParams AFTER the auth gate); (b) the widest documented query-param surface in the admin tree paired with deferred Zod validation (the route reads pagination ?page= / ?limit=, enum filters ?status= / ?interval=, free-text search ?search=, and order-targeting keys ?sortBy= / ?sortOrder=, then runs querySponsorAdsSchema.safeParse(queryParams) for shape validation and validatePaginationParams(searchParams) for pagination clamping -- but all of these reads run AFTER the auth gate; the spec includes a deliberate 'Unauthorized. Admin access required.' != 'Invalid query parameters' assertion that pins the auth-gate-before-Zod-validation order and would surface any future re-ordering as a 400 instead of a 401 on the unauth branch); (c) the deepest per-spec isolation walk-set in the admin tree (eight independent query-key isolations ?status= / ?interval= / ?page=&limit= / ?isAdmin= / ?userId= / ?token= / ?bypass= / Accept header, plus repeated-key walks ?as=admin&as=user / ?token=foo&token=bar / ?bypass=1&bypass=0 / ?status=active&status=pending, plus a cookie / X-* header side-channel walk asserting fabricated next-auth.session-token / authjs.session-token cookies and X-Forwarded-For / X-Real-IP headers do NOT bypass auth()). Documents the at-a-glance scenario tree (a 78-path bulk-loop walk asserting < 500; a longer-message envelope assertion with the canonical success: false key; a parameterised-vs-baseline status-stability comparison; six per-key isolation walks plus an Accept header walk; an auth-gate-before-Zod-validation assertion that invalid Zod query params return 401 'Unauthorized. Admin access required.' rather than 400 'Invalid query parameters'; a side-channel cookie / X-* header walk; a cross-route error-string isolation assertion that the GET response error does NOT echo 'Unauthorized' or 'Forbidden'; a repeated-key invariance walk). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md (the sibling per-spec-file references), admin-sponsorships-page-object.md (the admin sponsorships page-object driver paired with the same route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 5-of-N and the tests/api/ per-spec-file sub-rollout extends to 3-of-many.
  • E2E Admin Roles Query Spec (apps/web-e2e/tests/api/admin-roles-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin roles listing query-param smoke spec paired with apps/web-e2e/tests/api/admin-roles-query.spec.ts, the sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fourth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/roles/route.ts -- the first admin-tree route the smoke layer covers that documents an auth-gate-divergence finding: unlike every other admin-tree GET route smoke-covered by the sibling admin-sponsor-ads-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-settings-map-status-query-spec.md, and the other admin-tree query smoke specs (admin-categories-query.spec.ts, admin-clients-query.spec.ts, admin-comments-query.spec.ts, admin-companies-query.spec.ts, admin-dashboard-stats-query.spec.ts, admin-featured-items-query.spec.ts, admin-geo-analytics-query.spec.ts, admin-items-query.spec.ts, admin-items-stats-query.spec.ts, admin-location-index-query.spec.ts, admin-navigation-query.spec.ts, admin-notifications-query.spec.ts, admin-reports-query.spec.ts, admin-reports-stats-query.spec.ts, admin-roles-stats-query.spec.ts, admin-settings-query.spec.ts, admin-tags-query.spec.ts, admin-tags-all-query.spec.ts, admin-twenty-crm-test-connection-body.spec.ts, admin-users-query.spec.ts, admin-users-stats-query.spec.ts), the apps/web/app/api/admin/roles/route.ts GET handler does NOT call auth() and does NOT check session?.user?.isAdmin before delegating to roleRepository.findAllPaginated(...); the same absence holds for the sibling apps/web/app/api/admin/roles/active/route.ts GET handler. The spec is INVARIANT to the resolution of the auth-gate question (logged in docs/questions.md under Q-010b, recommended default "yes, add the same two-step gate as the sibling /api/admin/roles/stats route") -- every assertion uses either the < 500 envelope or the baseline-equality envelope so the spec stays green whether the route remains unauthenticated OR a future contributor adds an auth() gate. Documents three smaller postures the prior sibling specs do not document at this intersection: (a) pagination via the shared validatePaginationParams(searchParams) helper (the same helper used by every paginated admin surface; the helper short-circuits the handler with its { error, status } envelope BEFORE the repository call); (b) narrow inline enum coercion for ?status= (accepts ONLY the literal strings 'active' / 'inactive' and coerces every other value including 'pending' / 'archived' / 'deleted' to undefined via an inline ternary, NOT via a Zod schema); (c) narrow inline enum coercion for ?sortBy= / ?sortOrder= via the as cast on the searchParams.get(...) result with 'name' / 'asc' defaults. Documents the at-a-glance scenario tree (a ~80-path bulk-loop walk asserting < 500; a parameterised-vs-baseline status-stability comparison; a per-helper short-circuit assertion for ?page=invalid / ?limit=invalid; per-key isolation walks for ?status= / ?sortBy= / ?sortOrder= covering the beyond-the-enum values; per-key isolation walks for ?userId= / ?token= / ?bypass= / ?includeDeleted=; an Accept header walk; a side-channel cookie / X-* header walk; a repeated-key walk). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md (the sibling per-spec-file references), admin-roles-page-object.md (the admin roles page-object driver paired with the same route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 6-of-N and the tests/api/ per-spec-file sub-rollout extends to 4-of-many, and surfaces the first auth-gate-divergence finding the docs tree publishes via the question register.
  • E2E Admin Roles Active Query Spec (apps/web-e2e/tests/api/admin-roles-active-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin active-roles query-param smoke spec paired with apps/web-e2e/tests/api/admin-roles-active-query.spec.ts, the seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/roles/active/route.ts -- the second admin-tree route the smoke layer covers that documents the auth-gate-divergence finding opened by the immediately-preceding sibling admin-roles-query-spec.md: the handler does NOT call auth() and does NOT check session?.user?.isAdmin before delegating to roleRepository.findActive(), so the route is effectively public today (the e2e harness hits it without an authenticated session and receives the same 200-with-roles payload an authenticated admin would). The same Q-010b migration-path note in docs/questions.md applies (recommended default "yes, add the same two-step gate as the sibling /api/admin/roles/stats route"). Documents three postures distinct from the sibling listing route: (a) the bare zero-argument GET() Next 16 handler signature (the handler does NOT take a request parameter at all, so every ?…=… permutation is silently discarded by the Next.js routing layer before the handler body runs -- the route is INVARIANT to its query string and every permutation rounds-trips to the same status as the no-arg baseline; a regression that switches the signature to GET(request) and starts reading searchParams.get(...) would change the observable behavior on at least one of the permutations the spec walks); (b) the zero-argument roleRepository.findActive() repository call (the repository is invoked with NO options bag at all -- distinct from the sibling roleRepository.findAllPaginated(options) call; a regression that threads any of the query keys into a new options bag would change the auth-branch payload); (c) the active-roles-specific ?includeInactive=… per-key isolation walk (the route's whole purpose is to return only active roles; a regression that wires ?includeInactive=true into a new options bag would defeat that purpose -- the per-key isolation walk pins the baseline-equality envelope so any such regression surfaces via the auth-branch behavioral test out of scope for this spec). Documents the at-a-glance scenario tree (a ~85-path bulk-loop walk asserting < 500; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for ?status= / ?isAdmin= / ?sortBy= / ?sortOrder= / ?page= / ?userId= / ?token= / ?bypass= / ?includeInactive=; an Accept header walk; a side-channel cookie / X-* header walk; a repeated-key walk). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md (the sibling per-spec-file references), admin-roles-page-object.md (the admin roles page-object driver paired with the same admin route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 7-of-N and the tests/api/ per-spec-file sub-rollout extends to 5-of-many, and the second admin-tree route flagged by Q-010b picks up its own per-source-file reference (the first being admin-roles-query-spec.md).
  • E2E Admin Items Import Body Spec (apps/web-e2e/tests/api/admin-items-import-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-import-execute body / header / method smoke spec paired with apps/web-e2e/tests/api/admin-items-import-body.spec.ts, the seventeenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifteenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/items/import/route.ts -- the first admin-tree route the smoke layer covers that combines a static-path POST handler with a two-step body validation chain AFTER the gate AND AFTER the body parse, distinct from the single-step body validation of admin/items/[id]/review, the three-step body validation of admin/categories/reorder, and the six-step body validation of admin/items/bulk. Documents the unique combination of: (1) POST handler with a static path distinct from the dynamic-segment [id] routes covered by admin-items-id-review-body-spec.md and admin-items-id-history-query-spec.md; (2) single-step auth() chain matching the canonical longer message family (if (!session?.user?.isAdmin)), distinct from the two-step gates of admin/users/check-email / admin/users/check-username / admin/notifications/mark-all-read / admin/clients/bulk; (3) canonical longer 'Unauthorized. Admin access required.' 401 message matching the admin/items/bulk, admin/categories/reorder, admin/items/[id]/review, and admin/twenty-crm/* family; (4) success: false envelope key matching the same family, distinct from the bare { error: 'Unauthorized' } envelope of the two-step-gated routes; (5) body parse via await request.json() AFTER the gate -- the gate-then-parse-then-validate-then-service order is the load-bearing invariant of this route; (6) two-step body validation chain AFTER the gate AND AFTER the body parse with two distinct 400 messages ('Missing or invalid rows array.' on !body.rows || !Array.isArray(body.rows), 'Missing import options.' on !body.options) -- the first two-step body-validation admin-tree smoke the docs tree publishes, complementary to the single-step / three-step / six-step body-validation smokes the sub-rollout previously published; (7) service-call surface AFTER the gate AND AFTER both validation steps -- the handler instantiates new ItemImportService() and calls executeImport(rows, options) with the body's rows and the body's options merged with three defaults (duplicateStrategy ||= 'skip', defaultStatus ||= 'draft', submittedBy = session.user.email || 'admin'), with success-branch payload { success: true, result } where result is the ImportExecutionResult returned by the service; (8) safeErrorResponse(error, 'Failed to execute import') catch matching the admin/items/bulk and admin/items/[id]/history catch family; (9) method-resolution surface with POST-only export, so every other method (GET / PUT / PATCH / DELETE) must round-trip to a < 500 status. Documents the at-a-glance scenario tree (a ~21-header bulk-loop walk + a ~25-body bulk-loop walk both asserting < 500; a canonical-longer 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' } exactly; a negative-property assertion that the unauth response does NOT echo a result key or success: true; a gate-before-body-validation invariant pinning that BOTH 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the 'Failed to execute import' message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to < 500; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk; a service-not-entered invariance walk pinning that the unauth response does NOT echo a result object; a duplicate-strategy / default-status enum-shape invariance walk pinning that the gate fires before the default-fallback; a large-rows-array invariance walk pinning that 10-row and 100-row bodies round-trip to the same status as the empty-rows baseline). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, admin-items-bulk-body-spec.md, admin-clients-bulk-method-spec.md, and admin-items-id-history-query-spec.md (the sibling per-spec-file references), admin-items-page-object.md (the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 17-of-N and the tests/api/ per-spec-file sub-rollout extends to 15-of-many, and the first two-step-body-validation admin-tree smoke lands as a complementary surface to the single-step / three-step / six-step body-validation smokes the sub-rollout previously published.
  • E2E Admin Items Import Validate Body Spec (apps/web-e2e/tests/api/admin-items-import-validate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-import-validate (dry-run) multipart-body / header / method smoke spec paired with apps/web-e2e/tests/api/admin-items-import-validate-body.spec.ts, the eighteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixteenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/items/import/validate/route.ts -- the first admin-tree route the smoke layer covers that combines a static-path POST handler with a multipart/form-data body parsed via await request.formData() AFTER the gate, distinct from every prior admin-tree smoke (which all parse JSON via await request.json()). Documents the unique combination of: (1) POST handler with a static path (sibling of the JSON-body admin-items-import-body-spec.md route); (2) single-step auth() chain matching the canonical longer message family (if (!session?.user?.isAdmin)), distinct from the two-step gates of admin/users/check-email / admin/users/check-username / admin/notifications/mark-all-read / admin/clients/bulk; (3) canonical longer 'Unauthorized. Admin access required.' 401 message matching the admin/items/import, admin/items/bulk, admin/categories/reorder, admin/items/[id]/review, and admin/twenty-crm/* family; (4) success: false envelope key matching the same family; (5) body parse via await request.formData() AFTER the gate -- the first admin-tree smoke spec that documents a formData()-based body parse, complementary to the JSON-based body parses of every prior admin-tree smoke; (6) five-step file / mapping validation chain AFTER the gate AND AFTER the formData parse with five distinct 400 messages ('No file provided.', 'Invalid file type. Only CSV and XLSX files are supported.', 'File too large. Maximum size is 10 MB.', 'Invalid column mapping JSON.', 'File contains no data rows.') -- the first five-step file/mapping-validation admin-tree smoke the docs tree publishes, complementary to the single-step / two-step / three-step / six-step body-validation smokes the sub-rollout previously published; (7) service-call surface AFTER the gate AND AFTER every validation step -- the handler instantiates new ItemImportService() and calls parseCSV(...) / parseXLSX(...) followed by validateRows(...), with success-branch payload { success: true, headers, suggestedMapping, validationResults, summary } (four success-branch keys plus the success: true flag); (8) safeErrorResponse(error, 'Failed to validate import file') catch matching the admin/items/import, admin/items/bulk, and admin/items/[id]/history catch family; (9) method-resolution surface with POST-only export, so every other method (GET / PUT / PATCH / DELETE) must round-trip to a < 500 status. Documents the at-a-glance scenario tree (a ~22-header bulk-loop walk + a ~24-multipart-body bulk-loop walk both asserting < 500; a canonical-longer 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' } exactly; a negative-property assertion that the unauth response does NOT echo any of the four success-branch keys or success: true; a gate-before-validation invariant pinning that ALL FIVE 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the 'Failed to validate import file' message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to < 500; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a malformed-multipart-body invariance walk pinning that the gate fires before the formData parse; a service-not-entered invariance walk pinning that the unauth response does NOT echo any of the four success-branch keys; a file-extension shape invariance walk (whitelisted .csv / .xlsx / .xls plus non-whitelisted .txt / .json / .pdf / extensionless) pinning that the extension whitelist is NOT evaluated on the unauth branch; a mapping-JSON shape invariance walk (valid + invalid + broken + empty + missing) pinning that JSON.parse(mapping) is NOT evaluated on the unauth branch; a duplicate-strategy / default-status enum-shape invariance walk pinning that the gate fires before the default-fallback). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, admin-items-bulk-body-spec.md, admin-clients-bulk-method-spec.md, admin-items-id-history-query-spec.md, and admin-items-import-body-spec.md (the sibling per-spec-file references), admin-items-page-object.md (the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 18-of-N and the tests/api/ per-spec-file sub-rollout extends to 16-of-many, and the first multipart/form-data admin-tree smoke lands as a complementary surface to the JSON-body smokes the sub-rollout previously published.
  • E2E Admin Notifications [id] Read Method Spec (apps/web-e2e/tests/api/admin-notifications-id-read-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-notification mark-as-read method / id / header smoke spec paired with apps/web-e2e/tests/api/admin-notifications-id-read-method.spec.ts, the nineteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventeenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/notifications/[id]/read/route.ts -- the first admin-tree route the smoke layer covers that combines a dynamic-segment [id] PATCH handler with the two-step !session?.user?.id!tenantId gate envelope. Documents the unique combination of: (1) dynamic-segment [id] PATCH handler -- the first dynamic-segment PATCH handler the admin-tree smoke layer pins, distinct from the static-path PATCH of admin/notifications/mark-all-read, the dynamic-segment POST of admin/items/[id]/review, and the dynamic-segment GET of admin/items/[id]/history; (2) two-step gate (!session?.user?.id → 401 'Unauthorized', then AFTER params and AFTER 400 missing-id branch: !tenantId → 403 'Tenant not found') -- SAME envelope as the sibling admin/notifications/mark-all-read, distinct from the single-step !session?.user?.isAdmin gate of the canonical-longer-message admin routes; (3) bare 'Unauthorized' 401 message matching the sibling admin/notifications/mark-all-read, distinct from the canonical longer 'Unauthorized. Admin access required.' of the single-step-gated routes; (4) bare { error: ... } envelope with NO success key on either the 401 or 403 branch -- matching the sibling admin/notifications/mark-all-read, distinct from the { success: false, error: ... } envelope of the canonical-longer-message routes; (5) path-id surface -- the handler reads id from await params AFTER the auth gate, and the unauth branch must NEVER reach the params resolution; (6) tenant-resolution surface AFTER params and AFTER the 400 missing-id branch (!tenantId → 403 'Tenant not found'), unreachable on the unauth branch; (7) DB-update surface AFTER both gates -- the handler issues a Drizzle db.update(notifications) with set({ isRead: true, readAt: ..., updatedAt: ... }) and a three-clause where (id + userId + tenantId), then .returning(), with success-branch payload { success: true, notification: <row> }; (8) console.error + bare 'Internal server error' catch matching the admin/users/check-email / admin/users/check-username family, distinct from the safeErrorResponse(...) catch of the canonical-longer-message routes; (9) method-resolution surface with PATCH-only export, so every other method (GET / POST / PUT / DELETE) must round-trip to a < 500 status. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~20-header bulk-loop walk + a ~9-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion pinning { error: 'Unauthorized' } exactly; a strict envelope-shape assertion Object.keys(body) === ['error']; a negative-property assertion that the unauth response does NOT echo a notification key or success: true; a gate-before-post-auth invariant pinning that NONE of the four post-auth messages ('Notification ID is required', 'Tenant not found', 'Notification not found', 'Internal server error') must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison pinning that the params resolution does NOT happen on the unauth branch; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / POST / PUT / DELETE round-trip to < 500; a malformed-JSON-body invariance walk; a DB-update-not-entered invariance walk pinning that the unauth response does NOT echo a notification key from the Drizzle .returning() payload). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, admin-items-bulk-body-spec.md, admin-clients-bulk-method-spec.md, admin-items-id-history-query-spec.md, admin-items-import-body-spec.md, and admin-items-import-validate-body-spec.md (the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 19-of-N and the tests/api/ per-spec-file sub-rollout extends to 17-of-many, and the first dynamic-segment [id] PATCH admin-tree smoke lands as the dynamic-segment sibling of admin/notifications/mark-all-read.
  • E2E Admin Sponsor Ads [id] Approve Method Spec (apps/web-e2e/tests/api/admin-sponsor-ads-id-approve-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ad approval method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-sponsor-ads-id-approve-method.spec.ts, the twentieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighteenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/sponsor-ads/[id]/approve/route.ts -- the first admin-tree route the smoke layer covers that combines a dynamic-segment [id] POST handler with a forgiving body parse inside its own try/catch AFTER the gate AND AFTER params resolution, AND a multi-error-code catch chain that maps three distinct service-thrown Error.message values to three distinct HTTP envelopes. Documents the unique combination of: (1) dynamic-segment [id] POST handler (sibling of admin/items/[id]/review but with a different gate / body / catch posture); (2) compound single-if gate !session?.user?.isAdmin || !session.user.id -- a single-step gate that ANDs the canonical isAdmin predicate with a !session.user.id falsity probe, observably equivalent to the single-step gate from the unauth client's perspective; (3) canonical longer 'Unauthorized. Admin access required.' 401 message matching the canonical-longer-envelope family; (4) success: false envelope key matching the same family; (5) params resolution AFTER the gate -- every id shape round-trips to the same 401 status as the canonical id baseline; (6) body parse inside its own try/catch AFTER params AND AFTER the gate -- the forceApprove flag defaults to false if the body is missing, malformed, or omits the key; (7) service-call surface AFTER both the gate AND the body parse with sponsorAdService.approveSponsorAd(id, session.user.id, forceApprove), success-branch payload { success: true, data: <ad>, message: 'Sponsor ad approved and activated successfully' }; (8) multi-error-code catch chain mapping 'Sponsor ad not found' → 404, 'PAYMENT_NOT_RECEIVED' → 400, error.message.includes('Cannot approve') → 400, with safeErrorResponse(error, 'Failed to approve sponsor ad') fallback -- the first multi-error-code catch chain admin-tree smoke the docs tree publishes; (9) service-zero-rows fallback returning 500 { success: false, error: 'Failed to approve sponsor ad' } if the service returns falsy; (10) method-resolution surface with POST-only export. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~21-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a canonical-longer 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' } exactly; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a negative-property assertion that the unauth response does NOT echo a data key, a message key, or success: true; a gate-before-post-auth invariant pinning that NONE of the four post-gate messages ('Sponsor ad not found', 'PAYMENT_NOT_RECEIVED', 'Failed to approve sponsor ad', 'Sponsor ad approved and activated successfully') must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to < 500; a malformed-JSON-body invariance walk pinning that the inner try/catch never surfaces a 400; a service-not-entered invariance walk pinning that the unauth response does NOT echo a data key from the service payload; a forceApprove enum-shape invariance walk pinning that the gate fires before the flag evaluation). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, admin-items-bulk-body-spec.md, admin-clients-bulk-method-spec.md, admin-items-id-history-query-spec.md, admin-items-import-body-spec.md, admin-items-import-validate-body-spec.md, and admin-notifications-id-read-method-spec.md (the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 20-of-N and the tests/api/ per-spec-file sub-rollout extends to 18-of-many, and the first multi-error-code catch chain admin-tree smoke lands as a complementary surface to the single-message catch families the sub-rollout previously published.
  • E2E Admin Sponsor Ads [id] Reject Method Spec (apps/web-e2e/tests/api/admin-sponsor-ads-id-reject-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ad rejection method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-sponsor-ads-id-reject-method.spec.ts, the twenty-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the nineteenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/sponsor-ads/[id]/reject/route.ts -- the first admin-tree route the smoke layer covers that combines a dynamic-segment [id] POST handler with a Zod-safeParse(...) body validation AFTER the gate AND AFTER params resolution AND AFTER the body parse, AND a two-branch catch chain that maps two distinct service-thrown Error.message values to two distinct HTTP envelopes. Sibling of admin-sponsor-ads-id-approve-method-spec.md sharing the SAME compound single-if gate (!session?.user?.isAdmin || !session.user.id), the SAME canonical longer 401 envelope, and the SAME { success: false, error: ... } envelope shape. Documents the unique combination of: (1) dynamic-segment [id] POST handler; (2) compound single-if gate; (3) canonical longer 'Unauthorized. Admin access required.' 401 message and success: false envelope key matching the canonical-longer-envelope family; (4) body parse via .catch(() => ({})) Promise-chain catch -- a single-expression catch that returns an empty object on parse failure, distinct from the inner try/catch block of the approve route; (5) Zod-safeParse(...) body validation AFTER the gate and AFTER params resolution and AFTER the body parse, with a 400 response that echoes validationResult.error.issues[0]?.message || 'Invalid request body' -- a dynamic error message drawn from the Zod schema, distinct from the hand-rolled string literals of every prior admin-tree smoke -- the first Zod-safeParse(...) admin-tree smoke the docs tree publishes; (6) schema constraints -- rejectionReason is required with minLength: 10 and maxLength: 500, with id from params co-validated through the schema ({ id, rejectionReason: body.rejectionReason }), distinct from the approve route which validates only the forceApprove flag; (7) service-call surface AFTER both the gate AND the Zod validation with sponsorAdService.rejectSponsorAd(id, session.user.id, validationResult.data.rejectionReason), success-branch payload { success: true, data: <ad>, message: 'Sponsor ad rejected successfully' }; (8) two-branch catch chain mapping error.message.includes('Cannot reject') → 400, 'Sponsor ad not found' → 404, with safeErrorResponse(error, 'Failed to reject sponsor ad') fallback -- a complementary surface to the three-branch catch chain of the sibling approve route; (9) service-zero-rows fallback returning 500 { success: false, error: 'Failed to reject sponsor ad' }; (10) method-resolution surface with POST-only export. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~21-header bulk-loop walk + a ~15-body bulk-loop walk all asserting < 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four post-gate messages ('Sponsor ad not found', 'Failed to reject sponsor ad', 'Sponsor ad rejected successfully', 'Invalid request body') must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe; a malformed-JSON-body invariance walk; a service-not-entered invariance walk; a rejectionReason length / shape invariance walk pinning that the rejectSponsorAdSchema.safeParse(...) is NOT evaluated on the unauth branch -- across every rejectionReason shape (valid 70-char + 10-char-min boundary + 5-char-too-short + empty + null + numeric + 501-char-too-long + missing)). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, admin-items-bulk-body-spec.md, admin-clients-bulk-method-spec.md, admin-items-id-history-query-spec.md, admin-items-import-body-spec.md, admin-items-import-validate-body-spec.md, admin-notifications-id-read-method-spec.md, and admin-sponsor-ads-id-approve-method-spec.md (the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 21-of-N and the tests/api/ per-spec-file sub-rollout extends to 19-of-many, and the first Zod-safeParse(...) admin-tree smoke lands as a complementary surface to the manual-validation smokes the sub-rollout previously published.
  • E2E Admin Sponsor Ads [id] Cancel Method Spec (apps/web-e2e/tests/api/admin-sponsor-ads-id-cancel-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ad cancellation method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-sponsor-ads-id-cancel-method.spec.ts, the twenty-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twentieth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/sponsor-ads/[id]/cancel/route.ts -- the first admin-tree route the smoke layer covers that combines a dynamic-segment [id] POST handler with a pure single-step !session?.user?.isAdmin gate (NOT the compound !isAdmin || !id gate of the sibling approve / reject routes), AND a Zod-safeParse(...) body validation against an optional-only schema (cancelReason has only maxLength: 500 and is NOT required), AND a reverse-ordered two-branch catch chain that puts the not-found 404 branch BEFORE the 'Cannot cancel' 400 branch -- distinct from the sibling reject route which puts 'Cannot reject' 400 BEFORE 'Sponsor ad not found' 404. Documents the unique combination of: (1) dynamic-segment [id] POST handler; (2) pure single-step !session?.user?.isAdmin gate -- a single-step gate that ONLY checks isAdmin, distinct from the compound !isAdmin || !id gate of the sibling approve / reject routes; (3) canonical longer 'Unauthorized. Admin access required.' 401 message and success: false envelope key matching the canonical-longer-envelope family; (4) body parse via .catch(() => ({})) Promise-chain catch; (5) Zod-safeParse(...) body validation AFTER the gate and AFTER params resolution and AFTER the body parse, with cancelSponsorAdSchema co-validating id from params with cancelReason from body; (6) optional-only cancelReason with maxLength: 500 constraint -- a missing / undefined / null cancelReason would pass validation on the auth branch (whereas the sibling reject route requires rejectionReason with minLength: 10) -- the first optional-Zod-field admin-tree smoke the docs tree publishes; (7) service-call surface with sponsorAdService.cancelSponsorAd(id, validationResult.data.cancelReason) (NOTE: no session.user.id audit-user threaded through, distinct from the sibling approve / reject routes), success-branch payload { success: true, data: <ad>, message: 'Sponsor ad cancelled successfully' }; (8) reverse-ordered two-branch catch chain mapping 'Sponsor ad not found' → 404 FIRST, then error.message.includes('Cannot cancel') → 400, with safeErrorResponse(error, 'Failed to cancel sponsor ad') fallback -- distinct from the sibling reject route's order; (9) service-zero-rows fallback returning 500; (10) method-resolution surface with POST-only export. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~21-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four post-gate messages must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe; a malformed-JSON-body invariance walk; a service-not-entered invariance walk; a cancelReason length / shape invariance walk pinning that the cancelSponsorAdSchema.safeParse(...) is NOT evaluated on the unauth branch -- across every cancelReason shape (missing + empty + null + valid + 500-char-boundary + 501-char-too-long + numeric)). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, admin-items-bulk-body-spec.md, admin-clients-bulk-method-spec.md, admin-items-id-history-query-spec.md, admin-items-import-body-spec.md, admin-items-import-validate-body-spec.md, admin-notifications-id-read-method-spec.md, admin-sponsor-ads-id-approve-method-spec.md, and admin-sponsor-ads-id-reject-method-spec.md (the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 22-of-N and the tests/api/ per-spec-file sub-rollout extends to 20-of-many, and the first optional-Zod-field admin-tree smoke lands as a complementary surface to the required-Zod-field and manual-validation smokes the sub-rollout previously published.
  • E2E Admin Roles [id] Permissions Method Spec (apps/web-e2e/tests/api/admin-roles-id-permissions-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin role-permissions dual-method (GET + PUT) / dynamic-id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-roles-id-permissions-method.spec.ts, the twenty-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-first under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/roles/[id]/permissions/route.ts — the first admin-tree route the smoke layer covers that combines (1) a dynamic-segment [id] handler exporting BOTH a GET and a PUT (a true dual-method surface, distinct from every prior single-method admin-id smoke), AND (2) an auth gate that delegates to the checkAdminAuth() helper at apps/web/lib/auth/admin-guard.ts (NOT inline !session?.user?.isAdmin), AND (3) a shorter 'Unauthorized' 401 envelope (NOT the canonical longer 'Unauthorized. Admin access required.' envelope that every prior admin-id smoke pins), AND (4) an imperative permissions-array validation against isValidPermission(permission) from apps/web/lib/permissions/definitions.ts (NOT a Zod safeParse(...) schema, NOT a manual ['approved', 'rejected'].includes(...) allowlist). Documents the checkAdminAuth() helper's three-branch envelope (401 'Unauthorized' no-session / 401 'User ID not found' no-id / 403 'Insufficient permissions' non-admin-auth — distinct from every prior admin-tree route which returns 401 for both unauth AND non-admin-auth), the side-channel invalidPermissions array echoed in the auth-branch 400 envelope (a UNIQUE envelope key no prior admin-tree smoke pins), and pins the gate-before-post-auth, gate-before-params-resolution, gate-before-body-parse, gate-before-service, gate-before-validation, cross-method envelope-equality, side-channel invalidPermissions non-disclosure, and first-branch landing invariants — the tests/ per-spec-file docs rollout extends to 23-of-N and the tests/api/ per-spec-file sub-rollout extends to 21-of-many, and the first dual-method checkAdminAuth()-helper admin-tree smoke lands as a complementary surface to the canonical-longer-envelope, inline-!session?.user?.isAdmin-gated, Zod / manual-validation smokes the sub-rollout previously published.
  • E2E Admin Items [id] Method Spec (apps/web-e2e/tests/api/admin-items-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-item CRUD GET / PUT / DELETE method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-items-id-method.spec.ts, the twenty-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-second under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/items/[id]/route.ts — the first triple-method admin-tree smoke the docs tree publishes (every prior dynamic-segment admin-id smoke pins a single-method export; the admin-roles-id-permissions-method-spec.md smoke pins a dual-method GET + PUT export; this route ships THREE distinct HTTP-verb handlers GET + PUT + DELETE from a single file). All three handlers share the SAME inline !session?.user?.isAdmin gate, the SAME canonical longer 401 envelope ('Unauthorized. Admin access required.'), and the SAME { success: false, error: ... } envelope shape — but each diverges on its post-gate surface: GET has no body parse, calls itemRepository.findById(id) with a 404 'Item not found' branch, returns { success: true, data: <item> }, and catches with safeErrorResponse(error, 'Failed to fetch item'); PUT parses JSON via await request.json() (NOT wrapped in a per-call try/catch — a malformed body would 500 via the outer catch on the auth branch), spreads the body into an UpdateItemRequest, builds an audit-user from session.user.id / name / email, calls itemRepository.update(id, updateData, auditUser), optionally syncs to Twenty CRM (gated by process.env.TWENTY_CRM_ENABLED !== 'false' and a body brand field) and to the Location Index (gated by getLocationEnabled()), returns { success: true, data: <item>, message: 'Item updated successfully' }, and catches with safeErrorResponse(error, 'Failed to update item'); DELETE has no body parse, builds the same audit-user, calls itemRepository.delete(id, auditUser), optionally removes from the Location Index (gated by getLocationEnabled()), returns { success: true, message: 'Item deleted successfully' } (NOTE: NO data key — distinct from GET / PUT success payloads which both include data), and catches with safeErrorResponse(error, 'Failed to delete item'). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~20-header × 3-method bulk-loop walk + a ~17-PUT-body bulk-loop walk all asserting < 500; per-method canonical-longer 401-envelope assertions across GET / PUT / DELETE; a cross-method envelope-equality assertion pinning that all three handlers emit byte-identical 401 envelopes; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the six post-auth messages ('Item not found', 'Failed to fetch item', 'Failed to update item', 'Failed to delete item', 'Item updated successfully', 'Item deleted successfully') must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT pinning that the gate fires before the body parse; a service-not-entered invariance walk pinning that NONE of the three repository calls is entered on the unauth branch; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the full set of sibling per-spec-file references under tests/api/ and the dual-method admin-roles-id-permissions-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 24-of-N and the tests/api/ per-spec-file sub-rollout extends to 22-of-many, and the first triple-method admin-tree smoke lands as a complementary surface to the single-method and dual-method smokes the sub-rollout previously published.
  • E2E Admin Clients [clientId] Method Spec (apps/web-e2e/tests/api/admin-clients-clientid-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-client profile CRUD GET / PUT / DELETE method / clientId / body / header smoke spec paired with apps/web-e2e/tests/api/admin-clients-clientid-method.spec.ts, the twenty-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-third under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/clients/[clientId]/route.ts — the second triple-method admin-tree smoke the docs tree publishes (after admin-items-id-method-spec.md) but the first that exposes the bare { error: 'Unauthorized' } envelope (NO success: false key) AND uses a non-canonical [clientId] dynamic-segment param name (every prior dynamic-segment admin smoke uses [id]). All three handlers share the SAME single-step inline !session?.user?.isAdmin gate but the 401 envelope is the bare { error: 'Unauthorized' } (NO success: false key) — distinct from every prior dynamic-segment-[id] admin smoke. Documents the unique combination of: (1) [clientId] dynamic-segment param name -- await params resolves to { clientId: string } AFTER the gate; (2) single-step inline !session?.user?.isAdmin gate with a bare 401 envelope; (3) bare { error: 'Unauthorized' } envelope with NO success key on the 401 branch -- distinct from the canonical-longer envelope of the sibling triple-method admin/items/[id] route; (4) direct query-function calls (getClientProfileById / updateClientProfile / deleteClientProfile from @/lib/db/queries) instead of a repository class -- distinct from the ItemRepository of the sibling route; (5) console.error + bare { error: '<msg>' } 500 catch with handler-specific messages ('Failed to fetch client' / 'Failed to update client' / 'Failed to delete client') -- distinct from the safeErrorResponse(...) catch of the sibling route; (6) GET success payload { success: true, data: <client> }; (7) PUT success payload { success: true, data: <client> } (NO message key -- distinct from the sibling admin/items/[id] PUT which includes 'Item updated successfully'); (8) DELETE success payload { success: true, message: 'Client deleted successfully' } (NO data key); (9) PUT CRM-sync side effect -- two-step (company → person) chain wrapped in its own try/catch, gated by process.env.TWENTY_CRM_ENABLED !== 'false', distinct from the sibling route's single-step (company-only) sync; (10) method-resolution surface with GET / PUT / DELETE-only export. Documents the at-a-glance scenario tree (a ~6-clientId × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~17-PUT-body bulk-loop walk all asserting < 500; per-method bare 401-envelope assertions across GET / PUT / DELETE pinning the divergence vs the canonical longer envelope; a strict envelope-shape assertion Object.keys(body) === ['error'] with body.success undefined; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the five post-auth messages must appear in any unauth response; a per-clientId-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all three query-function calls; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the full set of sibling per-spec-file references under tests/api/, the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the dual-method admin-roles-id-permissions-method-spec.md, and the bare-envelope dynamic-segment admin-notifications-id-read-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 25-of-N and the tests/api/ per-spec-file sub-rollout extends to 23-of-many, and the first bare-envelope-no-success-key triple-method admin-tree smoke lands as a complementary surface to the canonical-longer-envelope triple-method smoke the sub-rollout previously published.
  • E2E Admin Users [id] Method Spec (apps/web-e2e/tests/api/admin-users-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-user CRUD GET / PUT / DELETE method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-users-id-method.spec.ts, the twenty-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-fourth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/users/[id]/route.ts — the third triple-method admin-tree smoke the docs tree publishes (after admin-items-id-method-spec.md and admin-clients-clientid-method-spec.md) but the first that combines a two-step gate (!session?.user → 401 'Unauthorized', then !session.user.isAdmin → 403 'Forbidden'), a hybrid 401 envelope { success: false, error: 'Unauthorized' } (bare message PLUS success: false key — distinct from both the canonical-longer envelope of admin/items/[id] AND the no-success-key bare envelope of admin/clients/[clientId]), an eight-step PUT body-validation chain with handler-specific error messages for each branch (a-h: object-shape / email / username / name / title / avatar / role-DB-lookup / status-enum), a DELETE self-deletion guard (session.user.id === id → 400 'Cannot delete your own account' — a unique safety check no other admin-tree route enforces), and an error.message-pass-through catch posture on PUT and DELETE that returns 400 with the raw error message instead of a fixed 500 string when the error is an Error instance. Documents the unique combination of: (1) [id] dynamic-segment param name with two-step gate; (2) two-step gate with bare-message + success: false envelope key; (3) hybrid 401 envelope { success: false, error: 'Unauthorized' } matching the admin/roles/[id]/permissions envelope; (4) eight-step PUT body-validation chain AFTER the gate AND AFTER params resolution AND AFTER the body parse, with the role-validation step issuing a roleRepository.findById(body.role) DB lookup that returns 400 'Invalid role' if not found -- the first DB-lookup body-validation step admin-tree smoke the docs tree publishes; (5) DELETE self-deletion guard -- a unique safety check no other admin-tree route enforces; (6) error.message-pass-through catch on PUT and DELETE -- distinct from every prior smoke's fixed-message catch; (7) GET success payload { success: true, data: <user> }; (8) PUT success payload { success: true, data: <updatedUser> } (NO message key); (9) DELETE success payload { success: true, message: 'User deleted successfully' } (NO data key); (10) method-resolution surface with GET / PUT / DELETE-only export. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~28-PUT-body bulk-loop walk all asserting < 500; per-method hybrid 401-envelope assertions across GET / PUT / DELETE; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a cross-method envelope-equality assertion; an unauth-lands-on-401-not-403 invariant pinning that the response must NEVER be 403 and must NEVER echo 'Forbidden'; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the twelve post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all three repository calls; a gate-before-eight-step-validation invariance walk pinning that EVERY step-(a)–(h) probe (~13 probes covering all eight validation branches) round-trips to the same 401 status; a DELETE self-deletion-guard invariance walk pinning that every id shape -- including session-shaped ids that would trigger the guard on the auth branch -- round-trips to the same 401 status, and the unauth response must NEVER echo 'Cannot delete your own account'). Cross-references the full set of sibling per-spec-file references under tests/api/, the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the bare-envelope-no-success-key triple-method admin-clients-clientid-method-spec.md, and the dual-method admin-roles-id-permissions-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 26-of-N and the tests/api/ per-spec-file sub-rollout extends to 24-of-many, and the first hybrid-envelope two-step-gated triple-method admin-tree smoke lands as a complementary surface to the canonical-longer-envelope and bare-envelope triple-method smokes the sub-rollout previously published.
  • E2E Admin Categories [id] Method Spec (apps/web-e2e/tests/api/admin-categories-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-category CRUD GET / PUT / DELETE method / id / body / query / header smoke spec paired with apps/web-e2e/tests/api/admin-categories-id-method.spec.ts, the twenty-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-sixth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/categories/[id]/route.ts — the second triple-method admin-tree smoke the docs tree publishes (after admin-items-id-method-spec.md) and the first triple-method admin smoke with a DELETE-only ?hard=true query-parameter branch that flips the service call from categoryRepository.delete(id) (soft delete / deactivation) to categoryRepository.hardDelete(id) (permanent removal), AND the first DELETE smoke with a query-flag-driven success-message dichotomy ('Category deactivated successfully' vs 'Category permanently deleted'). All three handlers share the SAME single-step inline !session?.user?.isAdmin gate and the SAME canonical longer 401 envelope ('Unauthorized. Admin access required.') and the SAME { success: false, error: ... } envelope shape, but each diverges on its post-gate surface: (a) GET — no body parse, no query parse, calls categoryRepository.findById(id) with a 404 'Category not found' branch, returns { success: true, data: <category> }, catches with safeErrorResponse(error, 'Failed to fetch category'); (b) PUT — JSON body parse via await request.json() AFTER the gate (NOT wrapped in a per-call try/catch — a malformed body would 500 via the outer catch on the auth branch), spreads body.name into an UpdateCategoryRequest with id from params, calls categoryRepository.update(updateData), runs await invalidateContentCaches() on the success branch, returns { success: true, data: <category>, message: 'Category updated successfully' }, has THREE message-pattern catch branches BEFORE the outer safeErrorResponse(error, 'Failed to update category') catch ('not found' → 404, 'already exists' → 409, 'must be' → 400, each echoing the raw error.message); (c) DELETE — no body parse, parses searchParams.get('hard') === 'true' AFTER the gate, calls categoryRepository.hardDelete(id) if hard === true else categoryRepository.delete(id), runs await invalidateContentCaches() on the success branch, returns { success: true, message: 'Category permanently deleted' } for hard === true or { success: true, message: 'Category deactivated successfully' } otherwise (NO data key — distinct from GET / PUT), has ONE message-pattern catch branch ('not found' → 404 echoing error.message) BEFORE the outer safeErrorResponse(error, 'Failed to delete category') catch. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~20-header × 3-method bulk-loop walk + a ~14-PUT-body bulk-loop walk + a ~9-DELETE-?hard=…-query bulk-loop walk all asserting < 500; per-method canonical-longer 401-envelope assertions across GET / PUT / DELETE; a cross-method envelope-equality assertion; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a DELETE-query envelope-invariance assertion pinning that all ?hard=… permutations round-trip to the SAME 401 envelope as the no-query baseline; a success-branch-key non-disclosure assertion across all three methods (and across both DELETE query branches); a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages -- including BOTH DELETE success messages -- must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk pinning that NONE of the four repository calls (findById / update / delete / hardDelete) is entered on the unauth branch; a side-effects-not-entered invariance walk pinning that the invalidateContentCaches() call is unreachable on the unauth branch for both PUT and DELETE; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the full set of sibling per-spec-file references under tests/api/, including the first triple-method admin-items-id-method-spec.md, the bare-envelope triple-method admin-clients-clientid-method-spec.md, the hybrid-envelope triple-method admin-users-id-method-spec.md, the dual-method admin-roles-id-permissions-method-spec.md, and the nested dual-method admin-collections-id-items-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 28-of-N and the tests/api/ per-spec-file sub-rollout extends to 26-of-many, and the first triple-method admin smoke with a DELETE-only query-parameter branch and a query-flag-driven success-message dichotomy lands as a complementary surface to the prior triple-method smokes the sub-rollout previously published.
  • E2E Admin Reports [id] Method Spec (apps/web-e2e/tests/api/admin-reports-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-report CRUD GET / PUT method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-reports-id-method.spec.ts, the twenty-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-seventh under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/reports/[id]/route.ts — the first 403-on-unauth admin-tree smoke the docs tree publishes (every prior admin-tree route returns 401 on the unauth branch with one of three envelope shapes: canonical-longer / bare-Unauthorized / hybrid-success: false-bare; this route returns 403 'Forbidden' on the unauth branch instead). Both handlers share several unique characteristics: (1) checkDatabaseAvailability() pre-gate that runs BEFORE the auth gate -- distinct from every prior admin-tree smoke where auth() is the FIRST guard; (2) single-step !session?.user?.isAdmin gate that returns 403 { success: false, error: 'Forbidden' } on the unauth branch -- distinct from every prior admin-tree route which returns 401; (3) success: false envelope key on the 403 branch with strict envelope-shape Object.keys(body).sort() === ['error', 'success']; (4) dynamic [id] segment resolved AFTER both gates; (5) dev-gated console.error catch that only logs when process.env.NODE_ENV === 'development'. Each handler also has its own divergent post-gate surface: GET runs getReportById(id) → 404 'Report not found', returns { success: true, data: <report> }; PUT runs the existence check FIRST (getReportById(id) BEFORE the body parse — distinct from every prior PUT smoke), then parses body and validates status / resolution against the ReportStatus / ReportResolution enums (with dynamically-interpolated 400 messages prefixed 'Invalid status\|resolution. Must be one of: ...'), then calls updateReport(...) followed by a conditional moderation-action chain that runs removeContent / warnUser / suspendUser / banUser from the moderation service based on resolution (with getContentOwner(...) lookup for user-action resolutions), then a final getReportById(id) for the full updated report, and returns { success: true, message: moderationResult?.message || 'Report updated successfully', data: <report>, moderationResult } (FOUR success-branch keys). Documents the at-a-glance scenario tree (a ~6-id × 2-method bulk-loop walk + a ~17-header × 2-method bulk-loop walk + a ~22-PUT-body bulk-loop walk all asserting < 500; per-method 403-envelope assertions across GET and PUT; a NEVER-401 invariant pinning that the unauth client lands on 403, not 401; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion that NONE of the route-specific data and moderationResult keys plus message and success: true must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the five static post-auth messages AND none of the dynamic 'Invalid status\|resolution. Must be one of: ...' 400 messages (matched via regex prefix) must appear in any unauth response; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH / DELETE round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across the entire moderation chain; a status / resolution enum-shape invariance walk; a moderation-chain-not-entered invariance walk pinning that every resolution value that would trigger a moderation action -- content_removed / user_warned / user_suspended / user_banned -- round-trips to the same 403 status with NO moderationResult key in the response). Cross-references the full set of sibling per-spec-file references under tests/api/, including the 401-on-unauth dual-method admin-collections-id-items-method-spec.md and the 401-on-unauth triple-method admin-items-id-method-spec.md, admin-clients-clientid-method-spec.md, admin-users-id-method-spec.md, and admin-categories-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 29-of-N and the tests/api/ per-spec-file sub-rollout extends to 27-of-many, and the first 403-on-unauth admin-tree smoke lands as a complementary surface to the 401-on-unauth smokes the sub-rollout previously published.
  • E2E Admin Sponsor Ads [id] Method Spec (apps/web-e2e/tests/api/admin-sponsor-ads-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-sponsor-ad GET / DELETE method / id / header smoke spec paired with apps/web-e2e/tests/api/admin-sponsor-ads-id-method.spec.ts, the thirtieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-eighth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/sponsor-ads/[id]/route.ts — the first GET + DELETE-only dual-method admin-tree smoke the docs tree publishes (the sponsor-ad write path is split across the sibling admin-sponsor-ads-id-approve-method-spec.md, admin-sponsor-ads-id-reject-method-spec.md, and admin-sponsor-ads-id-cancel-method-spec.md action routes which the smoke layer already covers separately; with this entry the sponsor-ad area smoke coverage is complete). Both handlers share the SAME single-step inline !session?.user?.isAdmin gate, the SAME canonical longer 401 envelope, and the SAME { success: false, error: ... } envelope shape, but each diverges on its catch posture -- the load-bearing divergence: GET uses a console.error + 500 'Failed to fetch sponsor ad' catch, while DELETE uses a narrow-match error.message === 'Sponsor ad not found' → 404 catch followed by a safeErrorResponse(error, 'Failed to delete sponsor ad') fallback. The DELETE handler is the first admin DELETE smoke where the catch chain begins with a narrow-match equality check on a service-thrown sentinel string (rather than a .includes(...) substring match or a fixed fallback). Documents the at-a-glance scenario tree (a ~6-id × 2-method bulk-loop walk + a ~17-header × 2-method bulk-loop walk all asserting < 500; per-method canonical-longer 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting POST / PUT / PATCH round-trip to < 500; a service-not-entered invariance walk; a DELETE narrow-match-catch-not-entered invariance walk pinning that the unauth response must NEVER echo the service-thrown sentinel 'Sponsor ad not found'; a per-handler catch-message-divergence walk pinning that NONE of the two distinct catch messages must appear in any unauth response). Cross-references the three sponsor-ad action smokes admin-sponsor-ads-id-approve-method-spec.md, admin-sponsor-ads-id-reject-method-spec.md, and admin-sponsor-ads-id-cancel-method-spec.md, the canonical-longer-envelope triple-method admin-items-id-method-spec.md, and the dual-method admin-roles-id-permissions-method-spec.md and admin-reports-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 30-of-N and the tests/api/ per-spec-file sub-rollout extends to 28-of-many, and the first GET + DELETE-only dual-method admin-tree smoke lands -- closing the sponsor-ad-area smoke coverage at four routes (the leaf-[id] root with GET + DELETE plus the three nested-[id]/<action> POST routes for approve / reject / cancel).
  • E2E Admin Comments [id] Method Spec (apps/web-e2e/tests/api/admin-comments-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-comment CRUD GET / PUT / DELETE method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-comments-id-method.spec.ts, the thirty-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-ninth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/comments/[id]/route.ts — the first 403-on-unauth triple-method admin-tree smoke the docs tree publishes (every prior triple-method admin smoke returns 401 on the unauth branch; the sibling admin-reports-id-method-spec.md 403-on-unauth route is dual-method GET + PUT; this is the first triple-method 403 admin smoke). All three handlers share the SAME single-step inline !session?.user?.isAdmin gate that returns 403 { success: false, error: 'Forbidden' }, the SAME { success: false, error: ... } envelope shape, and the SAME console.error('Failed to <verb> comment:', error) + 500 'Internal Server Error' catch posture. Each handler diverges on its post-gate surface: GET runs getTenantId() AFTER await params → 403 'Tenant not found' if missing, then issues an inline Drizzle query with leftJoin to clientProfiles and tenant scoping, returning 404 'Comment not found' or { success: true, data: <comment-with-user> }; PUT runs getTenantId() BEFORE await params (NOTE: ordering distinct from GET) → 403 'Tenant not found', parses JSON body, validates content?.trim() → 400 'Content is required' if falsy, runs a soft-delete-aware getCommentById(id) existence check (existingComment.deletedAt → 404 'Comment not found'), then issues an inline Drizzle re-query (NOTE: the actual updateComment call is commented out in the source — the route currently re-fetches the comment without updating it; a regression-sensitive note), returning { success: true, data: <comment-with-user>, message: 'Comment updated successfully' }; DELETE has NO getTenantId() call (distinct from GET / PUT), runs a soft-delete-aware getCommentById(id) existence check, calls deleteComment(id) (soft delete via setting deletedAt), and returns { success: true, message: 'Comment deleted successfully' } (NO data key). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~12-PUT-body bulk-loop walk all asserting < 500; per-method 403-envelope assertions across GET / PUT / DELETE; a NEVER-401 invariant pinning that the unauth client lands on 403 across all three methods; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the six post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across the inline Drizzle queries plus getCommentById(...) / deleteComment(...) / getTenantId() calls; a tenant-resolution-not-entered invariance walk pinning that the unauth response must NEVER echo 'Tenant not found' for GET / PUT). Cross-references the dual-method 403-on-unauth admin-reports-id-method-spec.md, the GET + DELETE-only dual-method admin-sponsor-ads-id-method-spec.md, and the canonical-longer-envelope triple-method admin-items-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 31-of-N and the tests/api/ per-spec-file sub-rollout extends to 29-of-many, and the first 403-on-unauth triple-method admin-tree smoke lands as a complementary surface to the dual-method 403-on-unauth and the various 401-on-unauth triple-method smokes the sub-rollout previously published.
  • E2E Admin Companies [id] Method Spec (apps/web-e2e/tests/api/admin-companies-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-company CRUD GET / PUT / DELETE method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-companies-id-method.spec.ts, the thirty-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirtieth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/companies/[id]/route.ts — the first triple-method admin-tree smoke the docs tree publishes that combines the bare { error: 'Unauthorized' } 401 envelope (NO success key — matching admin/clients/[clientId]) with a Zod parse() (NOT safeParse()) body-validation step that emits a details: [{field, message}] 400 envelope (a UNIQUE envelope key no prior admin-tree smoke pins) AND two 409 Conflict pre-update uniqueness checks (getCompanyByDomain(...) and getCompanyBySlug(...)) with dynamically-interpolated messages ('Company with domain '<domain>' already exists' / 'Company with slug '<slug>' already exists') AND an outer-catch unique-constraint translation chain that maps error.message.includes('unique constraint' \| 'duplicate key') to one of three 409 envelope variants based on domain / slug substring detection. All three handlers share the SAME single-step inline !session?.user?.isAdmin gate that returns 401 { error: 'Unauthorized' }, the SAME bare envelope shape, and the SAME console.error + bare { error: '<msg>' } 500 catch posture (with handler-specific messages 'Failed to fetch\|update\|delete company'). Each handler diverges on its post-gate surface: GET calls getCompanyById(id) → 404 / 200 { success: true, data: <company> }; PUT runs the existence check FIRST (getCompanyById(id) BEFORE the body parse — distinct from every prior PUT smoke), then parses JSON body and runs Zod parse() (NOT safeParse()) wrapped in inline try/catch (catches ZodError and returns 400 with custom details: [{field, message}] array), then runs TWO 409 pre-update uniqueness checks (getCompanyByDomain / getCompanyBySlug) with dynamically-interpolated messages, then updateCompany(id, ...) returning 404 if falsy, then optional CRM sync (gated by process.env.TWENTY_CRM_ENABLED !== 'false', wrapped in its own try/catch), returning { success: true, data: <company> }; PUT also has an outer-catch unique-constraint translation chain that maps DB error messages to one of three 409 envelopes; DELETE calls deleteCompany(id) returning boolean → 404 if false / 200 { success: true, message: 'Company deleted successfully' }. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~16-PUT-body bulk-loop walk all asserting < 500; per-method bare 401-envelope assertions; a strict envelope-shape assertion Object.keys(body) === ['error'] with no success key; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion that NONE of the route-specific data, details, message keys plus success: true must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the nine static post-auth messages plus none of the dynamic 'Company with domain\|slug '<...>' already exists' 409 messages (matched via regex prefix) must appear in any unauth response; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all five DB-query calls; a Zod-validation-not-entered invariance walk pinning that every Zod-invalid body shape round-trips to the same 401 status with NO details key in the response; a uniqueness-check-409-not-entered invariance walk pinning that the unauth response must NEVER match the dynamic /^Company with (domain\|slug) '/ regex prefixes; a unique-constraint-outer-catch-not-entered invariance walk pinning that the unauth response must NEVER echo any of the three static unique-constraint translation messages; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the bare-envelope triple-method admin-clients-clientid-method-spec.md, the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the hybrid-envelope triple-method admin-users-id-method-spec.md, the 403-on-unauth triple-method admin-comments-id-method-spec.md, the categories-CRUD triple-method admin-categories-id-method-spec.md, and the Zod-safeParse(...) single-method admin-sponsor-ads-id-reject-method-spec.md and admin-sponsor-ads-id-cancel-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 32-of-N and the tests/api/ per-spec-file sub-rollout extends to 30-of-many, and the first Zod-parse()-with-details-envelope admin-tree smoke lands as a complementary surface to the prior Zod-safeParse(...) and manual-validation triple-method smokes the sub-rollout previously published.
  • E2E Admin Collections [id] Method Spec (apps/web-e2e/tests/api/admin-collections-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-collection CRUD GET / PUT / DELETE method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-collections-id-method.spec.ts, the thirty-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-second under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/collections/[id]/route.ts — the first triple-method admin-tree smoke the docs tree publishes that combines a canonical longer 'Unauthorized. Admin access required.' 401 envelope with a Zod safeParse(...).error.flatten() 400 envelope posture (distinct from every prior triple-method admin smoke which use either an inline-untyped destructure with no Zod, an inline-untyped destructure with a DELETE-only ?hard=true query branch, a Zod parse() (THROWS) with details: ZodError.errors-style catch envelope, or a validation-less PUT with seven body fields shoved straight into db.update(...)). All three handlers share the SAME inline !session?.user?.isAdmin gate (NOT delegated to a checkAdminAuth(...) helper), the SAME canonical longer 401 envelope, the SAME safeErrorResponse(...) outer-catch fallback (with handler-specific messages 'Failed to fetch\|update\|delete collection'), the SAME findById pre-action 404 check on PUT + DELETE (distinct from admin/categories/[id] PUT which lets the service throw, and distinct from admin/featured-items/[id] PUT which uses the .returning() length-zero check), and the SAME revalidatePath(...) cache invalidation pattern AFTER the repository call (with invalidateContentCaches() called in addition). Each handler diverges on its post-gate surface: GET calls collectionRepository.findById(id) returning 404 'Collection not found' if missing or { success: true, data: <collection> }; PUT parses JSON body AFTER the gate, runs Zod safeParse(updateCollectionSchema) → 400 { success: false, error: 'Invalid collection payload', details: parsed.error.flatten() } (UNIQUE flatten()-shaped details: { formErrors: string[], fieldErrors: Record<string, string[]> } envelope — DIFFERENT from the error.errors array a parse()-then-catch route would emit), then runs the pre-update findById, then collectionRepository.update(updateData), has THREE distinct catch branches (already exists → 409 with bare-message echo, must → 400 with bare-message echo, safeErrorResponse(...) fallback), then runs the conditional slug-revalidation branch (revalidates the OLD slug's path if it changed) plus the always-emitted new-slug + index revalidation, returning { success: true, data: <updated>, message: 'Collection updated successfully' }; DELETE runs the pre-delete findById, calls collectionRepository.delete(id), has TWO distinct catch branches (not found → 404 with bare-message echo, safeErrorResponse(...) fallback), then runs invalidateContentCaches() + two revalidatePath(...) calls, returning { success: true, message: 'Collection deleted successfully' } (NO data key — distinct from the GET / PUT success payloads which both include data). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~16-header × 3-method bulk-loop walk + a ~21-PUT-body bulk-loop walk all asserting < 500; per-method canonical-401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages must appear in any unauth response — including the Zod safeParse 400 envelope's fixed 'Invalid collection payload' error string; a gate-before-Zod-safeParse invariant pinning that the unauth response NEVER carries the details / formErrors / fieldErrors keys even when the body would have failed Zod validation; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a repository-call-not-entered invariance walk across all three handlers; a cache-invalidation-side-effect-not-entered invariance walk across PUT + DELETE; a per-handler-catch-message-non-disclosure walk pinning that NONE of the three distinct safeErrorResponse(...) fallback strings must appear in any unauth response; a gate-before-409-/-'must'-400-catch invariance walk pinning that the unauth status MUST be 401 — NOT 400 / 409 — and the envelope MUST be the canonical 401 envelope). Cross-references the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the bare-envelope triple-method admin-clients-clientid-method-spec.md, the hybrid-envelope triple-method admin-users-id-method-spec.md, the categories-CRUD triple-method admin-categories-id-method-spec.md, the 403-on-unauth triple-method admin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-method admin-companies-id-method-spec.md, the validation-less / non-admin-gated / soft-delete-DELETE triple-method admin-featured-items-id-method-spec.md, and the companion nested-dual-method admin-collections-id-items-method-spec.md, and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, and docs/questions.md for the governing specs. With this entry the per-spec-file docs rollout extends to 34-of-N and the tests/api/ per-spec-file sub-rollout extends to 32-of-many, and the first Zod-safeParse(...)-with-flatten()-envelope admin-tree smoke lands as a complementary surface to the prior Zod-parse() and validation-less and inline-untyped triple-method smokes the sub-rollout previously published.
  • E2E Admin Tags [id] Method Spec (apps/web-e2e/tests/api/admin-tags-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-tag CRUD GET / PUT / DELETE method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-tags-id-method.spec.ts, the thirty-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-third under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/tags/[id]/route.ts — the first triple-method admin-tree smoke the docs tree publishes that combines the hybrid bare-Unauthorized + success: false 401 envelope (matching admin/users/[id], admin/featured-items/[id], admin/roles/[id]/permissions) with a single-step inline !session?.user?.isAdmin gate AND a PUT outer-catch three-branch error.message.includes(...) chain that maps 'not found' → 404, 'already exists' → 409, 'required' \| 'must be' → 400 (each echoing the raw error.message). All three handlers share the SAME single-step inline !session?.user?.isAdmin gate that returns 401 { success: false, error: 'Unauthorized' }, the SAME hybrid envelope shape, and the SAME console.error + 500 catch posture (with handler-specific messages 'Failed to fetch\|update\|delete tag'). Each handler diverges on its post-gate surface: GET calls tagRepository.findById(id) returning 404 'Tag not found' or { success: true, data: <tag> }; PUT parses JSON body, runs if (!name) → 400 'Tag name is required', calls tagRepository.update(id, { name, isActive }), runs await invalidateContentCaches() on the success branch, returns { success: true, data: <tag>, message: 'Tag updated successfully' }, has THREE catch branches ('not found' → 404 echoing raw message, 'already exists' → 409, 'required' \| 'must be' → 400) plus 500 'Failed to update tag' fallback; DELETE calls tagRepository.delete(id), runs await invalidateContentCaches(), returns { success: true, message: 'Tag deleted successfully' } (NO data key), has ONE catch branch ('not found' → 404 echoing raw message) plus 500 'Failed to delete tag' fallback. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~14-PUT-body bulk-loop walk all asserting < 500; per-method hybrid 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk; a cache-invalidation-side-effect-not-entered invariance walk pinning that the unauth response must NEVER echo 'Tag updated successfully' or 'Tag deleted successfully'; a three-branch-catch-chain-not-entered invariance walk pinning that the PUT outer-catch's error.message.includes(...) branches are unreachable on the unauth branch). Cross-references the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the bare-envelope triple-method admin-clients-clientid-method-spec.md, the hybrid-envelope triple-method admin-users-id-method-spec.md and admin-featured-items-id-method-spec.md, the categories-CRUD triple-method admin-categories-id-method-spec.md, the 403-on-unauth triple-method admin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-method admin-companies-id-method-spec.md, and the Zod-safeParse(...)-with-flatten()-envelope triple-method admin-collections-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 35-of-N and the tests/api/ per-spec-file sub-rollout extends to 33-of-many, and the first hybrid-envelope-with-3-branch-error.message.includes(...)-catch admin-tree smoke lands as a complementary surface to the canonical-longer-envelope and bare-envelope and 403-on-unauth triple-method smokes the sub-rollout previously published.
  • E2E Admin Roles [id] Method Spec (apps/web-e2e/tests/api/admin-roles-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-role CRUD GET / PUT / DELETE method / id / body / query / header smoke spec paired with apps/web-e2e/tests/api/admin-roles-id-method.spec.ts, the thirty-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-fourth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/roles/[id]/route.ts — the first triple-method admin-tree smoke the docs tree publishes that combines the two-step !session?.user!session.user.isAdmin gate (matching admin/users/[id] and admin/reports/[id], distinct from the single-step gates of every other prior triple-method admin smoke) with a DELETE ?hard=true query-parameter branch that flips the soft-delete ('Role deleted (marked as inactive)') and hard-delete ('Role permanently deleted') messages — matching the admin/categories/[id] DELETE-?hard=true pattern but with a distinct soft-delete success message — AND a three-step manual PUT body validation chain with FIXED error messages (NOT dynamically interpolated): (a) 'Role name cannot be empty' on !updateData.name.trim(); (b) 'Role name must be between 3 and 100 characters' on length out-of-range; (c) 'Role description must be at most 500 characters' on description > 500. PUT also does an explicit existence check AFTER the body parse (NOT before, distinct from admin/reports/[id] and admin/companies/[id] which check BEFORE). All three handlers share the SAME two-step gate, the SAME hybrid bare-message + success: false 401 envelope ({ success: false, error: 'Unauthorized' }), and the SAME console.error + 500 catch posture (with handler-specific messages 'Failed to fetch\|update\|delete role'). Each handler diverges on its post-gate surface: GET calls roleRepository.findById(id) returning 404 'Role not found' or { success: true, data: <role> }; PUT parses JSON body AFTER both gate steps, runs the existence check, then the three-step validation, then roleRepository.update(id, ...), returns { success: true, data: <role>, message: 'Role updated successfully' }; DELETE parses searchParams.get('hard') === 'true' query AFTER both gate steps, runs the existence check, branches on hardDelete boolean (hardDelete === trueroleRepository.hardDelete(id); else roleRepository.delete(id)), returns { success: true, message: 'Role permanently deleted' } for hard === true or { success: true, message: 'Role deleted (marked as inactive)' } otherwise (NO data key). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~15-PUT-body bulk-loop walk + an ~8-DELETE-?hard=...-query bulk-loop walk all asserting < 500; per-method hybrid 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; an unauth-lands-on-401-not-403 invariant pinning that the FIRST gate step fires for unauth clients (the response is 401 not 403, and must NEVER echo 'Forbidden'); a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the eleven post-auth messages must appear in any unauth response — including the three PUT validation messages and both DELETE success messages; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a DELETE ?hard=... query-shape invariance walk pinning that every query shape (true / false / TRUE uppercase / 1 numeric / empty / unrelated key / extra key) round-trips to the same 401 status; a cross-method side-channel walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all four repository calls (findById / update / delete / hardDelete); a three-step-validation invariance walk pinning that EVERY step-(a)/(b)/(c) probe round-trips to the same 401 status; a hard-delete-branch-not-entered invariance walk pinning that the unauth response must NEVER echo 'Role deleted (marked as inactive)' or 'Role permanently deleted'). Cross-references the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the bare-envelope triple-method admin-clients-clientid-method-spec.md, the hybrid-envelope triple-method admin-users-id-method-spec.md and admin-featured-items-id-method-spec.md and admin-tags-id-method-spec.md, the categories-CRUD triple-method admin-categories-id-method-spec.md, the 403-on-unauth triple-method admin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-method admin-companies-id-method-spec.md, and the Zod-safeParse(...)-with-flatten()-envelope triple-method admin-collections-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 36-of-N and the tests/api/ per-spec-file sub-rollout extends to 34-of-many, and the first two-step-gate-with-DELETE-?hard-query admin-tree smoke lands as a complementary surface to the prior single-step-gate triple-method smokes the sub-rollout previously published.
  • E2E Admin Items Create Body Spec (apps/web-e2e/tests/api/admin-items-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level item-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-items-create-body.spec.ts, the thirty-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-fifth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/items/route.ts (the POST export) — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines a five-field required-validation chain (!id || !name || !slug || !description || !source_url in a single guard expression returning 400 'Item ID, name, slug, description, and source URL are required') with TWO 409 Conflict pre-create duplicate checks (itemRepository.checkDuplicateId(...) and itemRepository.checkDuplicateSlug(...)) using dynamically-interpolated messages ('Item with ID '<id>' already exists' / 'Item with slug '<slug>' already exists') AND an audit-user-threading + CRM-company-link + Location-Index side-effect chain on the success branch. The companion admin-items-query.spec.ts covers the GET (paginated list) surface of the same route. The POST handler shares the SAME single-step inline !session?.user?.isAdmin gate with its GET sibling and returns the canonical longer 401 envelope { success: false, error: 'Unauthorized. Admin access required.' } with strict envelope-shape preservation. CRM sync is gated by process.env.TWENTY_CRM_ENABLED === 'true' (NOTE: strict-equals comparison, distinct from admin/items/[id]/route.ts PUT which uses !== 'false'), wrapped in its own try/catch, walking a four-step chain (getOrCreateCompanyFromBrandlinkItemToCompany → conditional CRM sync via upsertCompany if newly linked). Location Index side effect is gated by getLocationEnabled(). Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~19-body bulk-loop walk all asserting < 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of data, item, id, slug, success: true keys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that the two static post-auth messages plus the dynamic 'Item with (ID\|slug) '<...>' already exists' 409 messages (matched via regex prefix) must NEVER appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to < 500; a malformed-JSON-body invariance walk; a required-field-validation-not-entered invariance walk; a duplicate-id-/-duplicate-slug-409-not-entered invariance walk; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201; a CRM-sync-side-effect-not-entered invariance walk pinning that a body with brand does NOT change the unauth status; a Location-Index-side-effect-not-entered invariance walk pinning that a body with location does NOT change the unauth status). Cross-references the companion admin-items-query.spec.ts, the canonical-longer-envelope triple-method admin-items-id-method-spec.md, and the body-validation single-method admin-items-import-body-spec.md / admin-items-import-validate-body-spec.md / admin-items-bulk-body-spec.md / admin-items-id-review-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 37-of-N and the tests/api/ per-spec-file sub-rollout extends to 35-of-many, and the first POST-only collection-level admin-tree smoke lands -- complementing the existing query-surface coverage of the same items-collection route.
  • E2E Admin Users Create Body Spec (apps/web-e2e/tests/api/admin-users-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level user-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-users-create-body.spec.ts, the thirty-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-sixth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/users/route.ts — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the two-step !session?.user!session.user.isAdmin gate (matching admin/users/[id], admin/roles/[id], admin/reports/[id]) with an eight-step body validation chain (object-shape / 5-required-fields / email-format / username-regex / name-length / password-Zod-safeParse / title-length / avatar-length / role-DB-lookup) AND Zod passwordSchema.safeParse(body.password) for password-only validation (returning a dynamically-interpolated passwordResult.error.issues[0]?.message on failure — distinct from prior smokes that use Zod for the body-as-a-whole) AND a username regex validation (/^[a-zA-Z0-9_-]{3,30}$/ — the first regex-based username validation in admin smoke) AND the error.message-pass-through outer catch (matching admin/users/[id] PUT/DELETE) that returns 400 with the raw error message instead of a fixed 500 string when the error is an Error instance. The companion admin-users-query.spec.ts covers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~32-body bulk-loop walk all asserting < 500; a hybrid 401-envelope assertion { success: false, error: 'Unauthorized' }; a strict envelope-shape assertion; an unauth-lands-on-401-not-403 invariant; a success-branch-key non-disclosure assertion that NONE of data, user, id, success: true keys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the thirteen post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to < 500; a malformed-JSON-body invariance walk; an eight-step-validation invariance walk pinning that EVERY step-(a)-(i) probe round-trips to the same 401 status; a role-DB-lookup-not-entered invariance walk pinning that the unauth response must NEVER echo 'Invalid role'; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201). Cross-references the companion admin-users-query.spec.ts, the leaf-[id] triple-method admin-users-id-method-spec.md covering the same eight-step validation pattern on PUT updates, the collection-level POST companion admin-items-create-body-spec.md, and the body-validation single-method admin-users-check-email-body-spec.md and admin-users-check-username-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 38-of-N and the tests/api/ per-spec-file sub-rollout extends to 36-of-many, and the first eight-step-validation collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same users-collection route.
  • E2E Admin Categories Create Body Spec (apps/web-e2e/tests/api/admin-categories-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level category-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-categories-create-body.spec.ts, the thirty-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-seventh under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/categories/route.ts — the first POST-only collection-level admin-tree smoke the docs tree publishes that uses a category success-payload key (NOT data) plus a single-field required validation plus a three-branch outer catch chain ('already exists' → 409, 'must be' → 400, safeErrorResponse(...) fallback). The category success-key matches the sibling POST /api/admin/collections which uses collection (not data), suggesting a convention divergence between create endpoints that serve a single resource (categories / collections / tags) vs those that serve nested or composite payloads (items / users — which use data). The companion admin-categories-query.spec.ts covers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of category, data, message, success: true keys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to < 500; a malformed-JSON-body invariance walk; a required-field-check-not-entered invariance walk pinning that the unauth response must NEVER echo 'Category name is required'; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201, must NOT contain a category key, and must NOT echo 'Category created successfully'; a three-branch-outer-catch-not-entered invariance walk pinning that the unauth response must echo the canonical 401 envelope, not any branch of the outer catch chain). Cross-references the companion admin-categories-query.spec.ts, the leaf-[id] triple-method admin-categories-id-method-spec.md covering the same three-branch outer catch on PUT updates, the collection-level POST companions admin-items-create-body-spec.md and admin-users-create-body-spec.md, and the body-validation single-method admin-categories-reorder-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 39-of-N and the tests/api/ per-spec-file sub-rollout extends to 37-of-many, and the first category-key collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same categories-collection route.
  • E2E Admin Tags Create Body Spec (apps/web-e2e/tests/api/admin-tags-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level tag-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-tags-create-body.spec.ts, the fortieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-eighth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/tags/route.ts — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the hybrid bare-Unauthorized + success: false 401 envelope (matching admin/tags/[id] GET/PUT/DELETE, admin/users/[id], admin/featured-items/[id], admin/roles/[id]/permissions) with a tag success-payload key (NOT data) — distinct from the canonical-longer-envelope admin/categories and admin/collections POST smokes (which also use single-resource success-keys but with the canonical longer 401 envelope). The POST handler runs a two-field required check (if (!id || !name) → 400 'Tag ID and name are required'), calls tagRepository.create({ id, name, isActive: isActive ?? true }) (defaults isActive to true if not provided — distinct from prior POST smokes that don't default a boolean field), runs await invalidateContentCaches() on success, and returns { success: true, tag: <tag> } with status 201 (NO message key — distinct from admin/categories POST 'Category created successfully' and admin/collections POST 'Collection created successfully'). The outer catch uses a three-branch chain matching admin/tags/[id] PUT: 'already exists' → 409, 'required' \| 'must be' → 400, else fixed-message 500 'Failed to create tag' fallback (NOT safeErrorResponse(...) — distinct from the admin/categories POST). The companion admin-tags-query.spec.ts covers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a hybrid 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of tag, data, success: true keys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the two static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to < 500; a malformed-JSON-body invariance walk; a required-field-check-not-entered invariance walk; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201 and must NOT contain a tag key; a three-branch-outer-catch-not-entered invariance walk pinning that the unauth response must echo the canonical 401 envelope, not any branch of the outer catch chain — including the fixed-message 500 fallback). Cross-references the companion admin-tags-query.spec.ts, the leaf-[id] triple-method admin-tags-id-method-spec.md covering the same hybrid envelope and three-branch outer catch on PUT updates, the collection-level POST companions admin-items-create-body-spec.md, admin-users-create-body-spec.md, and admin-categories-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 40-of-N and the tests/api/ per-spec-file sub-rollout extends to 38-of-many, and the first hybrid-envelope tag-key collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same tags-collection route.
  • E2E Admin Clients Create Body Spec (apps/web-e2e/tests/api/admin-clients-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level client-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-clients-create-body.spec.ts, the forty-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-ninth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/clients/route.ts — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the bare { error: 'Unauthorized' } envelope (NO success key — matching the admin/clients/[clientId] smoke and admin/companies/[id]) with a get-or-create user side-effect chain that uses crypto.randomBytes(6) to generate a temporary password for newly-created users (Temp<hex>!) AND a status-200 success branch (NOT 201, distinct from every prior collection-level POST smoke). The POST handler runs an email-or-userId fallback (const email = raw.email ?? raw.userId — distinct from prior POST smokes that have a single named required field), a single-field required check (if (!email) → 400 { error: 'Email is required' } bare envelope), getUserByEmail(email) lookup, an inner-try/catch user-create branch that on failure returns 400 with dynamically-interpolated 'Failed to create user: <err.message>' message, a get-or-create fallback validation (if (!user || !user.id) → 400 'Failed to create or find user for client'), then createClientProfile(clientData) with defaults (status='active', plan='free', accountType='individual'), an optional CRM sync side-effect gated by process.env.TWENTY_CRM_ENABLED !== 'false' wrapped in its own try/catch, and returns { success: true, data: <client>, message: 'Client created successfully' } with status 200 (NOT 201). The outer catch returns 500 { error: 'Failed to create client' } (BARE envelope, NO success key). The companion admin-clients-query.spec.ts covers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion pinning the divergence vs the canonical longer envelope; a strict envelope-shape assertion Object.keys(body) === ['error'] with body.success undefined; a success-branch-key non-disclosure assertion that NONE of data, success, message keys must appear in any unauth response and the unauth response status must NOT be 200; a gate-before-post-auth invariant pinning that the four static post-auth messages plus the dynamic 'Failed to create user: ...' regex prefix must NOT appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-email-check-not-entered invariance walk; a get-or-create-user-side-effect-not-entered invariance walk pinning that the unauth response must NEVER match the dynamic 'Failed to create user: ...' regex prefix and must NEVER echo 'Failed to create or find user for client'; a createClientProfile-call-not-entered invariance walk pinning that the unauth response status must NOT be 200 and must NOT contain a data key). Cross-references the companion admin-clients-query.spec.ts, the leaf-[clientId] triple-method admin-clients-clientid-method-spec.md covering the same bare-envelope shape on GET / PUT / DELETE, the collection-level POST companions admin-items-create-body-spec.md, admin-users-create-body-spec.md, admin-categories-create-body-spec.md, and admin-tags-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 41-of-N and the tests/api/ per-spec-file sub-rollout extends to 39-of-many, and the first bare-envelope-with-get-or-create-user-side-effect collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same clients-collection route.
  • E2E Admin Companies Create Body Spec (apps/web-e2e/tests/api/admin-companies-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level company-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-companies-create-body.spec.ts, the forty-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fortieth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/companies/route.ts — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the bare { error: 'Unauthorized' } envelope (NO success key) with a Zod parse() (NOT safeParse()) body validation emitting a details: [{field, message}] 400 envelope AND two dynamically-interpolated 409 pre-create uniqueness checks (getCompanyByDomain / getCompanyBySlug) AND an outer-catch unique-constraint translation chain that maps DB errors to one of three 409 envelope variants. Sibling of admin-companies-id-method-spec.md PUT — they share the SAME bare envelope, the SAME Zod-parse()-with-details-envelope validation chain, the SAME TWO 409 pre-create/-update uniqueness checks (with dynamically-interpolated messages), and the SAME outer-catch unique-constraint translation chain. The POST diverges from the PUT on: NO existence check (distinct from the PUT which checks the existing company FIRST), createCompany(validatedData) call instead of updateCompany(id, ...), and status-201 success branch with { success: true, data: <company> }. The companion admin-companies-query.spec.ts covers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of data, details, success keys must appear in any unauth response; a gate-before-post-auth invariant pinning that the five static post-auth messages plus the dynamic 'Company with (domain\|slug) '<...>' already exists' 409 messages (matched via regex prefix) must NEVER appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a Zod-validation-not-entered invariance walk pinning that every Zod-invalid body shape round-trips to the same 401 status with NO details key; a uniqueness-check-409-not-entered invariance walk pinning that the unauth response must NEVER match the dynamic /^Company with (domain\|slug) '/ regex prefixes; a createCompany-call-not-entered invariance walk pinning that the unauth response status must NOT be 201 and must NOT contain a data key; a unique-constraint-outer-catch-not-entered invariance walk pinning that the unauth response must NEVER echo any of the three static unique-constraint translation messages). Cross-references the companion admin-companies-query.spec.ts, the leaf-[id] triple-method admin-companies-id-method-spec.md covering the same Zod-parse()-with-details-envelope validation chain on PUT updates, the collection-level POST companions admin-items-create-body-spec.md, admin-users-create-body-spec.md, admin-categories-create-body-spec.md, admin-tags-create-body-spec.md, and admin-clients-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 42-of-N and the tests/api/ per-spec-file sub-rollout extends to 40-of-many, and the first bare-envelope-Zod-parse()-with-details-envelope collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same companies-collection route.
  • E2E Admin Collections Create Body Spec (apps/web-e2e/tests/api/admin-collections-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level collection-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-collections-create-body.spec.ts, the forty-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-first under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/collections/route.ts — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines a per-call inline try { body = await request.json() } catch (jsonError) { ... } wrapper emitting an 'Invalid JSON in request body' 400 envelope (the FIRST collection-level admin POST route the smoke layer covers that wraps the request.json() call in its own try/catch — every prior collection-level POST smoke uses the bare await request.json() form) with a manual TWO-field required check (!createData.id || !createData.name → 400 'Collection ID and name are required') plus a two-revalidatePath cache-invalidation chain on the success branch (revalidatePath('/collections') PLUS revalidatePath(\/collections/${newCollection.slug}`)slug-aware, in addition toawait invalidateContentCaches()). Sibling of [admin-categories-create-body-spec.md](./plugins/admin-categories-create-body-spec.md) — they share the SAME canonical-longer 401 envelope ({ success: false, error: 'Unauthorized. Admin access required.' }), the SAME three-branch outer catch chain ('already exists'→ 409 /'must'→ 400 /safeErrorResponse(...) fallback), and the SAME non-data success-payload key (collectionhere,categorythere). The collections POST diverges from the categories POST on: per-callrequest.json()try/catch (categories uses bareawait request.json()), TWO-field required check (categories has only ONE), and TWO revalidatePathcalls on success (categories has none). Returns{ success: true, collection: , message: 'Collection created successfully' } with status 201. The companion [admin-collections-query.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/admin-collections-query.spec.ts) covers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~15-body bulk-loop walk all asserting < 500; a canonical-longer 401-envelope assertion pinning the divergence vs the bare-envelope sibling routes (admin/companies, admin/clients); a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']ANDbody.success === false; a success-branch-key non-disclosure assertion that NONE of collection, data, messagekeys plussuccess: true and the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the four static post-auth messages ('Invalid JSON in request body', 'Collection ID and name are required', 'Failed to create collection', 'Collection created successfully') must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-*header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to< 500; a per-call-request.json-try/catch-not-entered invariance walk pinning that the 'Invalid JSON in request body'400 envelope must NEVER appear on the unauth branch — distinct from prior POST smokes which use the bareawait request.json()form; a required-field-check-not-entered invariance walk; a create-call-+-cache-invalidation-not-entered invariance walk pinning that the unauth response status must NOT be 201, must NOT contain acollectionkey, and must NOT echo'Collection created successfully' — and pinning the gate-before-revalidatePath-side-effect order; a three-branch-outer-catch-not-entered invariance walk pinning that the unauth response must echo the canonical 401 envelope, not any branch of the 'already exists'/'must'/safeErrorResponse(...) outer catch chain). Cross-references the companion [admin-collections-query.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/admin-collections-query.spec.ts), the leaf-[id] triple-method [admin-collections-id-method-spec.md](./plugins/admin-collections-id-method-spec.md) covering the GET / PUT / DELETE surface on the /api/admin/collections/[id]/route.tsroute (it uses the same canonical-longer 401 envelope and the samecollectionsuccess-payload key but a different validation chain — ZodsafeParse(...).error.flatten() 400 envelope on PUT), the leaf-[id]/items dual-method [admin-collections-id-items-method-spec.md](./plugins/admin-collections-id-items-method-spec.md) covering the GET / POST surface on the /api/admin/collections/[id]/items/route.ts nested route, the closest-sibling collection-level POST companion [admin-categories-create-body-spec.md](./plugins/admin-categories-create-body-spec.md), and the other collection-level POST companions [admin-tags-create-body-spec.md](./plugins/admin-tags-create-body-spec.md), [admin-companies-create-body-spec.md](./plugins/admin-companies-create-body-spec.md), [admin-clients-create-body-spec.md](./plugins/admin-clients-create-body-spec.md), [admin-items-create-body-spec.md](./plugins/admin-items-create-body-spec.md), and [admin-users-create-body-spec.md](./plugins/admin-users-create-body-spec.md), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) and [Spec 009 -- Admin Dashboard](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/009-admin-dashboard) for the governing specs. With this entry the **per-spec-file docs rollout extends to 43-of-N** and the **tests/api/ per-spec-file sub-rollout extends to 41-of-many**, and the **first per-call-request.json-try/catch + two-revalidatePath` collection-level POST admin-tree smoke** lands -- complementing the existing query-surface coverage of the same collections-collection route.
  • E2E Admin Roles Create Body Spec (apps/web-e2e/tests/api/admin-roles-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's non-admin-gated collection-level role-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-roles-create-body.spec.ts, the forty-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-second under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/roles/route.ts — documenting the fifth Q-010b-style auth-gate-divergence finding in the admin-tree smoke layer: the route's POST handler does NOT call auth() at all, so any unauthenticated client can create roles (including admin-flagged roles by sending { name: 'X', description: 'Y', isAdmin: true }). The companion admin-roles-query.spec.ts already documents the same Q-010b finding for the GET surface. With no gate, the unauth client receives the same response an authenticated client would: 400 'Missing required fields: name, description' on no body, 400 'Unable to derive a valid role ID from name' on names that normalize to empty, 400 'Role name must be between 3 and 100 characters' on length out-of-range, 400 'Role description must be at most 500 characters' on description > 500, 409 'Role with similar name already exists' on duplicate ID, or 201 { success: true, data: <role>, message: 'Role created successfully' } on valid bodies. The POST handler additionally has a stable-ID-derivation step (the name is normalized via .normalize('NFKD'), diacritic stripping, lowercasing, and slug-style hyphen collapsing — the first POST smoke that walks a slug-derivation step), a soft-delete-aware uniqueness check (roleRepository.exists(id, { includeDeleted: true })), and an outer-catch translation that maps 'already exists' \| 'unique constraint' \| 'duplicate key' to a single fixed 409 message. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a NEVER-401-or-403 invariant pinning the auth-gate-divergence finding; a success-key envelope-shape assertion; a per-header-permutation status-stability comparison for the same body; a side-channel walk pinning that fabricated session cookies and X-* headers do NOT escalate privilege; a cross-method probe; a malformed-JSON-body invariance walk; a required-field-check-first-validation-fires invariant pinning the route's only "protection"; a length-validation deterministic-fire invariant). Cross-references the companion admin-roles-query.spec.ts, the leaf-[id] triple-method admin-roles-id-method-spec.md which DOES have a two-step gate (so the gate is on the [id] sub-resources but NOT on the collection root), the dual-method admin-roles-id-permissions-method-spec.md, and the other Q-010b finding admin-featured-items-id-method-spec.md, and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, and docs/questions.md (the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 44-of-N and the tests/api/ per-spec-file sub-rollout extends to 42-of-many, and the fifth Q-010b auth-gate-divergence finding lands -- documenting that the admin/roles POST endpoint is publicly accessible.
  • E2E Admin Notifications Create Body Spec (apps/web-e2e/tests/api/admin-notifications-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's tenant-only-gated collection-level notification-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-notifications-create-body.spec.ts, the forty-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-third under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/notifications/route.ts — documenting the sixth Q-010b-style auth-gate-divergence finding in the admin-tree smoke layer: the route's POST handler does NOT call !isAdmin at any point. It DOES require an authenticated user (!session?.user?.id → 401), so the route is tenant-scoped to authenticated users but is effectively non-admin-restricted. The POST handler combines a two-step gate (!session?.user?.id → 401 { success: false, error: 'Unauthorized' }, then !tenantId after getTenantId() AFTER body parse + required-fields check → 403 { success: false, error: 'Tenant not found' }) — distinct from prior two-step gates which run getTenantId() BEFORE body parse — this route's tenant resolution is INTERLEAVED with body validation. Hybrid bare-Unauthorized + success: false envelope (matching admin/users/[id], admin/featured-items/[id], admin/roles/[id]/permissions). Four-field required check (type, title, message, userId) → 400 'Missing required fields' BEFORE tenant resolution. getTenantId() AFTER required-fields check — the first collection-level POST smoke that runs the tenant-resolution check AFTER body validation. Inline Drizzle insert with notifications schema + JSON-stringified data field — distinct from prior POST smokes which delegate to a repository class. Success payload with notification success-key (NOT data) — { success: true, notification: <newNotification[0]> } with status 200 (NOT 201). console.error + 500 'Internal server error' catch — distinct from safeErrorResponse(...)-using POST smokes. The companion admin-notifications-query.spec.ts covers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~16-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a hybrid 401-envelope assertion; a strict envelope-shape assertion; an unauth-lands-on-401-not-403 invariant; a success-branch-key non-disclosure assertion that NONE of notification, data, success: true keys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-fields-check-not-entered invariance walk; a tenant-resolution-check-not-entered invariance walk pinning that the unauth response must NEVER echo 'Tenant not found'; a Drizzle-insert-not-entered invariance walk pinning that the unauth response must NEVER echo a notification key from the inserted row). Cross-references the companion admin-notifications-query.spec.ts, the leaf-[id] PATCH admin-notifications-id-read-method-spec.md, the static-path PATCH admin-notifications-mark-all-read-method-spec.md, and the other Q-010b findings admin-roles-create-body-spec.md (no auth at all) and admin-featured-items-id-method-spec.md (also tenant-only-gated, but on a leaf-[id] route), and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, and docs/questions.md (the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 45-of-N and the tests/api/ per-spec-file sub-rollout extends to 43-of-many, and the sixth Q-010b auth-gate-divergence finding lands -- documenting that the admin/notifications POST endpoint is tenant-scoped but NOT admin-gated.
  • E2E Admin Featured Items Create Body Spec (apps/web-e2e/tests/api/admin-featured-items-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's non-admin-gated collection-level featured-items-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-featured-items-create-body.spec.ts, the forty-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-fourth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/featured-items/route.ts — documenting the seventh Q-010b-style auth-gate-divergence finding in the admin-tree smoke layer. The route's POST handler does NOT call !isAdmin at any point. It DOES require an authenticated user (!session?.user?.id → 401) and a tenant (!tenantId → 403 BEFORE body parse — a "tenant-first" two-step gate ordering distinct from admin/notifications POST which runs getTenantId() AFTER body parse). The POST handler runs a two-field required check (!itemSlug || !itemName → 400 'Item slug and name are required'), an already-featured check via inline Drizzle select from featuredItems with eq(itemSlug) + eq(isActive, true) + tenant scoping → 400 'Item is already featured' if a row exists (the first POST smoke that uses a 400 -- not 409 -- for an already-exists check), then an inline Drizzle insert with featuredUntil parsed as new Date() if provided, featuredBy = session.user.id, featuredOrder defaults to 0 via destructure default. Returns { success: true, data: <featuredItem>, message: 'Item featured successfully' } with status 200. Outer catch is console.error + 500 'Failed to create featured item'. Documents the at-a-glance scenario tree (a ~16-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a hybrid 401-envelope assertion; a strict envelope-shape assertion; an unauth-lands-on-401-not-403 invariant; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the five static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-fields-check-not-entered invariance walk; an already-featured-check-not-entered invariance walk pinning that the unauth response must NEVER echo 'Item is already featured'; a Drizzle-insert-not-entered invariance walk). Cross-references the leaf-[id] triple-method admin-featured-items-id-method-spec.md covering the same tenant-only-gated route on GET / PUT / DELETE, the other Q-010b findings admin-roles-create-body-spec.md (no auth at all) and admin-notifications-create-body-spec.md (also tenant-only-gated, but with interleaved tenant resolution), and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, and docs/questions.md (the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 46-of-N and the tests/api/ per-spec-file sub-rollout extends to 44-of-many, and the seventh Q-010b auth-gate-divergence finding lands -- documenting that the admin/featured-items POST endpoint is tenant-scoped but NOT admin-gated, with a tenant-first two-step gate ordering distinct from the sibling notifications POST.
  • E2E Admin Categories Git Create Body Spec (apps/web-e2e/tests/api/admin-categories-git-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-CMS-write category-create POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-categories-git-create-body.spec.ts, the forty-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-fifth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/categories/git/route.ts — the first POST-only Git-CMS-write admin-tree smoke the docs tree publishes (distinct from the regular admin/categories POST which writes to the DB; this Git POST commits a new category file to the configured DATA_REPOSITORY GitHub repository via createCategoryGitService). The POST handler combines a single-step inline !session?.user?.isAdmin gate that returns 401 { error: 'Unauthorized. Admin access required.' } — NOTE: canonical longer message but WITHOUT success: false envelope key, a UNIQUE envelope shape that mixes the canonical longer message of admin/items/[id] etc. WITH the bare envelope of admin/clients/[clientId]/admin/companies/[id] etc. (no other admin-tree route combines these two). The POST handler runs a two-field required check (!id || !name → 400 { success: false, error: 'Category ID and name are required' } — NOTE: includes success: false key, distinct from the 401 envelope which lacks it), then a DATA_REPOSITORY env-var validation chain (missing → 500 'DATA_REPOSITORY not configured. Please set DATA_REPOSITORY environment variable.', malformed → 500 'Invalid DATA_REPOSITORY format. Expected: https://github.com/owner/repo'), then a GH_TOKEN env-var validation (missing → 500 'GitHub token not configured. Please set GH_TOKEN environment variable.'), then createCategoryGitService(gitConfig).createCategory({ id, name }) — the load-bearing Git-service call. Returns { success: true, category: <newCategory>, message: 'Category created and committed to Git repository' } with status 200 (NOT 201). Outer catch is two-branch: error.message.includes('already exists') → 409 echoing raw error.message, else safeErrorResponse(error, 'Failed to create category via Git'). The companion admin-categories-git-query.spec.ts covers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~12-body bulk-loop walk all asserting < 500; a canonical-longer-bare-envelope 401 assertion; a strict envelope-shape assertion Object.keys(body) === ['error'] with body.success undefined; a success-branch-key non-disclosure assertion that NONE of category, data, success, message keys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the six static post-auth messages (including the three env-var validation messages) must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-field-check-not-entered invariance walk; an env-var-validation-chain-not-entered invariance walk pinning that the unauth response must NEVER echo any of the three env-var error messages; a Git-service-call-not-entered invariance walk pinning that the unauth response must NEVER echo a category key from the Git-committed payload). Cross-references the companion admin-categories-git-query.spec.ts and the DB-write companion admin-categories-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 47-of-N and the tests/api/ per-spec-file sub-rollout extends to 45-of-many, and the first Git-CMS-write POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same Git-CMS route.
  • E2E Admin Settings Update Method Spec (apps/web-e2e/tests/api/admin-settings-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin settings-update PATCH body / header smoke spec paired with apps/web-e2e/tests/api/admin-settings-update-method.spec.ts, the forty-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-sixth under apps/web-e2e/tests/api/. Pairs with the PATCH export of apps/web/app/api/admin/settings/route.ts — the first PATCH-only collection-level config-write admin-tree smoke the docs tree publishes. It is also the first admin-tree smoke that uses getCachedApiSession(req) instead of auth() — a cached-session-lookup variant. The PATCH handler combines a single-step !session?.user?.isAdmin gate that returns 401 { error: 'Unauthorized' } (BARE envelope, NO success key, SHORT message), a JSON body parse, a single-field required check (if (!key) → 400 { error: 'Key is required' }), configManager.updateNestedKey('settings.${key}', value) for the load-bearing works.yml write, an update-failed branch (500 'Failed to update setting' if falsy), success payload { success: true, key, value } with status 200 (UNIQUE: echoes the input key and value), and outer catch console.error + 500 'Failed to update settings'. The companion admin-settings-query.spec.ts covers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~16-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion Object.keys(body) === ['error']; a success-branch-key non-disclosure assertion that NONE of success, key, value keys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-key-check-not-entered invariance walk; a configManager-update-not-entered invariance walk pinning that the unauth response must NEVER echo a key or value from the input). Cross-references the companion admin-settings-query.spec.ts and the map-status sub-route admin-settings-map-status-query.spec.ts, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 48-of-N and the tests/api/ per-spec-file sub-rollout extends to 46-of-many, and the first cached-session-lookup config-write PATCH admin-tree smoke lands -- complementing the existing query-surface coverage of the same settings-collection route.
  • E2E Admin Categories Git Query Spec (apps/web-e2e/tests/api/admin-categories-git-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-repository-status / categories GET smoke spec paired with apps/web-e2e/tests/api/admin-categories-git-query.spec.ts, the fiftieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-eighth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/admin/categories/git/route.ts -- the GET-companion of the recently-landed admin-categories-git-create-body-spec.md (POST). Where the POST handler commits a new category file to the configured DATA_REPOSITORY GitHub repository, the GET handler reads Git repository status and categories via the GitHub API. The route combines a unique combination of FOUR distinct contracts: (1) a zero-argument GET() handler signature that does not take a NextRequest argument and reads no searchParams at all (same posture as the notifications route), (2) a bare { error: 'Unauthorized. Admin access required.' } envelope without the success discriminant key -- the ONLY admin-tree GET route that combines the bare-envelope shape with the canonical longer role-context-specific message, (3) a GitHub-API-backed service via createCategoryGitService(gitConfig) that makes live HTTPS calls to the GitHub API using the configured GITHUB_TOKEN / DATA_REPOSITORY environment variables -- distinct from every other admin-tree route's drizzle / DB posture and from the file-system Git-CMS reader of the categories/all and tags/all routes, and (4) three distinct configuration-error 500 envelopes after the gate (canonical envelope with success: false, NOT bare -- a deliberate inconsistency between the unauth-branch and post-auth configuration-error branches). Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting < 500; a bare 401-envelope assertion with the canonical longer message; a strict envelope-shape assertion body.success === undefined; a negative-message assertion body.error !== 'Unauthorized' and body.error !== 'Forbidden'; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for ?userId= / ?token= / ?bypass= / ?repo=&branch=&owner= / ?path= key families; side-channel walks for Accept header / cookie / IP headers; a gate-before-config-validation invariant pinning that the three configuration-error 500 envelopes (DATA_REPOSITORY not set, invalid format, GITHUB_TOKEN not set) must NEVER fire on the unauth branch; a gate-before-Git-service invariant pinning that the createCategoryGitService(gitConfig) GitHub-API service must NEVER be entered on the unauth branch). Cross-references the POST companion admin-categories-git-create-body-spec.md, the DB-backed sibling admin-categories-query.spec.ts, the Git-CMS file-system sibling admin-categories-all-query.spec.ts, the Git-CMS file-system sibling admin-tags-all-query-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 50-of-N and the tests/api/ per-spec-file sub-rollout extends to 48-of-many, and the first GitHub-API-backed admin-tree GET smoke lands -- complementing the existing POST coverage of the same Git-CMS route.
  • E2E Sponsor Ads User Method Spec (apps/web-e2e/tests/api/sponsor-ads-user-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-scoped sponsor-ads GET + POST body / header smoke spec paired with apps/web-e2e/tests/api/sponsor-ads-user-method.spec.ts, the one-hundred-and-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-third under apps/web-e2e/tests/api/. Pairs with the GET AND POST exports of apps/web/app/api/sponsor-ads/user/route.ts — the first per-source-file dual-method smoke the docs tree publishes that pins Zod-safeParse validation on BOTH a query-parameter surface AND a body surface (GET validates query via querySponsorAdsSchema.safeParse; POST validates body via createSponsorAdSchema.safeParse). UNIQUE: the FIRST per-source-file dual-method smoke pinning Zod schema validation across both query and body. Distinct from EVERY prior dual-method smoke: Zod-safeParse on BOTH query AND body (UNIQUE); dynamic environment-based payment provider (ACTIVE_PAYMENT_PROVIDER = process.env.NEXT_PUBLIC_PAYMENT_PROVIDER || PaymentProvider.STRIPE is a module-level constant -- UNIQUE: FIRST per-source-file dual-method smoke pinning a module-level env-based provider constant where the handler ALWAYS uses this provider regardless of body); POST returns 201 status (NOT 200 -- UNIQUE among sponsor-ads POST smokes); POST 400 for invalid JSON with 'Invalid JSON in request body' distinct from body-validation 400 (FIRST per-source-file POST smoke pinning a try/catch around await request.json() with a distinct message); conditional already-exists 400 catch branch -- error.message.includes('already have') → 400 'You already have an active sponsor ad' (UNIQUE: FIRST per-source-file POST smoke pinning a message-substring catch dispatcher with a status override via safeErrorResponse(error, message, 400)); pagination success payload on GET with hasNext/hasPrev computed booleans (UNIQUE: FIRST per-source-file GET smoke pinning a hasNext/hasPrev computed-pagination contract); approval-workflow success message on POST -- 'Sponsor ad submission created successfully. Pending admin approval.' (UNIQUE: FIRST per-source-file POST smoke pinning an approval-workflow success message); TWO-key 401 envelope { success: false, error: 'Unauthorized' } on both methods. The handlers combine: GET handler with auth() session lookup, searchParams extraction, build queryParams with userId: session.user.id, querySponsorAdsSchema.safeParse(queryParams), getSponsorAdsPaginated(...) load-bearing DB read, success returns paginated payload; POST handler with auth(), JSON body parse with try/catch, createSponsorAdSchema.safeParse({ ...body, paymentProvider: ACTIVE_PAYMENT_PROVIDER }), createSponsorAd(userId, validated) load-bearing DB write, success returns 201 with { data, message }, conditional 400 catch-branch on 'already have' message substring; method-resolution surface where the route exports GET + POST (PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~6 headers × 2 methods + ~8 POST bodies all asserting < 500; canonical TWO-key 401-envelope assertions on GET AND POST; a cross-method 401-envelope-equality assertion; a strict TWO-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear including the conditional already-exists 400 message; a createSponsorAd-not-entered invariance walk on POST -- CRITICAL: pinning that XSS markers in the body are NEVER echoed back; a gate-before-Zod-query-validation invariance walk on GET pinning that invalid query values still produce 401 NOT 400; a gate-before-body-parse-and-Zod-body-validation invariance walk on POST pinning that malformed JSON / invalid body all produce 401 NOT 400; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk on POST). Cross-references the companion sponsor-ads checkout sibling sponsor-ads-checkout-body-spec.md, the companion sponsor-ads cancel sibling sponsor-ads-user-id-cancel-body-spec.md, the companion sponsor-ads renew sibling sponsor-ads-user-id-renew-body-spec.md, the companion public-sponsor-ads spec sponsor-ads-public.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 105-of-N and the tests/api/ per-spec-file sub-rollout extends to 103-of-many, and the first per-source-file dual-method smoke pinning Zod-safeParse validation across BOTH query AND body lands -- pinning a module-level env-based provider constant contract, a message-substring catch dispatcher with status override, a hasNext/hasPrev computed-pagination contract, and an approval-workflow success message that no prior dual-method smoke covers.
  • E2E Client Items [id] Method Spec (apps/web-e2e/tests/api/client-items-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client per-id item GET + PUT + DELETE dynamic-segment / body / header smoke spec paired with apps/web-e2e/tests/api/client-items-id-method.spec.ts, the one-hundred-and-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-seventh under apps/web-e2e/tests/api/. Pairs with the GET, PUT, AND DELETE exports of apps/web/app/api/client/items/[id]/route.ts — the first per-source-file triple-method smoke the docs tree publishes that pins FIVE distinct helper imports from the @/lib/utils/client-auth utility module (requireClientAuth + serverErrorResponse + notFoundResponse + forbiddenResponse + badRequestResponse) on a single source file. UNIQUE: every prior per-source-file smoke imports between 1 and 3 helpers from this utility; this is the FIRST to pin a 5-helper-import contract from a single utility module. Extends the client-items-method-spec.md sibling (which pins the requireClientAuth helper on the COLLECTION-level GET + POST surface) into the PER-ID GET + PUT + DELETE dynamic-segment surface. Distinct from EVERY prior triple-method smoke: 5-helper-import contract (UNIQUE -- FIRST per-source-file smoke pinning a 5-helper import from a single utility); itemIdParamSchema.safeParse({ id }) Zod validation on a path param (UNIQUE: FIRST per-source-file triple-method smoke pinning Zod validation on a dynamic-segment parameter vs query / body); GET success payload with engagement sub-object -- { success, item, engagement: { views, likes } } (UNIQUE: FIRST per-source-file GET smoke pinning a nested engagement-metrics sub-object derived from the item entity via views ?? 0, likes ?? 0); PUT empty-update guard -- Object.keys(updateData).length === 0 → 400 'No fields to update' (UNIQUE: FIRST per-source-file PUT smoke pinning a no-op-update guard that triggers AFTER successful Zod validation but BEFORE the repository write); PUT statusChanged dynamic message -- success message changes based on result.statusChanged ('Item updated successfully' vs 'Item updated successfully. Since it was previously approved, it has been moved to pending for re-review.' -- UNIQUE: FIRST per-source-file PUT smoke pinning a dynamic-by-result-flag success message); PUT FOUR-branch nested catch dispatcher -- error.message === 'Item not found' → 404, error.message.includes('permission') → 403, error.message.includes('deleted') → 400, default → outer serverErrorResponse (UNIQUE: FIRST per-source-file PUT smoke pinning a four-branch message-substring catch dispatcher layered inside an outer try/catch); DELETE THREE-branch nested catch dispatcher -- error.message === 'Item not found' → 404, error.message.includes('permission') → 403, error.message.includes('already deleted') → 400 (UNIQUE: FIRST per-source-file DELETE smoke pinning a three-branch message-substring catch dispatcher); 'Unauthorized. Please sign in to continue.' longer-message TWO-key envelope (matches client-items-method-spec.md and client-items-stats-query-spec.md); notFoundResponse(message) 404-helper + forbiddenResponse(message) 403-helper -- UNIQUE: FIRST per-source-file smoke pinning dedicated builder helpers for 404 and 403 responses (vs raw NextResponse.json(..., { status: 404 })). The handlers combine: GET handler with requireClientAuth(), itemIdParamSchema.safeParse({ id }), clientItemRepository.findByIdForUser(id, userId) ownership-checked load, !itemnotFoundResponse('Item not found or you do not have permission to view it'), success returns { success, item, engagement: { views, likes } }, outer catch serverErrorResponse(error, 'Failed to fetch item'); PUT handler with requireClientAuth(), param Zod, clientUpdateItemSchema.safeParse(body) → 400 with issues-joined message on failure, empty-update guard → 400, clientItemRepository.updateAsClient(id, userId, updateData) load-bearing DB write, success returns { success, item, statusChanged, previousStatus, message } with dynamic message based on statusChanged, FOUR-branch nested catch then outer serverErrorResponse(error, 'Failed to update item'); DELETE handler with requireClientAuth(), param Zod, clientItemRepository.softDeleteForUser(id, userId) load-bearing DB write, success returns { success, message: 'Item deleted successfully' }, THREE-branch nested catch then outer serverErrorResponse(error, 'Failed to delete item'); method-resolution surface where the route exports GET + PUT + DELETE (POST / PATCH must round-trip to < 500). Documents the at-a-glance scenario tree (four bulk-loop walks -- ~6 headers × 3 methods + ~7 PUT bodies all asserting < 500; longer-message TWO-key 401-envelope assertions on GET, PUT, AND DELETE; a cross-method 401-envelope-equality assertion across all three methods; a strict TWO-key envelope-shape assertion with no item or engagement leak; a gate-before-post-auth invariant pinning that NONE of EIGHT candidate post-auth messages -- spanning GET / PUT / DELETE branches -- must appear; an updateAsClient-not-entered invariance walk on PUT -- CRITICAL: pinning that XSS markers in the PUT body are NEVER echoed back; a softDeleteForUser-not-entered invariance walk on DELETE -- CRITICAL: pinning that the URL itemId marker is NEVER echoed back; a gate-before-FOUR-branch-catch invariance walk on PUT pinning that NONE of 'Item not found' / 'permission' / 'deleted' leak; a gate-before-Zod-body-validation invariance walk on PUT pinning that invalid body shapes still produce 401 NOT 400; a cross-method probe (POST / PATCH); a side-channel walk on PUT; a cross-id invariance walk pinning that the auth gate fires BEFORE any per-id branch -- the notFoundResponse / forbiddenResponse paths are unreachable on unauth). Cross-references the companion client-items collection sibling client-items-method-spec.md (pins the requireClientAuth helper on the COLLECTION-level GET + POST surface; this spec extends it into the PER-ID dynamic-segment surface), the companion client-items-stats sibling client-items-stats-query-spec.md (uses the same requireClientAuth() helper on a single GET surface for the stats endpoint), the companion client-protected sibling client-protected.spec.ts, the admin per-id sibling admin-items-id-method-spec.md (admin-gated GET + PUT + DELETE surface for the same item entity -- admin-scoped instead of client-scoped), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 109-of-N and the tests/api/ per-spec-file sub-rollout extends to 107-of-many, and the first per-source-file triple-method smoke pinning a 5-helper-import contract from a single utility lands -- pinning a Zod-on-path-param contract, an engagement-metrics nested sub-object GET payload, an empty-update guard, a dynamic-by-result-flag PUT success message, a FOUR-branch PUT catch dispatcher, a THREE-branch DELETE catch dispatcher, and dedicated notFoundResponse / forbiddenResponse builder helpers that no prior triple-method smoke covers.
  • E2E Collections Exists Query Spec (apps/web-e2e/tests/api/collections-exists-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public collections-existence probe query-param smoke spec paired with apps/web-e2e/tests/api/collections-exists-query.spec.ts. Pairs with the GET export of apps/web/app/api/collections/exists/route.ts -- the second member of the public-existence-probe trio to receive a per-source-file landing page (after the previously-landed categories-exists-query-spec.md Git-CMS catch-and-200 sibling and the previously-landed surveys-exists-query-spec.md DB-service catch-and-200 sibling). With this entry the three-member existence-probe trio is now fully documented per-source-file. The collections-exists route is uniquely the catch-and-500 member of the trio: every thrown error inside collectionRepository.findAll is caught and the route returns a 500 status with the extra error: 'Failed to check collections existence' field -- distinct from both siblings whose catch branches return 200 OK. Distinct from every other public-route per-source-file GET smoke the docs tree has published to date -- the route reads zero query parameters today (the _request parameter is underscored to mark it deliberately unused), runs above the DB-repository backing store via collectionRepository.findAll (NOT a Git-CMS reader and NOT a service-layer wrapper), returns the { exists, count } envelope on the happy path AND the { exists: false, count: 0, error: 'Failed to check collections existence' } envelope on the catch path, and logs every catch-branch error to console.error unconditionally (NOT only in development mode like the categories-exists sibling, and NOT silently like the surveys-exists sibling). The handler combines: no auth gate (intentionally public); zero query-param read (the _request parameter is underscored to mark it deliberately unused -- UNIQUE within the existence-probe trio); hard-coded { includeInactive: false } repository flag (load-bearing -- a future contributor who wires ?includeInactive=true into the call would also need to flip the response envelope shape or add a separate inactiveCount field); DB-repository-backed collectionRepository.findAll read (distinct from the categories-exists sibling's Git-CMS fetchItems reader and from the surveys-exists sibling's surveyService.getMany service-layer reader -- the collections-exists route talks directly to a DB repository without a service wrapper); happy-path success payload { exists: <bool>, count: <number> } with status 200; catch-and-500 fallback returns { exists: false, count: 0, error: 'Failed to check collections existence' } with status 500 (UNIQUE within the public-existence trio); unconditional console.error logging (the catch branch fires on every environment); GET(_request: NextRequest) Next-specific handler signature with the underscored parameter (UNIQUE within the public-existence trio: the only handler whose parameter is underscored to mark it deliberately unused). Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting >= 200 && < 600 covering ?locale= permutations across all eight illustrated locales, i18n aliases, cache-busting, strict / validate flags, content projection, ?includeInactive= repository-flag flips, content negotiation, status / active filters, multi-tenancy, empty values, repeated keys, special-character payloads, long values, and bogus / typo'd keys; a canonical { exists: boolean, count: number } envelope assertion via expect([200, 500]).toContain(...); a parameterised-vs-baseline status-stability comparison; a three-way shape-stability walk; a UNIQUE zero-query-input contract walk pinning that the explicit-locale, empty-locale, and absent-locale cases all return the same status because the route reads no query input; a UNIQUE ?includeInactive= invariance walk pinning that the hard-coded includeInactive: false repository flag is NOT flipped by a caller-supplied ?includeInactive=true query param). Cross-references the catch-and-200 Git-CMS-backed sibling categories-exists-query-spec.md, the catch-and-200 DB-service-backed sibling surveys-exists-query-spec.md, the cross-cutting feature-existence.spec.ts (also probes GET /api/collections/exists BUT only the no-arg baseline), the DB-backed admin sibling at /api/admin/collections (covered by admin-collections-query.spec.ts), the collection-detail GET / PUT / DELETE sibling admin-collections-id-method-spec.md, the collection-create POST sibling admin-collections-create-body-spec.md, and to Spec 010 -- End-to-End Test Coverage for the governing spec. With this entry the public-existence-probe trio is now fully documented per-source-file (categories-exists Git-CMS catch-and-200 + surveys-exists DB-service catch-and-200 + collections-exists DB-repo catch-and-500), and the first per-source-file GET smoke pinning a catch-and-500 DB-repository-backed public existence probe lands -- pinning a zero-query-input contract, a hard-coded includeInactive: false flag invariance, an unconditional console.error logging contract, and a generic-error-message-with-no-information-disclosure contract that no prior public-existence-probe smoke covers.
  • E2E Surveys Exists Query Spec (apps/web-e2e/tests/api/surveys-exists-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public surveys-existence probe query-param smoke spec paired with apps/web-e2e/tests/api/surveys-exists-query.spec.ts. Pairs with the GET export of apps/web/app/api/surveys/exists/route.ts -- the third member of the public-existence-probe trio (after the categories-exists-query-spec.md catch-and-200 Git-CMS sibling and the still-undocumented DB-backed collections-exists-query.spec.ts catch-and-500 sibling) -- the first per-source-file GET smoke the docs tree publishes that pins a DB-backed public existence probe whose catch branch ALSO returns 200 OK AND whose response envelope is the leaner { exists } shape with NO count field. Distinct from every other public-route per-source-file GET smoke the docs tree has published to date -- both the categories-exists and the collections-exists siblings live above the Git-CMS / DB-repository backing stores and emit the { exists, count } envelope; this route lives above a DB-backed surveyService.getMany that selects published surveys from the configured database, returns the leaner { exists } shape (since the limit: 1 short-circuit makes the count uninformative anyway), and is the catch-and-no-count sibling of the categories-exists route -- same catch-and-200 posture but lean envelope. The handler combines: no auth gate (intentionally public); ?type= query-param read via searchParams.get('type') (the route reads exactly ONE query param; every other key is silently ignored -- distinct from the categories-exists sibling which reads ?locale= and from the collections-exists sibling which reads zero query params via the underscored _request parameter); strict byte-for-byte type coercion typeParam === SurveyTypeEnum.ITEM ? SurveyTypeEnum.ITEM : SurveyTypeEnum.GLOBAL (every non-'item' value -- null for the absent key, '' for the empty value, 'global' for the explicit value, every typo / unknown / case-variant -- maps to the same GLOBAL branch); DB-backed surveyService.getMany({ type, status: PUBLISHED, limit: 1 }) read with the load-bearing limit: 1 short-circuit (the route only needs to know whether AT LEAST ONE published survey exists for the requested type); happy-path success payload { exists: <bool> } with status 200 (exists computed as (result.surveys?.length || 0) > 0); catch-and-empty fallback returns { exists: false } with status 200 (NOT 500) -- same catch-and-200 posture as the categories-exists sibling; no development-mode logging (the catch branch is silent on every environment -- distinct from the categories-exists sibling which logs in development mode and from the collections-exists sibling which logs unconditionally); GET(request: NextRequest) Next-specific handler signature with new URL(request.url) rather than request?.nextUrl?.searchParams?.get(...) optional-chaining triple. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting < 500 covering ?type= permutations across item / global, case-variants (ITEM / Item / iTeM / GLOBAL / Global), unknown values (location / tag / category / unknown / null), empty / whitespace values (?type= / ?type=%20), future filter keys (?status= / ?published=), pagination keys (?limit=), i18n keys (?locale= / ?lang=), cache-busting (?refresh= / ?force= / ?fresh= / ?nocache=), strict / validate flags, content projection (?include= / ?fields= / ?select= / ?expand=), content negotiation (?format=), multi-tenancy (?tenant= / ?tenantId=), combined keys, repeated keys, special-character payloads, long values, and bogus / typo'd keys; a canonical leaner { exists: boolean } envelope assertion; a parameterised-vs-baseline status-stability comparison; a three-way shape-stability walk; a UNIQUE typeParam === SurveyTypeEnum.ITEM fallback-semantics walk pinning that the no-arg, the explicit ?type=global, the unknown ?type=unknown, the case-variant ?type=ITEM, and the empty ?type= paths all land in the same GLOBAL branch and return the same status; a branch-split shape-invariance walk pinning that the ITEM branch (?type=item) and the GLOBAL branch (no-arg) land in different surveyService.getMany calls but both return the same { exists } envelope and both return 200). Cross-references the catch-and-200 Git-CMS-backed sibling categories-exists-query-spec.md (same catch-and-200 posture but distinct query-param surface (?locale= vs ?type=) and distinct envelope shape ({ exists, count } vs { exists })), the catch-and-500 DB-backed sibling collections-exists-query.spec.ts, the cross-cutting feature-existence.spec.ts (also probes GET /api/surveys/exists BUT only the no-arg baseline; this per-source-file spec adds the query-param surface so a regression that introduces a ?status= filter, a ?lang= filter, a ?refresh= cache-bust, or a non-200 status on an unknown ?type= value is caught immediately as a status divergence between the no-arg and parameter-laden branches), the DB-backed sibling surveys-id-method-spec.md, the DB-backed sibling surveys-id-responses-method-spec.md, the DB-backed sibling surveys-responses-id-query-spec.md, the public-route per-source-file items-popularity-scores-query-spec.md, Spec 010 -- End-to-End Test Coverage, and Spec 005 -- Internationalisation (the navigation shell that consumes all three existence probes inherits the locale-fallback semantics, even though this route does NOT read ?locale= itself).
  • E2E Surveys [surveyId] Responses Method Spec (apps/web-e2e/tests/api/surveys-id-responses-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-survey-responses GET / POST dynamic-segment / body / header smoke spec paired with apps/web-e2e/tests/api/surveys-id-responses-method.spec.ts, the one-hundred-and-twelfth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-tenth under apps/web-e2e/tests/api/. Pairs with the GET and POST exports of apps/web/app/api/surveys/[surveyId]/responses/route.ts -- the first per-source-file dual-method smoke the docs tree publishes that pins a SPLIT-auth gate contract on a single per-source-file route — GET is admin-gated (returns 401 'Unauthorized' for non-admin callers) while POST is PUBLIC (any caller may submit a response, with optional session capture for the userId field). Distinct from the sibling surveys/[surveyId]/route.ts MIXED-auth gate (public-GET + admin-PUT + admin-DELETE) covered by surveys-id-method-spec.md. Distinct from EVERY prior dual-method smoke: SPLIT-auth gate (UNIQUE: FIRST per-source-file dual-method smoke pinning an admin-GET + public-POST contract on the SAME dynamic-segment route -- most dual-method siblings either gate both methods or leave both public); POST is public + 404-survey-existence guard -- the POST handler does NOT call auth() for the gate; it calls surveyService.getOne(surveyId) after body validation and returns 404 'Survey not found' if the survey does not exist (UNIQUE: FIRST per-source-file POST smoke pinning a 404-existence guard BEFORE submission rather than as a 401 gate); body.data JSON-object guard -- POST requires body.data to be a non-null object, 400 'Invalid request body: "data" is required' otherwise (UNIQUE: a manual typeof body.data === 'object' && body.data != null guard, NOT a Zod safeParse); IP / user-agent header capture -- POST captures x-forwarded-for (first comma-segment), falls back to x-real-ip, then to 'unknown'; captures user-agent with 'unknown' fallback (UNIQUE: FIRST per-source-file POST smoke pinning an IP / user-agent header-capture contract); itemId sourced from the SURVEY -- the POST handler sets responseData.itemId = survey.itemId (NOT body.itemId) -- UNIQUE: the handler IGNORES any caller-provided itemId and sources it from the survey row; 201 Created on success POST (UNIQUE: FIRST per-source-file POST smoke pinning a 201 success status); { success: true, data: <responses> } GET payload + paginated filter shape -- GET accepts itemId / userId / startDate / endDate / page / limit query parameters with a strict /^\d+$/ regex on page / limit (anything else falls back to undefined); distinct catch-helper messages -- safeErrorResponse(error, 'Failed to fetch responses') outer-catch on GET vs safeErrorResponse(error, 'Failed to submit response') outer-catch on POST. The handlers combine: GET handler with outer try/catch around auth() session lookup (!session?.user?.isAdmin → 401 TWO-key envelope), query-param parsing with /^\d+$/ regex on page / limit, surveyService.getResponses(surveyId, filters) load-bearing service call, success returns { success: true, data: <responses> }, outer catch safeErrorResponse(error, 'Failed to fetch responses'); POST handler with outer try/catch around JSON body parse, body.data JSON-object guard 400, surveyService.getOne(surveyId) existence guard 404, OPTIONAL auth() session capture for the userId field (NOT a gate -- public callers submit anonymously), IP / user-agent header capture, surveyService.submitResponse(responseData) load-bearing service call, success returns { success: true, data: <response>, message: 'Response submitted successfully' } with status 201, outer catch safeErrorResponse(error, 'Failed to submit response'); method-resolution surface where the route exports GET AND POST (PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~9 headers × 2 methods + ~7 query-string permutations + ~10 POST bodies all asserting < 500; canonical TWO-key 401 envelope on GET; canonical TWO-key 404 envelope on POST; canonical TWO-key 400 envelope on POST; strict TWO-key envelope-shape preservation across every error envelope; gate-before-post-auth invariant on GET; validation-before-existence invariant on POST; gate-before-service-delegation invariant on GET (CRITICAL); survey-existence-before-service-delegation invariant on POST (CRITICAL); cross-method probe (PUT / PATCH / DELETE); side-channel walk on both GET and POST; IP / user-agent header isolation on the 404 branch; SPLIT-auth distinct-envelope contract pinning that GET 401 and POST 404 envelopes are NOT byte-identical). Cross-references the companion survey detail sibling surveys-id-method-spec.md, the companion per-response sibling surveys-responses-id-query-spec.md, the companion survey collection sibling surveys.spec.ts, the companion survey-existence sibling surveys-exists-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 112-of-N and the tests/api/ per-spec-file sub-rollout extends to 110-of-many, and the first per-source-file dual-method smoke pinning a SPLIT-auth gate contract (admin-GET + public-POST) lands -- pinning a 404-survey-existence guard, a body.data JSON-object guard, an IP / user-agent header capture, a survey-derived itemId contract, a 201 Created success status, and distinct GET vs POST catch-helper messages that no prior dual-method smoke covers.
  • E2E Client Items Import Sample Query Spec (apps/web-e2e/tests/api/client-items-import-sample-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped sample-template GET / query-param / header smoke spec paired with apps/web-e2e/tests/api/client-items-import-sample-query.spec.ts, the one-hundred-and-fourteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-twelfth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/client/items/import/sample/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins a requireClientAuth()-gated binary-stream sample-template handler that delegates to ItemExportService.generateSampleCSV() / generateSampleXLSX(). UNIQUE: every prior per-source-file client-items* GET smoke (client-items-method, client-items-id-method, client-items-stats-query) returns a JSON envelope; this is the FIRST requireClientAuth()-gated GET smoke that returns a binary stream with a Content-Disposition: attachment; filename="..." header on the happy path. It also pins the exportQuerySchema.parse(...) Zod-enum query parse AFTER the gate (matches the admin sibling admin/items/export/sample schema BUT with the longer-message client-auth envelope on the unauth branch), the safeErrorResponse(error, 'Failed to generate sample template') outer-catch helper (BYTE-IDENTICALLY matches the admin sibling 500-message), the new ItemExportService() direct-instantiation pattern with the per-format generateSampleCSV() / generateSampleXLSX() service entry points, the binary-stream success contract with a Content-Disposition: attachment; filename="..." header (UNIQUE: every prior client-items* GET smoke pins a JSON envelope), and 'Unauthorized. Please sign in to continue.' longer-message TWO-key 401 envelope (matches the prior client-items* siblings). Distinct from EVERY prior per-source-file GET smoke: requireClientAuth() + exportQuerySchema pair (UNIQUE: FIRST per-source-file GET smoke that gates a Zod exportQuerySchema.parse(...) query parse with the requireClientAuth discriminated-union helper -- the sibling client-items-method / client-items-id-method / client-items-stats-query parse no query schema; the admin sibling admin-items-export-sample-query uses the SAME exportQuerySchema but gates with bare auth() + session.user.isAdmin instead of requireClientAuth()); binary-stream success contract (UNIQUE: FIRST requireClientAuth()-gated GET smoke pinning a NextResponse(new Uint8Array(…), { headers: { 'Content-Type': …, 'Content-Disposition': 'attachment; filename="…"' } }) binary-stream success contract -- distinct from JSON-envelope shape every prior client-items* GET smoke pins; the unauth branch is invariant to this distinction (still a 401 JSON envelope), but the post-auth contract is fundamentally different); safeErrorResponse(error, 'Failed to generate sample template') outer-catch (UNIQUE: FIRST requireClientAuth()-gated GET smoke pinning a safeErrorResponse cross-utility helper, NOT serverErrorResponse like the client-items-stats-query sibling -- the catch message BYTE-IDENTICALLY matches the admin sibling admin/items/export/sample route's catch message); ItemExportService direct service-class posture (UNIQUE: FIRST requireClientAuth()-gated GET smoke pinning a new ItemExportService() direct-instantiation pattern, NOT a repository factory -- distinct from the getClientItemRepository() factory pattern of the client-items-stats-query sibling and the ItemImportService instantiation of the client-items-import-method / client-items-import-validate-method siblings); format= Zod-enum case-sensitivity -- Zod enums match exactly, so format=CSV / format=XLSX round-trip to 4xx on the auth branch via the safeErrorResponse(...) catch (the unauth branch is invariant to this distinction); format= enum default -- the schema has a .default('csv'), so omitting format yields CSV on the auth branch (the unauth branch is invariant to this distinction). The GET handler combines: outer try/catch around requireClientAuth() discriminated-union check, searchParams extraction, exportQuerySchema.parse(Object.fromEntries(searchParams)) Zod-validated format enum ('csv' | 'xlsx' with a 'csv' default), new ItemExportService() instantiation, per-format dispatch (generateSampleXLSX() for 'xlsx' else generateSampleCSV()), success returns a NextResponse(<bytes / string>, { headers: { 'Content-Type': ..., 'Content-Disposition': 'attachment; filename="..."' } }), outer catch safeErrorResponse(error, 'Failed to generate sample template'); method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (two bulk-loop walks -- ~50 query permutations + ~11 headers all asserting < 500; a longer-message TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion with no data / format / filename leak; a gate-before-catch invariance walk pinning that the 500-catch message never fires on unauth; a gate-before-binary-stream-header invariance walk -- CRITICAL: pinning that the Content-Disposition: attachment; … header NEVER appears on the unauth branch; a gate-before-binary-stream-content-type invariance walk -- CRITICAL: pinning that the unauth branch emits application/json, NOT text/csv or the XLSX spreadsheetml MIME type; a gate-before-Zod-parse invariance walk pinning every format= value (valid / empty / invalid / case-variant) collapses to the no-arg baseline status; an impersonation / token / bypass / filename-traversal invariance walk pinning every dangerous-passthrough query key collapses to the no-arg baseline status; an Accept-header invariance walk pinning the route does not negotiate content-types; a side-channel walk on GET (Cookie / Authorization / X-User-Id / X-Forwarded-For / X-Real-IP); a repeated-key invariance walk pinning searchParams.get(name)'s first-value semantics; a cross-method probe (POST / PUT / PATCH / DELETE); a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every parameter combination). Cross-references the companion client-items-import sibling client-items-import-method-spec.md (pins the requireClientAuth() helper on the COMMIT-mode batch-import POST surface; this spec extends the family into the SAMPLE-TEMPLATE GET surface), the companion client-items-import-validate sibling client-items-import-validate-method-spec.md (pins the requireClientAuth() helper on the DRY-RUN VALIDATE-mode multipart POST surface; this spec is the GET sibling that emits the sample template the user fills before calling the validate / import POST endpoints), the companion client-items collection sibling client-items-method-spec.md, the companion client-items per-id sibling client-items-id-method-spec.md, the companion client-items-stats sibling client-items-stats-query-spec.md (uses the same requireClientAuth() helper on a single GET surface for the stats endpoint -- serverErrorResponse catch, distinct from this spec's safeErrorResponse catch), the companion client-protected sibling client-protected.spec.ts, the admin-tree counterpart at apps/web/app/api/admin/items/export/sample/route.ts (admin-gated equivalent covered separately by admin-items-export-sample-query.spec.ts -- uses bare auth() + isAdmin instead of requireClientAuth(), and the SAME exportQuerySchema Zod parse + the SAME 'Failed to generate sample template' catch message + the SAME ItemExportService.generateSample* service calls), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 114-of-N and the tests/api/ per-spec-file sub-rollout extends to 112-of-many, and the first per-source-file GET smoke pinning a requireClientAuth()-gated binary-stream sample-template handler lands -- pinning a Zod-exportQuerySchema query-parse contract gated by requireClientAuth(), a Content-Disposition: attachment; filename="..." binary-stream success contract, a safeErrorResponse(error, 'Failed to generate sample template') cross-utility outer-catch helper that BYTE-IDENTICALLY matches the admin sibling, an ItemExportService direct service-class instantiation pattern, a format= Zod-enum case-sensitivity contract, and a format= enum-default contract that no prior requireClientAuth()-gated GET smoke covers.
  • E2E Featured Items Query Spec (apps/web-e2e/tests/api/featured-items-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public featured-items GET query-param smoke spec paired with apps/web-e2e/tests/api/featured-items-query.spec.ts, the one-hundred-and-seventeenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-fourteenth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/featured-items/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins a public (no-auth-gate) tenant-resolving listing endpoint combining a Number.parseInt(searchParams.get('limit') ?? '6', 10) default-6 parse path, a Math.min(Math.max(rawLimit, 1), 50) two-sided silent clamp, a Number.isFinite(rawLimit) non-finite fallback, a strict-string searchParams.get('includeExpired') === 'true' boolean-from-string parse (anything other than the literal string 'true' keeps includeExpired false), an await getTenantId() tenant-resolution short-circuit (a null tenant returns { success: true, data: [], count: 0 } without ever touching the DB), and a try / catch empty-list fallback that swallows internal errors and returns { success: true, data: [], count: 0 } rather than 500ing. UNIQUE: every prior per-source-file public-route GET smoke (sponsor-ads-public, items-popularity-scores, agent-discovery) pins a route whose error / null-state branch returns either a distinct envelope or NO envelope (a 4xx); this is the FIRST per-source-file GET smoke that pins a route whose null-tenant branch, DB-unavailable branch, AND outer-catch branch ALL collapse onto the SAME { success: true, data: [], count: 0 } empty-list envelope. Distinct from EVERY prior public-route GET smoke: tenant-resolution short-circuit (UNIQUE -- the FIRST per-source-file GET smoke pinning a route whose null-tenant branch returns a 200-empty envelope rather than a 401 / 403); Number.parseInt(value ?? '6', 10) default-6 parse path with explicit radix-10 second argument (UNIQUE -- distinct from items-popularity-scores's implicit-radix parseInt(...); the explicit radix-10 makes decimal interpretation load-bearing for any caller submitting a leading-0 value that some parseInt implementations would treat as octal); Math.min(Math.max(rawLimit, 1), 50) two-sided silent clamp (UNIQUE -- distinct from items-popularity-scores's one-sided Math.min(parseInt(limit), 100) upper-clamp-only and sponsor-ads-public's Math.min(Math.max(1, Math.floor(rawLimit)), 50) clamp; this route's clamp covers BOTH endpoints of the [1, 50] range -- values below 1 are silently raised to 1, values above 50 are silently lowered to 50); strict-string === 'true' boolean-from-string parse (UNIQUE -- the FIRST per-source-file GET smoke pinning a strict-string boolean parser on a query parameter -- only the literal lowercase 'true' flips the default; ?includeExpired=TRUE, ?includeExpired=1, ?includeExpired=false, ?includeExpired= all keep false); isActive: true + tenantId two-condition WHERE clause (UNIQUE -- the FIRST per-source-file GET smoke pinning a public listing route that combines an isActive flag check with a tenant-scoping check inside the same and(...) clause); multi-key composite ORDER BY (UNIQUE -- the FIRST per-source-file GET smoke pinning a Drizzle two-key composite ordering desc(featuredItems.featuredOrder), desc(featuredItems.featuredAt)); { success, data, count } three-key envelope (UNIQUE -- the FIRST per-source-file GET smoke pinning a public-route success envelope that adds a count: number cardinality key alongside success / data); try / catch empty-list fallback (NOT 500) (UNIQUE -- the FIRST per-source-file GET smoke pinning a route that catches every internal error and returns the same empty-list envelope as the null-tenant branch and the checkDatabaseAvailability() short-circuit); public (no-auth-gate) route (distinct from the auth-gated admin/featured-items and admin/featured-items/[id] siblings); method-resolution surface -- the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (one query-string bulk-loop walk covering ~30 permutations -- no-arg baseline, valid limit 1/6/10/50, out-of-range upper limit 51/999/10000 admit-clamped to 50, out-of-range lower limit 0/-5 admit-clamped to 1, empty / abc / NaN limit Number.parseInt-default fallback to '6' and Number.isFinite(NaN) === false non-finite branch, float limit 6.5/49.9 Number.parseInt integer-truncation, whitespace / + limit %2010 / %2B10 Number.parseInt tolerance, strict-string includeExpired true/false/1/0/empty/TRUE pinning the === 'true' strict-equality check, combined limit + includeExpired, unknown query keys silently ignored -- all asserting < 500). Cross-references the cross-cutting items.spec.ts (also probes GET /api/featured-items BUT only the no-arg baseline; this per-source-file spec adds the query-param surface so a regression in Number.parseInt, the Math.min / Math.max clamp, the Number.isFinite fallback, the === 'true' strict-equality check, the getTenantId() === null short-circuit, or the try / catch empty-list fallback is caught explicitly), the neighbouring auth-gated admin sibling admin-featured-items-id-method-spec.md (auth-gated single-featured-item CRUD GET / PUT / DELETE on /api/admin/featured-items/[id] -- the two routes share the featuredItems Drizzle table but diverge entirely on auth posture and method surface), the neighbouring admin listing sibling admin-featured-items-create-body-spec.md, the neighbouring sponsor-ads sibling sponsor-ads-checkout-body-spec.md, the neighbouring popularity-scores sibling items-popularity-scores-query-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 117-of-N and the tests/api/ per-spec-file sub-rollout extends to 114-of-many, and the first per-source-file GET smoke pinning a public (no-auth-gate) tenant-resolving featured-items listing handler lands -- pinning a Number.parseInt(value ?? '6', 10) default-6 parse path with explicit radix-10, a Math.min(Math.max(rawLimit, 1), 50) two-sided silent clamp, a Number.isFinite(rawLimit) non-finite fallback, a strict-string === 'true' boolean-from-string parse, a tenant-resolution null-short-circuit, an isActive: true + tenantId two-condition WHERE clause, a desc(featuredOrder), desc(featuredAt) multi-key composite ORDER BY, a { success, data, count } three-key envelope, and a try / catch empty-list fallback that no prior per-source-file public-route GET smoke covers.
  • E2E Health Database Query Spec (apps/web-e2e/tests/api/health-database-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public database-health GET query-param smoke spec paired with apps/web-e2e/tests/api/health-database-query.spec.ts, the one-hundred-and-eighteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-fifteenth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/health/database/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins a public (no-auth-gate) zero-argument health-probe endpoint combining a hard-coded db.execute(sql\SELECT 1 as test`)round-trip (no parameter binding, no URL-driven SQL), a two-branch (200-healthy / 500-unhealthy) status envelope determined by the database's reachability NOT the URL, a shared{ status, database, timestamp } envelope shape across both branches with a branch-specific fourth key (resulton success,erroron failure), and a bare zero-argumentGET() Next 16 handler signature that NEVER reads the request URL. UNIQUE: every prior per-source-file public-route GET smoke (featured-items-query, items-popularity-scores, sponsor-ads-public, agent-discovery) asserts a generic < 500contract because their500is a regression signal; this is the FIRST per-source-file GET smoke that asserts the tighter[200, 500]two-valid-status contract because the route's500is an EXPECTED outcome (catch branch when the configured database is unreachable, which the e2e environment does not guarantee). Distinct from EVERY prior public-route GET smoke: **two-valid-status[200, 500]contract** (UNIQUE -- the FIRST per-source-file GET smoke pinning a route whose500status is an EXPECTED outcome rather than a regression signal -- the bulk-loop assertion isexpect([200, 500]).toContain(response.status()), surfacing 502 / 503 / 504 gateway failures or any 4xx parameter-rejection regression as a test failure); **status invariance under URL changes** (UNIQUE -- the FIRST per-source-file GET smoke pinning that the response branch never drifts from the baseline regardless of how many bogus / typo'd / SQL-injection-shaped query params the caller appends -- a regression that introduces a request.nextUrl.searchParams.get(...)call would be caught by this branch-equality assertion); **bare zero-argumentGET()handler signature for a PUBLIC route** (the FIRST per-source-file GET smoke pinning a public route that uses theexport async function GET()signature with NOrequestparameter at all -- the siblingadmin-roles-active-query-spec.mdALSO pins a bare zero-argument GET signature but that route is admin-tree); **SQL-injection invariance contract** (UNIQUE -- the FIRST per-source-file GET smoke pinning that SQL-injection-shaped values in?schema=/?table=do NOT reach the SQL layer -- the route runs a hard-codedsql`SELECT 1 as test`with no parameter binding, so injected payloads cannot alter the executed statement); **canonical health-envelope contract** (UNIQUE -- the FIRST per-source-file GET smoke pinning a Kubernetes-style health-probe envelope with the three-key{ status, database, timestamp }shape enumerable across both branches;status ∈ {'healthy', 'unhealthy'}, database ∈ {'connected', 'disconnected'}, timestamp Date.parse-able ISO-8601 string); **non-JSON formatinvariance** (the route always respondsapplication/jsonregardless of anyAcceptheader or?format=text/?format=prometheusparameter); **public (no-auth-gate) route** (distinct from the auth-gatedclient/dashboard/stats, client/geo-stats, client/items/coordinatesclient-tree health-probe siblings); **method-resolution surface** -- the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (one query-string bulk-loop walk covering ~50 permutations -- no-arg baseline, ?refresh=/?force=cache-bypass keys,?schema=/?database=/?table=scoping keys,?timeout=fail-fast keys,?check=/?probe=different-check keys including K8s liveness / readiness / startup,?format=content-negotiation keys includingtext/prometheus, ?verbose=/?debug=diagnostics keys,?locale=/?lang=i18n keys, empty values, repeated keys, SQL-injection-shaped values including'OR'1'='1/';+DROP+TABLE+users;+--/%27/%22/%3B/%2D%2D, 500-character long values, bogus / typo'd query keys -- all asserting the [200, 500] two-valid-status contract -- plus three hand-written tests: canonical-envelope shape assertion, status-invariance test pinning baseline-equality of both status and branch, SQL-injection invariance test). Cross-references the cross-cutting [health.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/health.spec.ts) (also probes GET /api/health/database BUT only the no-arg baseline; this per-source-file spec adds the **query-param surface** + the **SQL-injection invariance contract** + the **canonical-envelope shape assertion**), the neighbouring [internal-db-init-query-spec.md](./plugins/internal-db-init-query-spec.md) (documents the related /api/internal/db-init surface that complements the database-health endpoint -- init-time vs probe-time database surfaces), the neighbouring [cron-sync-query-spec.md](./plugins/cron-sync-query-spec.md) (another zero-argument GET handler that mirrors this spec's bare GET() signature posture), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) for the governing spec. With this entry the **per-spec-file docs rollout extends to 118-of-N** and the **tests/api/per-spec-file sub-rollout extends to 115-of-many**, and the **first per-source-file GET smoke pinning a public (no-auth-gate) zero-argument health-probe endpoint** lands -- pinning a tighter[200, 500]two-valid-status contract that no prior per-source-file public-route GET smoke covers (every prior smoke asserts a generic< 500contract because their500is a regression signal; this route's500` is an EXPECTED outcome), a status-invariance under URL changes contract, a SQL-injection invariance contract, a canonical Kubernetes-style health-envelope shape contract, and a non-JSON content-negotiation invariance contract that no prior per-source-file public-route GET smoke covers.
  • E2E Client Items Coordinates Query Spec (apps/web-e2e/tests/api/client-items-coordinates-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's requireClientAuth()-gated client items-coordinates GET / query-param surface smoke spec paired with apps/web-e2e/tests/api/client-items-coordinates-query.spec.ts, pinning the third per-source-file requireClientAuth()-gated zero-argument GET handler (after the sibling client-dashboard-stats-query and client-geo-stats-query specs) combining a getClientItemRepository().getCoordinatesByUser(userId) repository-delegation pattern (NOT getStatsByUser like the client-items-stats-query sibling, NOT getGeoStatsByUser like the client-geo-stats-query sibling, NOT getStats(userId) on the dashboard repository like the client-dashboard-stats-query sibling), a nested-coordinates-keyed success envelope { success: true, coordinates: Array<{ slug, name, latitude, longitude }> } (the FIRST per-source-file GET smoke pinning a coordinates-keyed nested-array success envelope -- distinct from BOTH the spread-into-envelope shape pinned by client-dashboard-stats-query and client-geo-stats-query, AND the stats-keyed nested-object shape pinned by client-items-stats-query), a serverErrorResponse(error, 'Failed to fetch item coordinates') outer catch, and a nine-bypass-prevention assertion battery extending the six-test battery of the sibling client-geo-stats-query spec with a single-item-lookup bypass-prevention contract (?slug=… / ?itemId=… / ?itemSlug=… invariance), a content-negotiation bypass-prevention contract (?format=geojson / ?format=kml / ?format=xml / ?format=csv invariance), and an Accept-header invariance contract (Accept: application/geo+json / application/xml / text/html / */* round-trip to the same 401 as Accept: application/json). UNIQUE: this is the THIRD requireClientAuth()-gated GET smoke and the THIRD zero-argument handler in the requireClientAuth() family. Distinct from EVERY prior per-source-file GET smoke: getClientItemRepository().getCoordinatesByUser(userId) repository-delegation (UNIQUE -- the FIRST per-source-file GET smoke pinning a getClientItemRepository()-singleton-factory delegation to the getCoordinatesByUser(userId) method); nested-coordinates-keyed success envelope (UNIQUE -- the FIRST per-source-file GET smoke pinning a coordinates-keyed nested-array success contract -- a regression that switches the route to a { success: true, ...coordinates } spread-into-envelope shape would break consumer code that reads body.coordinates); nine-bypass-prevention assertion battery (UNIQUE -- the FIRST per-source-file GET smoke pinning a nine-test bypass-prevention battery extending the six-test battery of the sibling client-geo-stats-query spec with three additional contracts -- single-item lookup, content negotiation, Accept-header -- that no prior per-source-file GET smoke covers); single-item-lookup bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a ?slug=… / ?itemId=… / ?itemSlug=… query-parameter invariance contract on a collection endpoint -- a regression that reads searchParams.get('slug') / searchParams.get('itemId') before the gate would change the response payload shape on the auth branch from a collection Array<…> to a single-item lookup); content-negotiation bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a content-negotiation ?format=geojson / ?format=kml / ?format=xml / ?format=csv query-parameter invariance contract that pins ALL non-default format keys to the same 401 as the no-arg baseline); Accept-header invariance (UNIQUE -- the FIRST per-source-file GET smoke pinning an Accept-header invariance contract -- Accept: application/json / application/geo+json / application/xml / text/html / */* round-trip to the same 401); ?lat=NaN / ?lat=Infinity defensive spatial-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning defensive spatial-filter values NaN / Infinity on the unauth branch -- a regression that reads parseFloat(searchParams.get('lat')) before the gate could trigger a NaN-comparison bug in a future spatial-filter implementation; this spec pins that the gate fires first, neutralising the bug); ?zoom=… / ?center=lat,lng map-control bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning map-control query keys ?zoom=12 / ?center=40.7128,-74.0060 -- the obvious extensions for a future "items inside the current viewport" feature); serverErrorResponse(error, 'Failed to fetch item coordinates') outer-catch (matches the discriminated-union helper-contract shared with the client-dashboard-stats-query, client-geo-stats-query, and client-items-stats-query siblings -- NOT safeErrorResponse like client-items-import-sample-query); admin-allowed-on-client-endpoints note (matches the sibling client-dashboard-stats-query and client-geo-stats-query specs' contract for a parallel requireClientAuth()-gated client-tree endpoint -- the spec pins that the admin-status read happens via session.user.isAdmin, NEVER via ?admin=… / ?asAdmin=… / ?bypass=… / ?impersonate=… query parameters). The GET handler combines: requireClientAuth() discriminated-union auth helper (returns { success: false, response: <401 NextResponse> } on failure -- the route returns authResult.response directly via the early-return idiom -- or { success: true, userId: string } on success), getClientItemRepository() singleton-factory pattern (returns the client-item repository instance -- SHARED with the sibling client-items-stats-query and client-geo-stats-query routes, but invoked for a different method), clientItemRepository.getCoordinatesByUser(userId) load-bearing repository-delegation call (returns the per-user coordinate list as Array<{ slug: string, name: string, latitude: number, longitude: number }>), nested-coordinates-keyed success envelope NextResponse.json({ success: true, coordinates }) (the repository result is nested under a coordinates key -- NOT spread into the envelope like the client-dashboard-stats-query / client-geo-stats-query siblings, NOT under a stats key like the client-items-stats-query sibling), outer catch serverErrorResponse(error, 'Failed to fetch item coordinates') (maps any thrown error to a 500 with the documented message -- the catch can ONLY fire AFTER the auth gate has already let the call through, so the unauth branch is invariant to it); method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE round-trip to a 405 -- Next.js missing-method response). Documents the at-a-glance scenario tree (a single query-string bulk-loop walk covering ~110 permutations -- no-arg baseline, admin-impersonation keys, client-terminology variants, single-item-lookup keys (?itemId= / ?slug= / ?itemSlug=), magic-auth keys, geographic-filter keys, spatial / map-control filter keys (including ?zoom=12 / ?center=40.7128,-74.0060 / ?lat=NaN / ?lat=Infinity), item-status filter keys, pagination keys (including ?cursor=), projection keys, cache-busting keys, content-negotiation (including ?format=geojson / ?format=kml), i18n keys, sort-override keys, multi-tenancy keys, admin-override keys, empty values, repeated keys (including ?lat=40&lat=50), special-character values, 500-character long values, bogus / typo'd query keys, all asserting < 500, plus NINE hand-written tests pinning the canonical 401 envelope shape, the bogus-parameter status invariance, the ?userId=… session-gate-bypass-prevention, the ?token=… query-token-auth-bypass-prevention, the ?admin=… query-admin-override-prevention, the ?bbox=… spatial-filter-bypass-prevention (nine spatial keys), the ?slug=… single-item-lookup-bypass-prevention (three slug keys), the ?format=geojson content-negotiation-bypass-prevention (five format keys), the multi-permutation shape stability across three different parameter sets, and the Accept-header invariance across five Accept-header values). Cross-references the neighbouring requireClientAuth()-gated GET sibling client-geo-stats-query-spec.md (pairs with apps/web-e2e/tests/api/client-geo-stats-query.spec.ts and pins the spread-geo-stats success envelope shape on a stats-only payload vs the nested-coordinates-keyed array shape this spec pins on a coordinate-list payload -- shares the getClientItemRepository() singleton-factory with this route, but diverges on which repository method it invokes getGeoStatsByUser vs getCoordinatesByUser), the neighbouring requireClientAuth()-gated GET sibling client-dashboard-stats-query-spec.md (pairs with apps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts and pins the spread-stats success envelope shape vs the nested-coordinates-keyed array shape this spec pins -- both specs share the same requireClientAuth() discriminated-union auth-helper return contract and the same 'Unauthorized. Please sign in to continue.' longer-message TWO-key 401 envelope), the neighbouring requireClientAuth()-gated GET sibling client-items-stats-query-spec.md (pairs with apps/web-e2e/tests/api/client-items-stats-query.spec.ts and pins the { success: true, stats: ... } nested-stats success envelope shape on the auth branch vs the nested-coordinates-keyed array shape this spec pins -- shares the getClientItemRepository() singleton-factory with this route, but diverges on which repository method it invokes getStatsByUser vs getCoordinatesByUser), the neighbouring requireClientAuth()-gated client family specs client-items-method-spec.md, client-items-id-method-spec.md, client-items-import-method-spec.md, client-items-import-validate-method-spec.md, and client-items-import-sample-query-spec.md, the cross-cutting client-protected.spec.ts (covers the broader auth-protected client surface that this coordinates endpoint sits within), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 119-of-N and the tests/api/ per-spec-file sub-rollout extends to 116-of-many, and the third per-source-file requireClientAuth()-gated zero-argument GET smoke lands -- pinning a discriminated-union auth-gate contract, a nested-coordinates-keyed success envelope shape, a getClientItemRepository().getCoordinatesByUser(userId) singleton-factory repository-delegation, a serverErrorResponse('Failed to fetch item coordinates') outer-catch, and a nine-bypass-prevention assertion battery (?userId=… / ?token=… / ?admin=… / ?bbox=… / ?slug=… / ?format=geojson invariance + multi-permutation shape stability + Accept-header invariance) that no prior per-source-file GET smoke covers.
  • E2E Client Geo Stats Query Spec (apps/web-e2e/tests/api/client-geo-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's requireClientAuth()-gated client geo-stats GET / query-param surface smoke spec paired with apps/web-e2e/tests/api/client-geo-stats-query.spec.ts, the one-hundred-and-eighteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-fifteenth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/client/geo-stats/route.ts -- the second per-source-file GET smoke the docs tree publishes that pins a requireClientAuth()-gated zero-argument handler (the FIRST being the sibling client-dashboard-stats-query spec) combining a getClientItemRepository().getGeoStatsByUser(userId) repository-delegation pattern (NOT getClientDashboardRepository().getStats(userId) like the sibling client-dashboard-stats-query spec, NOT getClientItemRepository().getStatsByUser(userId) like the sibling client-items-stats-query spec), a spread-geo-stats success envelope { success: true, ...geoStats } (matches the spread-into-envelope shape pinned by client-dashboard-stats-query -- NOT the { success: true, stats: <statsObject> } nested shape pinned by client-items-stats-query), a serverErrorResponse(error, 'Failed to fetch geographic statistics') outer catch, and a six-bypass-prevention assertion battery (?userId=… admin-impersonation, ?token=… magic-token bypass, ?admin=… query-admin-override, ?country=… / ?city=… / ?lat=… geographic-filter bypass, multi-permutation shape stability) on top of the standard query-string bulk-loop walk. UNIQUE: this is the THIRD requireClientAuth()-gated GET smoke after client-items-stats-query and client-dashboard-stats-query, and the SECOND zero-argument handler in the requireClientAuth() family. Distinct from EVERY prior per-source-file GET smoke: getClientItemRepository().getGeoStatsByUser(userId) repository-delegation (UNIQUE -- the FIRST per-source-file GET smoke pinning a getClientItemRepository()-singleton-factory delegation to the getGeoStatsByUser(userId) method; the route shares the getClientItemRepository() singleton-factory with the client-items-stats-query route but diverges on which repository method it invokes); spread-geo-stats success envelope { success: true, ...geoStats } (UNIQUE within the spread-stats family -- the FIRST per-source-file GET smoke pinning a spread-stats envelope on a geographic dataset where the geo-stats fields total_items / items_with_location / items_remote / service_area_breakdown / top_cities / top_countries become top-level keys of the response envelope alongside success); six-bypass-prevention assertion battery (UNIQUE -- the FIRST per-source-file GET smoke pinning a six-test bypass-prevention battery -- extends the five-test battery of the sibling client-dashboard-stats-query spec with a geographic-filter bypass-prevention contract that no prior per-source-file GET smoke covers); geographic-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a geographic-filter query-parameter invariance contract on a stats endpoint -- the route returns the full per-user payload today; a regression that reads searchParams.get('country') / searchParams.get('city') / searchParams.get('lat') / searchParams.get('lng') / searchParams.get('bbox') / searchParams.get('radius') before the gate would change the response payload shape on the auth branch); ?lat=… / ?lng=… / ?bbox=… / ?radius=… spatial-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a spatial-filter query-parameter invariance contract -- a regression that adds spatial filtering "items near a point" or "items inside a bounding box" before the gate would change the response payload shape on the auth branch); ?serviceArea=… / ?service_area=… / ?coverage=… service-area-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a service-area-filter query-parameter invariance contract -- the service-area filter keys are particularly relevant to the service_area_breakdown array inside the response payload); ?topN=… / ?fields=top_cities per-bucket pagination/projection bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a per-bucket pagination key ?topN=… and per-bucket projection keys ?fields=top_cities / ?fields=top_countries,service_area_breakdown / ?select=items_with_location / ?include=items_remote invariance contract -- these keys are particularly tempting on a geo-stats endpoint where the top_cities and top_countries arrays could be paginated or filtered); ?format=geojson / ?format=kml content-negotiation bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a geographic-format content-negotiation invariance contract -- the route returns JSON exclusively today; a regression that adds GeoJSON or KML output formats would need to respect the gate); serverErrorResponse(error, 'Failed to fetch geographic statistics') outer-catch (matches the discriminated-union helper-contract shared with the client-dashboard-stats-query and client-items-stats-query siblings -- NOT safeErrorResponse like client-items-import-sample-query); admin-allowed-on-client-endpoints note (matches the sibling client-dashboard-stats-query spec's contract for a parallel requireClientAuth()-gated client-tree endpoint -- the spec pins that the admin-status read happens via session.user.isAdmin, NEVER via ?admin=… / ?asAdmin=… / ?bypass=… / ?impersonate=… query parameters). The GET handler combines: requireClientAuth() discriminated-union auth helper (returns { success: false, response: <401 NextResponse> } on failure -- the route returns authResult.response directly via the early-return idiom -- or { success: true, userId: string } on success), getClientItemRepository() singleton-factory pattern (returns the client-item repository instance -- SHARED with the sibling client-items-stats-query route, but invoked for a different method), clientItemRepository.getGeoStatsByUser(userId) load-bearing repository-delegation call (returns the geo-stats payload with total_items / items_with_location / items_remote / service_area_breakdown / top_cities / top_countries keys), spread-geo-stats success envelope NextResponse.json({ success: true, ...geoStats }) (the spread merges the geo-stats fields into the top level of the response envelope alongside success -- SHARED-SHAPE with the sibling client-dashboard-stats-query spec), outer catch serverErrorResponse(error, 'Failed to fetch geographic statistics') (maps any thrown error to a 500 with the documented message -- the catch can ONLY fire AFTER the auth gate has already let the call through, so the unauth branch is invariant to it); method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE round-trip to a 405 -- Next.js missing-method response). Documents the at-a-glance scenario tree (a single query-string bulk-loop walk covering ~95 permutations -- no-arg baseline, ?userId= / ?user_id= / ?uid= / ?id= admin-impersonation keys, ?clientId= / ?client_id= / ?clientID= client-terminology variants, ?token= / ?secret= / ?api_key= / ?authorization= / ?session= magic-auth keys, ?country= / ?city= / ?region= / ?area= / ?countryCode= geographic-filter keys, ?serviceArea= / ?service_area= / ?coverage= service-area filter keys, ?lat= / ?lng= / ?bbox= / ?radius= spatial-filter keys, ?period= / ?range= / ?window= time-window keys, ?limit= / ?offset= / ?page= / ?topN= pagination keys, ?fields= / ?select= / ?include= projection keys, ?refresh= / ?force= / ?fresh= / ?cache= / ?nocache= cache-busting keys, ?format=json / ?format=xml / ?format=csv / ?format=geojson / ?format=kml content-negotiation, ?locale= / ?lang= / ?currency= i18n keys, ?status= / ?type= filter keys, ?sort= / ?order= / ?direction= sort-override keys, ?tenant= / ?tenantId= / ?org= multi-tenancy keys, ?admin= / ?asAdmin= / ?bypass= / ?impersonate= admin-override keys, empty values, repeated keys, special-character values, 500-character long values, bogus / typo'd query keys, all asserting < 500, plus EIGHT hand-written tests pinning the canonical 401 envelope shape, the bogus-parameter status invariance, the ?userId=… session-gate-bypass-prevention, the ?token=… query-token-auth-bypass-prevention, the ?admin=… query-admin-override-prevention, the ?country=… / ?city=… / ?lat=… geographic-filter-bypass-prevention, and the multi-permutation shape stability across three different parameter sets). Cross-references the neighbouring requireClientAuth()-gated GET sibling client-dashboard-stats-query-spec.md (pairs with apps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts and pins the spread-stats { success: true, ...stats } shape this spec mirrors -- both specs share the same requireClientAuth() discriminated-union auth-helper return contract, the same 'Unauthorized. Please sign in to continue.' longer-message TWO-key 401 envelope, the same zero-argument handler signature, and the same spread-into-envelope success-payload shape -- but diverge on which repository they delegate to getClientDashboardRepository() vs getClientItemRepository() and on which bypass-prevention assertions they pin date-range vs geographic-filter), the neighbouring requireClientAuth()-gated GET sibling client-items-stats-query-spec.md (pairs with apps/web-e2e/tests/api/client-items-stats-query.spec.ts and pins the { success: true, stats: ... } nested-stats success envelope shape on the auth branch vs the spread-stats shape this spec pins -- shares the getClientItemRepository() singleton-factory with this route, but diverges on which repository method it invokes getStatsByUser vs getGeoStatsByUser), the neighbouring requireClientAuth()-gated client family specs client-items-method-spec.md, client-items-id-method-spec.md, client-items-import-method-spec.md, client-items-import-validate-method-spec.md, and client-items-import-sample-query-spec.md, the cross-cutting client-protected.spec.ts (covers the broader auth-protected client surface that this geo-stats endpoint sits within), the neighbouring sibling client-items-coordinates-query.spec.ts (covers another geographic-data endpoint /api/client/items/coordinates under the client-tree -- no per-source-file landing page yet for the coordinates sibling), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 118-of-N and the tests/api/ per-spec-file sub-rollout extends to 115-of-many, and the first per-source-file GET smoke pinning a requireClientAuth()-gated zero-argument geo-stats handler lands -- pinning a discriminated-union auth-gate contract, a spread-geo-stats success envelope, a getClientItemRepository().getGeoStatsByUser(userId) singleton-factory repository-delegation, a serverErrorResponse('Failed to fetch geographic statistics') outer-catch, and a six-bypass-prevention assertion battery (?userId=… / ?token=… / ?admin=… / ?country=… / ?city=… / ?lat=… invariance + multi-permutation shape stability) that no prior per-source-file GET smoke covers.
  • E2E Client Dashboard Stats Query Spec (apps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's requireClientAuth()-gated client dashboard-stats GET / query-param surface smoke spec paired with apps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts, the one-hundred-and-seventeenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-fourteenth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/client/dashboard/stats/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins a requireClientAuth()-gated zero-argument handler combining a getClientDashboardRepository().getStats(userId) repository-delegation pattern, a spread-stats success envelope { success: true, ...stats } (NOT the { success: true, stats: <statsObject> } nested shape used by the sibling client-items-stats-query spec), a serverErrorResponse(error, 'Failed to fetch dashboard statistics') outer catch, and a five-bypass-prevention assertion battery (?userId=… admin-impersonation, ?token=… magic-token bypass, ?admin=… query-admin-override, ?from=… date-range bypass, multi-permutation shape stability) on top of the standard query-string bulk-loop walk. UNIQUE: every prior requireClientAuth()-gated GET smoke (client-items-stats-query, client-items-method, client-items-id-method, client-items-import-sample-query) takes a request: NextRequest argument; this is the SECOND requireClientAuth() gate after client-items-stats-query and the SECOND zero-argument handler in the requireClientAuth() family, AND the FIRST per-source-file GET smoke pinning the spread-stats success envelope shape. Distinct from EVERY prior per-source-file GET smoke: requireClientAuth() discriminated-union auth gate (zero-argument handler) (UNIQUE -- the auth helper returns { success: false, response: <401 NextResponse> } on failure or { success: true, userId: string } on success -- the discriminated-union shape that the spec validates by asserting response.status() === 401); spread-stats success envelope { success: true, ...stats } (UNIQUE -- the FIRST per-source-file GET smoke pinning a ...stats spread-into-envelope success contract where the dashboard fields totalSubmissions / totalViews / totalVotesReceived / totalCommentsReceived / viewsAvailable / recentActivity / uniqueItemsInteracted / totalActivity / activityChartData / engagementChartData / submissionTimeline / engagementOverview / statusBreakdown / topItems become top-level keys of the response envelope alongside success); five-bypass-prevention assertion battery (UNIQUE -- the FIRST per-source-file GET smoke pinning a five-test bypass-prevention battery -- ?userId=… does NOT bypass the session gate, ?token=… does NOT introduce a query-token bypass, ?admin=… does NOT introduce a query-admin-override, ?from=… date-range params do NOT change the unauth branch, multi-permutation shape stability across three different parameter sets); serverErrorResponse(error, 'Failed to fetch dashboard statistics') outer-catch (matches the client-items-stats-query sibling helper contract -- NOT safeErrorResponse like client-items-import-sample-query); getClientDashboardRepository().getStats(userId) repository-delegation (UNIQUE -- the FIRST per-source-file GET smoke pinning a getClientDashboardRepository()-singleton-factory delegation vs the new ItemExportService() direct-instantiation pattern of client-items-import-sample-query); admin-allowed-on-client-endpoints note (UNIQUE -- the route's requireClientAuth() helper notes that admins are allowed to use client endpoints today; the spec pins that the admin-status read happens via session.user.isAdmin, NEVER via ?admin=… / ?asAdmin=… / ?bypass=… / ?impersonate=… query parameters); ?from=… / ?to=… date-range bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a date-range query-parameter invariance contract on a stats endpoint -- the route returns the full all-time payload today; a regression that reads searchParams.get('from') / searchParams.get('to') before the gate would change the response payload shape on the auth branch). The GET handler combines: requireClientAuth() discriminated-union auth helper (returns { success: false, response: <401 NextResponse> } on failure -- the route returns authResult.response directly via the early-return idiom -- or { success: true, userId: string } on success), getClientDashboardRepository() singleton-factory pattern (returns the dashboard repository instance), dashboardRepository.getStats(userId) load-bearing repository-delegation call (returns the dashboard payload with totalSubmissions / totalViews / totalVotesReceived / totalCommentsReceived / viewsAvailable / recentActivity / uniqueItemsInteracted / totalActivity / activityChartData / engagementChartData / submissionTimeline / engagementOverview / statusBreakdown / topItems keys), spread-stats success envelope NextResponse.json({ success: true, ...stats }) (the spread merges the dashboard fields into the top level of the response envelope alongside success), outer catch serverErrorResponse(error, 'Failed to fetch dashboard statistics') (maps any thrown error to a 500 with the documented message -- the catch can ONLY fire AFTER the auth gate has already let the call through, so the unauth branch is invariant to it); method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE round-trip to a 405 -- Next.js missing-method response). Documents the at-a-glance scenario tree (a single query-string bulk-loop walk covering ~60 permutations -- no-arg baseline, ?userId= / ?user_id= / ?uid= / ?id= admin-impersonation keys, ?clientId= / ?client_id= / ?clientID= client-terminology variants, ?token= / ?secret= / ?api_key= / ?authorization= / ?session= magic-auth keys, ?from= / ?to= / ?startDate= / ?endDate= date-range filter keys, ?period= / ?range= / ?window= time-window keys, ?limit= / ?offset= / ?page= pagination keys, ?fields= / ?select= / ?include= projection keys, ?refresh= / ?force= / ?fresh= / ?cache= / ?nocache= cache-busting keys, ?format= content-negotiation, ?locale= / ?lang= / ?currency= i18n keys, ?status= / ?type= filter keys, ?sort= / ?order= / ?direction= sort-override keys, ?tenant= / ?tenantId= / ?org= multi-tenancy keys, ?admin= / ?asAdmin= / ?bypass= / ?impersonate= admin-override keys, empty values, repeated keys, special-character values, 500-character long values, bogus / typo'd query keys, all asserting < 500, plus EIGHT hand-written tests pinning the canonical 401 envelope shape, the bogus-parameter status invariance, the ?userId=… session-gate-bypass-prevention, the ?token=… query-token-auth-bypass-prevention, the ?admin=… query-admin-override-prevention, the ?from=… date-range-bypass-prevention, and the multi-permutation shape stability across three different parameter sets). Cross-references the neighbouring requireClientAuth()-gated GET sibling client-items-stats-query-spec.md (pairs with apps/web-e2e/tests/api/client-items-stats-query.spec.ts and pins the { success: true, stats: ... } nested-stats success envelope on the auth branch vs the spread-stats { success: true, ...stats } shape this spec pins; both specs share the same requireClientAuth() discriminated-union auth-helper return contract and the same 'Unauthorized. Please sign in to continue.' longer-message TWO-key 401 envelope), the neighbouring requireClientAuth()-gated client family specs client-items-method-spec.md, client-items-id-method-spec.md, client-items-import-method-spec.md, client-items-import-validate-method-spec.md, and client-items-import-sample-query-spec.md, the cross-cutting client-protected.spec.ts (covers the broader auth-protected client surface that this dashboard-stats endpoint sits within), the neighbouring sibling client-geo-stats-query.spec.ts (covers the /api/client/geo-stats companion endpoint that returns geographic-distribution stats with a parallel requireClientAuth() gate -- no per-source-file landing page yet for the geo-stats sibling), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 117-of-N and the tests/api/ per-spec-file sub-rollout extends to 114-of-many, and the first per-source-file GET smoke pinning a requireClientAuth()-gated zero-argument dashboard-stats handler lands -- pinning a discriminated-union auth-gate contract, a spread-stats success envelope, a getClientDashboardRepository() singleton-factory repository-delegation, a serverErrorResponse('Failed to fetch dashboard statistics') outer-catch, and a five-bypass-prevention assertion battery (?userId=… / ?token=… / ?admin=… / ?from=… invariance + multi-permutation shape stability) that no prior per-source-file GET smoke covers.
  • E2E Auth Change-Password Spec (apps/web-e2e/tests/api/auth-change-password.spec.ts) -- Per-source-file reference for the Playwright e2e suite's bare two-test no-server-error password-change POST smoke spec paired with apps/web-e2e/tests/api/auth-change-password.spec.ts -- the bare-baseline companion to the already-documented auth-change-password-body-spec.md landing page (paired with auth-change-password-body.spec.ts). The body sibling drills into the rich body / header permutation surface (rate-limit-FIRST gate posture, Zod validation, multi-stage post-auth chain, OAuth-account guard, bcrypt-current / bcrypt-duplicate gates); this sibling is the two-test minimal < 500 no-server-error contract baseline that pins the basic guarantee that the route never blows up regardless of whether a session is attached or the body is well-formed. UNIQUE within the auth-change-password spec pair: this is the bare-baseline member of the pair. Every prior per-source-file landing page in the docs tree pairs to a SINGLE source spec; this is the first per-source-file landing page that documents one HALF of a two-spec pair covering the same route (the body sibling pairs with the rich auth-change-password-body.spec.ts and this sibling pairs with the bare auth-change-password.spec.ts). Distinct from the body sibling: bare two-test contract -- this spec emits exactly TWO tests (POST /api/auth/change-password without a session does not 5xx with a fully-shaped body and POST /api/auth/change-password with empty body does not 5xx with {}), each asserting expect(response.status()).toBeLessThan(500); no envelope-shape assertions -- the spec does NOT pin the canonical 401 / 400 / 429 envelopes (the body sibling does); no gate-ordering invariants -- the spec does NOT pin the rate-limit → auth → JSON-parse → Zod → tenant → user-DB → OAuth-guard → bcrypt-current → bcrypt-duplicate → bcrypt-hash → DB-update → email gate-ordering chain (the body sibling does); no bulk-loop walks -- the spec emits only two hand-written tests (the body sibling emits header / body bulk-loop walks); two-body coverage -- the spec hits a well-shaped body and an empty body, both must round-trip < 500; no method-resolution surface walk -- the spec does NOT emit a cross-method probe (GET / PUT / PATCH / DELETE); no side-channel walk -- the spec does NOT emit a side-channel walk (fabricated session cookies / Authorization headers). The route under test (apps/web/app/api/auth/change-password/route.ts) exports only POST. The POST handler combines: client-IP extraction; ratelimit('change-password:<clientIP>', 5, 15 minutes) FIRST gate (exhausted → 429 { success: false, error: 'Too many password change attempts. Please try again later.', retryAfter }); auth() session lookup (!session?.user?.id → 401 { success: false, error: 'Unauthorized. Please sign in.' }); await request.json() (no per-call try/catch); changePasswordSchema.safeParse(body) Zod validation (currentPassword non-empty + newPassword passwordSchema + confirmPassword equal-via-.refine(...)); getTenantId() (null → 403); user-DB select (null → 404); !user.passwordHash OAuth-account guard → 400; bcrypt.compare(currentPassword, user.passwordHash) (false → 400); bcrypt.compare(newPassword, user.passwordHash) (true → 400 same-password); bcrypt.hash(newPassword, 12); db.update(users); fire-and-forget sendPasswordChangeConfirmationEmail(...); success 200 { success: true, message: 'Password changed successfully' }; outer catch 500 { success: false, error: 'Internal server error' }. Documents the at-a-glance scenario tree (two hand-written tests covering the well-shaped-body and empty-body shapes -- both asserting < 500 and both expected to land on the unauth 401 branch under the rate-limit-not-tripped-yet posture). Cross-references the companion rich-permutation sibling auth-change-password-body-spec.md (pairs with auth-change-password-body.spec.ts and pins the rate-limit-FIRST gate posture, the canonical 401 / 400 / 429 envelopes, the bulk-loop header / body walks, and the gate-before-Zod / gate-before-tenant / gate-before-user-DB / gate-before-OAuth-guard / gate-before-bcrypt-current / gate-before-bcrypt-duplicate / gate-before-DB-update invariants), the companion auth-route smoke pair auth/forgot-password.spec.ts and auth/new-password.spec.ts (page-level forgot / reset password flows), Spec 003 -- Auth Providers (governs the auth-related routes at large), and Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 117-of-N and the tests/api/ per-spec-file sub-rollout extends to 114-of-many, and the first per-source-file landing page that documents one HALF of a two-spec pair covering the same route lands -- pinning the bare-baseline < 500 contract on the bare two-test smoke companion to an already-documented rich-permutation body smoke that no prior landing page covers (every prior landing page pairs to a SINGLE source spec).
  • E2E Categories Exists Query Spec (apps/web-e2e/tests/api/categories-exists-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public categories-existence probe query-param smoke spec paired with apps/web-e2e/tests/api/categories-exists-query.spec.ts. Pairs with the GET export of apps/web/app/api/categories/exists/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins a fully public Git-CMS-backed existence probe whose catch branch ALSO returns 200 OK (NOT 500). The route is the catch-and-200 sibling of the DB-backed collections-exists-query.spec.ts companion: same { exists, count } envelope, same nav-shell degradation contract, but the catch branch maps every thrown error to a 200 with { exists: false, count: 0 } rather than the 500 the collections-exists sibling emits. Distinction is load-bearing: the navigation shell hits both probes on every render and must degrade quietly (NOT block the whole page) when the content layer is unavailable. The handler combines: no auth gate (intentionally public); ?locale= query-param read via request?.nextUrl?.searchParams?.get('locale') || 'en' (the route reads exactly ONE query param; every other key is silently ignored — distinct from the collections-exists sibling which reads zero query params via the underscored _request parameter); fetchItems({ lang: locale }) Git-CMS read against the .content/ mirror cloned from DATA_REPOSITORY; happy-path success payload { exists: <bool>, count: <number> } with status 200 (exists computed as Array.isArray(categories) && categories.length > 0, count as categories?.length || 0); catch-and-empty fallback returns { exists: false, count: 0 } with status 200 (NOT 500); conditional development-mode logging — console.error fires inside the catch only when process.env.NODE_ENV === 'development' (distinct from the collections-exists sibling which logs unconditionally); GET(request: NextRequest) Next-specific handler signature with optional-chaining triple request?.nextUrl?.searchParams?.get(...) to keep the read safe under any future request === undefined refactor. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting < 500 covering ?locale= permutations across all eight illustrated locales (en / fr / es / de / ar / zh / pt / ja), i18n aliases (?lang= / combined ?locale=&lang=), cache-busting (?refresh= / ?force= / ?fresh= / ?nocache=), strict / validate flags, content projection (?include= / ?fields= / ?select= / ?expand=), content negotiation (?format=json / ?format=xml / ?format=csv), status / active filters, multi-tenancy (?tenant= / ?tenantId=), empty values (?locale= / ?lang= / ?refresh=), repeated keys (?locale=en&locale=fr), special-character payloads (%25 / %2F / %5C / %27 / %3Cscript%3E / %22%3Eoops), long values (500-char ?locale= / ?include=), and bogus / typo'd keys; a canonical { exists: boolean, count: number } envelope assertion; a parameterised-vs-baseline status-stability comparison; a three-way shape-stability walk across the no-arg baseline + a ?locale=&include= combined tuple + a ?refresh=&format=&unknown= cache-bust + content-negotiation tuple; and a UNIQUE searchParams.get('locale') || 'en' fallback-semantics walk pinning that the no-arg, the empty-string ?locale=, and the explicit-?locale=en paths all land in the same branch and return the same status). Cross-references the catch-and-500 DB-backed sibling collections-exists-query.spec.ts, the surveys existence probe surveys-exists-query.spec.ts, the Git-CMS-backed admin sibling admin-categories-all-query-spec.md, the DB-backed admin sibling at /api/admin/categories, the public-route per-source-file items-popularity-scores-query-spec.md, Spec 010 — End-to-End Test Coverage, and Spec 005 — Internationalisation which governs the ?locale= fallback semantics this route inherits.
  • E2E Admin Categories All Query Spec (apps/web-e2e/tests/api/admin-categories-all-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-CMS categories-listing query-param smoke spec paired with apps/web-e2e/tests/api/admin-categories-all-query.spec.ts. Pairs with the GET export of apps/web/app/api/admin/categories/all/route.ts -- the dedicated landing page for the first Git-CMS-backed admin-tree query smoke the docs tree ever published, previously covered indirectly via the client-trash-page-object.md co-tenant cross-link and called out repeatedly from the sibling admin-tags-all-query-spec.md without a dedicated landing page of its own. The categories-all route is the no-defensive-narrowing Git-CMS sibling of the tags-all route: same admin gate, same getCachedItems({ lang }) Git-CMS reader, same bare { success: false, error: 'Unauthorized' } 401 envelope, but NO defensive typeof locale !== 'string' narrowing (the only Git-CMS-backed admin-tree route that omits the dead-branch validator). The handler combines: auth() session lookup; single-step !session?.user?.isAdmin gate -> 401 with the bare canonical envelope (NOT 'Unauthorized. Admin access required.' / 'Forbidden'); ?locale= query-param read AFTER the gate via searchParams.get('locale') || 'en' (no narrowing); getCachedItems({ lang: locale }) Git-CMS read against the .content/ mirror cloned from DATA_REPOSITORY; success payload { success: true, data: categories } 200; outer catch console.error + 500 'Failed to fetch categories'; GET(request: NextRequest) Next-specific handler signature. UNIQUE: every other admin-tree GET route uses a drizzle / Postgres backend EXCEPT the sibling tags-all route; this is the no-narrowing Git-CMS sibling that documents the simpler posture. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting < 500 covering ?locale= permutations across all six supported locales plus invalid plus the empty string, locale variations (?lang= / ?language= / ?l=), pagination keys, status / active filters, content projection, cache-busting, impersonation, magic-token bypass, admin-override, multi-tenancy, Git-CMS-targeting, path-traversal payloads (../../etc/passwd / %00malicious), repeated keys, and bogus / typo'd keys; a bare 401-envelope assertion; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for ?locale= / ?userId= / ?token= / ?bypass= / ?repo=&branch=&commit= / ?path= / ?refresh= key families; an Accept header walk; a side-channel cookie / IP header walk; a repeated-key walk; a negative-message assertion pinning the bare 'Unauthorized' message and explicitly rejecting both the 'Forbidden' and 'Unauthorized. Admin access required.' alternatives; a path-traversal-resistance invariant pinning that even with ../../etc/passwd or %00malicious payloads the unauth branch returns the identical 401 envelope; a cache-bust-resistance invariant pinning that the in-memory Git-CMS cache is NEVER invalidated on the unauth branch). Cross-references the Git-CMS-sibling admin-tags-all-query-spec.md (same posture WITH a dead-branch defensive typeof locale !== 'string' narrowing -- the only Git-CMS-backed admin-tree route that carries the defensive validator), the DB-backed sibling admin-categories-query.spec.ts (database-backed /api/admin/categories listing route with pagination), the GitHub-API-backed sibling admin-categories-git-query-spec.md (live HTTPS calls to the GitHub API using GITHUB_TOKEN / DATA_REPOSITORY rather than the local .content/ mirror), the POST-companion of the GitHub-API-backed sibling admin-categories-git-create-body-spec.md (matching commit-a-new-category POST surface), the co-tenant page-object driver client-trash-page-object.md (the cross-link that originally introduced this route to the docs tree before this dedicated landing page was published), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 117-of-N and the dedicated landing page for the original Git-CMS-backed admin-tree query smoke lands -- closing a long-standing docs-tree gap where the categories-all route was referenced indirectly across several sibling docs without a per-source-file landing page.
  • E2E Client Items [id] Restore Method Spec (apps/web-e2e/tests/api/client-items-id-restore-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped per-id soft-delete restore POST body / header smoke spec paired with apps/web-e2e/tests/api/client-items-id-restore-method.spec.ts. Pairs with the POST export of apps/web/app/api/client/items/[id]/restore/route.ts -- the first per-source-file POST smoke the docs tree publishes that pins a requireClientAuth()-gated soft-delete restore action that delegates to a clientItemRepository.restoreForUser(id, userId) repository entry point with a THREE-branch nested catch dispatcher mapping repo-thrown error messages ('Item not found' exact match -> 404, 'permission' substring -> 403, 'not deleted' substring -> 400) to the @/lib/utils/client-auth builder helpers (notFoundResponse, forbiddenResponse, badRequestResponse) with a 'Failed to restore item' outer-catch default and the canonical 'Unauthorized. Please sign in to continue.' longer-message TWO-key 401 envelope. UNIQUE: every prior client-items* POST smoke covers a CRUD-style mutation or a batch-import service entry point -- this is the FIRST that pins a soft-delete restore action with the requireClientAuth helper. Companion to the broader smoke at client-item-restore.spec.ts which stays as the single-test "did it 5xx?" canary; this spec is the detailed per-source-file invariant-pinning sibling. Cross-references the parent-route sibling client-items-id-method-spec.md (5-helper-import contract on the parent [id] triple-method GET + PUT + DELETE surface; this spec extends it into the single-method POST /restore action sub-route), the broader client-items* family (client-items-method-spec.md, client-items-stats-query-spec.md, client-items-import-method-spec.md), and to Spec 010 -- E2E Test Coverage for the governing spec.
  • E2E Agent Discovery Spec (apps/web-e2e/tests/public/agent-discovery.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public agent-discovery surface smoke spec paired with apps/web-e2e/tests/public/agent-discovery.spec.ts, the one-hundred-and-sixteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the first under apps/web-e2e/tests/public/ to pin the agent-targeted discovery surface (vs the existing seo-manifests.spec.ts sibling that pins the crawler-targeted discovery surface). Pairs with the GET exports of apps/web/app/llms.txt/route.ts AND apps/web/app/items.json/route.ts -- the first per-source-file public-route smoke the docs tree publishes that pins two coupled routes in a single spec covering the llms.txt convention advertisement (/llms.txt) and the paired canonical-data JSON dump (/items.json) so downstream LLM agents can consume the directory without scraping HTML. UNIQUE: every prior public-route smoke covers a crawler-targeted SEO discovery surface (robots.txt, sitemap.xml, opengraph-image, favicon.ico); this is the FIRST that pins the agent-targeted discovery surface. Distinct from EVERY prior public-route smoke: two-route paired contract (UNIQUE -- the FIRST per-source-file smoke that pins two coupled routes in a single spec where /llms.txt advertises /items.json as the canonical-data anchor and the spec validates BOTH the advertisement (text/plain body contains /items.json) AND the data contract (JSON envelope shape and count === items.length invariant)); Cache-Control: public, max-age=300, s-maxage=900 (UNIQUE -- the FIRST per-source-file public-route smoke pinning the shared 5-minute / 15-minute browser / CDN cache-tiering); Access-Control-Allow-Origin: * CORS-open (UNIQUE -- the FIRST per-source-file smoke pinning a CORS-open public JSON endpoint -- agents must be able to fetch /items.json from any origin without a preflight gate); stable JSON envelope shape -- { site, generatedAt, count, items } is the documented downstream contract pinning count === items.length (load-bearing invariant), generatedAt ISO-8601 parseable, each item carrying slug + categories (array) + tags (array) at minimum; no side-channel branching (UNIQUE -- the FIRST per-source-file public-route smoke pinning that fabricated session cookies / Authorization headers do NOT alter the dispatch on EITHER route -- both routes are fully public); method-resolution surface -- both routes export ONLY GET (cross-method probes POST / PUT / PATCH / DELETE MUST round-trip < 500 -- Next.js returns 405). Documents the at-a-glance scenario tree (eight tests covering: /llms.txt text/plain body shape including the leading # , /items.json advertisement, sitemap + atom anchors; /llms.txt Cache-Control sanity; /items.json application/json + envelope shape + count === items.length invariant + ISO-8601 generatedAt parse; /items.json Cache-Control + CORS-open headers; /items.json per-item shape slug + array-typed categories + tags; cross-route side-channel invariance walk pinning fabricated cookies / Authorization headers do NOT alter dispatch on EITHER route; /items.json cross-method probe; /llms.txt cross-method probe -- all asserting < 500). Cross-references the neighbouring crawler-targeted SEO sibling seo-manifests.spec.ts (covers the closely related /robots.txt, /sitemap.xml, /opengraph-image, and /favicon.ico discovery surface; this sibling pins the agent-targeted discovery surface -- the /llms.txt convention is agent-targeted, NOT crawler-targeted), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 116-of-N, the tests/public/ per-spec-file sub-rollout extends from one (seo-manifests) to two, and the first per-source-file public-route smoke pinning the agent-targeted discovery surface lands -- pinning the llms.txt convention advertisement, a paired canonical-data /items.json JSON envelope, a Cache-Control: public, max-age=300, s-maxage=900 shared cache-tiering, an Access-Control-Allow-Origin: * CORS-open contract, a count === items.length envelope-shape invariant, and a no-side-channel public-dispatch contract that no prior per-source-file public-route smoke covers.
  • E2E Admin Clients Query Spec (apps/web-e2e/tests/api/admin-clients-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin client-profiles listing query-param smoke spec paired with apps/web-e2e/tests/api/admin-clients-query.spec.ts, the one-hundred-and-eighteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-fifteenth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/admin/clients/route.ts -- the first per-source-file admin-tree query smoke the docs tree publishes that pins a route whose single-step auth() + session.user.isAdmin gate returns the bare 'Unauthorized' message (no '. Admin access required.' suffix) AND fires BEFORE the shared validatePaginationParams(searchParams) helper, BEFORE the six optional ?search= / ?status= / ?plan= / ?accountType= / ?provider= query-param reads, AND BEFORE the legacy getClientProfiles({…}) query helper delegated to from @/lib/db/queries. UNIQUE: every prior admin-tree query smoke pins one of three different gate postures -- the canonical-longer-message 'Unauthorized. Admin access required.' family (admin/categories, admin/items, admin/items/import, admin/items/import/validate); the two-step !session?.user?.id → 401 then !session.user.isAdmin → 403 family (admin/notifications/[id]/read, admin/notifications/mark-all-read, admin/users/check-email, admin/users/check-username, admin/clients/bulk); OR the auth-gate-divergence-finding posture of the un-gated admin/roles / admin/roles/active family. This spec is the FIRST per-source-file admin-tree GET smoke pinning the bare-message single-step-collapse posture (matches the sibling admin/comments / admin/companies / admin/users routes). Distinct from EVERY prior admin-tree query smoke: bare 'Unauthorized' 401 message (NO suffix) (UNIQUE -- the FIRST per-source-file admin-tree GET smoke pinning the single-step-collapse bare-message envelope, distinct from the canonical-longer-message family AND the two-step-split family); single-step gate AHEAD of validatePaginationParams(...) helper (UNIQUE -- the FIRST per-source-file admin-tree GET smoke pinning a route whose single-step session?.user?.isAdmin gate fires BEFORE the shared pagination helper -- the helper short-circuits with its { error, status } 400 envelope on ?page=invalid / ?limit=invalid / ?page=-1 / ?limit=0 / ?limit=200, but only on the AUTH branch); six optional query-param reads, all AFTER the gate (UNIQUE -- the FIRST per-source-file admin-tree GET smoke pinning a route whose six query-param reads carry NO inline enum coercion or Zod schema validation -- distinct from the admin/roles route's narrow inline ternary enum coercion for ?status= and ?sortBy= / ?sortOrder=); legacy getClientProfiles({…}) query helper (distinct from the admin/categories route's categoryRepository.findAllPaginated(...) repository-pattern posture -- the handler imports getClientProfiles directly from @/lib/db/queries and passes a single options bag with the seven parsed fields; this spec stays green if a future contributor refactors the route to a clientRepository abstraction); three-key { success, data: { clients }, meta } success envelope -- the data key carries a single clients: [] sub-key, distinct from the admin/users route's bare { success, data: [...], pagination: {…} } shape; POST branch with environment-flag-gated CRM sync -- out of scope for this GET-only spec, but documented in the per-source-file landing page so future contributors who add a POST smoke must defend against the synchronous createTwentyCrmSyncServiceFromEnv() upsert via TWENTY_CRM_ENABLED=false environment override. The route under test combines: outer try / catch around auth() session lookup (!session?.user?.isAdmin → 401 { error: 'Unauthorized' }), URL(request.url).searchParams extraction, validatePaginationParams(searchParams) helper short-circuit on invalid pagination, six optional searchParams.get('…') || undefined reads (search, status, plan, accountType, provider), getClientProfiles({…}) legacy paginated query helper call, success returns { success: true, data: { clients: result.profiles }, meta: { page, totalPages, total, limit } }, outer catch with console.error + { error: 'Failed to fetch clients' } (status 500); method-resolution surface where the route exports GET AND POST (PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (one bulk-loop walk over ~60 paths + eleven hand-written scenarios pinning: the strict 401-on-no-arg-baseline + bare { error: 'Unauthorized' } envelope; status invariance across stacked-key permutations; per-key isolation walks for ?asAdmin= / ?as= / ?asUser= / ?impersonate= admin-impersonation, ?token= / ?secret= / ?api_key= / ?authorization= / ?session= / ?adminToken= magic-token, ?bypass= / ?admin= / ?override= / ?force= admin-override, ?status= and ?provider= filter-bypass; Accept header isolation; repeated-key walk; the bare-message envelope assertion pinning body.error === 'Unauthorized' AND body.error !== 'Unauthorized. Admin access required.' AND body.error !== 'Forbidden'). Cross-references the neighbouring per-id sibling admin-clients-clientid-method-spec.md, the neighbouring bulk sibling admin-clients-bulk-method-spec.md, the neighbouring create sibling admin-clients-create-body-spec.md (partitions the POST body surface on the SAME route file -- the two per-source-file specs together pin both the POST body surface and the GET query surface), the shared admin-clients page-object driver admin-clients-page-object.md, the prior per-source-file admin-tree GET smokes admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-sponsor-ads-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-settings-map-status-query-spec.md, admin-tags-all-query-spec.md, the admin-protected coverage spec admin-protected-extra.spec.ts (covers this route at the broad < 500 level; this per-source-file spec adds the deep query-surface walk on top), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 118-of-N and the tests/api/ per-spec-file sub-rollout extends to 115-of-many, and the first per-source-file admin-tree GET smoke pinning the bare-message single-step-collapse { error: 'Unauthorized' } 401 envelope lands -- pinning a single-step session?.user?.isAdmin gate ahead of the validatePaginationParams(...) helper, six gate-protected optional query-param reads with no inline enum coercion or Zod validation, the legacy getClientProfiles({…}) query helper posture, and the bare 401 envelope shape distinct from both the canonical-longer-message family and the two-step-split family.
  • E2E Items Popularity Scores Query Spec (apps/web-e2e/tests/api/items-popularity-scores.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public popularity-scores debug-endpoint GET query-param smoke spec paired with apps/web-e2e/tests/api/items-popularity-scores.spec.ts, the one-hundred-and-fifteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-thirteenth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/items/popularity-scores/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins a public (no-auth-gate) debug endpoint combining a Math.min(parseInt(limit), 100) admit-clamp invariant, a locale default-'en' fallback, a getCachedItems({ lang }) cache-aware fetch path, an empty-items short-circuit envelope { items: [], message: 'No items found' }, a logarithmic-scaling score formula Math.log10(value + 1) * weight, a featured-boost score cap (+10000), a three-tier recency-decay schedule (<30d1000, <90d500, <180d250), and a stable rank-after-sort mutation (sort by score desc + name.localeCompare asc, then mutate rank to the 1-based sort index). UNIQUE: every prior per-source-file items* GET smoke (items-engagement-query, items-export-query, items-export-settings-query) gates either with auth() or a feature-flag check; this is the FIRST per-source-file GET smoke that pins a route that is intentionally public -- exposing a debug-only sort breakdown for any caller that hits the URL. Distinct from EVERY prior items GET smoke: public (no-auth-gate) route (UNIQUE -- the FIRST per-source-file GET smoke pinning a fully public items* route -- no auth() / requireClientAuth() / isAdmin / feature-flag gate guards the handler); Math.min(parseInt(limit), 100) admit-clamp (UNIQUE -- the FIRST per-source-file GET smoke pinning a silent integer-clamp on a query parameter -- limit values above 100 are clamped to 100; parseInt of an empty / non-numeric string falls back to the default '20'; the route NEVER 4xxs on a malformed limit); logarithmic-scaling score formula (UNIQUE -- the FIRST per-source-file GET smoke pinning a Math.log10(value + 1) * weight engagement-scoring formula); featured boost (+10000) (UNIQUE -- the FIRST per-source-file GET smoke pinning a featured-item flat score boost as the load-bearing tie-breaker between featured and non-featured items); three-tier recency-decay schedule (UNIQUE -- the FIRST per-source-file GET smoke pinning a piecewise-linear recency-decay schedule); empty-items short-circuit envelope { items: [], message: 'No items found' } (UNIQUE -- the FIRST per-source-file GET smoke pinning a non-error early-return envelope on a getCachedItems({ lang }) cache miss -- the message key is ONLY emitted on the empty-items branch); stable rank-after-sort mutation (UNIQUE -- the FIRST per-source-file GET smoke pinning a sort-then-mutate-rank pattern); score-breakdown surface (UNIQUE -- the FIRST per-source-file GET smoke pinning a scoreBreakdown sub-object with seven labeled components -- featured, views, votes, rating, favorites, comments, recency); locale-fallback semantics -- locale defaults to 'en'; unknown locales return an empty items list (NOT an error). The GET handler combines: searchParams extraction (limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100); locale = searchParams.get('locale') || 'en'), getCachedItems({ lang: locale }) cache-aware items fetch (empty array short-circuits to { items: [], message: 'No items found' }), getEngagementMetricsPerItem(slugs) per-item engagement metrics map, per-item score (featured boost + logarithmic engagement scoring + avgRating * 500 + per-tier recency-decay; engagement-missing fallback heuristic seeds score from tags.length * 10 capped at 100, name-length tiers 50 / 25, icon_url 50, promo_code 75), sort by score desc + name.localeCompare asc + mutate rank to the 1-based sort index, slice + envelope { totalItems, showing: Math.min(limit, itemsWithScores.length), items: itemsWithScores.slice(0, limit) }, outer catch 500 { error: 'Failed to fetch popularity scores' }; method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (one query-string bulk-loop walk covering 15 permutations -- no-arg baseline, valid limit 5/20, out-of-range limit 999/10000 admit-clamped to 100, empty / abc / negative / zero limit parseInt-default fallback, known locale en/fr/zh, unknown locale empty-items short-circuit, combined limit + locale, combined out-of-range + locale clamp-then-locale order -- all asserting < 500). Cross-references the cross-cutting discovery.spec.ts (also probes GET /api/items/popularity-scores BUT only the no-arg baseline; this per-source-file spec adds the query-param surface so a regression in parseInt, the Math.min clamp, the locale default, or the empty-items branch is caught explicitly), the neighbouring engagement endpoint sibling items-engagement-query-spec.md (when published), the neighbouring item-detail public spec item-public.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 115-of-N and the tests/api/ per-spec-file sub-rollout extends to 113-of-many, and the first per-source-file GET smoke pinning a public (no-auth-gate) popularity-scores debug-endpoint handler lands -- pinning a Math.min(parseInt(limit), 100) admit-clamp invariant, a locale default-'en' fallback, an empty-items short-circuit envelope, a logarithmic-scaling score formula, a featured-boost score cap, a three-tier recency-decay schedule, and a stable rank-after-sort mutation that no prior per-source-file GET smoke covers.
  • E2E Surveys Responses [responseId] Query Spec (apps/web-e2e/tests/api/surveys-responses-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-response-detail GET dynamic-segment / header smoke spec paired with apps/web-e2e/tests/api/surveys-responses-id-query.spec.ts, the one-hundred-and-thirteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-eleventh under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/surveys/responses/[responseId]/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins an admin-gated survey-response-by-id lookup delegating to surveyService.getResponseById(responseId) with a 404 'Response not found' non-existence guard AFTER the auth gate. Distinct from EVERY prior per-source-file GET smoke: auth() + isAdmin gate BEFORE the lookup -- non-admin callers see 401 'Unauthorized' and the load-bearing surveyService.getResponseById(responseId) call is NEVER entered; single-route GET-only export -- the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500); { success: true, data: <response> } success payload + { success: false, error: 'Response not found' } 404 envelope (UNIQUE: FIRST per-source-file GET smoke pinning a Response not found 404 envelope -- distinct from the sibling surveys/[surveyId] route which uses Survey not found); safeErrorResponse(error, 'Failed to fetch response') outer-catch (UNIQUE: FIRST per-source-file GET smoke pinning a 'Failed to fetch response' (singular) 500-catch helper vs 'Failed to fetch responses' (plural) on the plural-collection sibling surveys-id-responses-method-spec.md). The GET handler combines: outer try/catch around auth() session lookup (!session?.user?.isAdmin → 401 TWO-key envelope), surveyService.getResponseById(responseId) load-bearing service call, 404 { success: false, error: 'Response not found' } if the result is null, success returns { success: true, data: <response> }, outer catch safeErrorResponse(error, 'Failed to fetch response'); method-resolution surface where the route exports ONLY GET. Documents the at-a-glance scenario tree (one bulk-loop walk -- ~7 headers asserting < 500; canonical TWO-key 401 envelope; strict TWO-key envelope-shape preservation; gate-before-post-auth invariant; gate-before-service-delegation invariant pinning that XSS markers in the path-segment are NEVER echoed AND surveyService.getResponseById NEVER executes on unauth (CRITICAL); side-channel isolation walk; cross-permutation status invariance walk pinning byte-identical 401 envelopes across every header permutation; cross-method probe (POST / PUT / PATCH / DELETE); catch-helper non-leak walk pinning 'Failed to fetch response' does NOT fire on the unauth branch). Cross-references the companion plural-collection sibling surveys-id-responses-method-spec.md (pins a SPLIT-auth gate on the parent apps/web/app/api/surveys/[surveyId]/responses/route.ts route -- the plural-collection sibling uses 'Failed to fetch responses' for its 500-catch helper, this spec pins 'Failed to fetch response' for the per-id helper), the companion survey detail sibling surveys-id-method-spec.md (pins a MIXED-auth gate on apps/web/app/api/surveys/[surveyId]/route.ts), the companion survey collection sibling surveys.spec.ts, the companion survey-existence sibling surveys-exists-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 113-of-N and the tests/api/ per-spec-file sub-rollout extends to 111-of-many, and the first per-source-file GET smoke pinning an admin-gated survey-response-by-id detail lookup lands -- pinning a 'Response not found' 404 envelope, a 'Failed to fetch response' (singular) 500-catch helper, and a single-route GET-only export contract that no prior per-source-file GET smoke covers.
  • E2E Client Items Import Validate Method Spec (apps/web-e2e/tests/api/client-items-import-validate-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped item-import-validate (dry-run) POST multipart / body / header smoke spec paired with apps/web-e2e/tests/api/client-items-import-validate-method.spec.ts, the one-hundred-and-eleventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-ninth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/client/items/import/validate/route.ts -- the first per-source-file POST smoke the docs tree publishes that pins a requireClientAuth()-gated multipart/form-data validate-only handler that delegates to ItemImportService.validateRows (a dry-run service entry point -- distinct from the sibling client/items/import route which calls executeImport). UNIQUE: every prior per-source-file client-items* smoke (client-items-method, client-items-id-method, client-items-stats-query, client-items-import-method) parses JSON via await request.json(); this is the FIRST that pins a requireClientAuth()-gated handler that parses multipart/form-data via await request.formData(). It also pins the 5-step file/mapping validation chain AFTER the gate (matches the admin sibling admin/items/import/validate chain BUT with the longer-message client-auth envelope on the unauth branch), the safeErrorResponse(error, 'Failed to validate import file') outer-catch helper (matches the admin sibling 500-message), the { success: true, headers, suggestedMapping, validationResults, summary } success payload with the service-derived validation result aggregate, hard-coded duplicateStrategy: 'skip' + defaultStatus: 'pending' validation options (UNIQUE: client requests CANNOT override either via the form data -- distinct from the admin sibling which DOES accept these as form fields), and 'Unauthorized. Please sign in to continue.' longer-message TWO-key 401 envelope (matches the prior client-items* siblings). Distinct from EVERY prior per-source-file POST smoke: requireClientAuth() + multipart/form-data pair (UNIQUE: the FIRST per-source-file POST smoke that gates a multipart/form-data body parse with the requireClientAuth discriminated-union helper -- the sibling client-items-import-method parses JSON, the sibling admin-items-import-validate-body parses multipart but uses auth() + isAdmin instead of requireClientAuth()); 5-step file/mapping validation chain AFTER the gate AND AFTER the formData parse with the five 400 envelopes 'No file provided.' / 'Invalid file type. Only CSV and XLSX files are supported.' / 'File too large. Maximum size is 10 MB.' / 'Invalid column mapping JSON.' / 'File contains no data rows.' (the unauth branch must NEVER reach ANY of the five steps); validateRows-not-executeImport service call -- the load-bearing call is importService.validateRows(parsed.rows, { ..., duplicateStrategy: 'skip', defaultStatus: 'pending' }) (UNIQUE: FIRST requireClientAuth()-gated POST smoke pinning a validateRows (dry-run) service entry point); { success: true, headers, suggestedMapping, validationResults, summary } success payload (UNIQUE: FIRST requireClientAuth()-gated POST smoke pinning a FOUR-key success payload vs result-keyed two-key payload of client-items-import-method); safeErrorResponse(error, 'Failed to validate import file') outer-catch (UNIQUE: shares the safeErrorResponse cross-utility helper with the sibling client-items-import-method and the admin sibling admin/items/import/validate -- FIRST per-source-file requireClientAuth()-gated POST smoke pinning a multipart-form-data validate-mode catch message that BYTE-IDENTICALLY matches the admin sibling); hard-coded validation options -- the handler hard-codes duplicateStrategy: 'skip' and defaultStatus: 'pending' when calling validateRows -- client requests CANNOT override either via the form data (UNIQUE: FIRST requireClientAuth()-gated POST smoke pinning a hard-coded validation-options contract distinct from the admin sibling which DOES accept duplicateStrategy + defaultStatus as form fields). The POST handler combines: outer try/catch around requireClientAuth() discriminated-union check, await request.formData() body parse, formData.get('file') !file guard 400, filename whitelist 400, file.size > 10 * 1024 * 1024 400, formData.get('mapping') non-null + JSON.parse failure 400, parseCSV(...) / parseXLSX(...) then parsed.rows.length === 0 400, validateRows(parsed.rows, { columnMapping: effectiveMapping, duplicateStrategy: 'skip', defaultStatus: 'pending' }) load-bearing service call, success returns { success: true, headers, suggestedMapping, validationResults, summary }, outer catch safeErrorResponse(error, 'Failed to validate import file'); method-resolution surface where the route exports ONLY POST (GET / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (two bulk-loop walks -- ~9 headers + ~12 multipart bodies all asserting < 500; a longer-message TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion with no headers / suggestedMapping / validationResults / summary leak; a gate-before-validation-chain invariant pinning the five 400-branch messages must NEVER appear; a gate-before-catch invariant pinning that the 500-catch message never fires on unauth; a validateRows-not-entered CRITICAL invariance walk pinning that XSS markers in the multipart body are NEVER echoed back AND that the load-bearing service call NEVER executes; a success-branch-key non-disclosure walk pinning none of the four success-branch JSON keys leak; a malformed-multipart invariance walk; a file-extension invariance walk; a cross-method probe (GET / PUT / PATCH / DELETE); a side-channel walk on POST; a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every multipart permutation). Cross-references the companion client-items-import sibling client-items-import-method-spec.md (pins the requireClientAuth() helper on the COMMIT-mode batch-import POST surface; this spec extends it into the DRY-RUN VALIDATE-mode batch-import POST surface), the companion client-items collection sibling client-items-method-spec.md, the companion client-items per-id sibling client-items-id-method-spec.md, the companion client-items-stats sibling client-items-stats-query-spec.md, the companion client-protected sibling client-protected.spec.ts, the admin-tree validate counterpart at apps/web/app/api/admin/items/import/validate/route.ts (admin-gated equivalent covered separately by admin-items-import-validate-body.spec.ts -- uses auth() + isAdmin instead of requireClientAuth(), and DOES accept duplicateStrategy + defaultStatus as form fields; the client variant does NOT), the companion client-items-import-sample sibling at apps/web/app/api/client/items/import/sample/route.ts (emits a sample CSV -- covered separately), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 111-of-N and the tests/api/ per-spec-file sub-rollout extends to 109-of-many, and the first per-source-file POST smoke pinning a requireClientAuth()-gated multipart/form-data validate-only handler lands -- pinning a 5-step file/mapping validation chain, a validateRows (dry-run) service entry point delegation, a FOUR-key { success, headers, suggestedMapping, validationResults, summary } success payload, a safeErrorResponse(error, 'Failed to validate import file') cross-utility outer-catch helper that BYTE-IDENTICALLY matches the admin sibling, and hard-coded { duplicateStrategy: 'skip', defaultStatus: 'pending' } validation options that client requests CANNOT override via the form data.
  • E2E Client Items Import Method Spec (apps/web-e2e/tests/api/client-items-import-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped item-import POST body / header smoke spec paired with apps/web-e2e/tests/api/client-items-import-method.spec.ts, the one-hundred-and-tenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-eighth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/client/items/import/route.ts -- the first per-source-file POST smoke the docs tree publishes that pins a requireClientAuth()-gated batch-import handler that delegates to an ItemImportService.executeImport service entry point. UNIQUE: every prior per-source-file client-items* smoke pins a CRUD-style handler (collection GET + POST list, per-id GET + PUT + DELETE, stats GET); this is the FIRST that pins a batch-import handler that fans out to a service layer. It also pins the NESTED body.rows array contract, the 'Missing or invalid rows array.' Zod-free 400 message (UNIQUE: a manual Array.isArray guard, NOT a Zod safeParse), the safeErrorResponse(error, 'Failed to execute import') outer-catch helper (UNIQUE -- sourced from @/lib/utils/api-error, NOT client-auth.serverErrorResponse), and the { success, result } success payload with the service-derived result aggregate ({ total, created, updated, skipped, errors }). Distinct from EVERY prior per-source-file POST smoke: requireClientAuth() + service-layer delegation pair (UNIQUE: the FIRST per-source-file POST smoke that gates a service-layer batch entry point with the requireClientAuth discriminated-union helper); nested body.rows array contract -- body.rows MUST be an Array.isArray non-null array, otherwise 400 'Missing or invalid rows array.' (UNIQUE: FIRST per-source-file POST smoke pinning a manual Array.isArray guard vs Zod safeParse); safeErrorResponse(error, 'Failed to execute import') outer-catch (UNIQUE: this helper comes from @/lib/utils/api-error NOT client-auth.serverErrorResponse -- FIRST per-source-file POST smoke pinning the safeErrorResponse cross-utility helper for a client-auth-gated handler); { success, result } success payload with service-derived result aggregate -- result has the shape { total, created, updated, skipped, errors } (UNIQUE: FIRST per-source-file POST smoke pinning a result-keyed success payload vs item-keyed, subscription-keyed, data-keyed, stats-keyed prior siblings); 'Unauthorized. Please sign in to continue.' longer-message TWO-key 401 envelope (matches client-items-method-spec.md, client-items-id-method-spec.md, and client-items-stats-query-spec.md); hard-coded import options -- the handler hard-codes duplicateStrategy: 'skip', defaultStatus: 'pending', and submittedBy: userId when calling executeImport (client requests CANNOT override any of these via the request body -- a bypass-attempt probe in the body bulk-loop walk pins this). The POST handler combines: requireClientAuth() discriminated-union check, JSON body parse (request.json() cast as ClientImportRequestBody), Array.isArray guard on body.rows -- 400 'Missing or invalid rows array.' on miss, new ItemImportService().executeImport(rows, { duplicateStrategy: 'skip', defaultStatus: 'pending', submittedBy: userId }) load-bearing service call, success returns { success: true, result } with the service-derived { total, created, updated, skipped, errors } aggregate, outer catch safeErrorResponse(error, 'Failed to execute import'); method-resolution surface where the route exports ONLY POST (GET / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (two bulk-loop walks -- ~6 headers + ~10 POST bodies all asserting < 500; a longer-message TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion with no result / total / created / updated / skipped / errors leak; a gate-before-post-auth invariant pinning the 400-branch message, the 500-catch message, and the result-aggregate keys must NEVER appear; an executeImport-not-entered invariance walk -- CRITICAL: pinning that XSS markers in the rows array body are NEVER echoed back AND that the load-bearing service call NEVER executes; a gate-before-Array.isArray-guard invariance walk pinning that even with rows missing or non-array, response is 401 NOT 400; a cross-method probe (GET / PUT / PATCH / DELETE); a side-channel walk on POST; a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every body permutation). Cross-references the companion client-items collection sibling client-items-method-spec.md (pins the requireClientAuth helper on the COLLECTION-level GET + POST surface; this spec extends it into the BATCH-IMPORT POST surface), the companion client-items per-id sibling client-items-id-method-spec.md (pins the requireClientAuth helper on the PER-ID GET + PUT + DELETE surface), the companion client-items-stats sibling client-items-stats-query-spec.md (uses the same requireClientAuth() helper on a single GET surface), the companion client-protected sibling client-protected.spec.ts, the admin-tree import counterpart at apps/web/app/api/admin/items/import/route.ts (admin-gated equivalent covered separately by admin-items-import-body.spec.ts), the companion client-items-import-validate sibling at apps/web/app/api/client/items/import/validate/route.ts (validates rows pre-execute -- covered separately), the companion client-items-import-sample sibling at apps/web/app/api/client/items/import/sample/route.ts (emits a sample CSV -- covered separately), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 110-of-N and the tests/api/ per-spec-file sub-rollout extends to 108-of-many, and the first per-source-file POST smoke pinning a requireClientAuth()-gated batch-import handler that delegates to a service-layer entry point lands -- pinning a nested body.rows Array.isArray guard contract, a safeErrorResponse(error, 'Failed to execute import') cross-utility outer-catch helper, a { success, result } service-derived result-aggregate success payload, and hard-coded { duplicateStrategy, defaultStatus, submittedBy } import options that no prior POST smoke covers.
  • E2E Client Items Method Spec (apps/web-e2e/tests/api/client-items-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client items collection-level GET + POST body / header smoke spec paired with apps/web-e2e/tests/api/client-items-method.spec.ts, the one-hundred-and-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-sixth under apps/web-e2e/tests/api/. Pairs with the GET AND POST exports of apps/web/app/api/client/items/route.ts — the first per-source-file dual-method smoke the docs tree publishes that pins the requireClientAuth() helper-based auth gate on BOTH GET AND POST (the client-items-stats-query-spec.md sibling pins the helper on a single GET surface; this spec extends to the dual-method usage). Also pins the badRequestResponse(message) 400-helper and the issues-joined Zod error message contract. Distinct from EVERY prior dual-method smoke: requireClientAuth() helper on BOTH methods (FIRST per-source-file dual-method smoke pinning the discriminated-union auth-helper return contract on both GET AND POST exports); badRequestResponse(message) 400-helper (UNIQUE -- a NEW helper distinct from safeErrorResponse and serverErrorResponse -- FIRST per-source-file smoke pinning a dedicated 400-builder helper); issues-joined Zod error message -- validationResult.error.issues.map((issue) => issue.message).join(', ') (UNIQUE: FIRST per-source-file smoke pinning a comma-joined Zod-issues 400 message, vs taking only the first issue like in the sponsor-ads-user sibling); GET success payload with FLAT keys at top level -- { success, items, total, page, limit, totalPages, stats } (UNIQUE: no data wrapper, no pagination wrapper, flat shape -- FIRST per-source-file GET smoke pinning a flat-pagination success payload); POST returns 201 status (NOT 200) with a review-workflow success message 'Item submitted successfully. It will be reviewed by our team before being published.'; ?deleted=true query branches to a different repo method (findDeletedByUser vs findByUserPaginated -- UNIQUE: FIRST per-source-file GET smoke pinning a query-driven repo-method dispatch contract); 'Unauthorized. Please sign in to continue.' longer-message TWO-key envelope (matches the client-items-stats-query sibling). The handlers combine: GET handler with requireClientAuth(), clientItemsListQuerySchema.safeParse(query), ?deleted=true branch to findDeletedByUser else findByUserPaginated, success returns flat payload; POST handler with requireClientAuth(), JSON body parse, clientCreateItemSchema.safeParse(body), clientItemRepository.createAsClient(userId, validated) load-bearing DB write, success returns 201 with { success, item, message }; method-resolution surface where the route exports GET + POST (PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~6 headers × 2 methods + ~9 POST bodies all asserting < 500; longer-message TWO-key 401-envelope assertions on GET AND POST; a cross-method 401-envelope-equality assertion; a strict TWO-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear including the review-workflow success message; a createAsClient-not-entered invariance walk on POST -- CRITICAL: pinning that XSS markers in the body are NEVER echoed back; a gate-before-Zod-query-validation invariance walk on GET pinning that invalid query values still produce 401 NOT 400; a gate-before-Zod-body-validation invariance walk on POST pinning that invalid body shapes still produce 401 NOT 400; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk on POST). Cross-references the companion client items-stats sibling client-items-stats-query-spec.md (uses the same requireClientAuth() helper on a single GET surface), the client per-id sibling at apps/web/app/api/client/items/[id]/route.ts (per-item resource -- covered separately), the companion client-protected sibling client-protected.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 108-of-N and the tests/api/ per-spec-file sub-rollout extends to 106-of-many, and the first per-source-file dual-method smoke pinning the requireClientAuth() helper on BOTH methods lands -- pinning a badRequestResponse(message) 400-helper contract, an issues-joined Zod error message contract, a flat-pagination GET success payload, a query-driven repo-method dispatch contract, and a review-workflow POST success message that no prior dual-method smoke covers.
  • E2E Sponsor Ads User [id] Query Spec (apps/web-e2e/tests/api/sponsor-ads-user-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-id user-scoped sponsor-ad lookup GET dynamic-segment / header smoke spec paired with apps/web-e2e/tests/api/sponsor-ads-user-id-query.spec.ts, the one-hundred-and-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-fifth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/sponsor-ads/user/[id]/route.ts — the first per-source-file dynamic-segment GET smoke the docs tree publishes that pins a 404-mask user-scoped IDOR -- when sponsorAd.userId !== session.user.id, the handler returns 404 'Sponsor ad not found' (NOT 403 'Forbidden') with the SAME envelope as the genuine not-found branch. UNIQUE: the FIRST per-source-file dynamic-segment GET smoke pinning a 404-mask security pattern on a USER-OWNED resource (the surveys-id sibling pins a 404-mask on STATUS-gated admin resources; this sponsor-ads-user-id sibling pins the pattern on a per-user-ownership resource). Distinct from EVERY prior dynamic-segment GET smoke: 404-mask user-scoped IDOR (UNIQUE -- the 404 envelope for cross-user access is BYTE-IDENTICAL to the 404 envelope for genuinely-non-existent IDs); TWO-key 401 envelope { success: false, error: 'Unauthorized' }; TWO-key 404 envelope { success: false, error: 'Sponsor ad not found' } used for BOTH not-found AND IDOR violations (intentional masking); TWO-key success payload { success: true, data: <sponsorAd> }; TWO-key 500 envelope { success: false, error: 'Failed to fetch sponsor ad' }. The GET handler combines auth() session lookup (!session?.user?.id → 401 TWO-key), { id } = await params dynamic-segment resolution, the load-bearing getSponsorAdById(id) DB read, !sponsorAd check (→ 404 TWO-key { success: false, error: 'Sponsor ad not found' }), 404-mask user-scoped IDOR check (sponsorAd.userId !== session.user.id → 404 with the SAME envelope -- intentional masking of cross-user access), success payload { success: true, data: <sponsorAd> } with status 200, outer catch 500 TWO-key { success: false, error: 'Failed to fetch sponsor ad' }, and method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk asserting < 500; a canonical TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion via Object.keys(body).sort(); a gate-before-post-auth invariant pinning that 'Sponsor ad not found' and 'Failed to fetch sponsor ad' must NEVER appear; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a getSponsorAdById-not-entered invariance walk -- CRITICAL: pinning that no data.userId / data.itemSlug / data.itemName / data.paymentProvider fields from a sponsor-ad row are leaked; a cross-id invariance walk pinning that the auth gate fires BEFORE any per-id branch -- the 404-mask is unreachable on unauth; a catch-branch-not-entered invariance walk pinning that the 500 catch dispatcher never fires on unauth). Cross-references the companion sponsor-ads cancel sibling sponsor-ads-user-id-cancel-body-spec.md (POST verb on the same [id] segment), the companion sponsor-ads renew sibling sponsor-ads-user-id-renew-body-spec.md, the collection-level GET + POST sibling sponsor-ads-user-method-spec.md (Zod-safeParse on both query and body), the surveys-id 404-mask sibling surveys-id-method-spec.md (404-mask pattern on STATUS-gated admin resources -- vs this user-ownership pattern), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 107-of-N and the tests/api/ per-spec-file sub-rollout extends to 105-of-many, and the first per-source-file dynamic-segment GET smoke pinning a 404-mask user-scoped IDOR on a per-user-ownership resource lands -- pinning a 404-mask security pattern that BYTE-IDENTICALLY masks cross-user access as not-found (vs the surveys-id sibling's status-based 404-mask) that no prior dynamic-segment GET smoke covers.
  • E2E Sponsor Ads User Stats Query Spec (apps/web-e2e/tests/api/sponsor-ads-user-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-scoped sponsor-ads stats GET header smoke spec paired with apps/web-e2e/tests/api/sponsor-ads-user-stats-query.spec.ts, the one-hundred-and-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-fourth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/sponsor-ads/user/stats/route.ts -- the first per-source-file GET smoke the docs tree publishes that pins a THREE-bucket nested-stats success payload -- { success: true, stats: { overview, byInterval, revenue } } where each bucket has its own required-keys contract: overview has SEVEN status-bucket counts (total, pendingPayment, pending, active, rejected, expired, cancelled); byInterval has TWO billing-interval counts (weekly, monthly); revenue has THREE revenue rollups in minor currency units (totalRevenue, weeklyRevenue, monthlyRevenue). UNIQUE -- every prior per-source-file GET stats smoke pins a flat shallow stats key set; this is the FIRST that pins a THREE-bucket nested-stats invariant where the stats object is a triple-nested aggregate. Distinct from EVERY prior session-gated GET smoke: THREE-bucket nested-stats success payload (UNIQUE); bare auth() session lookup distinct from the requireClientAuth() discriminated-union helper used by client-items-stats-query-spec.md; TWO-key 401 envelope { success: false, error: 'Unauthorized' } (same shape as the sponsor-ads/user parent route, distinct from the bare ONE-key envelope used by user-payments and subscription siblings); TWO-key success payload { success: true, stats } (uses stats key NOT data); service-call delegation -- sponsorAdService.getSponsorAdStatsByUser(session.user.id) is the ONLY post-auth load-bearing call (no DB-helper layer); TWO-key 500 catch envelope { success: false, error: 'Failed to fetch sponsor ad stats' } (distinct from parent route's 'Failed to fetch sponsor ads' and 'Failed to create sponsor ad'); zero-arg GET signature export async function GET() with NO request / context arguments. The handler combines: auth() session lookup → 401 TWO-key envelope on miss; sponsorAdService.getSponsorAdStatsByUser(userId) load-bearing service call returning the THREE-bucket aggregate; success payload { success: true, stats: { overview, byInterval, revenue } }; outer catch returns 500 { success: false, error: 'Failed to fetch sponsor ad stats' }; method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (a single header bulk-loop walk -- ~6 headers all asserting < 500; a canonical TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion; a gate-before-post-auth invariant pinning the 'Failed to fetch sponsor ad stats' message must NEVER appear; a sponsorAdService-not-entered CRITICAL invariance walk pinning that NEITHER the bucket names overview / byInterval / revenue NOR the inner status / interval / revenue keys leak on unauth; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a catch-branch isolation walk pinning the 500-catch never fires on unauth; a cross-permutation status invariance walk). Cross-references the companion sponsor-ads parent sibling sponsor-ads-user-method-spec.md (covers the GET + POST surface of the parent /sponsor-ads/user route), the companion sponsor-ads cancel sibling sponsor-ads-user-id-cancel-body-spec.md, the companion sponsor-ads renew sibling sponsor-ads-user-id-renew-body-spec.md, the companion client-items-stats sibling client-items-stats-query-spec.md (another per-source-file stats GET smoke that uses the requireClientAuth helper instead of bare auth()), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 106-of-N and the tests/api/ per-spec-file sub-rollout extends to 104-of-many, and the first per-source-file GET smoke pinning a THREE-bucket nested-stats payload lands -- pinning a triple-nested aggregate { overview, byInterval, revenue } invariant where prior stats smokes only ever pinned flat shallow stats key sets.
  • E2E Stripe Subscriptions Method Spec (apps/web-e2e/tests/api/stripe-subscriptions-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe collection-level subscriptions (plural) GET + POST + PUT + DELETE body / header smoke spec paired with apps/web-e2e/tests/api/stripe-subscriptions-method.spec.ts, the one-hundred-and-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-second under apps/web-e2e/tests/api/. Pairs with the GET, POST, PUT, AND DELETE exports of apps/web/app/api/stripe/subscriptions/route.ts — the first per-source-file QUAD-method smoke the docs tree publishes (every prior smoke covers 1, 2, or 3 methods). Distinct from the singular sibling at /api/stripe/subscription: PROPER USER-SCOPED IDOR on PUT AND DELETE -- subscription.userId !== session.user.id → 404 'Subscription not found' (CONTRAST with stripe-subscription-method-spec.md which has NO IDOR -- Q-010 finding -- this plural sibling does it correctly). Distinct from EVERY prior method-method smoke: FOUR-method export (GET + POST + PUT + DELETE -- UNIQUE: FIRST per-source-file QUAD-method smoke); GET conditional response shape (?active=true returns { data, plan, limits }; default returns { data, history, meta } -- UNIQUE: FIRST per-source-file GET smoke pinning a conditional response shape based on query); POST returns 201 status (NOT 200 -- UNIQUE among Stripe POST smokes); POST 409 Conflict for existing active subscription (UNIQUE: FIRST per-source-file POST smoke pinning a 409 Conflict status code); query-string DELETE -- DELETE uses query parameters (?id=&reason=&cancelAtPeriodEnd=) NOT body (UNIQUE: FIRST per-source-file DELETE smoke pinning a query-driven mutating DELETE -- vs body-driven DELETE in every other sibling); DYNAMIC success message on DELETE based on ?cancelAtPeriodEnd=true flag; bare ONE-key 401 envelope consistent across ALL FOUR methods; three-field required-check on POST with comma-joined-field-list 400 message 'Missing required fields: planId, paymentProvider, subscriptionId' (UNIQUE: FIRST per-source-file POST smoke pinning a comma-joined-field-list 400 message). The handlers combine: GET handler with auth(), query parsing (?active=true, ?history=true), branch on activeOnly vs all, success returns conditional shape; POST handler with auth(), JSON body parse, THREE-required-field check, hasActiveSubscription check → 409, createSubscription(...) load-bearing call, success returns 201 with { data, message: 'Subscription created successfully' }; PUT handler with auth(), JSON body parse, !subscriptionId → 400, getSubscriptionById + USER-SCOPED IDOR (!subscription || subscription.userId !== session.user.id → 404), updateSubscription(...) load-bearing call, success returns { data, message: 'Subscription updated successfully' }; DELETE handler with auth(), query parsing, !id → 400, getSubscriptionById + USER-SCOPED IDOR → 404, cancelSubscription(...) load-bearing call, success returns DYNAMIC message based on cancelAtPeriodEnd flag; method-resolution surface where the route exports GET + POST + PUT + DELETE (PATCH must round-trip to < 500). Documents the at-a-glance scenario tree (four header bulk-loop walks -- ~6 headers × 4 methods asserting < 500; canonical bare ONE-key 401-envelope assertions on GET AND POST; a cross-method 401-envelope-equality assertion across all four methods; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear including dynamic success messages and the 409 conflict message; an updateSubscription-not-entered invariance walk on PUT -- CRITICAL: pinning that XSS markers in the PUT body are NEVER echoed back; a cancelSubscription-not-entered invariance walk on DELETE with query-string ID -- CRITICAL: pinning that the query-string id is NEVER echoed back; a cross-query invariance walk on GET pinning that different ?active= / ?history= query permutations produce IDENTICAL unauth envelopes; a cross-method probe (PATCH); a side-channel walk on POST; a required-field-check-not-entered invariance walk on POST pinning that even empty-body POST returns 401 NOT 400). Cross-references the singular sibling stripe-subscription-method-spec.md (NO IDOR -- Q-010 finding -- this plural sibling DOES have proper user-scoped IDOR), the per-id update sibling stripe-subscription-id-update-body-spec.md (different IDOR pattern via getSubscriptionByProviderSubscriptionId), the per-id cancel sibling stripe-subscription-id-cancel-body-spec.md (uses POST verb -- not DELETE), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 104-of-N and the tests/api/ per-spec-file sub-rollout extends to 102-of-many, and the first per-source-file QUAD-method smoke lands -- pinning a conditional-GET-response-shape contract, a 201-status POST contract, a 409-Conflict contract, a query-driven DELETE contract, a dynamic-success-message-on-DELETE contract, and a comma-joined-field-list 400 message contract that no prior multi-method smoke covers.
  • E2E Stripe Subscription Method Spec (apps/web-e2e/tests/api/stripe-subscription-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe collection-level subscription POST + PUT + DELETE body / header smoke spec paired with apps/web-e2e/tests/api/stripe-subscription-method.spec.ts, the one-hundred-and-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundred-and-first under apps/web-e2e/tests/api/. Pairs with the POST, PUT, AND DELETE exports of apps/web/app/api/stripe/subscription/route.ts — the first per-source-file triple-method smoke the docs tree publishes for a Stripe subscription-management endpoint that documents a Q-010-style NO-IDOR finding on PUT AND DELETE -- the handlers authenticate the session but DO NOT verify that the subscriptionId from the body actually belongs to the calling user. ANY authenticated user can update or cancel ANY Stripe subscription by ID, bypassing the IDOR checks of the per-id siblings ([subscriptionId]/update, [subscriptionId]/cancel). Distinct from EVERY prior triple-method smoke: NO IDOR check on PUT or DELETE (UNIQUE -- FIRST per-source-file triple-method smoke pinning a Q-010-style NO-IDOR finding on mutating subscriptionId-keyed methods); different body-required field on POST vs PUT/DELETE -- POST requires priceId + paymentMethodId; PUT requires subscriptionId; DELETE requires subscriptionId (UNIQUE: FIRST per-source-file triple-method smoke pinning three DIFFERENT required-field shapes on the three methods); POST 400 'Failed to create customer' branch (UNIQUE -- only POST has the !customerId check; PUT and DELETE skip the customer-id resolution); returns RAW Stripe subscription object verbatim on ALL THREE methods (UNIQUE: no wrapper envelope on success); metadata: { userId: session.user.id } OVERWRITE on PUT (UNIQUE -- PUT writes the CALLER'S userId into the subscription's metadata regardless of who actually owns the subscription -- compounds the Q-010 finding by enabling ownership-record laundering); bare ONE-key 401 envelope { error: 'Unauthorized' } consistent across all three methods. The handlers combine: POST handler with !session?.user → 401, JSON body parse, getOrCreateStripeProvider, getCustomerId(session.user), !customerId → 400 'Failed to create customer', createSubscription(...) load-bearing call, success returns raw Stripe subscription object; PUT handler with !session?.user → 401, JSON body parse, getOrCreateStripeProvider, updateSubscription({ subscriptionId, priceId, cancelAtPeriodEnd, metadata }) -- NO IDOR CHECK, success returns raw Stripe subscription object; DELETE handler with !session?.user → 401, JSON body parse, getOrCreateStripeProvider, cancelSubscription(subscriptionId, cancelAtPeriodEnd) -- NO IDOR CHECK, success returns raw Stripe subscription object; method-resolution surface where the route exports POST + PUT + DELETE (GET / PATCH must round-trip to < 500). Documents the at-a-glance scenario tree (three header bulk-loop walks -- ~6 headers × 3 methods asserting < 500; canonical bare ONE-key 401-envelope assertions on POST, PUT, AND DELETE; a cross-method 401-envelope-equality assertion pinning byte-identical envelopes across all three methods; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear; an updateSubscription-not-entered invariance walk on PUT -- CRITICAL: even though PUT has NO IDOR check post-auth, the auth gate itself must fire BEFORE updateSubscription (no XSS-marker leak); a cancelSubscription-not-entered invariance walk on DELETE -- CRITICAL: same gate-before invariant; a cross-method probe (GET / PATCH); a side-channel walk on POST). Cross-references the per-id update sibling stripe-subscription-id-update-body-spec.md (DOES enforce a user-scoped IDOR check -- the proper way to update a subscription), the per-id cancel sibling stripe-subscription-id-cancel-body-spec.md (NO IDOR -- already a known finding; this collection-level route extends the no-IDOR surface to PUT), the per-id reactivate sibling stripe-subscription-id-reactivate-body-spec.md (tenant-only IDOR), the Stripe billing-portal sibling stripe-subscription-portal-body-spec.md, docs/questions.md for the Q-### entry tracking the no-IDOR finding, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 103-of-N and the tests/api/ per-spec-file sub-rollout extends to 101-of-many, and the first per-source-file triple-method smoke pinning a Q-010-style NO-IDOR finding on PUT AND DELETE lands -- pinning a no-IDOR-on-mutating-method contract (compounding by metadata-userId overwrite), three-different-required-field-shapes contract on POST/PUT/DELETE, and a raw-Stripe-object success contract that no prior triple-method smoke covers.
  • E2E Client Items Stats Query Spec (apps/web-e2e/tests/api/client-items-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client items-stats GET header smoke spec paired with apps/web-e2e/tests/api/client-items-stats-query.spec.ts, the one-hundred-and-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the one-hundredth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/client/items/stats/route.ts — the first per-source-file GET smoke the docs tree publishes that pins the requireClientAuth() helper-based auth gate with the 'Unauthorized. Please sign in to continue.' longer-message TWO-key envelope. UNIQUE: a different auth-helper abstraction than the bare auth() session lookup used in every other per-source-file smoke; uses the explicit client-auth utility helpers (requireClientAuth, serverErrorResponse). Distinct from EVERY prior GET smoke: requireClientAuth() helper-based auth gate (UNIQUE -- returns a discriminated union { success: false, response: NextResponse } on failure or { success: true, userId: string } on success -- FIRST per-source-file GET smoke pinning a discriminated-union auth-helper return contract); 'Unauthorized. Please sign in to continue.' 401 envelope message (UNIQUE -- longer specific message naming the action -- distinct from bare 'Unauthorized', 'Unauthorized. Admin access required.' admin-tree, and 'Authentication required' Stripe siblings); TWO-key 401 envelope { success: false, error: 'Unauthorized. Please sign in to continue.' }; TWO-key success payload { success: true, stats: <statsObject> } (UNIQUE: uses stats key NOT data like most success payloads); serverErrorResponse(error, 'Failed to fetch statistics') outer catch (UNIQUE helper distinct from safeErrorResponse); zero-arg GET signature (export async function GET() with NO request / context arguments). The GET handler combines requireClientAuth() discriminated-union auth-helper, getClientItemRepository() repository factory, the load-bearing clientItemRepository.getStatsByUser(userId) DB read, success payload { success: true, stats: { total, draft, pending, approved, rejected, deleted } }, outer catch serverErrorResponse(error, 'Failed to fetch statistics'), and method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk asserting < 500; a longer-message TWO-key 401-envelope assertion pinning success: false, error: 'Unauthorized. Please sign in to continue.'; a strict envelope-shape assertion via Object.keys(body).sort() === ['error', 'success']; a gate-before-post-auth invariant pinning that NEITHER 'Failed to fetch statistics' NOR the success-branch stats keys leak; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a clientItemRepository-getStatsByUser-not-entered invariance walk -- CRITICAL: pinning that the load-bearing DB read NEVER runs on unauth; a cross-permutation status invariance walk). Cross-references the companion client-dashboard-stats sibling client-dashboard-stats-query.spec.ts (another requireClientAuth-gated stats endpoint), the companion client-protected sibling client-protected.spec.ts, the companion client-geo-stats sibling client-geo-stats-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 102-of-N and the tests/api/ per-spec-file sub-rollout extends to 100-of-many (a centennial milestone for the tests/api/ sub-rollout), and the first per-source-file GET smoke pinning the requireClientAuth() helper-based auth-gate contract with a discriminated-union return type lands -- pinning a longer-message TWO-key 401 envelope, a stats-keyed success payload (vs data-keyed), and a serverErrorResponse error helper that no prior GET smoke covers.
  • E2E Payment Account [userId] Query Spec (apps/web-e2e/tests/api/payment-account-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-user payment-account-lookup GET dynamic-segment / query / header smoke spec paired with apps/web-e2e/tests/api/payment-account-id-query.spec.ts, the one-hundred-and-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-ninth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/payment/account/[userId]/route.ts — the first per-source-file dynamic-segment GET smoke the docs tree publishes that pins a strict user-id-IDOR check (session.user.id !== params.userId → 403 bare { error: 'Forbidden' } with NO message specifying ownership) on a per-user resource lookup endpoint. CRITICAL: provides the auth-gated counterpart to the payment-account-method-spec.md sibling which has NO auth gate (Q-010 finding) -- documenting a security-asymmetry where the GET on [userId] IS auth-gated while the POST/PUT on the parent route are NOT. Distinct from EVERY prior dynamic-segment GET smoke: strict user-id-IDOR check with bare 'Forbidden' message (UNIQUE -- distinct from payment-id-method's 'Forbidden: You do not own this subscription'); userId-then-IDOR-then-provider validation order (UNIQUE -- the IDOR check is INTERLEAVED between two validation checks, the FIRST per-source-file GET smoke pinning an IDOR check placed mid-validation-cascade); ?provider= query parameter required (consistent with the POST/PUT siblings' body-required check; this GET reads it from the query string); 404 with bare envelope { error: 'Payment account not found' } (UNIQUE: FIRST per-source-file GET smoke pinning a 404 with this literal message); returns raw paymentAccount fields in success (matches POST/PUT siblings); DOES have auth() gate (CONTRAST with the no-auth-gate POST/PUT siblings on the same parent route -- Q-010 finding -- the FIRST per-source-file GET smoke documenting an auth-gated GET sibling of an unguarded POST/PUT pair). The GET handler combines auth() session lookup (!session?.user?.id → 401 bare ONE-key), { userId } = await params dynamic-segment resolution, searchParams.get('provider') query extraction, !userId 400 check (impossible from dynamic segment but pinned), IDOR check (session.user.id !== userId → 403 bare { error: 'Forbidden' }), !provider 400 check, the load-bearing getUserPaymentAccountByProvider(userId, provider) DB read, 404 if null, success payload as raw paymentAccount fields, outer catch 500, and method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk + a ~8-query bulk-loop walk all asserting < 500; a canonical bare ONE-key 401-envelope assertion; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear including 'Forbidden', 'Provider is required', 'Payment account not found'; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a getUserPaymentAccountByProvider-not-entered invariance walk -- CRITICAL: pinning that the load-bearing DB read NEVER runs on unauth and no providerId / customerId / createdAt is leaked; a cross-userId invariance walk pinning that different user IDs produce IDENTICAL unauth envelopes -- the auth gate fires BEFORE the IDOR check; a cross-provider invariance walk pinning that different ?provider= values produce IDENTICAL unauth envelopes). Cross-references the companion POST + PUT sibling payment-account-method-spec.md (NO auth gate -- Q-010 finding -- this GET sibling IS auth-gated and does have an IDOR check), the companion subscription-update payment sibling payment-id-method-spec.md (uses a different IDOR message 'Forbidden: You do not own this subscription'), docs/questions.md for the Q-### entry tracking the security-asymmetry finding (auth-gated GET vs no-auth-gate POST/PUT on the same parent route), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 101-of-N and the tests/api/ per-spec-file sub-rollout extends to 99-of-many, and the first per-source-file dynamic-segment GET smoke pinning an IDOR-mid-validation-cascade contract lands -- pinning a strict user-id-IDOR check with bare 'Forbidden' message, an interleaved validation-order contract, and a security-asymmetry finding (auth-gated GET vs no-auth-gate POST/PUT on the same parent) that no prior dynamic-segment smoke covers; completing the payment/account triplet (POST + PUT no-auth + GET-by-userId auth-gated).
  • E2E Payment Account Method Spec (apps/web-e2e/tests/api/payment-account-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's payment-account create / update POST + PUT body / header smoke spec paired with apps/web-e2e/tests/api/payment-account-method.spec.ts, the one-hundredth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-eighth under apps/web-e2e/tests/api/. Pairs with the POST AND PUT exports of apps/web/app/api/payment/account/route.ts — the first per-source-file dual-method smoke the docs tree publishes that documents a Q-010-style NO-AUTH-GATE finding for a non-admin mutating route on BOTH POST AND PUT exports — the handler has NO auth() call, NO ownership check; ANY caller can create a payment account for ANY userId + customerId (POST) OR update any payment account by id (PUT). The smoke spec pins this finding as the CURRENT contract -- a future PR that adds auth would explicitly break this spec, prompting an update. Distinct from EVERY prior dual-method smoke: NO auth() gate on EITHER method (FIRST per-source-file dual-method smoke pinning a Q-010-style no-auth-gate finding on BOTH POST AND PUT exports); NO ownership check (POST trusts the caller-supplied userId + customerId directly; PUT trusts the caller-supplied id); setupUserPaymentAccount(provider, userId, customerId) runs UNCONDITIONALLY on both POST and PUT (UNIQUE: PUT does NOT check that the id matches an existing record; it just calls setupUserPaymentAccount with the body fields -- effectively the same logic as POST plus an id gate); THREE-required-field cascade on POST (provider, userId, customerId) and FOUR-required-field cascade on PUT (id, provider, userId, customerId) with each emitting a distinct 400 message via individual if (!field) checks (UNIQUE: FIRST per-source-file dual-method smoke pinning a per-field individual-required-check chain); bare ONE-key 400 envelope { error: 'Field X is required' } (NO success key); bare ONE-key 500 envelope { error: 'Internal server error' } (NO success key); returns raw paymentAccount fields in success payload { id, userId, providerId, customerId, createdAt, updatedAt } (NO wrapper envelope -- UNIQUE: most success responses use { success: true, data: {...} }). The handlers combine: POST handler with NO auth gate, JSON body parse, required-field cascade (provider → userId → customerId, each individually checked with distinct 400 message), load-bearing setupUserPaymentAccount(...) UNCONDITIONAL DB write, success payload as raw paymentAccount fields, outer catch 500 'Internal server error'; PUT handler with NO auth gate, JSON body parse, required-field cascade (id → provider → userId → customerId, each individually checked with distinct 400 message), load-bearing setupUserPaymentAccount(...) UNCONDITIONAL DB write (NOT an actual update by id), success payload as raw paymentAccount fields, outer catch 500 'Internal server error'; method-resolution surface where the route exports POST AND PUT (GET / PATCH / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (four bulk-loop walks -- ~6 headers × 2 methods + ~7 POST bodies + ~6 PUT bodies all asserting < 500; a NO-401 contract assertion on BOTH POST and PUT pinning that the response is NEVER 401 nor 403; an auth-signal-ignored contract walk pinning that fabricated session cookies / Authorization headers / X-User-Id headers produce the SAME status as bare requests; a required-field cascade canonical-messages assertion pinning the three distinct 400 messages on POST -- 'Provider is required', 'User ID is required', 'Customer ID is required'; a PUT FOUR-required-field cascade assertion pinning that 'Account ID is required' fires FIRST when all fields are missing; a strict ONE-key 400 envelope-shape assertion; a cross-method probe (GET / PATCH / DELETE); a no-catch-on-valid-body contract pinning that valid-shape requests do NOT 5xx). Cross-references the companion subscription-update payment sibling payment-id-method-spec.md (DOES enforce auth + ownership -- distinct from this no-auth-gate sibling), the provider-specific payment-methods sibling stripe-payment-methods-create-body-spec.md (auth-gated), docs/questions.md for the Q-### entry tracking the no-auth-gate finding, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 100-of-N (a centennial milestone) and the tests/api/ per-spec-file sub-rollout extends to 98-of-many, and the first Q-010-style no-auth-gate finding for a dual-method (POST + PUT) non-admin mutating route lands -- pinning the no-auth-gate contract on BOTH methods, a per-field individual-required-check cascade contract, and a raw-paymentAccount-fields success-payload contract that no prior dual-method smoke covers.
  • E2E Payment [subscriptionId] Method Spec (apps/web-e2e/tests/api/payment-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-subscription auto-renewal GET + PATCH dynamic-segment / body / header smoke spec paired with apps/web-e2e/tests/api/payment-id-method.spec.ts, the ninety-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-seventh under apps/web-e2e/tests/api/. Pairs with the GET AND PATCH exports of apps/web/app/api/payment/[subscriptionId]/route.ts — the first per-source-file dual-method smoke the docs tree publishes that pins a provider-agnostic auto-renewal toggle (the handler accepts provider / paymentProvider values from the PaymentProvider enum and routes the sync via getOrCreateProvider(provider), working with Stripe, LemonSqueezy, Polar, Solidgate). Distinct from EVERY prior dual-method smoke: provider-agnostic dual-method (FIRST per-source-file smoke pinning a getOrCreateProvider(provider) dispatch contract on a per-subscription endpoint -- vs the per-provider Stripe / LemonSqueezy / Polar siblings which hardcode their provider); provider-source split -- GET reads provider from the QUERY STRING (?provider=), PATCH reads paymentProvider from the BODY (UNIQUE: FIRST per-source-file dual-method smoke pinning a SAME-NAMED-FIELD-from-DIFFERENT-SOURCES contract); dynamic enum-validation 400 message -- 'Invalid payment provider. Must be one of: stripe, lemonsqueezy, polar, solidgate' (UNIQUE: FIRST per-source-file smoke pinning a 400 message that DYNAMICALLY lists valid enum values via validProviders.join(', ')); TWO distinct body-validation 400 messages on PATCH -- 'Invalid JSON in request body' (catch around await request.json()) vs 'Invalid request body. Expected a JSON object.' (post-parse non-object check) -- FIRST per-source-file PATCH smoke pinning a two-tier body-validation chain; explicit typeof enabled !== 'boolean' type-check -- 'Invalid request body. "enabled" must be a boolean.' (UNIQUE: pre-Zod boolean type-validation, vs Zod's opaque error messages); user-scoped IDOR with explicit message -- 'Forbidden: You do not own this subscription' (UNIQUE: FIRST per-source-file smoke pinning a user-scoped 403 message that names ownership); best-effort provider sync (if the provider sync call throws, the local DB update is preserved -- handler logs and returns success; UNIQUE: FIRST per-source-file PATCH smoke pinning a best-effort provider sync after a successful local DB write); dynamic success message (response message field is one of TWO distinct strings based on the enabled toggle -- 'Auto-renewal has been enabled...' / 'disabled...'). The handlers combine GET handler with auth() session lookup (!session?.user?.id → 401 ONE-key bare envelope), { subscriptionId } = await params, searchParams.get('provider') || 'stripe' provider extraction, enum validation, subscription lookup, user-scoped IDOR check (subscription.userId !== session.user.id → 403), success payload { subscriptionId, autoRenewal, cancelAtPeriodEnd, endDate }; PATCH handler with auth() session lookup, JSON body parse with try/catch (400 on malformed), non-object check (400 on array / null), typeof-enabled boolean check (400 on non-bool), enum validation, subscription lookup with same user-scoped IDOR, subscriptionService.setAutoRenewal(subscription.id, enabled) load-bearing DB write, best-effort provider-sync via getOrCreateProvider(paymentProvider).updateSubscription(...), success payload { success: true, subscription: <updatedSubscription>, message: <dynamic> }; method-resolution surface where the route exports GET AND PATCH (POST / PUT / DELETE must round-trip to < 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~6 headers × 2 methods + ~13 PATCH bodies all asserting < 500; canonical bare ONE-key 401-envelope assertions on GET AND PATCH; a cross-method 401-envelope-equality assertion pinning byte-identical envelopes; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of nine candidate messages must appear including the dynamic success messages; a setAutoRenewal-not-entered invariance walk on PATCH -- CRITICAL: pinning that XSS markers in the body are NEVER echoed back; a gate-before-enum-validation invariance walk on GET pinning that ?provider=fake-provider does NOT trigger the dynamic 400 message on unauth; a cross-method probe (POST / PUT / DELETE); a side-channel walk on PATCH; a gate-before-body-validation invariance walk pinning that malformed JSON / array body / non-bool enabled all produce 401 NOT 400; a cross-subscription-ID invariance walk pinning that different IDs produce IDENTICAL unauth envelopes -- the auth gate fires BEFORE any per-subscription-id branch). Cross-references the Stripe-specific subscription-update sibling stripe-subscription-id-update-body-spec.md (hardcodes the Stripe provider vs this provider-agnostic dispatch), the Stripe-specific subscription-cancel sibling stripe-subscription-id-cancel-body-spec.md, the Stripe-specific subscription-reactivate sibling stripe-subscription-id-reactivate-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 99-of-N and the tests/api/ per-spec-file sub-rollout extends to 97-of-many, and the first per-source-file dual-method smoke pinning a provider-agnostic auto-renewal toggle lands -- pinning a getOrCreateProvider dispatch contract, a SAME-NAMED-FIELD-from-DIFFERENT-SOURCES contract (provider in query for GET, paymentProvider in body for PATCH), a dynamic enum-validation 400 message contract, a two-tier body-validation chain contract, an explicit boolean type-check contract, and a best-effort provider-sync contract that no prior dual-method smoke covers.
  • E2E Verify ReCAPTCHA Body Spec (apps/web-e2e/tests/api/verify-recaptcha-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's ReCAPTCHA verification proxy POST body / header smoke spec paired with apps/web-e2e/tests/api/verify-recaptcha-body.spec.ts, the ninety-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-sixth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/verify-recaptcha/route.ts — the first per-source-file smoke the docs tree publishes that pins a dev-mode bypass envelope with status 200 { success: true, score: 1.0, action: 'bypass' } (when RECAPTCHA_SECRET_KEY is missing AND NODE_ENV === 'development'). It is also the first smoke the docs tree publishes that pins a route built on top of the externalClient.postForm<T>(url, body) helper (form-encoded outbound POST to Google's https://www.google.com/recaptcha/api/siteverify endpoint) AND the first smoke that pins the error_codes underscore-rename invariant (Google returns error-codes with a hyphen; the handler renames it to error_codes with an underscore in the response envelope). The cross-cutting method-guards.spec.ts ALSO probes POST /api/verify-recaptcha BUT only checks that an empty-object body produces a non-5xx response; this per-source-file spec drills into the four-branch dispatcher (token-required-400 / dev-bypass-200 / not-configured-500 / Google-proxy-pass-through). Distinct from EVERY prior POST smoke: form-encoded outbound POST via externalClient.postForm (UNIQUE -- every other proxy POST in the docs tree uses fetch / externalClient.post JSON body); error_codes underscore-rename invariant (UNIQUE -- no other proxy in the docs tree performs this hyphen-to-underscore rename); score / action surface (UNIQUE -- no other smoke exercises ReCAPTCHA scoring fields); dev-mode bypass branch (UNIQUE -- NO other smoke pins a 200 dev-bypass envelope with action: 'bypass'); not-configured 500 branch (status 500 with NO stack trace / sensitive content). The POST handler combines a JSON body parse via await request.json() (wrapped in outer try / catch so malformed JSON falls through to the 500 catch), an if (!token) token-required gate (→ 400 { success: false, error: 'ReCAPTCHA token is required' }), an !secretKey dev-bypass / not-configured branch (bifurcates on coreConfig.NODE_ENV === 'development'), an externalClient.postForm Google siteverify proxy (secret: secretKey, response: token form-encoded body), an apiUtils.isSuccess(response) check (on failure → 500 { success: false, error: 'Failed to verify ReCAPTCHA' }), a renamed-envelope success pass-through ({ success: data.success, score: data.score, action: data.action, hostname: data.hostname, challenge_ts: data.challenge_ts, error_codes: data['error-codes'] } -- HYPHEN → UNDERSCORE rename of error-codes), an outer catch (→ 500 { success: false, error: 'Verification failed' }), and method-resolution surface where the route exports ONLY POST (GET / PUT / PATCH / DELETE must round-trip to a < 500 status). Documents the at-a-glance scenario tree (a ~12-header bulk-loop walk + a ~16-body bulk-loop walk asserting non-crashing status; the load-bearing 400 token-required envelope assertion { success: false, error: 'ReCAPTCHA token is required' }; a strict envelope-shape assertion with NO featureDisabled key -- DIFFERENT from extract-body sibling; a falsy-token uniformity assertion; a no-token-echo invariant; a side-channel walk; a cross-method probe; a bypass-attempt-body-keys invariance assertion -- user-supplied RECAPTCHA_SECRET_KEY / secret / NODE_ENV body fields are IGNORED; an env-driven dispatch invariant assertion -- response MUST match exactly ONE of dev-bypass / not-configured / Google-proxy envelopes; the HYPHEN → UNDERSCORE rename invariant assertion; a 500-envelope no-leak assertion -- CRITICAL: NEVER leaks stack / cause / RECAPTCHA_SECRET_KEY / secretKey / siteverify / google.com fragments; an outer-catch malformed-JSON fallback assertion; a truthy non-string token gate-semantics assertion; a gate-before-post-validation order assertion). Cross-references the cross-cutting method-guards.spec.ts, the neighbouring extract-body-spec.md (also covers a POST-only proxy endpoint BUT uses Zod validation AND a featureDisabled envelope -- 200 status, featureDisabled: true key; this verify-recaptcha sibling uses hand-rolled if (!token) validation AND a error: 'ReCAPTCHA token is required' envelope -- 400 status, NO featureDisabled key), and to Spec 010 -- E2E Test Coverage for the governing spec and Spec 008 -- Analytics Providers for the analytics-providers spec the verify-recaptcha route sits inside. With this entry the per-spec-file docs rollout extends to 98-of-N and the tests/api/ per-spec-file sub-rollout extends to 96-of-many, and the first per-source-file POST smoke pinning a dev-mode bypass envelope + form-encoded outbound POST + error_codes underscore-rename invariant lands -- pinning a four-branch env-driven dispatcher contract, a Google-siteverify proxy contract, and a hyphen-to-underscore rename invariant that no prior POST smoke covers.
  • E2E Cron Subscription Reminders Method Spec (apps/web-e2e/tests/api/cron-subscription-reminders-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's subscription-reminders cron GET + POST header smoke spec paired with apps/web-e2e/tests/api/cron-subscription-reminders-method.spec.ts, the ninety-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-fifth under apps/web-e2e/tests/api/. Pairs with the GET AND POST exports of apps/web/app/api/cron/subscription-reminders/route.ts — the first per-source-file smoke the docs tree publishes that pins a 207 Multi-Status partial-success response (the handler returns 207 when result.success === false). The existing multi-cron sibling cron-jobs.spec.ts covers OTHER cron routes; this spec drills into the subscription-reminders handler specifically. Distinct from EVERY prior cron smoke: timing-safe comparison on the FULL Authorization header -- Buffer.from(authHeader) is compared to Buffer.from(\Bearer ${cronSecret}`)(UNIQUE: every other cron handler compares ONLY the secret portion after strippingBearer ); **BARE ONE-key 401 envelope** { error: 'Unauthorized' }(NOsuccesskey, NOmessagefield -- DIFFERENT from the cron-subscription-expiration sibling's TWO-key envelope); **207 Multi-Status response** (UNIQUE -- the FIRST per-source-file smoke pinning a 207 partial-success status code); **spread-result success / error pattern** (both branches spread the entire result object into the response --{ message: 'Subscription reminder job completed', ...result }and{ error: 'Job completed with errors', ...result }-- UNIQUE: distinct from subscription-expiration which constructs an explicitdataenvelope); **GET + POST dual-method-delegate exports** (POST simply doesreturn GET(request), matches subscription-expiration sibling); **outer catch via safeErrorResponse(error, 'Cron job failed')** (distinct message vs subscription-expiration's 'Failed to process expired subscriptions'). The handler combines the verifyCronSecret(request)helper (Bearer-token check with timing-safe comparison on the FULL authHeader), the dev-mode short-circuit, the load-bearingsubscriptionRenewalReminderJob() reminder-job call, the conditional 207 branch (if (!result.success)→ 207 Multi-Status with spread-result envelope), success payload{ message: 'Subscription reminder job completed', ...result }with status 200, outer catch withsafeErrorResponse(error, 'Cron job failed'), and method-resolution surface where the route exports GET AND POST (POST delegates to GET; PUT / PATCH / DELETE must round-trip to a < 500status). Documents the at-a-glance scenario tree (two header bulk-loop walks -- ~9 headers × 2 methods asserting< 500; a BARE ONE-key 401 envelope assertion when no Authorization header is present pinning error: 'Unauthorized'with NOsuccess/messagekeys; a strict envelope-shape assertion when 401 is reached; a no-Bearer-secret-echo invariant; a timing-safe length-mismatch handling assertion on the FULL header pinning that BOTH a too-short AND a too-long Authorization header produce< 500; a POST-delegates-to-GET assertion pinning byte-identical envelopes on POST and GET unauth branches; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk; a subscriptionRenewalReminderJob-not-entered invariance walk -- CRITICAL: the load-bearing reminder-job call NEVER runs on unauth and no spread-result key like processed/reminded is leaked; a gate-before-post-auth invariant; a no-207-on-unauth invariant pinning that the 207 partial-success status code is NEVER reached on the unauth branch). Cross-references the subscription-expiration cron sibling [cron-subscription-expiration-method-spec.md](./plugins/cron-subscription-expiration-method-spec.md) (uses ALSO timing-safe comparison BUT compares ONLY the secret portion after Bearer stripped and emits a TWO-key 401 envelope; this spec compares the FULLAuthorization header and emits a BARE ONE-key envelope), the cron/sync GET sibling [cron-sync-query-spec.md](./plugins/cron-sync-query-spec.md) (uses a DIFFERENT cron-auth contract -- exact string match, NOT timing-safe), the multi-cron sibling [cron-jobs.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/cron-jobs.spec.ts), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) for the governing spec. With this entry the **per-spec-file docs rollout extends to 97-of-N** and the **tests/api/` per-spec-file sub-rollout extends to 95-of-many**, and the first per-source-file smoke pinning a 207 Multi-Status partial-success response + timing-safe comparison on the FULL Authorization header + BARE ONE-key cron 401 envelope lands -- pinning a partial-success status-code contract, a full-header constant-time comparison contract, and a spread-result success/error envelope pattern that no prior cron smoke covers; completing the cron triplet (sync + expiration + reminders) on per-source-file coverage.
  • E2E Cron Subscription Expiration Method Spec (apps/web-e2e/tests/api/cron-subscription-expiration-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's subscription-expiration cron GET + POST header smoke spec paired with apps/web-e2e/tests/api/cron-subscription-expiration-method.spec.ts, the ninety-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-fourth under apps/web-e2e/tests/api/. Pairs with the GET AND POST exports of apps/web/app/api/cron/subscription-expiration/route.ts — the first per-source-file smoke the docs tree publishes that pins a timing-safe Bearer-token comparison via crypto.timingSafeEqual. The existing multi-cron sibling cron-jobs.spec.ts covers the OTHER cron routes; this spec drills into the subscription-expiration handler specifically AND its GET + POST dual-method-delegate export pattern. Distinct from EVERY prior cron smoke: timing-safe Bearer-token comparison via crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(cronSecret)) -- the FIRST per-source-file smoke pinning a constant-time comparison contract on a Bearer-token-gated endpoint; length-equality short-circuit -- providedSecret.length !== cronSecret.length → false (avoids timingSafeEqual length-mismatch throw -- UNIQUE); authHeader.replace('Bearer ', '') parsing -- extracts the token via .replace(...) rather than exact-match comparison like the cron/sync sibling; TWO-key 401 envelope { success: false, message: 'Unauthorized - Invalid or missing cron secret' } (UNIQUE: longer specific message naming the failure mode -- vs cron/sync's 'Unauthorized' -- uses message not error); GET + POST dual-method-delegate exports -- POST simply does return GET(request) (UNIQUE: FIRST per-source-file smoke pinning a method-delegate POST that re-routes to GET verbatim); email-service best-effort side-effect -- if the email service is unavailable, the cron does NOT fail; PII-stripped affectedUsers -- the response includes { subscriptionId, userId, planId } per affected user but NEVER email (intentional PII protection). The handler combines the verifyCronSecret(request) helper (Bearer-token check with timing-safe comparison), the dev-mode short-circuit (if (!cronSecret && process.env.NODE_ENV === 'development') → bypass), the load-bearing subscriptionService.processExpiredSubscriptions() DB-write call, the PII-strip transformation, the email-service side-effect (best-effort, does NOT fail), success payload { success: true, message: 'Processed X expired subscriptions', data: { processed, affectedUsers, errors, timestamp } } with status 200, outer catch with safeErrorResponse(error, 'Failed to process expired subscriptions'), and method-resolution surface where the route exports GET AND POST (POST delegates to GET; PUT / PATCH / DELETE must round-trip to a < 500 status). Documents the at-a-glance scenario tree (two header bulk-loop walks -- ~9 headers × 2 methods including various Authorization probes -- wrong Bearer, empty Bearer, non-Bearer scheme, Basic auth, Bearer with double-space -- plus side-channels asserting < 500; a TWO-key 401 envelope assertion when no Authorization header is present pinning success: false, message: 'Unauthorized - Invalid or missing cron secret', and NO error key; a strict envelope-shape assertion via Object.keys(body).sort() when 401 is reached; a no-Bearer-secret-echo invariant; a timing-safe length-mismatch handling assertion pinning that BOTH a too-short AND a too-long Bearer token produce < 500; a POST-delegates-to-GET assertion pinning that POST returns the SAME envelope as GET on the unauth branch; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk; a processExpiredSubscriptions-not-entered invariance walk -- CRITICAL: the load-bearing DB-write call NEVER runs on unauth and no affectedUsers / processed / subscriptionId is leaked; a gate-before-post-auth invariant). Cross-references the cron/sync GET sibling cron-sync-query-spec.md (uses a DIFFERENT cron-auth contract -- exact Bearer ${CRON_SECRET} string match, NOT timing-safe; cron/sync emits a 4-key 401 envelope vs this spec's TWO-key envelope), the multi-cron sibling cron-jobs.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 96-of-N and the tests/api/ per-spec-file sub-rollout extends to 94-of-many, and the first per-source-file smoke pinning a timing-safe Bearer-token comparison + GET + POST dual-method-delegate lands -- pinning the constant-time comparison contract, the length-equality short-circuit contract, and the POST-delegates-to-GET contract that no prior smoke covers.
  • E2E Subscription Query Spec (apps/web-e2e/tests/api/subscription-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's authenticated user-subscription GET query-param smoke spec paired with apps/web-e2e/tests/api/subscription-query.spec.ts, the ninety-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-third under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/user/subscription/route.ts — the first per-source-file GET smoke the docs tree publishes that pins an OBJECT-wrapped success response for a Stripe-customer-session endpoint where the no-customer-found branch ALSO returns an OBJECT (with hasActiveSubscription: false + a message field). UNIQUE — the direct sibling user-payments-query.spec.ts returns a top-level ARRAY for the same Stripe-customer-session pattern; together the two specs pin the divergence between the two response shapes that share an identical auth-gate + customer-resolution prologue. Distinct from EVERY prior session-gated GET smoke: OBJECT-wrapped success response (UNIQUE -- the FIRST per-source-file GET smoke pinning an OBJECT-wrapped Stripe-customer-derived response where the no-customer-found 200 branch ALSO returns an OBJECT, vs the empty-array fallback in the user-payments sibling); no-customer-found 200 OBJECT { hasActiveSubscription: false, message: 'No Stripe customer found' } (distinct from the user-payments sibling's [] empty-array fallback -- same auth() + getCustomerId(...) prologue, different fallback shape); bare ONE-key { error: 'Unauthorized' } 401 envelope (NO success key, NO message key -- same shape as user-payments sibling); two-tier 500 catch dispatcher -- inner Stripe-error → 500 'Failed to fetch subscription data from Stripe'; outer → 500 'Failed to fetch subscription data' (matches the user-payments sibling's two-tier dispatcher pattern but with subscription-specific messages); zero-arg GET signature (export async function GET() with NO request / context arguments -- matches user-payments sibling); Stripe Subscriptions list with expand: ['data.default_payment_method'] (UNIQUE -- the FIRST per-source-file GET smoke pinning a Stripe expansion-list invariant; the user-payments sibling pins a DUAL-list invoices.list + subscriptions.list but neither is expanded); active-subscription discriminator -- sub.status === 'active' || sub.status === 'trialing' isolates a currentSubscription from history (UNIQUE -- the FIRST per-source-file GET smoke pinning an active-or-trialing-only current-subscription discriminator); cents-to-major-units transform -- every amount is divided by 100 to convert from Stripe's cents representation to major currency units (UNIQUE -- the FIRST per-source-file GET smoke pinning a cents-to-major-units transform on the success branch); currency uppercase invariant -- sub.currency.toUpperCase() is applied to every subscription (UNIQUE -- the FIRST per-source-file GET smoke pinning a currency-case-normalisation invariant); caller-supplied-Stripe-key bypass attempt walked -- the spec walks ?stripeKey= / ?stripe_key= query parameters to pin that the handler does NOT forward a caller-supplied Stripe API key (CRITICAL -- prevents a future regression that would let a caller substitute their own Stripe key and read another customer's data). The GET handler combines auth() session lookup (!session?.user?.id → 401 ONE-key { error: 'Unauthorized' }), initializeStripeProvider() + getStripeInstance() AFTER the auth gate, the load-bearing customer-id lookup stripeProvider.getCustomerId(session.user) (null → 200 OBJECT { hasActiveSubscription: false, message: 'No Stripe customer found' }), the load-bearing stripe.subscriptions.list({ customer, limit: 100, expand: ['data.default_payment_method'] }) Stripe SDK call, the active-subscription discriminator, the transform (cents-to-major-units, currency-uppercase, ISO-format timestamps), inner catch 500 'Failed to fetch subscription data from Stripe', outer catch 500 'Failed to fetch subscription data', and method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to a < 500 status). Documents the at-a-glance scenario tree (a query-string bulk-loop walk over many parameter permutations -- no-arg baseline, admin-impersonation candidates ?userId= / ?customerId= / ?stripeCustomerId=, magic-token candidates ?token=, dangerous-passthrough candidates ?stripeKey=, status-claim candidates ?status=active, plus structural permutations -- all asserting < 500; a canonical ONE-key 401-envelope assertion on the unauth branch; a cross-query envelope-byte-equality assertion pinning that the 401 envelope is byte-identical across query permutations; a ?userId= / ?customerId= / ?stripeCustomerId= walk pinning that no admin-impersonation key short-circuits the session gate or customer-resolution step (CRITICAL); a ?stripeKey= / ?stripe_key= walk pinning that no caller-supplied Stripe key is forwarded to the Stripe SDK (CRITICAL); a ?token= walk pinning that no magic-token query parameter short-circuits the session gate (CRITICAL); a cross-permutation status-and-shape invariance assertion pinning that every query permutation produces the same status and the same envelope shape). Cross-references the companion payment-history GET sibling user-payments-query.spec.ts (SAME pattern but ARRAY-wrapped response), the Stripe billing-portal POST sibling stripe-subscription-portal-body-spec.md (SAME auth() + Stripe customer lookup but on a different HTTP method), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 95-of-N and the tests/api/ per-spec-file sub-rollout extends to 93-of-many, and the first per-source-file GET smoke pinning an OBJECT-wrapped Stripe-customer-derived response lands -- pinning a no-customer-found 200 OBJECT fallback contract (vs the user-payments sibling's empty-array fallback), a Stripe expansion-list invariant, an active-or-trialing-only current-subscription discriminator, a cents-to-major-units transform invariant, a currency-uppercase invariant, and a caller-supplied-Stripe-key forwarding-prevention invariant that no prior GET smoke covers.
  • E2E Favorites [itemSlug] Method Spec (apps/web-e2e/tests/api/favorites-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-favorite remove DELETE dynamic-segment / header smoke spec paired with apps/web-e2e/tests/api/favorites-id-method.spec.ts, the ninety-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-second under apps/web-e2e/tests/api/. Pairs with the DELETE export of apps/web/app/api/favorites/[itemSlug]/route.ts — the first per-source-file DELETE smoke the docs tree publishes that pins a THREE-field tenant-scoped IDOR check + SELECT-then-DELETE pattern on a non-admin per-item DELETE route. The companion collection-level POST + GET sibling favorites.spec.ts covers apps/web/app/api/favorites/route.ts. Distinct from EVERY prior DELETE smoke: checkDatabaseAvailability() as the FIRST gate (returns 503 with the DATABASE_UNAVAILABLE envelope when DATABASE_URL is missing -- the auth check fires AFTER the DB-availability check); TWO-key { success: false, error: 'Unauthorized' } 401 envelope + TWO-key { success: false, error: 'Tenant not found' } 403 envelope (TWO distinct gate-failure statuses with the SAME envelope shape but DIFFERENT messages -- UNIQUE: the FIRST per-source-file DELETE smoke pinning a 401 → 403 → 404 cascade with three distinct messages on the same TWO-key envelope shape); THREE-field tenant-scoped IDOR check -- the SELECT + DELETE WHERE clauses BOTH match on userId === session.user.id AND itemSlug === path.itemSlug AND tenantId === currentTenantId (UNIQUE: the FIRST per-source-file DELETE smoke pinning a three-field tenant-scoped IDOR check); SELECT-then-DELETE pattern -- the handler runs an inline db.select().from(favorites).where(...).limit(1) BEFORE the DELETE to surface a 404 if not found (distinct from single-step DELETE WHERE which would silently no-op); TWO-key success payload { success: true, message: 'Favorite removed successfully' } with NO data field (UNIQUE: most DELETE handlers return data: { ... } with deletion details). The DELETE handler combines checkDatabaseAvailability() (FIRST gate; 503 if DB unavailable), auth() session lookup (!session?.user?.id → 401 TWO-key), getTenantId() (null → 403 TWO-key { success: false, error: 'Tenant not found' }), { itemSlug } = await params dynamic-segment resolution, the SELECT pre-check via db.select().from(favorites).where(userId + itemSlug + tenantId).limit(1) (empty → 404 TWO-key { success: false, error: 'Favorite not found' }), the DELETE WHERE with the same three-field clause, success payload { success: true, message: 'Favorite removed successfully' } with status 200, outer catch with safeErrorResponse(error, 'Failed to remove favorite'), and method-resolution surface where the route exports ONLY DELETE (GET / POST / PUT / PATCH must round-trip to < 500). Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk including X-Tenant-Id and X-User-Id side-channel probes asserting < 500; a canonical TWO-key 401-envelope assertion accepting either 401 OR 503 (both pre-IDOR); a strict envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear on the unauth branch; a side-channel walk including X-Tenant-Id; a cross-method probe (GET / POST / PUT / PATCH); a SELECT-pre-check-and-DELETE-WHERE-not-entered invariance walk -- CRITICAL: pinning that the load-bearing SELECT pre-check AND the DELETE mutation NEVER run on unauth and that an XSS marker in the itemSlug URL is NEVER echoed back; a catch-branch-dispatcher-not-entered invariance walk pinning that 'Failed to remove favorite' is NOT echoed on the unauth branch; a cross-permutation status invariance walk; a cross-itemSlug invariance walk pinning that different slugs produce IDENTICAL unauth envelopes -- the auth gate fires BEFORE any per-item-slug branch). Cross-references the companion collection-level POST + GET sibling favorites.spec.ts, the companion engagement / favorites combination spec items-engagement-and-favorites.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 94-of-N and the tests/api/ per-spec-file sub-rollout extends to 92-of-many, and the first per-source-file DELETE smoke pinning a three-field tenant-scoped IDOR check + SELECT-then-DELETE pattern lands -- pinning a 401 → 403 → 404 cascade contract with three distinct messages on the same TWO-key envelope shape that no prior DELETE smoke covers.
  • E2E Surveys [surveyId] Method Spec (apps/web-e2e/tests/api/surveys-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-survey detail GET / PUT / DELETE dynamic-segment / body / header smoke spec paired with apps/web-e2e/tests/api/surveys-id-method.spec.ts, the ninety-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninety-first under apps/web-e2e/tests/api/. Pairs with the GET, PUT, AND DELETE exports of apps/web/app/api/surveys/[surveyId]/route.ts — the first per-source-file triple-method smoke the docs tree publishes that pins a MIXED-auth gate contract -- GET is publicly accessible for published surveys but admin-gated for unpublished surveys (with a UNIQUE 404-mask: non-admin callers see 404 'Survey not found' INSTEAD of 403 'Forbidden'); PUT and DELETE are admin-only. Distinct from EVERY prior triple-method smoke: MIXED-auth gate (FIRST per-source-file triple-method smoke pinning a public-GET + admin-PUT + admin-DELETE pattern, vs admin-collections-[id] which is admin-gated on ALL three methods); 404-mask on GET for non-published surveys (UNIQUE -- the FIRST per-source-file GET smoke pinning a 404-mask security pattern that hides the existence of unpublished resources from non-admins, with non-admin callers seeing 404 'Survey not found' for both not-found-at-all AND not-published-and-not-admin); ID-or-slug fallback lookup (UNIQUE -- the FIRST per-source-file dynamic-segment GET smoke pinning a dual-lookup-by-id-or-slug contract, the handler tries surveyService.getOne(surveyId) first then falls back to surveyService.getBySlug(surveyId)); error.message === 'Survey not found' catch-branch dispatch on PUT and DELETE (UNIQUE -- the FIRST per-source-file PUT/DELETE smoke pinning an Error.message equality-match catch-dispatcher that re-emits 404); TWO-key { success: false, error: 'Unauthorized' } 401 envelope on PUT and DELETE; data: null in DELETE success payload (UNUSUAL -- most DELETE handlers omit data or return data: { ... }). The handlers combine: GET handler with surveyService.getOne(surveyId) → fallback to surveyService.getBySlug(surveyId), 404 if both null, auth() gate AFTER the lookup only for non-published surveys, 404-mask if not admin, success payload { success: true, data: <survey> }; PUT handler with auth() session lookup (!session?.user?.isAdmin → 401 TWO-key), JSON body parse, ID-or-slug fallback lookup, 404 if both null, surveyService.update(survey.id, body), success payload { success: true, data: <updatedSurvey>, message: 'Survey updated successfully' }, outer catch with Error.message === 'Survey not found' → re-emit 404 else safeErrorResponse(error, 'Failed to update survey'); DELETE handler with auth() session lookup (!session?.user?.isAdmin → 401 TWO-key), ID-or-slug fallback lookup, 404 if both null, surveyService.delete(survey.id), success payload { success: true, data: null, message: 'Survey deleted successfully' }, same catch-dispatcher as PUT; method-resolution surface where the route exports GET / PUT / DELETE (POST / PATCH must round-trip to < 500). Documents the at-a-glance scenario tree (four bulk-loop walks -- ~6 headers × 3 methods + ~9 PUT bodies all asserting < 500; a canonical 404 envelope assertion for non-existent surveys on GET; canonical 401-envelope assertions on PUT AND DELETE; a cross-method 401-envelope-equality assertion pinning byte-identical envelopes; a strict envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear on PUT unauth; a surveyService-update-not-entered invariance walk on PUT -- CRITICAL: pinning that XSS markers are NEVER echoed back; a surveyService-delete-not-entered invariance walk on DELETE -- CRITICAL; a cross-method probe (POST / PATCH); side-channel walks on PUT and DELETE; a catch-branch-dispatcher-not-entered invariance walk pinning that 'Survey not found' is NOT echoed on the PUT unauth branch). Cross-references the companion surveys list / collection-level POST sibling surveys.spec.ts, the companion surveys-exists query sibling surveys-exists-query.spec.ts, the triple-method admin sibling admin-collections-id-method-spec.md (uses ALL-admin-gated GET / PUT / DELETE), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 93-of-N and the tests/api/ per-spec-file sub-rollout extends to 91-of-many, and the first per-source-file triple-method smoke pinning a MIXED-auth gate contract lands -- pinning a 404-mask security pattern, an ID-or-slug fallback-lookup contract, and an Error.message equality-match catch-dispatcher that no prior triple-method smoke covers.
  • E2E User Payments Query Spec (apps/web-e2e/tests/api/user-payments-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's authenticated user-payment-history GET header smoke spec paired with apps/web-e2e/tests/api/user-payments-query.spec.ts, the ninety-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninetieth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/user/payments/route.ts — the first per-source-file GET smoke the docs tree publishes that pins a top-level-ARRAY success response (NOT an object wrapper). The handler returns either [] (when the caller has no Stripe customer) OR a top-level payment-history array (transformed Stripe invoice data). UNIQUE — every prior per-source-file GET smoke pins an object-shaped response; this is the FIRST that pins a bare-array shape. Sibling subscription-query.spec.ts covers the GET export of apps/web/app/api/user/subscription/route.ts -- SAME auth() gate + Stripe customer lookup + two-tier catch dispatcher pattern, but returns an OBJECT-wrapped response. Distinct from EVERY prior session-gated GET smoke: top-level-ARRAY success response (UNIQUE -- the 200-branch returns paymentHistory as a JS array directly via NextResponse.json(paymentHistory)); no-customer-found 200 EMPTY ARRAY [] (if customerId is null, the handler returns [] with status 200, NOT 401 / 404 / 4xx -- distinct from the subscription sibling which returns { hasActiveSubscription: false, message: 'No Stripe customer found' }); bare ONE-key { error: 'Unauthorized' } 401 envelope (NO success key, NO message key -- same shape as subscription sibling); two-tier 500 catch dispatcher -- inner Stripe-error → 500 'Failed to fetch payment data from Stripe'; outer → 500 'Failed to fetch payment data' (UNIQUE -- TWO different 500 messages with the SAME ONE-key { error } shape); zero-arg GET signature (export async function GET() with NO request / context arguments); Stripe Invoices + Subscriptions DUAL-list load-bearing chain -- the handler calls BOTH stripe.invoices.list AND stripe.subscriptions.list to enrich each invoice with subscription metadata (FIRST per-source-file GET smoke pinning a dual-Stripe-list invariant); filtered status whitelist (only invoices with status === 'paid' || status === 'open' appear in the response). The GET handler combines auth() session lookup (!session?.user?.id → 401 ONE-key { error: 'Unauthorized' }), initializeStripeProvider() + getStripeInstance() AFTER the auth gate, the load-bearing customer-id lookup stripe.getCustomerId(session.user as any) (null → 200 EMPTY ARRAY []), stripe.invoices.list({ customer, limit: 100 }) load-bearing invoice list, stripe.subscriptions.list({ customer, limit: 100 }) load-bearing subscription list (DUAL-list invariant), filter + transform + sort (paid/open only, sort by date desc), inner catch 500 'Failed to fetch payment data from Stripe', outer catch 500 'Failed to fetch payment data', and method-resolution surface where the route exports ONLY GET (POST / PUT / PATCH / DELETE must round-trip to a < 500 status). Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk asserting < 500; a canonical ONE-key 401-envelope assertion; a strict ONE-key envelope-shape assertion (no success / message / data leak); a no-array-leak CRITICAL invariant pinning that the unauth response is NEVER an array; a gate-before-post-auth invariant pinning that NEITHER 500 message must appear in any unauth response; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a Stripe-SDK-calls-not-entered invariance walk -- CRITICAL: initializeStripeProvider / invoices.list / subscriptions.list / getCustomerId must NEVER run on unauth (no hosted_invoice_url / invoice_pdf / amount_paid / paymentProvider / subscriptionId / billingInterval leak); a two-tier-catch-dispatcher-not-entered invariance walk; a no-stripe-error-message-leak invariant; a cross-permutation status invariance walk). Cross-references the companion subscription GET sibling subscription-query.spec.ts (SAME pattern but OBJECT-wrapped response), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 92-of-N and the tests/api/ per-spec-file sub-rollout extends to 90-of-many, and the first per-source-file GET smoke pinning a top-level-ARRAY success response lands -- pinning a no-customer-found 200 empty-array fallback contract, a two-tier 500 catch dispatcher with two distinct messages on the same envelope shape, and a Stripe Invoices + Subscriptions DUAL-list load-bearing invariant that no prior GET smoke covers.
  • E2E Cron Sync Query Spec (apps/web-e2e/tests/api/cron-sync-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Vercel-cron sync GET header smoke spec paired with apps/web-e2e/tests/api/cron-sync-query.spec.ts, the ninety-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-ninth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/cron/sync/route.ts — the first per-source-file GET smoke the docs tree publishes for a Bearer-token-secret-gated cron endpoint. The existing multi-cron sibling cron-jobs.spec.ts covers the OTHER cron routes (subscription-expiration, subscription-reminders); this spec drills into the cron/sync handler specifically. Distinct from EVERY prior GET smoke: Bearer-token-secret auth -- the handler accepts ONLY Authorization: Bearer ${CRON_SECRET} (NOT session-based auth) -- the FIRST per-source-file GET smoke pinning a Bearer-token-only auth contract; dev-mode short-circuit -- if CRON_SECRET is NOT configured AND env is development, the handler allows access without auth (same pattern as lemonsqueezy/update's dev-mode short-circuit); FOUR-key 401 envelope { success: false, timestamp: <ISO>, duration: <ms>, message: 'Unauthorized' } -- UNIQUE: NO error field; uses message (not error) for the auth failure -- the FIRST per-source-file smoke pinning a 401 envelope WITHOUT an error field; performance tracking via startTime = Date.now() and duration: Date.now() - startTime in BOTH the unauth response AND the success/catch responses (matches lemonsqueezy/update's richest-envelope spec but with a message-only envelope); custom Cache-Control: no-cache, no-store, must-revalidate header on success; conditional success status -- { status: result.success ? 200 : 500 } based on the sync result. The GET handler combines a Bearer-token-secret check (request.headers.get('authorization') === \Bearer ${cronSecret}` -- failure → 401 4-key envelope unless dev-mode short-circuit), the dev-mode short-circuit (if (!cronSecret && isDevelopment)→ bypass), the load-bearingtriggerManualSync()call, success payload{ success, timestamp, duration, message, details? }with status 200 (ifresult.success) or 500, outer catch with safeErrorMessage(error, 'Unknown error')extracted into the catch envelope at status 500, and a method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to a< 500status). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk including various Authorization probes -- wrong Bearer, empty Bearer, non-Bearer scheme, Basic auth -- plus side-channels asserting< 500; a 4-key 401 envelope assertion when no Authorization header is present pinning success: false, message: 'Unauthorized', ISO timestamp, numeric duration, and NO errorkey; a strict envelope-shape assertion viaObject.keys(body).sort()when 401 is reached; an ISO timestamp regex assertion on whichever envelope is reached; a numeric duration field assertion -- the FIRST per-source-file GET smoke pinning request-duration measurement on the unauth branch; a no-Bearer-secret-echo invariant pinning that the caller-supplied secret marker is NEVER echoed in the response body; a cross-method probe (POST / PUT / PATCH / DELETE); a side-channel walk pinning that fabricated session cookies / X-User-Id / X-Forwarded-For do NOT bypass the Bearer-token auth; a triggerManualSync-not-entered invariance walk pinning that the load-bearing sync call NEVER runs on unauth and nodetails from a sync result is leaked). Cross-references the multi-cron sibling [cron-jobs.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/cron-jobs.spec.ts), the LemonSqueezy update POST sibling [lemonsqueezy-update-body-spec.md](./plugins/lemonsqueezy-update-body-spec.md) (also pins a dev-mode short-circuit + performance-tracking envelope but with a different shape), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) for the governing spec. With this entry the **per-spec-file docs rollout extends to 91-of-N** and the **tests/api/per-spec-file sub-rollout extends to 89-of-many**, and the **first per-source-file GET smoke for a Bearer-token-secret-gated cron endpoint** lands -- pinning a 4-key 401 envelope WITHOUT anerror` field, performance-tracking measurement on the unauth branch, and a Bearer-token-only auth contract that no prior GET smoke covers.
  • E2E Stripe Subscription Portal Body Spec (apps/web-e2e/tests/api/stripe-subscription-portal-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe billing-portal session-creation POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-subscription-portal-body.spec.ts, the ninetieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-eighth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/subscription/portal/route.ts — the first per-source-file POST smoke the docs tree publishes for a Stripe billing-portal session-creation endpoint. Distinct from EVERY prior Stripe per-source-file POST smoke AND the polar-subscription-portal-body sibling: zero-arg POST signature (UNIQUE — the FIRST per-source-file POST smoke pinning a zero-arg POST contract with NO request / context arguments — every other Stripe POST handler reads the request to parse a body or extract a header); !session?.user gate with a ONE-key { error: 'Unauthorized' } envelope (NO success key, NO message key — distinct from stripe-checkout's TWO-key shape and stripe-setup-intent-id's { success: false, error } shape); getCustomerId(...) returns null → 404 ONE-key { error: 'Stripe customer ID not found' }; buildUrl('/settings/billing') + new URL(...) URL-validation contract — invalid URL throws → 500 TWO-key { error: 'Invalid return URL configuration', message: 'The application URL is not properly configured' } (UNIQUE — no prior per-source-file smoke pins a new URL() validation contract on a constructed return URL); FOUR-key Stripe-error catch envelope on billingPortal.sessions.create(...) failures → 400 { error: 'Invalid request to Stripe', message, code, type } (UNIQUE — the FIRST per-source-file POST smoke pinning a FOUR-key envelope with both code AND type fields surfaced from the Stripe error object, vs payment-methods-create's THREE-key shape and other handlers' TWO-key shapes); structured Logger.create('StripePortal') call in the inner catch (FIRST per-source-file POST smoke pinning a structured-logger contract on the inner Stripe-error branch); safeErrorMessage helper in BOTH the inner-stripe-error catch AND the outer catch; TWO-key outer-catch 500 envelope { error: 'Failed to create billing portal session', message }. The POST handler combines auth() session lookup (!session?.user → 401 ONE-key), initializeStripeProvider() + getStripeInstance() AFTER auth gate, stripe.getCustomerId(session.user as any) load-bearing customer-id lookup (null → 404), buildUrl('/settings/billing') + new URL(...) URL-validation (invalid → 500 TWO-key), stripeInstance.billingPortal.sessions.create(...) load-bearing Stripe SDK call wrapped in INNER try/catch, inner-stripe-error catch (400 FOUR-key), success payload 200 { success: true, data: response, message: 'Billing portal session created' }, outer catch 500 TWO-key, and method-resolution surface (the route exports ONLY POST; GET / PUT / PATCH / DELETE must round-trip to a < 500 status). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~9-body bulk-loop walk all asserting < 500; a canonical 401 ONE-key Unauthorized envelope assertion; a strict ONE-key envelope-shape assertion pinning that success / message / data / code / type keys are all undefined; a no-portal-url-leak CRITICAL security invariant pinning that the success-branch portal url / id / customer must NEVER leak; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear on the unauth branch; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); an initializeStripeProvider-and-getCustomerId-and-billingPortal-sessions-create-not-entered invariance walk — CRITICAL — pinning that the load-bearing Stripe SDK calls NEVER run on unauth; a URL-validation-catch-not-entered invariance walk pinning the gate-before-URL-validation order; an inner-stripe-error-catch-FOUR-key-envelope-not-entered invariance walk — CRITICAL — pinning that the FOUR-key { error, message, code, type } envelope NEVER surfaces on unauth; an outer-catch-not-entered invariance walk pinning the gate-before-outer-catch order; a no-stripe-error-message-leak invariant pinning that 'No such customer' / 'resource_missing' / 'billing_portal' substrings NEVER leak; a body-shape invariance walk pinning that the unauth 401 envelope is IDENTICAL across body shapes (the handler is zero-arg POST and never reads the body); a cross-permutation status-invariance assertion; a no-XSS / open-redirect leak invariant pinning that hostile body content never appears in the response). Cross-references the Polar customer-portal sibling polar-subscription-portal-body-spec.md (different provider's portal pattern), the Stripe checkout root POST sibling stripe-checkout-body-spec.md (TWO-key Unauthorized envelope vs ONE-key here), the Stripe setup-intent [id] GET sibling stripe-setup-intent-id-query-spec.md (different { success: false, error } TWO-key shape), the Stripe payment-methods create POST sibling stripe-payment-methods-create-body-spec.md (different envelope shape on Stripe-error catch), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 90-of-N and the tests/api/ per-spec-file sub-rollout extends to 88-of-many, and the first per-source-file POST smoke for a Stripe billing-portal session-creation endpoint lands -- pinning the zero-arg POST contract no prior smoke covers, the new URL() validation contract no prior smoke covers, and the FOUR-key Stripe-error catch envelope (with both code AND type surfaced) no prior smoke covers.
  • E2E Item Comments Rating [commentId] Update Method Spec (apps/web-e2e/tests/api/item-comments-rating-id-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-comment rating-update PATCH dynamic-segment / body / header smoke spec paired with apps/web-e2e/tests/api/item-comments-rating-id-update-method.spec.ts, the eighty-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-seventh under apps/web-e2e/tests/api/. Pairs with the PATCH export of apps/web/app/api/items/[slug]/comments/rating/[commentId]/route.ts — the first per-source-file PATCH smoke the docs tree publishes that documents a Q-010-style NO-AUTH-GATE finding for a non-admin mutating route (the handler has NO auth() call, NO ownership check, NO rating validation; ANY caller can update ANY comment's rating to ANY value so long as DATABASE_URL is configured). The spec pins this finding as the CURRENT contract -- a future PR that adds auth would explicitly break this spec, prompting an update. Distinct from EVERY prior mutating-method smoke: NO auth() gate; NO ownership check (handler trusts path-param commentId directly); NO rating validation (any value passed straight to updateCommentRating(...)); production-leftover console.log with debug arrow '============rating=============>' (NOT dev-gated); returns raw comment row verbatim (no wrapper envelope); checkDatabaseAvailability() as the SOLE gate. The PATCH handler combines checkDatabaseAvailability() (the ONLY gate; 503 if missing), { commentId } = await params dynamic-segment resolution, { rating } = await request.json() body parse with NO validation, the production-leftover console.log debug statement, the load-bearing UNGUARDED updateCommentRating(commentId, rating) DB write, success payload as raw comment row verbatim, and outer catch 500 'Failed to update comment rating'. Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a NO-401 contract assertion; an auth-signal-ignored contract pinning that fabricated auth headers produce SAME status as bare; a no-validation contract pinning that invalid rating values produce SAME status as valid values; a cross-method probe (POST / PUT / DELETE); a no-catch-on-valid-body assertion; a no-wrapper-envelope assertion pinning the UNUSUAL raw-comment-row response shape). Cross-references the companion minimal smoke item-comment-rating-by-id.spec.ts, the parent route's PUT/DELETE handlers at item-comments-id-method-spec.md (DO enforce auth + ownership), the companion comment-create POST sibling item-comments-create-body-spec.md (same checkDatabaseAvailability() but with explicit auth() gate), the public per-item rating-aggregate GET sibling item-comments-rating-query-spec.md, docs/questions.md for the Q-### entry tracking the no-auth-gate finding, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 89-of-N and the tests/api/ per-spec-file sub-rollout extends to 87-of-many, and the first Q-010-style no-auth-gate finding for a non-admin mutating route lands -- pinning the no-auth-gate contract until a future PR adds authentication.
  • E2E Stripe Payment-Methods [id] Method Spec (apps/web-e2e/tests/api/stripe-payment-methods-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe per-id payment-method GET + DELETE dual-method dynamic-segment / header smoke spec paired with apps/web-e2e/tests/api/stripe-payment-methods-id-method.spec.ts, the eighty-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-sixth under apps/web-e2e/tests/api/. Pairs with the GET AND DELETE exports of apps/web/app/api/stripe/payment-methods/[id]/route.ts — the first per-source-file GET + DELETE dual-method smoke the docs tree publishes for any Stripe per-id primitive route. Sibling specs: stripe-payment-methods-delete-body-spec.md (DELETE-by-body on the static /delete path with id-in-body, NO dynamic segment); stripe-setup-intent-id-query-spec.md (GET-only on a dynamic-segment per-id path with NO DELETE); stripe-payment-methods-update-method-spec.md (PUT + PATCH dual-method smoke on a STATIC /update path with NO dynamic segment). Distinct from EVERY prior per-id Stripe smoke: TWO methods exported on the same dynamic-segment path (GET retrieves filtered fields AND DELETE detaches with default-reassignment cascade — the FIRST per-source-file GET + DELETE dual-method smoke pinning both methods on the same [id] route); customer-metadata-driven IDOR check on BOTH methods via customer.metadata?.userId !== session.user.id → 403 'Unauthorized - payment method does not belong to user'; !paymentMethod.customer check distinct for each method (GET → 400 'Payment method not associated with any customer' with any; DELETE → 400 'Payment method not associated with a customer' with a — UNIQUE: the only known per-source-file smoke pinning a one-word article-shift any vs a between two methods on the same handler); DELETE default-reassignment cascade — if the deleted method was the customer's default and there are other methods, re-assign default to the first remaining method; if there are no remaining methods, set default to undefined (the FIRST per-source-file DELETE smoke pinning a default-reassignment cascade); THREE-branch StripeError catch on BOTH methods — error.code === 'resource_missing' → 404 'Payment method not found'; other StripeError → 400 with raw error.message; default → 500 ('Failed to retrieve payment method' for GET, 'Failed to delete payment method' for DELETE — distinct 500 messages per method). The handler chain on each method combines auth() session lookup (!session?.user?.id → 401 { success: false, error: 'Unauthorized' } SAME envelope on BOTH methods); { id } = await params dynamic-segment resolution; !id check (→ 400 'Payment method ID is required' SAME on both methods); the load-bearing stripe.paymentMethods.retrieve(id) call on BOTH methods; !paymentMethod.customer check on DELETE / paymentMethod.customer ? … : else branch on GET (diverges on this critical structural difference between the two methods); stripe.customers.retrieve(...) second load-bearing call on BOTH methods; string-or-deleted customer check → 404 'Customer not found' on both methods; customer-metadata IDOR check → 403 on both methods; DELETE-only default-reassignment cascade via stripe.paymentMethods.list + stripe.customers.update; DELETE-only stripe.paymentMethods.detach(id) mutation; GET success payload with filtered fields ({ id, type, card, billing_details, created, metadata, is_default, customer_id }); DELETE success payload { success: true, message: 'Payment method deleted successfully', data: { was_default } }; THREE-branch outer catch on each method; method-resolution surface (the route exports GET AND DELETE; POST / PUT / PATCH must round-trip to a < 500 status). Documents the at-a-glance scenario tree (TWO header bulk-loop walks — ~7 headers each, GET + DELETE — all asserting < 500; canonical 401-envelope assertions on GET AND DELETE; a cross-method envelope-equality assertion pinning byte-identical 401 envelopes on GET and DELETE; strict envelope-shape assertions on both methods (no leak of the success-branch data / message fields); a gate-before-post-auth invariant pinning that NONE of nine candidate messages must appear on either method; a gate-before-success-build invariant on GET — CRITICAL — pinning no leak of card / billing_details / is_default / customer_id; a gate-before-success-build invariant on DELETE — CRITICAL — pinning no leak of was_default; a side-channel walk; a cross-method probe (POST / PUT / PATCH); a paymentMethods-retrieve-and-customers-retrieve-and-IDOR-and-detach-and-default-reassignment-not-entered invariance walk — CRITICAL — pinning that load-bearing Stripe SDK calls NEVER run on unauth; a catch-branch-dispatcher-not-entered invariance walk pinning the gate-before-catch-dispatcher order across distinct 500 messages per method; a no-stripe-error-message-leak invariant pinning that 'No such payment_method' / 'resource_missing' substrings NEVER leak; a cross-id-invariance walk pinning that three different payment-method IDs produce IDENTICAL unauth envelopes on BOTH methods; a no-XSS-id-substring leak invariant on either method; a default-reassignment-cascade-not-entered invariance walk on DELETE — CRITICAL — pinning that the customer-default mutation cascade NEVER runs on unauth). Cross-references the Stripe payment-methods delete DELETE-by-body sibling stripe-payment-methods-delete-body-spec.md (uses static /delete path with id-in-body vs this dynamic-segment route), the Stripe setup-intent GET-by-id sibling stripe-setup-intent-id-query-spec.md (uses SAME customer-metadata-driven IDOR check on a GET-only dynamic-segment route), the Stripe payment-methods PUT + PATCH dual-method sibling stripe-payment-methods-update-method-spec.md (uses different dual-method pair on a static path), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 88-of-N and the tests/api/ per-spec-file sub-rollout extends to 86-of-many, and the first per-source-file GET + DELETE dual-method smoke for a Stripe per-id primitive route lands -- pinning the default-reassignment cascade contract no prior DELETE smoke covers, the article-shift contract (any vs a) no prior smoke covers, and the customer-default mutation cascade gate-before invariant on a per-id DELETE surface.
  • E2E Stripe Setup-Intent [id] Query Spec (apps/web-e2e/tests/api/stripe-setup-intent-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe per-id setup-intent retrieval GET dynamic-segment / header smoke spec paired with apps/web-e2e/tests/api/stripe-setup-intent-id-query.spec.ts, the eighty-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-fifth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/stripe/setup-intent/[id]/route.ts — the first per-source-file GET smoke the docs tree publishes for a Stripe per-id primitive route (the setup-intent root POST is documented at stripe-setup-intent-body-spec.md) AND the first per-source-file GET smoke that pins a error.code === 'resource_missing' substring detection in the catch (UNIQUE: dispatches on Stripe's enum-typed code property to surface a 404). Distinct from the stripe-setup-intent (POST) root sibling: GET method (not POST); !session?.user?.id gate with { success: false, error: 'Unauthorized' } envelope (vs root POST's { error: 'Unauthorized' } ONE-key envelope without success key); customer-metadata-driven IDOR check (customer.metadata?.userId !== session.user.id → 403); filtered SetupIntent fields in success payload (vs root POST's raw provider object); Stripe-error.code === 'resource_missing' substring detection in catch → 404. The GET handler combines auth() session lookup (!session?.user?.id → 401), { id } = await params dynamic-segment resolution, !id check (→ 400 'Setup intent ID is required'), the load-bearing stripe.setupIntents.retrieve(id) call, customer-metadata IDOR check via stripe.customers.retrieve + customer.metadata?.userId !== session.user.id → 403, success payload with filtered SetupIntent fields, and THREE-branch outer catch (error.code === 'resource_missing' → 404, other StripeError → 400 with raw error.message, default → 500). Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk asserting < 500; a canonical 401-envelope assertion; a strict envelope-shape assertion; a no-client_secret-leak CRITICAL security invariant; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a setupIntents-retrieve-and-customers-retrieve-and-IDOR-check-not-entered invariance walk -- CRITICAL: load-bearing Stripe SDK calls must NEVER run on unauth; a catch-branch-dispatcher-not-entered invariance walk; a no-stripe-error-message-leak invariant pinning that 'No such setupintent' / 'resource_missing' substrings NEVER leak; a cross-id-invariance walk pinning that three different setup-intent IDs produce IDENTICAL unauth envelopes). Cross-references the Stripe setup-intent POST root sibling stripe-setup-intent-body-spec.md, the Stripe payment-methods delete DELETE sibling stripe-payment-methods-delete-body-spec.md (uses SAME customer-metadata-driven IDOR check), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 87-of-N and the tests/api/ per-spec-file sub-rollout extends to 85-of-many, and the first per-source-file GET smoke for a Stripe per-id primitive route lands -- pinning the error.code substring-detection contract no prior smoke covers and the no-client_secret-leak CRITICAL security invariant on a per-id GET surface.
  • E2E Stripe Payment-Methods Update Method Spec (apps/web-e2e/tests/api/stripe-payment-methods-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-method-update PUT + PATCH method / body / header smoke spec paired with apps/web-e2e/tests/api/stripe-payment-methods-update-method.spec.ts, the eighty-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-fourth under apps/web-e2e/tests/api/. Pairs with the PUT and PATCH exports of apps/web/app/api/stripe/payment-methods/update/route.ts — the first per-source-file PUT + PATCH smoke the docs tree publishes for a non-admin payment-method route. Sibling to the stripe-payment-methods-delete-body-spec.md route which exports DELETE. Distinct: TWO mutation methods exported on the same path -- PUT (full update with { payment_method_id, metadata, billing_details, set_as_default }) AND PATCH (set-default-only with { payment_method_id }); the FIRST per-source-file mutating smoke pinning a PUT + PATCH dual-method export. Shared helper-function-extraction design with the delete sibling -- both PUT and PATCH use validateSession, validatePaymentMethodOwnership, and handleApiError. PUT preserves existing metadata via spread (metadata: { ...paymentMethod.metadata, ...metadata, userId }; the FIRST per-source-file PUT smoke pinning a metadata-merge contract); userId always present in metadata via explicit set AFTER the spread (caller cannot override). Both handlers share the same auth + ownership chain: validateSession() → 401 { success: false, error: 'Authentication required' }; JSON body parse; PUT uses updatePaymentMethodSchema.parse(body) while PATCH uses setDefaultPaymentMethodSchema.parse(body); validatePaymentMethodOwnership helper; PUT calls stripe.paymentMethods.update(...) while PATCH calls stripe.customers.update(...). Documents the at-a-glance scenario tree (a doubled header walk -- ~6 headers × 2 methods; per-method body walks -- ~9 PUT bodies + ~5 PATCH bodies; canonical 401-envelope assertions on PUT AND PATCH; a cross-method envelope-equality assertion pinning byte-identical 401 envelopes on PUT and PATCH; a gate-before-post-auth invariant pinning that NONE of eight candidate messages must appear on either method; a no-metadata.userId-leak invariant on PUT; a cross-method probe (GET / POST / DELETE); an ownership-check-helper-and-paymentMethods-update-and-customers-update-not-entered invariance walk -- CRITICAL: the load-bearing Stripe SDK calls must NEVER run on unauth; a no-payment_method_id-leak invariant for XSS-shaped values). Cross-references the Stripe payment-methods delete DELETE sibling stripe-payment-methods-delete-body-spec.md (SAME helper-function-extraction design), the Stripe payment-methods create POST sibling stripe-payment-methods-create-body-spec.md, the per-comment edit/delete sibling item-comments-id-method-spec.md (another dual-method spec PUT + DELETE; this Stripe update spec is PUT + PATCH), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 86-of-N and the tests/api/ per-spec-file sub-rollout extends to 84-of-many, and the first PUT + PATCH dual-method export smoke lands -- pinning the metadata-merge contract no prior PUT smoke covers and completing the Stripe payment-methods CRUD trio (create POST + update PUT/PATCH + delete DELETE).
  • E2E Stripe Payment-Methods Delete Body Spec (apps/web-e2e/tests/api/stripe-payment-methods-delete-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-method-delete DELETE body / header smoke spec paired with apps/web-e2e/tests/api/stripe-payment-methods-delete-body.spec.ts, the eighty-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-third under apps/web-e2e/tests/api/. Pairs with the DELETE export of apps/web/app/api/stripe/payment-methods/delete/route.ts — the first per-source-file DELETE smoke the docs tree publishes for a non-admin payment-method route (note: the route's mutation method is DELETE, NOT POST as is typical for other payment-method mutations) AND the first per-source-file mutating smoke that pins a multi-helper-function-extraction handler design (the handler delegates to FIVE helper functions: validateSession, validatePaymentMethodOwnership, handleDefaultPaymentMethodReassignment, checkAffectedSubscriptions, and handleApiError) AND the first per-source-file mutating smoke that pins a customer-metadata-driven IDOR check (the handler retrieves the Stripe customer associated with the payment method and verifies customer.metadata?.userId === userId → 403 if mismatch). Distinct from the stripe-payment-methods-create sibling: DELETE method (not POST); ONE-key 401 envelope { success: false, error: 'Authentication required' }; multi-helper-function-extraction design (5 helpers); customer-metadata IDOR check; Stripe-error-echo with 'Stripe error: ' prefix; default-payment-method reassignment side-effect; affected-subscriptions read-only count. The DELETE handler combines validateSession() helper, JSON body parse AFTER auth gate, deletePaymentMethodSchema.parse(body) Zod throwing parse, validatePaymentMethodOwnership(paymentMethodId, userId) helper with three-stage chain, handleDefaultPaymentMethodReassignment side-effect, checkAffectedSubscriptions count, the load-bearing stripe.paymentMethods.detach(paymentMethodId) call, success payload { success: true, message, data: { was_default, affected_subscriptions, new_default_payment_method } }, and handleApiError THREE-helper catch dispatcher. Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~11-body bulk-loop walk all asserting < 500; a canonical 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-Stripe-error-prefix invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / POST / PUT / PATCH); a Zod-throw-catch-not-entered invariance walk; an ownership-check-helper-and-detach-and-reassignment-and-sub-count-not-entered invariance walk -- CRITICAL: paymentMethods.detach must NEVER run on unauth (catastrophic); a no-paymentMethodId-leak invariant pinning that XSS-shaped paymentMethodId is NEVER echoed back). Cross-references the Stripe payment-methods create POST sibling stripe-payment-methods-create-body-spec.md, the Stripe webhook POST sibling stripe-webhook-body-spec.md, the Stripe checkout POST sibling stripe-checkout-body-spec.md, the per-comment edit/delete sibling item-comments-id-method-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 85-of-N and the tests/api/ per-spec-file sub-rollout extends to 83-of-many, and the first DELETE smoke pinning a multi-helper-function-extraction design + customer-metadata-driven IDOR check lands -- pinning three FIRST contracts no prior mutating smoke covers.
  • E2E Stripe Subscription [subscriptionId] Update Body Spec (apps/web-e2e/tests/api/stripe-subscription-id-update-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe subscription-plan-update POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-subscription-id-update-body.spec.ts, the eighty-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-second under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/subscription/[subscriptionId]/update/route.ts — the third sibling in the Stripe subscription-management trio (cancel + reactivate + update), completing the Stripe POST trio that mirrors the LemonSqueezy subscription-management trio (cancel + reactivate + update-plan) and the Polar pair (cancel + reactivate), bringing the cross-provider subscription-management surface to eight per-source-file POST smokes (three Stripe + three LemonSqueezy + two Polar). It is the first per-source-file POST smoke the docs tree publishes that pins a USER-SCOPED IDOR check on a Stripe subscription endpoint -- after the tenant-scoped DB lookup the handler also compares userSubscription.userId !== session.user.id and returns a merged 404 'Subscription not found or access denied'; this sits at the user-scoped end of the stripe-subscription IDOR spectrum (cancel = no IDOR; reactivate = tenant-only; update = full user-scoped). It is also the first per-source-file POST smoke pinning a THREE-state allow-list pre-check 400 (subscription.status !== 'active' && subscription.status !== 'pending' && subscription.status !== 'paused' → 400 'Subscription is not active' -- distinct from the reactivate sibling's SINGLE-flag pre-check on cancelAtPeriodEnd), the first per-source-file POST smoke pinning a PaymentPlan-enum-from-@/lib/constants includes-validation (Object.values(PaymentPlan).includes(newPlanId) → 400 'Invalid plan ID' -- distinct from the LemonSqueezy update-plan sibling which uses Zod safeParse for its validation chain), and the first per-source-file POST smoke pinning a conditional tenant filter on a Drizzle UPDATE WHERE clause (...(tenantId ? [eq(subscriptions.tenantId, tenantId)] : []) -- spreads zero or one Drizzle filter into the AND clause based on whether getTenantId() returns a truthy value at request time; every prior tenant-scoped POST smoke uses an unconditional tenant filter). Distinct from EVERY prior POST smoke: USER-scoped IDOR check; THREE-state pre-check 400; PaymentPlan-enum-includes validation; conditional tenant-filter on DB UPDATE WHERE; body parsing IS used (await request.json() IS called, matches stripe cancel + LS update-plan siblings, distinct from stripe reactivate sibling); TWO required fields ({ newPlanId, newPriceId }); multi-step write chain (provider call + DB update + email side-effect); plan-changed email contract with BOTH oldPlanName: subscription.planId AND newPlanName: newPlanId (FIRST per-source-file POST smoke pinning an email with both old + new plan names); dynamic success message (Plan updated to ${newPlanId} successfully template literal with newPlanId interpolation, distinct from reactivate sibling's static message); returns raw updatedSubscription in the data field (Stripe SDK provider object verbatim); generic 500 catch (single static 'Failed to update subscription', NO substring detection). The POST handler combines auth() session lookup (!session?.user → 401 { error: 'Unauthorized' } bare envelope), { newPlanId, newPriceId } = await request.json() body parse AFTER auth gate, { subscriptionId } = await params dynamic-segment param resolution, PaymentPlan-enum-includes validation (!Object.values(PaymentPlan).includes(newPlanId) → 400 'Invalid plan ID'), getOrCreateStripeProvider() singleton, the merged tenant-scoped DB IDOR check + user-id equality check (getSubscriptionByProviderSubscriptionId('stripe', subscriptionId) returns null OR userSubscription.userId !== session.user.id → 404 'Subscription not found or access denied'), the THREE-state pre-check 400, the load-bearing stripeProvider.updateSubscription({ subscriptionId, priceId: newPriceId }) provider call, getTenantId() resolution + DB UPDATE with conditional tenant filter, async paymentEmailService.sendSubscriptionPlanChangedEmail(...) side-effect (failure does NOT fail the update), success payload { success: true, data: <updatedSubscription>, message: 'Plan updated to ${newPlanId} successfully' } with status 200, and outer catch 500 { error: 'Failed to update subscription' }. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-dynamic-success-message-leak invariant via regex; a no-input-echo invariant pinning that caller-supplied newPlanId / newPriceId markers must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel walk including X-User-Id AND X-Tenant-Id probes; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a no-500-from-body-parse-error invariant; a PaymentPlan-enum-includes-validation-not-entered invariance walk (4 shapes); a user-scoped-IDOR-check-not-entered invariance walk; a THREE-state-pre-check-400-not-entered invariance walk (3 shapes); an updateSubscription-DB-update-email-send-chain-not-entered invariance walk; a no-'Failed to update subscription'-leak invariant; a body-completely-ignored invariance walk; a cross-subscription-ID invariance walk pinning that the tenant-scoped DB read AND the user-id equality check are NOT entered upstream of the auth gate). Cross-references the Stripe subscription-cancel POST sibling stripe-subscription-id-cancel-body-spec.md (NO IDOR), the Stripe subscription-reactivate POST sibling stripe-subscription-id-reactivate-body-spec.md (TENANT-only IDOR + SINGLE-flag pre-check), the LemonSqueezy update-plan POST sibling lemonsqueezy-update-plan-body-spec.md (Zod multi-field validation; this Stripe update spec uses raw enum-includes validation), the Polar subscription-cancel POST sibling polar-subscription-id-cancel-body-spec.md (full user-scoped IDOR via customer-id-equality; this Stripe update spec uses user-id-equality), the Stripe webhook signature-verified POST sibling stripe-webhook-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec and docs/questions.md for the Q-### entries tracking the Stripe subscription IDOR-spectrum findings (Q-010-family). With this entry the per-spec-file docs rollout extends to 84-of-N and the tests/api/ per-spec-file sub-rollout extends to 82-of-many, and the first user-scoped IDOR + THREE-state pre-check 400 + PaymentPlan-enum-includes validation + conditional tenant-filter POST smoke lands -- completing the Stripe subscription-management POST trio and pinning FOUR FIRST contracts no prior POST smoke covers.
  • E2E LemonSqueezy Update Body Spec (apps/web-e2e/tests/api/lemonsqueezy-update-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy generic-subscription-update POST body / header smoke spec paired with apps/web-e2e/tests/api/lemonsqueezy-update-body.spec.ts, the eighty-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighty-first under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/lemonsqueezy/update/route.ts — the richest per-source-file POST smoke the docs tree publishes, pinning SIX FIRST contracts: (1) success: false-AND-code-typed FIVE-key 401 envelope { success: false, error: 'Unauthorized', code: 'UNAUTHORIZED', requestId: <uuid>, timestamp: <ISO> } -- distinct from cancel/reactivate/update-plan sibling THREE-key envelope; FIRST per-source-file POST smoke pinning a 5-key envelope with both requestId and timestamp; (2) Per-request UUID via crypto.randomUUID?.() || Math.random().toString(36).substring(2) (UUID v4 with browser-fallback; FIRST per-source-file POST smoke pinning per-request UUID generation with optional-chain fallback); (3) Performance tracking via startTime = Date.now() and duration: ${Date.now() - startTime}ms in the catch envelope (FIRST per-source-file POST smoke pinning request-duration measurement); (4) Development-mode short-circuit via if (process.env.NODE_ENV === 'development') returns 200 with input echoed BEFORE calling the provider (FIRST per-source-file POST smoke pinning a dev-mode short-circuit contract); (5) Custom response headers -- Cache-Control: no-cache, no-store, must-revalidate, X-Request-ID, X-Response-Time (FIRST per-source-file POST smoke pinning custom response headers); (6) Five-tier catch dispatcher -- errorCode extracted from error.code, dispatched to VALIDATION_ERROR → 400, UNAUTHORIZED → 401, SUBSCRIPTION_NOT_FOUND → 404, PROVIDER_UNAVAILABLE → 503, default → 500 (FIRST per-source-file POST smoke pinning a five-tier catch dispatcher). Distinct from the cancel + reactivate + update-plan siblings: !session?.user gate (NOT !session?.user?.email); code: 'UNAUTHORIZED' (NOT 'AUTH_REQUIRED'); 5-key 401 envelope with success: false + code + requestId + timestamp; dev-mode short-circuit. Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk -- including fabricated X-Request-ID to verify request-id-forgery prevention -- + a ~13-body bulk-loop walk all asserting < 500; a FIVE-key 401-envelope assertion with all fields verified including ISO-format timestamp; a strict envelope-shape assertion; a per-request-UUID-uniqueness assertion (three requests must produce three different requestIds); a request-id-forgery-prevention assertion pinning that caller-supplied X-Request-ID: 'attacker-injected-uuid' is NEVER echoed in the body's requestId field; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of three messages and four codes may appear on unauth; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a validation-chain-not-entered invariance walk; a dev-mode-short-circuit-and-provider-call-and-5-tier-catch-not-entered invariance walk; a no-custom-header invariant on the unauth branch -- pinning that X-Request-ID and X-Response-Time only appear on success/catch). Cross-references the companion cancel sibling lemonsqueezy-cancel-body-spec.md, the companion reactivate sibling lemonsqueezy-reactivate-body-spec.md, the companion update-plan sibling lemonsqueezy-update-plan-body-spec.md, the LemonSqueezy webhook POST sibling lemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST sibling lemonsqueezy-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 83-of-N and the tests/api/ per-spec-file sub-rollout extends to 81-of-many, and the richest per-source-file POST smoke lands -- pinning SIX FIRST contracts no prior smoke covers (5-key envelope with requestId + timestamp; per-request UUID with browser-fallback; performance tracking; dev-mode short-circuit; custom response headers; five-tier catch dispatcher).
  • E2E Stripe Subscription [subscriptionId] Reactivate Body Spec (apps/web-e2e/tests/api/stripe-subscription-id-reactivate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe subscription-reactivate POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-subscription-id-reactivate-body.spec.ts, the eighty-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eightieth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/subscription/[subscriptionId]/reactivate/route.ts — the first per-source-file POST smoke pinning a TENANT-SCOPED-but-NOT-USER-SCOPED partial-IDOR finding (the handler authenticates the user via auth() and looks up the subscription via getSubscriptionByProviderSubscriptionId('stripe', subscriptionId), which scopes the query by tenantId but NOT by userId; sits between the stripe/cancel sibling which has NO IDOR check at all and the polar/cancel sibling which enforces full user-scoped IDOR; FIRST per-source-file POST smoke pinning a tenant-scoped-but-NOT-user-scoped IDOR contract) AND the first per-source-file POST smoke pinning a STATE-MACHINE PRE-CHECK 400 contract (the handler reads subscription.cancelAtPeriodEnd from the DB row and returns 400 'Subscription is not scheduled for cancellation' BEFORE calling the provider; distinct from the polar/subscription/[id]/reactivate sibling which surfaces the same 400 via a catch-substring detection on the upstream Polar error message; this Stripe variant has the 400 baked into the handler's own state-machine pre-check from a DB-row column read). Cross-references the four sibling per-source-file POST smokes stripe-subscription-id-cancel-body-spec.md, polar-subscription-id-cancel-body-spec.md, polar-subscription-id-reactivate-body-spec.md, and lemonsqueezy-cancel-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 82-of-N and the tests/api/ per-spec-file sub-rollout extends to 80-of-many, and the first tenant-scoped-but-NOT-user-scoped partial-IDOR contract + state-machine pre-check 400 contract POST smoke lands.
  • E2E LemonSqueezy Update-Plan Body Spec (apps/web-e2e/tests/api/lemonsqueezy-update-plan-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy plan-update POST body / header smoke spec paired with apps/web-e2e/tests/api/lemonsqueezy-update-plan-body.spec.ts, the eighty-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-ninth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/lemonsqueezy/update-plan/route.ts — the third sibling in the LemonSqueezy subscription-management trio (cancel + reactivate + update-plan), all sharing the same email-gated auth contract, THREE-key 401 envelope with code: 'AUTH_REQUIRED', Zod safeParse validation, and timestamp field in success envelope. Distinct from the cancel + reactivate siblings: (a) Multi-field Zod schema with defaults -- the updatePlanSchema has TWO required fields (subscriptionId, variantId) AND FOUR OPTIONAL fields with defaults (proration, invoiceImmediately, disableProrations, billingAnchor); FIRST per-source-file POST smoke pinning a multi-field-with-defaults Zod schema; (b) z.coerce.number().positive() -- FIRST per-source-file POST smoke pinning a Zod coerce-number contract (string-to-number coercion); (c) z.enum with default -- FIRST per-source-file POST smoke pinning a Zod enum-with-default contract; (d) z.number().min(1).max(31) for billingAnchor (day-of-month range constraint); (e) Plan-update-specific metadata -- writes 7 metadata fields including session.user.email as updatedBy; same email-in-metadata pattern as reactivate sibling, but with FOUR additional flag fields. The POST handler combines auth() session lookup (!session?.user?.email → 401 THREE-key envelope), JSON body parse, updatePlanSchema.safeParse(body) with multi-field schema (failure → 400 with code: 'VALIDATION_ERROR'), getOrCreateLemonsqueezyProvider() singleton, the load-bearing lemonsqueezy.updateSubscription({...metadata: { action, proration, invoiceImmediately, disableProrations, billingAnchor, updatedAt, updatedBy } }) call, success payload with timestamp, and outer catch safeErrorResponse(error, 'Failed to update subscription plan'). Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a THREE-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-VALIDATION_ERROR-or-UPDATE_FAILED-codes invariant; a no-updatedBy-leak invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a multi-field-validation-chain-not-entered invariance walk pinning that NONE of five validation-failure shapes (negative variantId, invalid enum, out-of-range billingAnchor, etc.) may surface on unauth; an updateSubscription-call-with-metadata-write-not-entered invariance walk). Cross-references the companion cancel sibling lemonsqueezy-cancel-body-spec.md, the companion reactivate sibling lemonsqueezy-reactivate-body-spec.md (same email-in-metadata pattern), the LemonSqueezy webhook POST sibling lemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST sibling lemonsqueezy-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 81-of-N and the tests/api/ per-spec-file sub-rollout extends to 79-of-many, and the first multi-field-with-defaults Zod schema POST smoke lands -- completing the LemonSqueezy subscription-management trio (cancel + reactivate + update-plan) and pinning four FIRST Zod-schema-pattern contracts no prior smoke covers.
  • E2E Stripe Subscription [subscriptionId] Cancel Body Spec (apps/web-e2e/tests/api/stripe-subscription-id-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe subscription-cancel POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-subscription-id-cancel-body.spec.ts, the eightieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-eighth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/subscription/[subscriptionId]/cancel/route.ts — the first per-source-file POST smoke the docs tree publishes that documents a Q-010-style IDOR finding for a Stripe subscription endpoint (the handler authenticates the user via auth() but does NOT verify that the subscriptionId from the path belongs to the authenticated user; compare to the polar/subscription/[id]/cancel sibling which DOES enforce ownership via getCustomerIdgetPolarSubscriptionsubscriptionCustomerId === userPolarCustomerId; the Stripe cancel handler trusts the path parameter directly) AND the first per-source-file POST smoke that pins a DB-sync-after-provider-call contract (after stripeProvider.cancelSubscription(...) succeeds, the handler ALSO calls updateSubscriptionBySubscriptionId({...}) to sync the cancellation state back to the local DB). Distinct from the polar/subscription/[id]/cancel sibling: NO IDOR-protection (FIRST per-source-file POST smoke pinning a Q-010-style finding for a Stripe subscription endpoint); NO Content-Length 413 pre-check; DB sync side-effect (FIRST per-source-file POST smoke pinning a DB-sync-after-provider-call contract); email-send with fault-tolerance (paymentEmailService.sendSubscriptionCancellingEmail(...) wrapped in try/catch -- failure does NOT fail the cancellation); NO try/catch around request.json(). The POST handler combines auth() session lookup (!session?.user → 401 { error: 'Unauthorized' } bare envelope), JSON body parse with destructured default { cancelAtPeriodEnd = true }, { subscriptionId } param resolution, getOrCreateStripeProvider() singleton, the load-bearing stripeProvider.cancelSubscription(subscriptionId, cancelAtPeriodEnd) call WITHOUT IDOR protection, updateSubscriptionBySubscriptionId({...}) DB sync side-effect, email-send with fault-tolerance, success payload { success: true, data: <cancelledSubscription>, message: <conditional> }, and outer catch 500 { error: 'Failed to cancel subscription' }. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~10-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a cancelSubscription-DB-sync-email-send-not-entered invariance walk; a catch-branch-generic-500-not-echoed invariance walk; a NO-IDOR-protection contract walk pinning that the unauth 401 envelope is IDENTICAL across different subscription IDs -- pinning the CURRENT contract). Cross-references the polar/subscription/[id]/cancel POST sibling polar-subscription-id-cancel-body-spec.md (enforces IDOR protection; this Stripe spec documents the lack of it), the polar/subscription/[id]/reactivate POST sibling polar-subscription-id-reactivate-body-spec.md, the Stripe webhook signature-verified POST sibling stripe-webhook-body-spec.md, the Stripe checkout POST sibling stripe-checkout-body-spec.md, the LemonSqueezy subscription-cancel POST sibling lemonsqueezy-cancel-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec and docs/questions.md for the Q-### entry tracking the Stripe IDOR finding. With this entry the per-spec-file docs rollout extends to 80-of-N and the tests/api/ per-spec-file sub-rollout extends to 78-of-many, and the first Q-010-style IDOR finding for a Stripe subscription endpoint lands -- pinning the no-IDOR-protection contract until a future PR adds ownership verification (which would explicitly break this spec, prompting an update).
  • E2E LemonSqueezy Reactivate Body Spec (apps/web-e2e/tests/api/lemonsqueezy-reactivate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy subscription-reactivate POST body / header smoke spec paired with apps/web-e2e/tests/api/lemonsqueezy-reactivate-body.spec.ts, the seventy-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-seventh under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/lemonsqueezy/reactivate/route.ts — the complement to the lemonsqueezy-cancel-body-spec.md sibling: both routes share the same email-gated auth contract, THREE-key 401 envelope with code: 'AUTH_REQUIRED', Zod safeParse validation, and timestamp field in the success envelope. The reactivate route differs in: (a) Reactivation-specific metadata -- the handler calls updateSubscription({..., metadata: { action: 'reactivate', reactivateAction: true, reactivatedAt: <ISO>, reactivatedBy: session.user.email }}); the FIRST per-source-file POST smoke pinning a session.user.email-in-metadata contract (the user's email is written to provider-side metadata as reactivatedBy); (b) safeErrorResponse(...) direct in catch -- single line, distinct from the sibling cancel route's manual FOUR-key catch envelope; (c) Static success message 'Subscription reactivated successfully' (no conditional branch). The POST handler combines auth() session lookup (!session?.user?.email → 401 THREE-key envelope), JSON body parse via await request.json() AFTER auth gate, reactivateSubscriptionSchema.safeParse(body) (failure → 400 with code: 'VALIDATION_ERROR'), getOrCreateLemonsqueezyProvider() singleton, the load-bearing lemonsqueezy.updateSubscription({ subscriptionId, cancelAtPeriodEnd: false, metadata: { action, reactivateAction, reactivatedAt, reactivatedBy } }) call (writes session.user.email to provider-side metadata), success payload with timestamp, and outer catch safeErrorResponse(error, 'Failed to reactivate subscription'). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~10-body bulk-loop walk all asserting < 500; a THREE-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-VALIDATION_ERROR-code invariant; a no-reactivatedBy-leak invariant pinning that caller-supplied attacker email is NEVER echoed; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a validation-chain-not-entered invariance walk; an updateSubscription-call-with-metadata-write-not-entered invariance walk). Cross-references the companion cancel sibling lemonsqueezy-cancel-body-spec.md, the LemonSqueezy webhook POST sibling lemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST sibling lemonsqueezy-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 79-of-N and the tests/api/ per-spec-file sub-rollout extends to 77-of-many, and the first session.user.email-in-metadata POST smoke lands -- pinning the user's email being written to provider-side metadata as a contract.
  • E2E Polar Subscription [subscriptionId] Reactivate Body Spec (apps/web-e2e/tests/api/polar-subscription-id-reactivate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Polar subscription-reactivate POST body / header smoke spec paired with apps/web-e2e/tests/api/polar-subscription-id-reactivate-body.spec.ts, the seventy-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-sixth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/polar/subscription/[subscriptionId]/reactivate/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a NO-BODY POST handler (the handler does NOT call request.json() at all; the body is COMPLETELY ignored upstream of the auth gate; EVERY prior POST smoke either parses the body (lemonsqueezy, polar/cancel, stripe/checkout) OR explicitly extracts a header (sponsor-ads); FIRST per-source-file POST smoke pinning a body-less POST contract) AND the first per-source-file POST smoke that pins a THREE-string error-message-detection catch with a 400 minted from the catch dispatcher (NOT from a schema validation step; EVERY prior POST smoke that pins a 400 does so via Zod safeParse; this is the FIRST 400 minted from the catch's substring-detection on a business-rule violation: error.message.includes('not scheduled for cancellation') → 400 'Subscription is not scheduled for cancellation'). Distinct from EVERY prior POST smoke: NO body parsing (request.json() is NEVER called); NO Content-Length 413 pre-check (distinct from polar/cancel sibling which DOES pin a 413 pre-check); THREE-string catch dispatcher ('not found'/'404' → 404, 'Unauthorized'/'401' → 401, 'not scheduled for cancellation' → 400); 400-from-catch contract; static success message 'Subscription reactivated successfully' (NOT conditional based on a body flag, distinct from polar/cancel and lemonsqueezy/cancel siblings); same IDOR-protection chain as polar/cancel sibling (getCustomerId → 403, private (polarProvider as any).polar extraction → 500, getPolarSubscription ownership check → merged 404). The POST handler combines auth() session lookup (!session?.user → 401 { error: 'Unauthorized' } bare envelope), { subscriptionId } param resolution, the IDOR-protection chain (getCustomerId → 403, polar client extraction → 500, getPolarSubscription → 404), the load-bearing polarProvider.reactivateSubscription(subscriptionId) call with NO body-driven flags, success payload { success: true, data: { id, status, cancelAtPeriodEnd, currentPeriodEnd, priceId, customerId }, message: 'Subscription reactivated successfully' }, and THREE-string error-message-detection catch dispatching to 404 / 401 / 400 / 500 via safeErrorResponse(...). Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~9-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of seven candidate messages must appear; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a no-body-parse contract walk on malformed JSON; an IDOR-protection-chain-not-entered invariance walk pinning that NONE of the three IDOR-chain error messages may appear; a reactivateSubscription-call-not-entered invariance walk; a catch-branch-THREE-string-dispatcher-not-entered invariance walk; a 400-catch-dispatcher-contract-not-entered invariance walk; a body-completely-ignored invariance walk -- the strongest no-body-parse contract in the rollout). Cross-references the Polar subscription-cancel POST sibling polar-subscription-id-cancel-body-spec.md (uses SAME IDOR-protection chain and private (polarProvider as any).polar extraction, BUT pins a TWO-string catch dispatcher (no 400 branch), Content-Length 413 pre-check, body parsing with fault-tolerance, and conditional success message), the Polar webhook signature-verified POST sibling polar-webhook-body-spec.md, the Polar checkout POST sibling polar-checkout-body-spec.md (uses SAME (polarProvider as any).polar private-property-access pattern), the Polar subscription portal POST sibling polar-subscription-portal-body.spec.ts, the LemonSqueezy subscription-cancel POST sibling lemonsqueezy-cancel-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 78-of-N and the tests/api/ per-spec-file sub-rollout extends to 76-of-many, and the first NO-body + 400-from-catch + THREE-string-catch-dispatcher POST smoke lands -- pinning two FIRST contracts no prior smoke covers and completing the polar/subscription/[id]/* POST pair (cancel + reactivate).
  • E2E Polar Subscription [subscriptionId] Cancel Body Spec (apps/web-e2e/tests/api/polar-subscription-id-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Polar subscription-cancel POST body / header smoke spec paired with apps/web-e2e/tests/api/polar-subscription-id-cancel-body.spec.ts, the seventy-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-fifth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/polar/subscription/[subscriptionId]/cancel/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a Content-Length 413 pre-check (the handler reads request.headers.get('content-length') BEFORE the body parse and returns 413 'Request body too large. Maximum size is 1024 bytes.' if declared length exceeds 1KB; EVERY prior POST smoke uses 4xx/5xx in standard ranges; this is the FIRST 413 (Payload Too Large) contract in the rollout) AND the first per-source-file POST smoke that pins an IDOR-protection chain (after getCustomerId lookup → 403, the handler retrieves the subscription via getPolarSubscription(...) and explicitly checks subscriptionCustomerId === userPolarCustomerId, returning a merged 404 message 'Subscription not found or access denied' for both not-found AND ownership-mismatch cases; FIRST per-source-file POST smoke pinning a merged 404+403 IDOR-protection message). Distinct from EVERY prior POST smoke: Content-Length 413 pre-check; IDOR-protection chain with merged 404+403 message; private property access via (polarProvider as any).polar for direct Polar client access (matches polar-checkout one_time branch); helper-function injection -- getPolarSubscription(...) takes formatErrorMessage AND logger as dependency-injected helpers; TWO-string error-message-detection catch ('not found' || '404' → 404, 'Unauthorized' || '401' → 401, default → 500); conditional success message based on cancelAtPeriodEnd; body-parse fault tolerance with size-error detection (catches 'exceeded', 'too large', '75000' → 413). The POST handler combines auth() session lookup (!session?.user → 401 { error: 'Unauthorized' } bare envelope), Content-Length 413 pre-check, body parse with fault-tolerance (silently defaults cancelAtPeriodEnd = true), { subscriptionId } param resolution, the IDOR-protection chain (getCustomerId → 403, polar client extraction → 500, getPolarSubscription → 404), the load-bearing polarProvider.cancelSubscription(subscriptionId, cancelAtPeriodEnd) call, success payload { success: true, data: { id, status, cancelAtPeriodEnd, currentPeriodEnd, priceId, customerId }, message: <conditional> }, and TWO-string error-message-detection catch dispatching to 404 / 401 / 500 via safeErrorResponse(...). Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~10-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of eight candidate messages must appear; a 413-pre-check-not-triggered-on-unauth invariance walk; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; an IDOR-protection-chain-not-entered invariance walk pinning that NONE of the three IDOR-chain error messages may appear; a cancelSubscription-call-not-entered invariance walk; a catch-branch-error-message-detection-not-entered invariance walk). Cross-references the Polar webhook signature-verified POST sibling polar-webhook-body-spec.md, the Polar checkout POST sibling polar-checkout-body-spec.md (uses SAME (polarProvider as any).polar private-property-access pattern), the Polar subscription portal POST sibling polar-subscription-portal-body.spec.ts, the LemonSqueezy subscription-cancel POST sibling lemonsqueezy-cancel-body-spec.md (email-gated auth, THREE-key 401 envelope, code field; this Polar cancel spec uses bare 401 envelope and IDOR-protection), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 77-of-N and the tests/api/ per-spec-file sub-rollout extends to 75-of-many, and the first 413-pre-check + IDOR-protection-chain POST smoke lands -- pinning two FIRST contracts no prior smoke covers.
  • E2E LemonSqueezy Cancel Body Spec (apps/web-e2e/tests/api/lemonsqueezy-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy subscription-cancel POST body / header smoke spec paired with apps/web-e2e/tests/api/lemonsqueezy-cancel-body.spec.ts, the seventy-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-fourth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/lemonsqueezy/cancel/route.ts — the first per-source-file POST smoke the docs tree publishes that pins an email-gated auth contract -- !session?.user?.email (NOT !session?.user, !session?.user?.id, or !session?.user?.id?.); the FIRST per-source-file POST smoke gating on session email -- AND the first per-source-file POST smoke that pins a code field in the 401 envelope ({ error: 'Unauthorized', message: 'Authentication required', code: 'AUTH_REQUIRED' }; EVERY prior 401 envelope is at most TWO keys; this is the FIRST THREE-key 401 envelope with an enum-typed code field) AND the first per-source-file POST smoke that pins a timestamp field in the success AND catch envelopes (both branches add an ISO timestamp via new Date().toISOString()). Distinct from EVERY prior POST smoke: email-gated auth; THREE-key 401 envelope with code: 'AUTH_REQUIRED'; code field in 400 validation envelope (code: 'VALIDATION_ERROR'); FOUR-key catch envelope with code: 'CANCEL_FAILED' AND timestamp (the FIRST per-source-file POST smoke pinning a 4-key catch envelope); conditional success message based on cancelAtPeriodEnd flag ('Subscription will be cancelled at the end of the current period' vs 'Subscription cancelled immediately'); timestamp field in success AND catch envelopes; safeErrorMessage extracted into the catch envelope's message field (NOT into the error field as in stripe-checkout-body). The POST handler combines auth() session lookup (!session?.user?.email → 401 THREE-key envelope), JSON body parse via await request.json() AFTER auth gate (NO try/catch), cancelSubscriptionSchema.safeParse(body) (failure → 400 with code: 'VALIDATION_ERROR'), getOrCreateLemonsqueezyProvider() singleton, the load-bearing lemonsqueezy.cancelSubscription(subscriptionId, cancelAtPeriodEnd) call, conditional success message, FOUR-key success payload { success: true, data: <result>, message: <conditional>, timestamp: <ISO> }, and FOUR-key outer catch with code: 'CANCEL_FAILED' AND timestamp. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a THREE-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear; a no-VALIDATION_ERROR-or-CANCEL_FAILED-codes invariant; a no-timestamp-leak invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a validation-chain-not-entered invariance walk; a cancelSubscription-call-not-entered invariance walk; a catch-branch-four-key-envelope-not-echoed invariance walk). Cross-references the LemonSqueezy webhook signature-verified POST sibling lemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST sibling lemonsqueezy-checkout-body-spec.md (uses ENUM-typed error codes in the catch -- not in the 401 -- so this cancel route extends the same enum-typed-code pattern into the 401 envelope), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 76-of-N and the tests/api/ per-spec-file sub-rollout extends to 74-of-many, and the first email-gated + 3-key-401 + timestamp-fielded POST smoke lands -- pinning three FIRST contracts no prior smoke covers.
  • E2E Stripe Payment-Methods Create Body Spec (apps/web-e2e/tests/api/stripe-payment-methods-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-method-create-from-setup-intent POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-payment-methods-create-body.spec.ts, the seventy-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-third under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/payment-methods/create/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a Zod parse (NOT safeParse) contract -- createPaymentMethodSchema.parse(body) THROWS on validation failure and the outer catch detects error instanceof z.ZodError to dispatch a 400 envelope; EVERY prior per-source-file POST smoke uses safeParse to handle validation gracefully -- this is the FIRST throw-on-invalid Zod contract -- AND the first per-source-file POST smoke that pins a Stripe-error-echo contract in the outer catch (error instanceof Stripe.errors.StripeError → 400 { success: false, error: error.message } reflects the raw Stripe error message in the response; EVERY prior catch uses static-string messages). Distinct from EVERY prior POST smoke: Zod .parse(body) throwing; stripe-error-echo catch; multi-step Stripe SDK orchestration (setupIntents.retrieve → conditional customers.create → conditional paymentMethods.attach → conditional paymentMethods.update → conditional customers.update (default payment method) → re-retrieve -- the most complex stripe SDK orchestration in any per-source-file POST smoke); formatted response payload (success branch extracts { id, type, card: { brand, last4, exp_month, exp_year, funding } | null, created, metadata } -- NOT raw provider object, distinct from setup-intent and payment-intent which return raw). The POST handler combines auth() session lookup (!session?.user?.id → 401 { success: false, error: 'Unauthorized' }), JSON body parse via await request.json() AFTER auth gate, createPaymentMethodSchema.parse(body) Zod throwing parse, stripe.setupIntents.retrieve(setup_intent_id) lookup, setupIntent.status !== 'succeeded' check (→ 400), !setupIntent.payment_method check (→ 400), get-or-create customer via getUserStripeCustomerId / saveUserStripeCustomerId, conditional attach via paymentMethods.attach, conditional metadata update via paymentMethods.update, conditional default update via customers.update, re-retrieve via paymentMethods.retrieve, formatted success payload, and three-branch outer catch (z.ZodError → 400 with details, StripeError → 400 with error.message echoed, default → 500). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a canonical 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear; a Zod-throw-catch-not-entered invariance walk pinning that the 'Invalid request data' 400 with details must NEVER fire on unauth; a stripe-error-echo-catch-not-entered invariance walk pinning that no stripe error message can leak; a no-card-details-leak CRITICAL security invariant pinning that card.last4 / card.brand / etc. are NEVER exposed; a no-metadata-userId-spread-leak invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a multi-step-Stripe-orchestration-not-entered invariance walk; a catch-branch-generic-500-not-echoed invariance walk). Cross-references the Stripe setup-intent POST sibling stripe-setup-intent-body-spec.md (the source of the setup_intent_id consumed by this payment-methods/create POST), the Stripe payment-intent POST sibling stripe-payment-intent-body-spec.md, the Stripe checkout POST sibling stripe-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 75-of-N and the tests/api/ per-spec-file sub-rollout extends to 73-of-many, and the first throw-on-invalid Zod + stripe-error-echo POST smoke lands -- pinning two FIRST contracts no prior smoke covers and the most complex Stripe SDK orchestration in the rollout.
  • E2E Stripe Payment-Intent Body Spec (apps/web-e2e/tests/api/stripe-payment-intent-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-intent creation POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-payment-intent-body.spec.ts, the seventy-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-second under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/payment-intent/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a NO-body-validation contract (the handler destructures { amount, currency = 'usd', metadata, planId } and passes them straight to stripeProvider.createPaymentIntent(...) with NO if (!amount) check, NO Zod, NO type checking; EVERY prior POST smoke has at least one body-validation gate -- this is the FIRST trust-the-body POST contract), and the second per-source-file POST smoke that pins a raw payment-provider object as the success payload (after stripe-setup-intent-body-spec.md) -- the PaymentIntent's client_secret field is the same critical-leak vector. Distinct from the stripe-setup-intent sibling: body destructure with currency = 'usd' default; caller-controlled metadata: { userId, planId, ...metadata } spread (the caller's metadata.userId OVERRIDES the session userId because ...metadata spreads AFTER -- the spec pins this risk-surface metadata is NEVER reached on unauth); trust-the-body contract; GET sibling with ?payment_intent_id= query-param-required check. Distinct from EVERY prior POST smoke: NO-body-validation contract; bare 401 envelope { error: 'Unauthorized' } matches setup-intent (distinct from canonical { success: false, error } envelope); raw PaymentIntent object as success payload; caller-controlled metadata spread allows session userId override. The POST handler combines auth() session lookup (!session?.user → 401 { error: 'Unauthorized' }), JSON body parse via destructured await request.json() AFTER auth gate (NO try/catch, NO validation), getOrCreateStripeProvider() singleton, stripeProvider.getCustomerId(session.user) lookup (null → 400 'Failed to create customer'), the load-bearing stripeProvider.createPaymentIntent({ amount, currency, customerId, metadata: { userId, planId, ...metadata } }) call, raw PaymentIntent object as success payload, and outer catch 500 'Failed to create payment intent'. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~15-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of id / client_secret / amount / customer may appear; a gate-before-post-auth invariant; a no-PaymentIntent-client_secret-leak CRITICAL security invariant; a no-PaymentIntent-fields-leak invariant pinning the full set; a no-metadata-userId-spread-leak invariant pinning that caller-supplied metadata.userId (e.g. 'attacker_user_id') is NEVER echoed; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a createPaymentIntent-and-getCustomerId-not-entered invariance walk; a catch-branch-not-entered invariance walk). Cross-references the Stripe setup-intent POST sibling stripe-setup-intent-body-spec.md (zero-arg signature; this payment-intent uses body-destructure), the Stripe checkout POST sibling stripe-checkout-body-spec.md (TWO-key 401 envelope, NOT bare like this payment-intent spec), the Stripe webhook signature-verified POST sibling stripe-webhook-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 74-of-N and the tests/api/ per-spec-file sub-rollout extends to 72-of-many, and the first NO-body-validation POST smoke lands -- pinning the trust-the-body contract no prior smoke covers and the same CRITICAL client_secret-leak security invariant as setup-intent applied to PaymentIntent.
  • E2E Sponsor-Ads User [id] Renew Body Spec (apps/web-e2e/tests/api/sponsor-ads-user-id-renew-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-owned sponsor-ad renew POST body / header smoke spec paired with apps/web-e2e/tests/api/sponsor-ads-user-id-renew-body.spec.ts, the seventy-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventy-first under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/sponsor-ads/user/[id]/renew/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a swallow-and-continue body-parse contract (try { const body = await request.json(); successUrl = body.successUrl; cancelUrl = body.cancelUrl; } catch { /* Body is optional */ } — malformed JSON OR missing body silently leaves successUrl / cancelUrl as undefined; FIRST swallow-and-continue contract in the rollout, distinct from the sibling cancel route's silent-coalesce-to-{} pattern). Also the first per-source-file POST smoke pinning TWO open-redirect-validated URLs in the SAME body (successUrl AND cancelUrl, each through validateRedirectUrl comparing protocol, hostname, AND port against appUrl) AND a multi-provider switch dispatch with default-case 400 (Stripe / LemonSqueezy / Polar / default → 400 'Payment configuration is incomplete. Please contact support.') AND a state-machine 400 branch with status interpolation (Cannot renew sponsor ad with status: ${sponsorAd.status}. Only active or expired ads can be renewed.; whitelist gate renewableStatuses = [ACTIVE, EXPIRED]). The smoke spec pins a canonical 401 { success: false, error: 'Unauthorized' } two-key envelope, a strict envelope-shape assertion, gate-before-post-auth / -ownership / -state-machine / -provider-switch / -outer-catch invariants, swallow-and-continue body-parse fault-tolerance, a CRITICAL no-attacker-URL-leak invariant pinning that caller-supplied successUrl / cancelUrl open-redirect attacker URLs are NEVER echoed in the unauth response or in any redirect-style header, a no-dangerous-URL-pseudo-protocol-leak invariant pinning that javascript: / data: / file: / protocol-relative //host URLs are NEVER echoed, a status-interpolation walk pinning that XSS-shaped caller-supplied status values are NEVER reflected, a side-channel walk, and a cross-method probe (GET / PUT / PATCH / DELETE) — pinning the swallow-and-continue + TWO-URL open-redirect-prevention sponsor-ad renew contract and CRITICAL no-attacker-URL-leak security invariant. Completes the user-owned sponsor-ad action POST pair with the sibling cancel route covered by sponsor-ads-user-id-cancel-body-spec.md. Cross-references the sibling sponsor-ads checkout POST smoke sponsor-ads-checkout-body-spec.md (similar multi-provider dispatch but for ad CREATION rather than RENEWAL), the public sponsor-ads list smoke sponsor-ads-public.spec.ts, and to Spec 010 -- E2E Test Coverage and Spec 004 -- Payment Providers for the governing specs. With this entry the per-spec-file docs rollout extends to 73-of-N and the tests/api/ per-spec-file sub-rollout extends to 71-of-many, and the first swallow-and-continue body-parse + TWO-URL open-redirect-prevention POST smoke lands -- pinning four FIRST contracts no prior smoke covers and completing the user-owned sponsor-ad action POST pair.
  • E2E Stripe Setup-Intent Body Spec (apps/web-e2e/tests/api/stripe-setup-intent-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe setup-intent creation POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-setup-intent-body.spec.ts, the seventy-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventieth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/setup-intent/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a zero-argument POST() handler signature (no request parameter at all; EVERY prior POST smoke takes either a NextRequest or Request parameter; this is the FIRST zero-arg POST contract in the rollout), and the first per-source-file POST smoke that pins a raw payment-provider object as the success payload (return NextResponse.json(setupIntent) returns the Stripe SetupIntent object verbatim, NO { success, data, message } wrapper envelope; this makes the spec's no-client_secret-leak assertion a CRITICAL security invariant -- a regression that ran createSetupIntent(...) before the auth gate would expose the SetupIntent's client_secret field, giving any caller the ability to attach a payment method to the fabricated customer). Distinct from EVERY prior POST smoke: zero-argument POST() signature; bare 401 envelope { error: 'Unauthorized' } (UNIQUE -- distinct from stripe-checkout's TWO-key envelope and the canonical { success: false, error } envelope); !session?.user gate (matches stripe-checkout); raw provider-object success payload (no wrapper envelope); single-line catch ({ error: 'Failed to create setup intent' } 500); only one load-bearing call (stripeProvider.createSetupIntent). The POST handler combines auth() session lookup (!session?.user → 401 { error: 'Unauthorized' }), getOrCreateStripeProvider() singleton initialization AFTER the auth gate, the load-bearing stripeProvider.createSetupIntent(session.user) call (NO body parse, NO body validation), the raw SetupIntent object as success payload, and outer catch 500 { error: 'Failed to create setup intent' }. Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~10-body bulk-loop walk -- all bodies IGNORED by zero-arg handler -- all asserting < 500; a bare 401-envelope assertion { error: 'Unauthorized' }; a strict envelope-shape assertion -- exactly error key, no success/message/data leak; a gate-before-post-auth invariant; a no-SetupIntent-client_secret-leak CRITICAL security invariant; a no-SetupIntent-fields-leak invariant pinning the full set of fields -- id / client_secret / status / usage / customer / created -- as forbidden on unauth; a body-IGNORED invariance walk pinning that the zero-arg handler ignores body content; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a createSetupIntent-and-provider-not-entered invariance walk; a catch-branch-not-entered invariance walk). Cross-references the Stripe checkout POST sibling stripe-checkout-body-spec.md (TWO-key 401 envelope, NOT bare like this setup-intent spec), the Stripe webhook signature-verified POST sibling stripe-webhook-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 72-of-N and the tests/api/ per-spec-file sub-rollout extends to 70-of-many, and the first zero-arg POST + raw-provider-object payload smoke lands -- pinning the simplest auth-gated POST contract in the rollout and a CRITICAL no-client_secret-leak security invariant.
  • E2E Sponsor-Ads User [id] Cancel Body Spec (apps/web-e2e/tests/api/sponsor-ads-user-id-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-owned sponsor-ad cancel POST body / header smoke spec paired with apps/web-e2e/tests/api/sponsor-ads-user-id-cancel-body.spec.ts, the seventy-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-ninth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/sponsor-ads/user/[id]/cancel/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a body-parse-fault-tolerant contract via await request.json().catch(() => ({})) ?? {} -- malformed JSON OR null body OR empty body silently coalesces to {} (NO 400 for malformed JSON); EVERY prior POST smoke either has a per-call try/catch returning 400 OR no try/catch at all -- this is the FIRST silent-coalesce contract -- and the first per-source-file POST smoke that pins a conditional Zod validation contract (cancelSponsorAdSchema.omit({ id: true }).safeParse(body) runs unconditionally but the 400-rejection only fires if validation FAILS AND body.cancelReason !== undefined). Distinct from EVERY prior POST smoke: (a) Body-parse-fault-tolerant contract await request.json().catch(() => ({})) ?? {}; (b) Conditional Zod validation with .omit({ id: true }) -- the FIRST per-source-file POST smoke pinning a schema-omit + conditional-validation contract; (c) Default-fallback string for cancelReason (parsed.data.cancelReason?.trim() || 'Cancelled by user'); (d) THREE-branch outer catch with mixed exact-string + substring detection -- error.message === 'Sponsor ad not found' → 404 (exact); error.message.includes('Cannot cancel') → 400 (substring); default → 500 -- the FIRST per-source-file POST smoke pinning a mixed-detection catch dispatcher. The POST handler combines auth() session lookup (!session?.user?.id → 401 { success: false, error: 'Unauthorized' }), { id } param resolution via dynamic-segment route, body parse with silent coalesce (await request.json().catch(() => ({})) ?? {}), cancelSponsorAdSchema.omit({ id: true }).safeParse(body) with conditional 400 rejection, cancelReason default fallback to 'Cancelled by user', sponsorAdService.getSponsorAdById(id) lookup (null → 404 'Sponsor ad not found'), ownership verification (sponsorAd.userId !== session.user.id → 403), the load-bearing sponsorAdService.cancelSponsorAd(id, cancelReason) call (!cancelledAd → 500 'Failed to cancel sponsor ad'), success payload { success: true, data: <cancelledAd>, message: 'Sponsor ad cancelled successfully' }, and three-branch outer catch. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~12-body bulk-loop walk all asserting < 500; a canonical one-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear in any unauth response; a silent-coalesce-body-parse-without-400 invariance walk pinning that malformed JSON does NOT produce an 'Invalid JSON' 400; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a conditional-Zod-validation-not-entered invariance walk pinning that over-length / wrong-type cancelReason must NEVER produce validation messages on unauth; an ownership-and-cancelSponsorAd-not-entered invariance walk; a three-branch-outer-catch-not-entered invariance walk; a no-cancelReason-leak assertion pinning that XSS-shaped values must NEVER be echoed). Cross-references the sibling sponsor-ads checkout POST smoke sponsor-ads-checkout-body-spec.md (uses the SAME sponsorAdService.getSponsorAdById(...) service), the public sponsor-ads list smoke sponsor-ads-public.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 71-of-N and the tests/api/ per-spec-file sub-rollout extends to 69-of-many, and the first silent-coalesce body-parse + conditional-Zod + three-branch-catch POST smoke lands -- pinning four FIRST contracts no prior smoke covers.
  • E2E Auth Change-Password Body Spec (apps/web-e2e/tests/api/auth-change-password-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's rate-limit-FIRST password-change POST body / header smoke spec paired with apps/web-e2e/tests/api/auth-change-password-body.spec.ts, the seventieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-eighth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/auth/change-password/route.ts — the first per-source-file POST smoke the docs tree publishes that pins a rate-limit-FIRST gate posture -- the rate-limit check fires BEFORE the auth gate (the handler calls ratelimit('change-password:<clientIP>', 5, 15 minutes) as the FIRST gate, then runs auth(), then Zod validation, then a multi-stage post-auth chain -- tenant / user / OAuth / bcrypt-current / bcrypt-duplicate); EVERY prior per-source-file POST smoke pins auth as the first gate, this is the FIRST rate-limit-before-auth contract in the rollout. The companion minimal spec auth-change-password.spec.ts pins only the < 500 no-server-error contract; this spec drills into the body / header surface with detailed invariants. Distinct from EVERY prior per-source-file POST smoke: (a) Rate-limit-FIRST gate posture -- returns 429 { success: false, error: 'Too many password change attempts. Please try again later.', retryAfter: <seconds> }; the FIRST per-source-file POST smoke that pins a retryAfter field in the response body; (b) 'Unauthorized. Please sign in.' 401 message -- UNIQUE imperative-phrased 401 envelope distinct from all prior 401 messages; (c) OAuth-account check -- !user.passwordHash → 400 'Password change not available for OAuth accounts. Please contact support.'; the FIRST per-source-file POST smoke that pins an OAuth-account-restriction contract; (d) Dual bcrypt.compare gates -- current-password verification AND duplicate-password prevention; the FIRST per-source-file POST smoke that pins a dual bcrypt.compare contract; (e) Cross-field Zod validation via .refine -- the changePasswordSchema uses .refine to check newPassword === confirmPassword; the FIRST per-source-file POST smoke that pins a cross-field validation contract; (f) Email-send fault tolerance -- the sendPasswordChangeConfirmationEmail(...) call is wrapped in a try/catch that does NOT fail the password change. The POST handler combines a rate-limit gate FIRST (ratelimit('change-password:<ip>', 5, 15 minutes) → 429), auth() session lookup (!session?.user?.id → 401 { success: false, error: 'Unauthorized. Please sign in.' }), Zod safeParse(body) with cross-field .refine (failure → 400 { success: false, error: 'Invalid input data', details: <zod issues> }), getTenantId() resolution (null → 403 'Tenant not found'), user lookup by id AND tenantId (not found → 404 'User not found'), OAuth-account check (!user.passwordHash → 400), bcrypt.compare(currentPassword, hash) (false → 400 'Current password is incorrect'), bcrypt.compare(newPassword, hash) for duplicate detection (true → 400 'New password must be different from current password'), the load-bearing bcrypt.hash(newPassword, 12) + db.update(users) write, the fault-tolerant sendPasswordChangeConfirmationEmail(...) side-effect, success payload { success: true, message: 'Password changed successfully' }, and outer catch 500 { success: false, error: 'Internal server error. Please try again later.' }. Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk -- including X-Forwarded-For and X-Real-Ip for rate-limit -- + a ~13-body bulk-loop walk all asserting < 500; an imperative-phrased 401-envelope assertion { success: false, error: 'Unauthorized. Please sign in.' }; a strict envelope-shape assertion -- exactly success + error keys, no details leak on 401; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the eight candidate static messages must appear in any unauth response; a 429-envelope-includes-retryAfter assertion when rate-limit fires; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a Zod-validation-chain-not-entered invariance walk; a bcrypt-compare-gates-not-entered invariance walk; an OAuth-account-check-and-db-update-and-email-send-not-entered invariance walk pinning that the unauth branch must NEVER reach the password-write logic). Cross-references the companion minimal smoke auth-change-password.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 70-of-N and the tests/api/ per-spec-file sub-rollout extends to 68-of-many, and the first rate-limit-FIRST POST smoke lands -- pinning a gate posture distinct from every prior auth-FIRST POST smoke and adding a retryAfter-field 429 envelope contract no prior smoke covers.
  • E2E Item Comments [commentId] Method Spec (apps/web-e2e/tests/api/item-comments-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-comment edit / delete PUT + DELETE method / body / header smoke spec paired with apps/web-e2e/tests/api/item-comments-id-method.spec.ts, the sixty-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-seventh under apps/web-e2e/tests/api/. Pairs with the PUT and DELETE exports of apps/web/app/api/items/[slug]/comments/[commentId]/route.ts — the first per-source-file PUT + DELETE smoke the docs tree publishes for a public per-comment edit / delete route, and the first per-source-file PUT or DELETE smoke that pins a plain-text 401 envelope instead of a JSON one (the unauth branches return new NextResponse('Unauthorized', { status: 401 }) -- a plain-text body, NOT JSON; EVERY prior per-source-file mutating-method smoke pins a JSON 401 envelope; this is the FIRST plain-text 401 contract in the rollout). Distinct from the comment-create POST sibling item-comments-create-body-spec.md: plain-text 401 envelope (NOT JSON like comments POST); plain-text 404 / 403 envelopes for client-profile / tenant errors (NOT JSON); MIXED-envelope contract -- auth / profile / tenant errors return PLAIN-TEXT, body-validation errors (PUT only) return JSON with { error } (the FIRST per-source-file smoke pinning a mixed plain-text + JSON envelope contract on the same handler); three-step ownership chain -- PUT and DELETE both call checkDatabaseAvailability() first, then auth(), then getClientProfileByUserId(...), then getTenantId(), then a Drizzle query that filters by userId === clientProfile.id AND tenantId === <user's tenant> AND deletedAt IS NULL (single query); DELETE returns 204 No Content (NOT 200 with a body); PUT body validation content === undefined && rating === undefined → 400 JSON { error: 'At least one of content or rating must be provided' } (the FIRST per-source-file PUT smoke pinning a partial-update validation contract). Documents the at-a-glance scenario tree (a doubled header walk -- ~10 headers × 2 methods = ~20 tests; a PUT body walk -- ~12 bodies; a canonical plain-text 401 envelope assertion on PUT; a canonical plain-text 401 envelope assertion on DELETE; a no-JSON-prefix invariant for unauth bodies on both methods; a gate-before-post-auth invariant on PUT pinning that NONE of five candidate messages -- mixed plain + JSON -- must appear in any unauth response; a gate-before-post-auth invariant on DELETE across four candidate messages; a parameterised-vs-baseline status-stability comparison; a cross-method probe (POST / PATCH); a malformed-JSON-body invariance walk for PUT; a body-validation-chain-not-entered invariance walk for PUT pinning that the JSON 'At least one of content or rating' 400 must NEVER fire on unauth; a Drizzle-ownership-query-not-entered invariance walk for both methods; an updateComment-and-deleteComment-not-entered invariance walk pinning that DELETE must NEVER return 204 and PUT must NEVER return a comment payload). Cross-references the companion comment-create POST sibling item-comments-create-body-spec.md, the public per-item comment-list GET smoke item-comments-query.spec.ts, the per-comment rating sibling item-comment-rating-by-id.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 69-of-N and the tests/api/ per-spec-file sub-rollout extends to 67-of-many, and the first per-source-file PUT + DELETE smoke pinning a plain-text 401 envelope lands -- expanding the rollout's mutating-method coverage beyond the JSON-envelope family for the first time and pinning the mixed plain-text + JSON envelope contract no prior smoke covers.
  • E2E Sponsor-Ads Checkout Body Spec (apps/web-e2e/tests/api/sponsor-ads-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated multi-provider sponsor-ad checkout-session creation POST body / header smoke spec paired with apps/web-e2e/tests/api/sponsor-ads-checkout-body.spec.ts, the sixty-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-sixth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/sponsor-ads/checkout/route.ts — the first per-source-file POST smoke for an auth-gated MULTI-PROVIDER dispatching checkout endpoint the docs tree publishes, extending the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe) into a quintet by adding a checkout endpoint that is NOT tied to a single provider but instead switch-dispatches to all three providers based on process.env.NEXT_PUBLIC_PAYMENT_PROVIDER. Distinct from ALL FOUR siblings in the checkout quartet: (a) Multi-provider switch dispatch -- the handler switch (ACTIVE_PAYMENT_PROVIDER) between PaymentProvider.STRIPE, LEMONSQUEEZY, and POLAR, falling through to a 400 default on unknown providers; the FIRST per-source-file POST smoke that pins a three-way provider dispatch via env var; (b) success: false envelope on every error branch -- distinct from the quartet's two-key { error, message } envelopes; sponsor-ads/checkout returns { success: false, error } on every error branch (matching the admin endpoints' shape); (c) Open-redirect validation -- the handler calls validateRedirectUrl(successUrl) and validateRedirectUrl(cancelUrl) to reject cross-origin URLs at the application boundary; the FIRST per-source-file POST smoke that pins an open-redirect-prevention contract: any XSS-shaped or cross-origin URL is silently replaced with a default ${appUrl}/sponsor/... route, with a console.warn (no echo on the wire); (d) Three-stage post-auth gate stack (404 → 403 → 400) -- after !session?.user?.id → 401, the handler runs sponsorAdService.getSponsorAdById → 404, then sponsorAd.userId !== session.user.id → 403 (UNIQUE -- no other checkout has a forbidden branch), then sponsorAd.status !== SponsorAdStatus.PENDING_PAYMENT → 400; (e) getPriceId(interval, provider) map -- the handler maps (WEEKLY | MONTHLY) × (STRIPE | LEMONSQUEEZY | POLAR) to env-driven price IDs and short-circuits to a generic 400 if the price is not configured; the FIRST per-source-file POST smoke that pins a 2×3 price-matrix lookup; (f) !session?.user?.id gate (matches lemonsqueezy; distinct from polar + solidgate + stripe's !session?.user); (g) Generic 500 on outer catch -- distinct from stripe's three-key envelope and solidgate's safeErrorMessage extraction; sponsor-ads/checkout returns the generic { success: false, error: 'Failed to create checkout session' } on every catch (no detail leak); (h) POST-only export -- distinct from the quartet (which all export GET + POST); sponsor-ads/checkout only exports POST, so GET joins PUT / PATCH / DELETE in the cross-method walk. The POST handler combines auth() session lookup (load-bearing first gate; !session?.user?.id → 401 { success: false, error: 'Unauthorized' }), JSON body parse via destructured await request.json() AFTER the auth gate (NO per-call try/catch), !sponsorAdId → 400, sponsorAdService.getSponsorAdById(sponsorAdId) lookup (null → 404), sponsorAd.userId !== session.user.id → 403, sponsorAd.status !== SponsorAdStatus.PENDING_PAYMENT → 400 with current-status echo, getPriceId(interval, provider) lookup (null → 400 'Payment configuration is incomplete. Please contact support.'), validateRedirectUrl(successUrl / cancelUrl) open-redirect prevention (invalid URLs replaced with default ${appUrl}/sponsor/{success|cancel} routes), the switch-on-ACTIVE_PAYMENT_PROVIDER dispatch (STRIPEcreateStripeCheckout, LEMONSQUEEZYcreateLemonSqueezyCheckout, POLARcreatePolarCheckout, default → 400), !checkoutResult.url → 500 'Failed to create checkout URL', success payload { success: true, data: { checkoutId, checkoutUrl, provider }, message: 'Checkout session created successfully' }, and outer catch { success: false, error: 'Failed to create checkout session' } with status 500 (generic message, no detail leak). Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a canonical success-false 401-envelope assertion { success: false, error: 'Unauthorized' }; a strict envelope-shape assertion -- exactly success + error keys, success: false discriminant, no message/data/details leak; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the seven candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a sponsorAdId-required-validation-not-entered invariance walk; an ownership/status/not-found-checks-not-entered invariance walk; a provider-switch-dispatch-not-entered invariance walk pinning that all three provider branches must NEVER produce a data.checkoutUrl on the unauth branch; a catch-branch-not-entered invariance walk; a no-redirect-leak assertion pinning that XSS-shaped successUrl / cancelUrl values must NEVER be echoed; a provider-name non-disclosure assertion pinning that data.provider and the literal strings 'stripe'/'lemonsqueezy'/'polar' must NEVER appear). Cross-references the four sibling auth-gated checkout POST smokes solidgate-checkout-body-spec.md, polar-checkout-body-spec.md, lemonsqueezy-checkout-body-spec.md, and stripe-checkout-body-spec.md, the public read-side counterpart sponsor-ads-public.spec.ts, the five admin write-side counterparts under admin/sponsor-ads/, the multi-provider sibling payment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 68-of-N and the tests/api/ per-spec-file sub-rollout extends to 66-of-many, and the first per-source-file POST smoke for an auth-gated MULTI-PROVIDER dispatching checkout endpoint lands -- extending the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe) into a quintet and pinning the multi-provider switch dispatch + open-redirect validation + three-stage post-auth gate stack contract that distinguishes sponsor-ads/checkout from its four siblings.
  • E2E Stripe Checkout Body Spec (apps/web-e2e/tests/api/stripe-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated Stripe checkout-session creation POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-checkout-body.spec.ts, the sixty-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-fifth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/checkout/route.ts — the fourth and final per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes, completing the auth-gated checkout quartet (after solidgate-checkout-body-spec.md, polar-checkout-body-spec.md, and lemonsqueezy-checkout-body-spec.md). Distinct from ALL three siblings: (a) Three-way mode ternary mapping -- mode === 'one_time' ? 'payment' : mode === 'subscription' ? 'subscription' : 'setup' -- UNIQUE: unknown mode values fall through to the 'setup' Stripe mode (Setup Intent flow); distinct from polar's two-way subscription/one_time dispatch and lemonsqueezy/solidgate which take whatever mode the body provides; (b) Trial-amount validation -- hasTrial = trialPeriodDays > 0 && isAuthorizedTrialAmount; if hasTrial && !trialAmountId → 400 { error: 'Invalid trial configuration', message: 'trialAmountId is required when trial is enabled' }; the FIRST per-source-file POST smoke that pins a trial-config validation contract; (c) Helper-function pipeline -- the handler chains buildCheckoutLineItems(...), createBaseCheckoutParams(...), and applySubscriptionConfig(...) from the co-located ./helpers module; the FIRST per-source-file POST smoke that pins a multi-helper assembly pipeline; (d) safeErrorMessage (NOT safeErrorResponse) in catch -- the outer catch uses safeErrorMessage(error, 'Failed to create checkout session') to extract the message and wraps it in a manual 500 envelope with { error, message, details: <dev-only-stack> }; distinct from polar's safeErrorResponse(...) and solidgate's safeErrorMessage(...) -- stripe-checkout returns THREE keys on catch (matching solidgate's success-branch shape); (e) Stripe SDK direct call -- the handler calls stripe.checkout.sessions.create(checkoutParams) via stripeProvider.getStripeInstance(); the FIRST per-source-file POST smoke that pins a direct-SDK-instance access contract via a public method (NOT private property as any like polar's one_time branch); (f) !session?.user gate (matches polar + solidgate; distinct from lemonsqueezy's !session?.user?.id). The POST handler combines auth() session lookup (load-bearing first gate; missing → 401 { error: 'Unauthorized', message: 'Authentication required' }), getOrCreateStripeProvider() + getStripeInstance(), JSON body parse via destructured await request.json() AFTER the auth gate (NO per-call try/catch), the three-way mode ternary mapping, stripeProvider.getCustomerId(session.user) lookup (failure → 400 'Failed to create customer'), the trial-config validation, the helper-function pipeline, the load-bearing stripe.checkout.sessions.create(checkoutParams) Stripe SDK call, success payload { data: { id, url }, status: 200, message: 'Checkout session created successfully' }, and outer catch with safeErrorMessage(error, 'Failed to create checkout session') and dev-only stack-trace details (500 with three-key envelope). Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~16-body bulk-loop walk all asserting < 500; a canonical two-key 401-envelope assertion; a strict envelope-shape assertion -- exactly error + message keys, no success or details leak; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a trial-config-validation-not-entered invariance walk; a mode-ternary-not-entered invariance walk pinning that all three mode branches must NEVER produce a data.url on the unauth branch; a helper-pipeline-and-stripe-SDK-not-entered invariance walk; a catch-branch-not-entered invariance walk pinning that details (dev-only stack) must NEVER appear on the unauth branch; a no-redirect-leak assertion pinning that XSS-shaped successUrl / cancelUrl values must NEVER be echoed). Cross-references the first auth-gated checkout POST smoke solidgate-checkout-body-spec.md, the second polar-checkout-body-spec.md, the third lemonsqueezy-checkout-body-spec.md, the Stripe webhook companion stripe-webhook-body-spec.md (signature-verified webhook on same provider, completely different gate posture), the multi-provider sibling payment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 67-of-N and the tests/api/ per-spec-file sub-rollout extends to 65-of-many, and the fourth and final per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- completing the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe) and pinning the three-way mode ternary + trial-config validation + helper-function pipeline contract that distinguishes Stripe from its three siblings.
  • E2E LemonSqueezy Checkout Body Spec (apps/web-e2e/tests/api/lemonsqueezy-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated LemonSqueezy checkout-session creation POST body / header smoke spec paired with apps/web-e2e/tests/api/lemonsqueezy-checkout-body.spec.ts, the sixty-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-fourth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/lemonsqueezy/checkout/route.ts — the third per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes (after solidgate-checkout-body-spec.md and polar-checkout-body-spec.md). Distinct from BOTH siblings: (a) !session?.user?.id gate -- NOT !session?.user like polar and solidgate; pins the user-id-required 401 contract; (b) Custom validator returning { isValid, errors[] } -- the handler calls validateCheckoutRequestBody(body) from @/lib/payment/config/validation (NOT Zod safeParse like solidgate; NOT simple if (!field) like polar); errors joined with ', '; the FIRST per-source-file POST smoke that pins a custom-validator contract; (c) Per-call try/catch around await request.json() with explicit 'Invalid JSON in request body' 400 envelope (matches solidgate; distinct from polar which has NO try/catch); (d) Dev-only PII-sanitized console.log -- in NODE_ENV === 'development' the handler logs the truncated email, redacted custom price, and dark-mode flag; the FIRST per-source-file POST smoke that pins a PII-sanitized dev-only logging contract; (e) FOUR-string-scan catch with THREE different status codes -- 'Missing required environment variables' → 500 CONFIGURATION_ERROR; 'Invalid email format' → 400 VALIDATION_ERROR; 'Custom price must be' → 400 VALIDATION_ERROR; 'Lemonsqueezy' → 503 PAYMENT_SERVICE_ERROR; the FIRST per-source-file POST smoke that pins a four-string error-message-detection chain spanning 400 / 500 / 503; (f) ERROR_TYPES enum-typed error field -- uses constants from @/lib/payment/config/types (VALIDATION_ERROR, CONFIGURATION_ERROR, PAYMENT_SERVICE_ERROR, INTERNAL_ERROR); UNIQUE: the FIRST per-source-file POST smoke that pins enum-typed error codes; (g) GET export with NO auth gate -- the GET handler reads query params and creates checkouts WITHOUT auth -- a Q-010-style finding; the cross-method probe pins this divergence from POST; (h) success: true discriminant in success payload -- distinct from polar + solidgate which use literal status: 200. The POST handler combines auth() session lookup (load-bearing first gate; !session?.user?.id → 401 { error: 'Unauthorized', message: 'Authentication required' }), JSON body parse via await request.json() INSIDE per-call try/catch (failure → 400 { error: 'VALIDATION_ERROR', message: 'Invalid JSON in request body' }), getOrCreateLemonsqueezyProvider() singleton initialization, validateCheckoutRequestBody(body) (failure → 400 with joined errors), dev-only PII-sanitized console.log, the load-bearing lemonsqueezyProvider.createCustomCheckout({ email, customPrice, variantId, metadata, dark }) call, success payload { success: true, data: { checkoutUrl, email, customPrice, variantId, metadata }, message: 'Checkout session created successfully' }, and outer catch with FOUR error-message-scan branches. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~17-body bulk-loop walk all asserting < 500; a canonical two-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the five candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion across Unauthorized and four ERROR_TYPES codes; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a JSON-parse-failure-AFTER-auth-gate invariance walk pinning that the unauth branch must produce 401 even with malformed JSON; a validation-chain-not-entered invariance walk; a createCustomCheckout-not-entered invariance walk; a four-string-scan-catch-not-entered invariance walk pinning that NONE of CONFIGURATION_ERROR / PAYMENT_SERVICE_ERROR / INTERNAL_ERROR may appear on the unauth branch). Cross-references the first auth-gated checkout POST smoke solidgate-checkout-body-spec.md, the second polar-checkout-body-spec.md, the LemonSqueezy webhook companion lemonsqueezy-webhook-body-spec.md (signature-verified webhook on same provider, completely different gate posture), the LemonSqueezy list sibling lemonsqueezy-list-query.spec.ts, the multi-provider sibling payment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 66-of-N and the tests/api/ per-spec-file sub-rollout extends to 64-of-many, and the third per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- expanding the rollout's payment-provider checkout coverage from two providers (Solidgate + Polar) to three (Solidgate + Polar + LemonSqueezy) and pinning the custom-validator + four-string-scan catch + enum-typed-error-codes contract no prior auth-gated payment-provider POST smoke covers.
  • E2E Polar Checkout Body Spec (apps/web-e2e/tests/api/polar-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated Polar checkout-session creation POST body / header smoke spec paired with apps/web-e2e/tests/api/polar-checkout-body.spec.ts, the sixty-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-third under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/polar/checkout/route.ts — the second per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes (after solidgate-checkout-body-spec.md). Distinct from solidgate-checkout: (a) Branching mode dispatch -- the handler branches on mode === 'subscription' (default, calls polarProvider.createSubscription(...)) vs mode === 'one_time' (calls private polar.checkouts.create(...) via (polarProvider as any).polar); the FIRST per-source-file POST smoke that pins a mode-dispatched two-branch POST contract; (b) NO Zod validation -- distinct from solidgate which uses checkoutSchema.safeParse(json); polar uses simple if (!productId) check; (c) NO try/catch around request.json() -- malformed JSON cascades to the OUTER catch (distinct from solidgate's per-call try/catch); (d) 503 error-message detection -- outer catch scans error.message for three strings ('Payments are currently unavailable', 'needs to complete their payment setup', 'payment setup incomplete') and downgrades 500 → 503 with a custom payment-setup-incomplete message; the FIRST per-source-file POST smoke that pins a 503-via-error-message-scan contract; (e) Private property access via as any -- the 'one_time' branch reaches into (polarProvider as any).polar and .organizationId; the FIRST per-source-file POST smoke that pins a private-property-bypass contract; (f) GET export companion -- the route exports GET for retrieve-checkout-by-id (unauth GET → 401 ONE-key envelope, distinct from POST's TWO-key envelope). The POST handler combines auth() session lookup (load-bearing first gate; missing → 401 { error: 'Unauthorized', message: 'Authentication required' }), getOrCreatePolarProvider() singleton initialization, JSON body parse via destructured await request.json() AFTER the auth gate (NO per-call try/catch), productId required check (if (!productId) → 400 { error: 'Invalid request', message: 'Product ID is required' }), polarProvider.getCustomerId(session.user) lookup (failure → 400 { error: 'Failed to create customer', message: 'Unable to create Polar customer' }), the mode dispatch (subscription default OR one_time branch with private-property access), success payload { data: { id, url }, status: 200, message: 'Checkout session created successfully' }, and outer catch with payment-setup-incomplete-string scan (downgrades 500 → 503 with custom message) or safeErrorResponse(error, 'Failed to create checkout session'). Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a canonical two-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of data and literal status: 200 may appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the six candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a productId-required-check-not-entered invariance walk; a mode-dispatch-not-entered invariance walk pinning that the unauth response must NEVER echo a data.url or data.id; a 503-payment-setup-incomplete-not-triggered-on-unauth invariance walk; a no-redirect-leak assertion pinning that XSS-shaped successUrl / cancelUrl values must NEVER be echoed). Cross-references the first auth-gated checkout POST smoke solidgate-checkout-body-spec.md, the Polar webhook companion polar-webhook-body-spec.md (signature-verified webhook on the same provider, completely different gate posture), the Polar subscription portal POST sibling polar-subscription-portal-body.spec.ts (single-key 401 envelope), the multi-provider sibling payment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 65-of-N and the tests/api/ per-spec-file sub-rollout extends to 63-of-many, and the second per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- expanding the rollout's payment-provider checkout coverage from a single drilled-into provider (Solidgate) to two (Solidgate + Polar) and pinning the mode-dispatched + 503-via-error-message-scan contract no prior auth-gated payment-provider POST smoke covers.
  • E2E Stripe Webhook Body Spec (apps/web-e2e/tests/api/stripe-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-provider webhook POST body / header smoke spec paired with apps/web-e2e/tests/api/stripe-webhook-body.spec.ts, the sixty-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-second under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/stripe/webhook/route.ts — the fourth and final per-source-file webhook POST smoke the docs tree publishes, completing the four-provider webhook quartet (after polar-webhook-body-spec.md, lemonsqueezy-webhook-body-spec.md, and solidgate-webhook-body-spec.md). This is the simplest of the four handlers: (a) Single-header signature check via stripe-signature -- unique header name distinct from polar (webhook-signature), lemonsqueezy (x-signature), and solidgate (x-signature || solidgate-signature); (b) NO JSON parse -- the handler reads the raw body via await request.text() and passes it as a STRING directly to stripeProvider.handleWebhook(body, signature) (matches lemonsqueezy; distinct from polar and solidgate which parse via JSON.parse(body)); (c) NO validateWebhookPayload check -- distinct from polar's 4-tier chain; (d) NO idempotency check -- distinct from solidgate's in-memory Set<string> tracker; (e) NO event-type-string-fallback in the switch dispatcher -- matches ONLY the WebhookEventType enum values (8 mapped + the UNIQUE BILLING_PORTAL_SESSION_UPDATED Stripe-specific event = 9 cases; distinct from solidgate which accepts both enum AND lowercase strings); (f) BILLING_PORTAL_SESSION_UPDATED in switch -- UNIQUE Stripe-specific event handler that NO other webhook smoke covers; (g) POST-only export -- matches polar and lemonsqueezy; distinct from solidgate which also exports a documentation GET handler; (h) Same 400-default catch as all three siblings -- outer catch returns 400 (NOT 500) via raw NextResponse.json({ error: 'Webhook processing failed' }, { status: 400 }). The POST handler combines a raw-body read via await request.text(), a stripe-signature header presence check (missing → 400 { error: 'No signature provided' }), stripeProvider.handleWebhook(body, signature) for the load-bearing signature-verification call (receives the RAW body STRING, not a parsed object), a !webhookResult.received check (400 { error: 'Webhook not processed' }), the switch-statement event dispatcher (9 event types matched on WebhookEventType enum values ONLY -- no string fallback), success payload { received: true } with status 200, and outer catch console.error + 400 { error: 'Webhook processing failed' }. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~11-body bulk-loop walk all asserting < 500; a first-gate signature-header-presence-rejection assertion { error: 'No signature provided' }; a strict envelope-shape assertion Object.keys(body) === ['error'] across all rejection branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant pinning that NO unhandled error escapes as 5xx; an allowed-pre-delivery-error static-string allow-list assertion -- 3-message set; a sibling-provider-headers-ignored assertion pinning that polar / lemonsqueezy / solidgate signature headers do NOT satisfy the Stripe gate; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a signature-verification-call-gated-by-header-check invariant; a switch-statement-dispatcher-gated-by-signature-verification invariant pinning that invalid signatures must NEVER trigger any of the 9 event handlers including the Stripe-unique BILLING_PORTAL_SESSION_UPDATED case). Cross-references the first webhook POST smoke polar-webhook-body-spec.md, the second lemonsqueezy-webhook-body-spec.md, the third solidgate-webhook-body-spec.md, the multi-provider sibling webhooks.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 64-of-N and the tests/api/ per-spec-file sub-rollout extends to 62-of-many, and the fourth and final per-source-file webhook POST smoke lands -- completing the four-provider webhook quartet (Polar + LemonSqueezy + Solidgate + Stripe) and pinning the simplest webhook handler contract that distinguishes Stripe from its three siblings.
  • E2E Solidgate Checkout Body Spec (apps/web-e2e/tests/api/solidgate-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated Solidgate checkout-session creation POST body / header smoke spec paired with apps/web-e2e/tests/api/solidgate-checkout-body.spec.ts, the sixty-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixty-first under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/solidgate/checkout/route.ts — the first per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes (the existing multi-provider payment-checkouts.spec.ts covers all four providers' checkout endpoints with a single < 500 assertion each; this spec drills into the Solidgate handler specifically and pins its load-bearing 401-before-everything gate posture). Distinct from the closest analogue polar-subscription-portal-body-spec.md (auth-gated POST, 401-before-everything posture) in five ways: (a) TWO-key 401 envelope -- Solidgate returns { error: 'Unauthorized', message: 'Authentication required' } (TWO keys); polar-portal returns { error: 'Unauthorized' } (ONE key); UNIQUE: the FIRST per-source-file POST body smoke that pins a two-key 401 envelope on a payment-provider route; (b) Zod safeParse AFTER the auth gate -- the checkoutSchema.safeParse(json) and the surrounding try/catch around request.json() fire only AFTER auth(), so the unauth branch never reaches them; polar-portal does NOT call request.json() at all on the unauth branch; (c) FIVE-key success envelope -- solidgate's success branch returns { data: { id, url }, status, message } (THREE top-level keys, including a literal status: 200 field embedded in the body, separate from the HTTP status); UNIQUE: the FIRST per-source-file POST smoke that pins a literal-status-key success envelope; (d) 500 catch (NOT 400) -- solidgate's outer catch returns 500 with { error, message, details } (dev-only stack); polar-webhook uses safeErrorResponse(..., 400); UNIQUE: the FIRST per-source-file POST smoke that pins a 500-default catch on a payment-provider route -- but ONLY reachable AFTER the auth gate; (e) POST-only export -- GET / PUT / PATCH / DELETE are NOT exported; method-resolution returns 405. The POST handler combines auth() session lookup (load-bearing first gate; missing → 401 { error: 'Unauthorized', message: 'Authentication required' }), getOrCreateSolidgateProvider() singleton initialization, the Zod checkoutSchema (amount.positive(), currency.default('USD'), mode.enum(['one_time', 'subscription']).default('one_time'), successUrl.url(), cancelUrl.url(), metadata.optional()), a per-call request.json() try/catch (failure → 400 { error: 'Invalid JSON', message: 'Request body must be valid JSON' }; schema mismatch → 400 { error: 'Invalid request body', message: <zod-issues-joined> }), solidgateProvider.getCustomerId(session.user) (not found → 400 { error: 'Failed to create customer', message: 'Unable to create Solidgate customer' }), solidgateProvider.createPaymentIntent(...) for the load-bearing payment-provider call, success payload { data: { id, url }, status: 200, message: 'Checkout session created successfully' }, and outer catch safeErrorMessage(error, 'Failed to create checkout session') + dev-only details: error.stack → 500. Documents the at-a-glance scenario tree (a ~25-body bulk-loop walk + a ~12-header bulk-loop walk all asserting < 500; a canonical two-key 401-envelope assertion pinning the load-bearing envelope shape exactly; a strict envelope-shape invariance walk pinning Object.keys(body).sort() === ['error', 'message'] across every body permutation; a no-Zod-issue-leak invariance walk pinning that amount / successUrl / cancelUrl / mode / 'Invalid request body' / 'Invalid JSON' strings must NEVER appear in the unauth response -- a regression that ran safeParse(...) before the auth gate would surface here; a no-success-key-leak invariance walk pinning that data / id / url / literal status: 200 must NEVER appear -- a regression that ran createPaymentIntent(...) before the auth gate would surface here; a no-redirect-leak assertion pinning that caller-supplied successUrl / cancelUrl values must NEVER be echoed; a malformed-JSON-pre-gate-non-downgrade assertion pinning that request.json() parse-fail does NOT downgrade the unauth 401 to a 400 'Invalid JSON'; a catch-branch-non-entry walk pinning that the unauth branch must NEVER reach the 500 outer catch; a static-string allow-list assertion pinning a 1-message set (only 'Unauthorized' is reachable on the unauth surface); a side-channel walk pinning that fabricated cookies / Bearer tokens / admin-shaped headers do NOT satisfy auth(); a cross-method probe pinning that GET / PUT / PATCH / DELETE return < 500 (Next.js 405); a 401-status-invariance walk pinning that every documented bypass-key shape rounds-trips to the same 401 as the empty-body baseline). Cross-references the sibling Solidgate webhook smoke solidgate-webhook-body-spec.md (same provider singleton, completely different gate posture -- signature verification vs session auth), the closest analogue polar-subscription-portal-body-spec.md, the multi-provider sibling payment-checkouts.spec.ts (covers all four providers' checkout endpoints with a single < 500 assertion each), and to Spec 010 -- E2E Test Coverage and Spec 002 -- Plugin Architecture for the governing specs. With this entry the per-spec-file docs rollout extends to 63-of-N and the tests/api/ per-spec-file sub-rollout extends to 61-of-many, and the first per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- expanding the rollout's payment-provider checkout coverage from a single < 500 smoke to a deep body-surface walk on the Solidgate handler and pinning the two-key 401 envelope contract no prior auth-gated payment-provider POST smoke covers.
  • E2E Solidgate Webhook Body Spec (apps/web-e2e/tests/api/solidgate-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Solidgate payment-provider webhook POST body / header smoke spec paired with apps/web-e2e/tests/api/solidgate-webhook-body.spec.ts, the sixty-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixtieth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/solidgate/webhook/route.ts — the third per-source-file webhook POST smoke the docs tree publishes (after polar-webhook-body-spec.md and lemonsqueezy-webhook-body-spec.md). Distinct from BOTH polar and lemonsqueezy: (a) Two-header signature fallback -- Solidgate reads x-signature || solidgate-signature -- UNIQUE: NEITHER polar (webhook-signature) NOR lemonsqueezy (x-signature only) uses this two-header fallback pattern; (b) Manual JSON parse like polar but NO validateWebhookPayload check -- the handler calls JSON.parse(body) (matching polar) but DOES NOT validate the parsed shape (distinct from polar's 4-tier chain); (c) In-memory idempotency Set -- the FIRST webhook smoke that pins an idempotency contract; processedWebhooks: Set<string> tracks webhookId for 24 hours via setTimeout(...); duplicate webhook IDs return 200 { received: true } (NOT 400) -- the FIRST webhook smoke with TWO 200-success branches; (d) Switch dispatcher accepting BOTH enum AND string values -- 9 event types are matched on BOTH the WebhookEventType.PAYMENT_SUCCEEDED enum AND the lowercase 'payment_succeeded' string; (e) GET export with informative message -- the route exports a GET handler that returns 200 { message: 'Solidgate webhook endpoint', instructions: '...', method: 'POST' } -- UNIQUE: polar and lemonsqueezy export only POST; (f) Same 400-default catch as polar + lemonsqueezy. The POST handler combines a raw-body read via await request.text(), the two-header signature fallback (x-signature || solidgate-signature; missing BOTH → 400 'No signature provided'), JSON.parse(body) INSIDE the outer try block (failure cascades to catch), an idempotency check (webhookId = parsedBody.id || x-request-id; if processedWebhooks.has(webhookId) → 200 { received: true }), solidgateProvider.handleWebhook(parsedBody, signature, body) for the load-bearing signature-verification call (receives parsed body, raw signature, AND raw body string), a !webhookResult.received check (400 'Webhook not processed'), the switch-statement event dispatcher (9 event types matched on both enum and string), success payload { received: true } with status 200, and outer catch console.error + 400 'Webhook processing failed'. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~11-body bulk-loop walk all asserting < 500; a first-gate two-header-fallback rejection assertion pinning that missing BOTH x-signature AND solidgate-signature produces 400; a fallback-header-acceptance assertion pinning that solidgate-signature alone satisfies the gate; a strict envelope-shape assertion across all rejection branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant; an allowed-pre-delivery-error static-string allow-list assertion; a polar-shape-headers-ignored assertion pinning that polar's webhook-signature does NOT satisfy the Solidgate gate; a side-channel walk; a GET-200-with-informative-message assertion -- UNIQUE to Solidgate; a cross-method probe (PUT / PATCH / DELETE); a signature-verification-call-gated-by-header-check invariant; a switch-statement-dispatcher-gated-by-signature-verification invariant pinning that invalid signatures must NEVER trigger any of the 9 event handlers). Cross-references the first webhook POST smoke polar-webhook-body-spec.md, the second webhook POST smoke lemonsqueezy-webhook-body-spec.md, the multi-provider sibling webhooks.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 62-of-N and the tests/api/ per-spec-file sub-rollout extends to 60-of-many, and the third per-source-file webhook POST smoke lands -- expanding the rollout's payment-provider webhook coverage from two providers (Polar + LemonSqueezy) to three (Polar + LemonSqueezy + Solidgate) and pinning the in-memory idempotency contract no prior webhook smoke covers.
  • E2E Item Comments Rating Query Spec (apps/web-e2e/tests/api/item-comments-rating-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item rating-aggregate GET query-param / header smoke spec paired with apps/web-e2e/tests/api/item-comments-rating-query.spec.ts, the sixty-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-ninth under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/items/[slug]/comments/rating/route.ts — the first per-source-file query smoke for a public item-detail endpoint that uses checkDatabaseAvailability() as a graceful-fallback gate (NOT as a 503-returning gate like the sibling item-comments-create-body-spec.md POST). When process.env.DATABASE_URL is missing OR the tenant resolution returns null OR the Drizzle aggregate query throws, the handler returns the SAME success-shaped envelope { averageRating: 0, totalRatings: 0 } with status 200 — NEVER a 4xx or 5xx. This is a deliberately permissive contract: the item-detail page reads ratings on every render, and a 503 / 500 response would cause a render-time failure instead of a quiet zero-ratings display. It is also the first per-source-file query smoke that pins a two-key envelope shape ({ averageRating, totalRatings }) with NO success discriminant key — distinct from every prior per-source-file query spec which uses either the canonical { success: true, data: ... } envelope OR the bare { error } envelope. The GET handler is a zero-request-read signature: it only awaits params, calls checkDatabaseAvailability(), getItemIdFromSlug(slug), getTenantId(), and a single Drizzle select({ avg, count }) aggregate filtered by eq(itemId) + isNull(deletedAt) + eq(tenantId). There is NO request.url, request.headers, or searchParams.get(...) access anywhere — the route is invariant to any query parameter the caller appends. The handler combines the load-bearing checkDatabaseAvailability() graceful-fallback gate (NON-null → 200 zero-rating envelope), params resolve, getItemIdFromSlug(slug) synchronous slug→id mapping, getTenantId() graceful-fallback (null → 200 zero-rating envelope), the Drizzle select({ avg(rating), count() }) aggregate, success payload { averageRating: Number(avg) || 0, totalRatings: Number(count) || 0 } with status 200, and outer catch console.warn (dev-only) + 200 { averageRating: 0, totalRatings: 0 } (NOT a 500 -- the route NEVER surfaces a 5xx). Documents the at-a-glance scenario tree (a ~57-path bulk-loop walk asserting < 500; a canonical-envelope 200 zero-rating assertion { averageRating: 0, totalRatings: 0 }; a strict envelope-shape assertion Object.keys(body) === ['averageRating', 'totalRatings'] with NO success / data / error keys; a Number-cast invariant pinning that averageRating and totalRatings are both number -- a regression that bypasses the Number(...) cast (returning the raw Drizzle avg(...) string) would surface here; a graceful-degrade non-disclosure walk pinning that the response must NEVER echo error or success keys; a parameterised-vs-baseline status-stability comparison across five paths; an envelope-shape invariance walk pinning that all five parameterised paths return the same { averageRating: 0, totalRatings: 0 } envelope; an Accept header isolation walk; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a graceful-degrade catch-branch invariance walk pinning that no error path surfaces a 5xx). Cross-references the sibling item-comments-create-body-spec.md (uses checkDatabaseAvailability() as a load-bearing 503-returning gate -- the OPPOSITE of this route's graceful-fallback gate), the auth-gated item-votes-status-query-spec.md, the public count-only sibling item-vote-count-query.spec.ts, the per-comment rating sibling item-comment-rating-by-id.spec.ts, the public per-item-endpoint gauntlet item-public.spec.ts (which already pins the < 500 no-server-error contract for this route as one of many; this spec drills into the query-param surface specifically), and to Spec 010 -- E2E Test Coverage and Spec 002 -- Plugin Architecture for the governing specs. With this entry the per-spec-file docs rollout extends to 61-of-N and the tests/api/ per-spec-file sub-rollout extends to 59-of-many, and the first graceful-fallback checkDatabaseAvailability() query smoke the docs tree publishes lands as a complementary surface to the sibling 503-returning POST -- pinning a two-key zero-rating envelope contract no prior smoke covers.
  • E2E LemonSqueezy Webhook Body Spec (apps/web-e2e/tests/api/lemonsqueezy-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy payment-provider webhook POST body / header smoke spec paired with apps/web-e2e/tests/api/lemonsqueezy-webhook-body.spec.ts, the sixtieth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-eighth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/lemonsqueezy/webhook/route.ts — the second per-source-file webhook POST smoke the docs tree publishes (after polar-webhook-body-spec.md). Distinct from the polar sibling: (a) Different signature header -- LemonSqueezy uses x-signature (lowercase, single field); Polar uses webhook-signature + webhook-timestamp + webhook-id; (b) NO manual JSON parse -- the handler reads the raw body via await request.text() and passes it as a STRING to lemonSqueezyProvider.handleWebhook(body, signature); the provider parses the body itself, the route does NOT call JSON.parse(bodyText); (c) Simpler 2-tier rejection chain -- only 'No signature provided' (400) and 'Webhook not processed' (400). Polar has 4 tiers (Invalid JSON payload / Invalid webhook payload / No signature provided / Webhook not processed); (d) Switch-statement event dispatcher -- webhookResult.type is mapped via mapLemonSqueezyEventType(...) and dispatched into one of 8 distinct handlers (subscription created / updated / cancelled, payment succeeded / failed, subscription payment succeeded / failed, trial ending); (e) Same 400-default catch as polar but via raw NextResponse.json call -- outer catch is NextResponse.json({ error: 'Webhook processing failed' }, { status: 400 }), NOT safeErrorResponse(...) like polar uses. The POST handler combines a raw-body read via await request.text(), a x-signature header presence check (missing → 400 { error: 'No signature provided' }), lemonSqueezyProvider.handleWebhook(body, signature) for the load-bearing signature-verification call (receives the RAW body STRING, not a parsed object), a !webhookResult.received check (400 { error: 'Webhook not processed' }), mapLemonSqueezyEventType(webhookResult.type) mapping, the switch-statement event dispatcher (8 mapped handlers + default console.log), success payload { received: true } with status 200, and outer catch console.error + 400 { error: 'Webhook processing failed' }. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~12-body bulk-loop walk all asserting < 500; a first-gate signature-header-presence-rejection assertion { error: 'No signature provided' }; a strict envelope-shape assertion Object.keys(body) === ['error'] across all rejection branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant pinning that NO unhandled error escapes as 5xx; an allowed-pre-delivery-error static-string allow-list assertion -- 3-message set vs polar's 5-message set; a polar-shape-headers-ignored assertion pinning that polar's webhook-signature does NOT satisfy LemonSqueezy's x-signature gate; a side-channel walk; a cross-method probe -- POST is the only exported method; a signature-verification-call-gated-by-header-check invariant; a switch-statement-dispatcher-gated-by-signature-verification invariant pinning that invalid signatures must NEVER trigger any of the 8 event handlers). Cross-references the first webhook POST smoke polar-webhook-body-spec.md, the multi-provider sibling webhooks.spec.ts, the lemonsqueezy/list sibling lemonsqueezy-list-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 60-of-N and the tests/api/ per-spec-file sub-rollout extends to 58-of-many, and the second per-source-file webhook POST smoke lands -- expanding the rollout's payment-provider webhook coverage from one provider (Polar) to two (LemonSqueezy + Polar) and pinning the simpler 2-tier rejection chain that distinguishes LemonSqueezy from Polar's 4-tier chain.
  • E2E Item Votes Status Query Spec (apps/web-e2e/tests/api/item-votes-status-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated per-item vote-status GET query-param / header smoke spec paired with apps/web-e2e/tests/api/item-votes-status-query.spec.ts, the fifty-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-seventh under apps/web-e2e/tests/api/, and the tenth per-source-file query-spec the docs tree publishes (the first nine are all admin-tree). Pairs with the GET export of apps/web/app/api/items/[slug]/votes/status/route.ts — the first non-admin per-source-file query smoke for an auth-gated GET that returns the current user's vote record (or null) for a specific item (distinct from the public /api/items/[slug]/votes GET that item-votes-query.spec.ts covers and from the public count-only sibling item-vote-count-query.spec.ts). It is also the first per-source-file query smoke that pairs the 'Authentication required' 401 message (matching the sibling item-comments-create-body-spec.md POST) with the bare { error } envelope (no success: false wrapper) — distinct from the canonical { success: false, error: 'Unauthorized' } envelope used by the sibling item-votes-cast-body-spec.md POST. The GET handler is a zero-request-read signature: it awaits auth() first, then context.params, then calls getClientProfileByUserId(...) and getVoteByUserIdAndItemId(...). There is NO request.url, request.headers, or searchParams.get(...) access anywhere in the body — the route is invariant to any query parameter the caller appends. The handler combines auth() session lookup, a !session?.user?.id gate (→ 401 { error: 'Authentication required' } with the bare envelope and NO success key), context.params resolve, getClientProfileByUserId(session.user.id) lookup (not found → 404 { error: 'Client profile not found' }), getVoteByUserIdAndItemId(clientProfile.id, slug) for the load-bearing data read, success payload votes[0] || null with status 200 (the only docs-tree per-source-file smoke that pins a null-or-record payload contract), and outer catch console.error + 500 { error: 'Failed to fetch vote status' }. Documents the at-a-glance scenario tree (a ~57-path bulk-loop walk asserting < 500; a canonical-envelope authentication-required 401 assertion { error: 'Authentication required' }; a strict envelope-shape assertion Object.keys(body) === ['error'] with NO success key; a gate-before-post-auth invariant pinning that NONE of the three candidate post-auth static messages must appear in any unauth response; a vote-record non-disclosure walk pinning that NONE of the six record keys -- id, userId, itemId, voteType, createdAt, updatedAt -- may appear on the unauth branch; a null-payload non-disclosure pinning that the unauth response is NOT the literal null payload; a parameterised-vs-baseline status-stability comparison across five paths; an Accept header isolation walk; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a client-profile-lookup-not-entered invariance walk; a vote-record-read-not-entered invariance walk). Cross-references the sibling POST smokes item-votes-cast-body-spec.md (same auth-gate-then-data-read pattern, canonical { success: false, error: 'Unauthorized' } envelope) and item-comments-create-body-spec.md (same 'Authentication required' 401 message, but with { success: false, error } envelope), the public per-item votes GET smokes item-votes-public.spec.ts and item-votes-query.spec.ts, the auth-gated-endpoint gauntlet payment-protected.spec.ts (which already pins the < 500 no-server-error contract for this route as one of many; this spec drills into the query-param surface specifically), and to Spec 010 -- E2E Test Coverage for the governing spec and Spec 002 -- Plugin Architecture for the architectural target. With this entry the per-spec-file docs rollout extends to 59-of-N and the tests/api/ per-spec-file sub-rollout extends to 57-of-many, and the first auth-gated non-admin per-source-file query smoke the docs tree publishes lands as a complementary surface to the sibling vote-casting POST -- pinning a null-or-record success payload contract no prior smoke covers.
  • E2E Item Comments Create Body Spec (apps/web-e2e/tests/api/item-comments-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item comment-create POST body / header smoke spec paired with apps/web-e2e/tests/api/item-comments-create-body.spec.ts, the fifty-eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-sixth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/items/[slug]/comments/route.ts — the first non-admin per-source-file POST smoke the docs tree publishes that uses checkDatabaseAvailability() from apps/web/lib/utils/database-check.ts as the load-bearing FIRST gate (BEFORE auth()) -- when process.env.DATABASE_URL is missing, the helper returns a 503 { error: 'Database not configured', code: 'DATABASE_UNAVAILABLE', message: 'This feature requires database configuration' } envelope (the FIRST POST smoke that pins this helper-emitted envelope shape with a 503 status), the first non-admin POST smoke that uses the 'Authentication required' 401 message (distinct from the 'Unauthorized' message used by the sibling item-votes-cast-body-spec.md), and the second non-admin POST smoke that pins the isUserBlocked(clientProfile.status) moderation-status gate (the first being item-votes-cast-body-spec.md). In the e2e test environment DATABASE_URL IS configured, so the db-availability gate passes through and the auth gate fires for unauthenticated requests, producing the 401 'Authentication required' envelope. The POST handler combines a checkDatabaseAvailability() gate (the load-bearing FIRST gate; returns 503 with the DATABASE_UNAVAILABLE envelope when DATABASE_URL is missing, otherwise returns null), auth() session lookup, a !session?.user gate (→ 401 { success: false, error: 'Authentication required' } -- NOTE: 'Authentication required', NOT 'Unauthorized'), JSON body parse, content validation (!content?.trim() → 400 'Content is required'), rating range validation (typeof rating !== 'number' || rating < 1 || rating > 5 → 400 'Rating must be between 1 and 5'), getClientProfileByUserId(session.user.id!) lookup (not found → 404 'Client profile not found'), the isUserBlocked(clientProfile.status) moderation-status gate (if true → 403 with the dynamic getBlockReasonMessage message), the load-bearing createComment({ content, rating, userId, itemId }) write, getCommentWithUserById(comment.id) post-write lookup (if null → 500 'Failed to retrieve comment' -- the first POST smoke that pins a post-write null-check 500 envelope), success payload { success: true, comment: commentWithUser } with status 200, and outer catch console.error + 500 'Failed to create comment'. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~17-body bulk-loop walk all asserting < 500; a canonical-envelope authentication-required 401 assertion { success: false, error: 'Authentication required' }; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of comment keys must appear in any unauth response and success must be false; a gate-before-post-auth invariant pinning that NONE of the five candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion that includes 'Database not configured' for the 503 branch; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a content-and-rating-validation-chain-not-entered invariance walk; a client-profile-lookup-and-moderation-gate-not-entered invariance walk pinning that the unauth response is 401, NOT 403; a createComment-and-post-write-lookup-not-entered invariance walk pinning that the unauth response must NEVER echo a comment key or the 'Failed to retrieve comment' 500 message). Cross-references the companion public GET smoke comments.spec.ts, the first moderation-gated POST sibling item-votes-cast-body-spec.md (uses the SAME moderation gate but with the 'Unauthorized' 401 message), the dynamic-segment per-comment routes at items/[slug]/comments/[commentId]/route.ts, the rating sub-route at items/[slug]/comments/rating/route.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 58-of-N and the tests/api/ per-spec-file sub-rollout extends to 56-of-many, and the first checkDatabaseAvailability-helper-gated POST smoke the docs tree publishes lands as a complementary surface to the sibling vote-casting POST -- pinning a post-write null-check 500 envelope no prior smoke covers.
  • E2E Item Votes Cast Body Spec (apps/web-e2e/tests/api/item-votes-cast-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item vote-casting POST body / header smoke spec paired with apps/web-e2e/tests/api/item-votes-cast-body.spec.ts, the fifty-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-fifth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/items/[slug]/votes/route.ts — the first non-admin per-source-file POST smoke the docs tree publishes that pins a moderation-status gate: after the auth + body-validation + client-profile gates, the handler runs isUserBlocked(clientProfile.status) from apps/web/lib/db/queries/moderation.queries.ts and returns 403 with a dynamic message from getBlockReasonMessage(clientProfile.status) if the client is suspended or banned -- no prior POST smoke covers a moderation-status gate of this shape. The companion public GET smoke is item-votes-public.spec.ts which covers the GET surface (zero-vote fallback for unknown slugs); the mutating POST and DELETE surfaces have only generic < 500 coverage in items-engagement-and-favorites.spec.ts, so this spec drills into the POST surface specifically. The POST handler combines auth() session lookup + slug param resolution via Promise.all([auth(), Promise.resolve(params.params)]), a !session?.user?.id gate (→ 401 { success: false, error: 'Unauthorized' } with the canonical envelope and short message), JSON body parse via await request.json() AFTER the auth gate, vote-type enum validation (if (!type || (type !== 'up' && type !== 'down')) → 400 { success: false, error: "Invalid vote type. Must be 'up' or 'down'" }), getClientProfileByUserId(session.user.id) lookup (not found → 404 { success: false, error: 'Client profile not found' }), the isUserBlocked(clientProfile.status) moderation-status gate (load-bearing moderation invariant; if true → 403 with the dynamic getBlockReasonMessage message), existing-votes lookup + replace logic via getVoteByUserIdAndItemId(...) and deleteVote(...), the load-bearing createVote({ userId, itemId, voteType }) write (voteType derived from type === 'up' ? VoteType.UPVOTE : VoteType.DOWNVOTE), getVoteCountForItem(slug) for the post-write count, success payload { success: true, count, userVote: type } with status 200, and outer catch console.error + 500 { success: false, error: 'Internal server error' }. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a canonical-envelope bare-message 401 assertion { success: false, error: 'Unauthorized' }; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a success-branch-key non-disclosure assertion that NONE of count, userVote keys must appear in any unauth response and success must be false; a gate-before-post-auth invariant pinning that NONE of the three candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe walking only PUT and PATCH because GET + POST + DELETE are exported; a malformed-JSON-body invariance walk; a vote-type-validation-not-entered invariance walk; a client-profile-lookup-and-moderation-gate-not-entered invariance walk pinning that the unauth response is 401, NOT 403, regardless of body -- a regression that re-ordered isUserBlocked(...) before the auth gate would surface here; a createVote-and-getVoteCountForItem-not-entered invariance walk pinning that the unauth response must NEVER echo a count or userVote). Cross-references the companion public GET smoke item-votes-public.spec.ts, the public count-only sibling item-vote-count-query.spec.ts, the auth-gated votes/status GET sibling item-votes-query.spec.ts, and the generic mutating-method < 500 smoke items-engagement-and-favorites.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 57-of-N and the tests/api/ per-spec-file sub-rollout extends to 55-of-many, and the first moderation-status-gated POST smoke the docs tree publishes lands as the load-bearing invariant on a public, auth-gated vote-casting endpoint -- the third non-admin-tree per-source-file POST reference after extract-body-spec.md, polar-webhook-body-spec.md, and item-views-record-body-spec.md.
  • E2E Polar Webhook Body Spec (apps/web-e2e/tests/api/polar-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Polar payment-provider webhook POST body / header smoke spec paired with apps/web-e2e/tests/api/polar-webhook-body.spec.ts, the fifty-sixth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-fourth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/polar/webhook/route.ts — the first per-source-file webhook POST smoke the docs tree publishes (the existing multi-provider webhooks.spec.ts covers Stripe / LemonSqueezy / Polar / Solidgate with two assertions each -- GET-not-5xx and POST-unauthenticated-rejected; this spec drills into the Polar webhook handler specifically), the first POST smoke that uses await request.text() (raw body) instead of await request.json() (because Polar calculates signatures on the raw body, not the parsed JSON; the handler manually parses the raw text via JSON.parse(bodyText) inside a try/catch), and the first POST smoke that uses safeErrorResponse(..., 400) in the outer catch (defaulting to 400 NOT 500 for unhandled webhook errors -- preventing a 5xx crash on signature/parsing errors that would otherwise trip Polar's webhook-retry logic). The POST handler combines a raw-body read via await request.text(), a manual JSON parse via JSON.parse(bodyText) inside a per-call try/catch (failure → 400 { error: 'Invalid JSON payload' }), a validateWebhookPayload(body) structure check (payload must have string id, string type, and object data keys → 400 { error: 'Invalid webhook payload' } if any missing), a webhook-signature header presence check (missing → 400 { error: 'No signature provided' }), polarProvider.handleWebhook(body, signatureHeader, bodyText, timestampHeader, webhookIdHeader) for the load-bearing signature-verification call, a !webhookResult.received check (400 { error: 'Webhook not processed' }), routeWebhookEvent(webhookResult.type, webhookResult.data) for the load-bearing event-routing call (subscription lifecycle, payment events, checkout updates) on the success branch, success payload { received: true } with status 200, and outer catch safeErrorResponse(error, 'Webhook processing failed', 400) (NOTE: defaults to 400 NOT 500). Documents the at-a-glance scenario tree (a ~13-header bulk-loop walk + a ~14-body bulk-loop walk all asserting < 500; a first-gate JSON-parse-rejection assertion { error: 'Invalid JSON payload' }; a second-gate validate-payload-rejection assertion { error: 'Invalid webhook payload' }; a third-gate signature-header-presence-rejection assertion { error: 'No signature provided' }; a strict envelope-shape assertion Object.keys(body) === ['error'] across all three pre-delivery branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant pinning that NO unhandled error escapes as 5xx; an allowed-pre-delivery-error static-string allow-list assertion that every rejection message comes from the five-message set; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a signature-verification-call-gated-by-header-check invariant pinning that a valid payload without the webhook-signature header must produce 'No signature provided', not a 200 { received: true }). Cross-references the multi-provider sibling webhooks.spec.ts, the polar/subscription/portal sibling polar-subscription-portal-body.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 56-of-N and the tests/api/ per-spec-file sub-rollout extends to 54-of-many, and the first per-source-file webhook POST smoke lands -- expanding the rollout into the payment-provider webhook layer for the first time and pinning a raw-body-signature-verification contract no prior smoke covers.
  • E2E Item Views Record Body Spec (apps/web-e2e/tests/api/item-views-record-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item view-tracking POST body / header smoke spec paired with apps/web-e2e/tests/api/item-views-record-body.spec.ts, the fifty-fifth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-third under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/items/[slug]/views/route.ts -- the first non-admin POST smoke the docs tree publishes that pins a bot-detection graceful-degradation branch AS the load-bearing test invariant: the route imports isBot() from apps/web/lib/utils/bot-detection.ts whose BOT_PATTERNS regex array contains /bot/i, /crawl/i, /spider/i, /playwright/i, /puppeteer/i, /headless/i, /curl/i, /python-requests/i, /axios/i, /node-fetch/i AND treats an empty UA as a bot. The smoke spec EXPLICITLY sets a known-bot User-Agent (Googlebot/2.1) on the deterministic-assertion tests so the bot gate fires regardless of the Playwright runtime's default UA, BEFORE the route ever calls itemRepository.findBySlug(...), the auth() owner check, the cookies() viewer-id read, OR the recordItemView(...) write -- making the canonical envelope { success: true, counted: false, reason: 'bot' } (status 200) the load-bearing invariant for the spec. It is also the first POST smoke the docs tree publishes that pins a synthetic-User-Agent override branch -- the same endpoint, called with a non-bot Chrome UA against an intentionally non-existent slug, progresses past the bot gate, reaches itemRepository.findBySlug(slug), and lands on the if (!item) return 404 { success: false, error: 'Item not found' } branch. The two branches together pin the gate-before-find order as a load-bearing invariant: a regression that re-orders the findBySlug(...) call before the bot gate would surface here as a data-key disclosure on the bot branch OR as a status-code change. The POST handler combines a database-availability gate (if (!process.env.DATABASE_URL) → 503 { error: 'Database not configured', code: 'DATABASE_UNAVAILABLE', message: '...' }; in the e2e environment DATABASE_URL IS set so this branch must NOT fire), a bot-detection gate (if (isBot(userAgent)) → 200 { success: true, counted: false, reason: 'bot' }), an item existence check (const item = await itemRepository.findBySlug(slug); if (!item) → 404 { success: false, error: 'Item not found' }), an owner-exclusion gate (const session = await auth(); if (session?.user?.id && item.submitted_by === session.user.id) → 200 { success: true, counted: false, reason: 'owner' }), a viewer-cookie read / write step (reads ever_viewer_id cookie via cookies(), generates crypto.randomUUID() if absent, sets cookie with httpOnly: true, sameSite: 'lax', path: '/', and maxAge: VIEWER_COOKIE_MAX_AGE -- 365 days -- on the first-write branch), a view recording step (recordItemView({ itemId: slug, viewerId, viewedDateUtc }) returns counted: boolean; response: { success: true, counted }), and outer catch console.error (dev-only) + 500 { success: false, error: 'Failed to record view' }. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a 200-with-bot-envelope assertion { success: true, counted: false, reason: 'bot' }; a strict envelope-shape assertion Object.keys(body).sort() === ['counted', 'reason', 'success']; a post-bot-gate-key non-disclosure assertion that NONE of error, data, code keys must appear in any bot response and success must be true; a gate-before-post-bot invariant pinning that NONE of the three candidate static messages ('Item not found', 'Failed to record view', 'Database not configured') must appear in any bot response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a malformed-JSON-body invariance walk pinning the gate-before-body-read order -- the route never calls request.json(), so malformed JSON bodies still land on the same 200 envelope; an item-not-found-not-entered invariance walk pinning the gate-before-find order; a database-unavailable-not-entered invariance walk pinning the post-DATABASE_URL-configuration invariant; a non-bot-UA-override-progresses-to-404 assertion pinning the gate-before-find order from the non-bot side -- non-bot UA + non-existent slug → 404 { success: false, error: 'Item not found' }; a bot-branch-non-disclosure-on-the-non-bot-branch assertion that NO reason: 'bot' echo, NO counted: false echo, MUST have success: false; an owner-exclusion-not-entered invariance walk pinning that anonymous requests can NEVER receive reason: 'owner' regardless of UA OR submitted_by body-field bypass attempts). Cross-references the sibling extract-body-spec.md (the first non-admin-tree per-source-file body-surface reference, with feature-disabled graceful-degradation branch -- this views-record spec is the SECOND non-admin-tree body-surface reference and the FIRST that pins a bot-detection graceful-degradation branch), and to Spec 010 -- E2E Test Coverage for the governing e2e spec and Spec 008 -- Analytics Providers for the analytics-providers spec the views route sits inside. With this entry the per-spec-file docs rollout extends to 55-of-N and the tests/api/ per-spec-file sub-rollout extends to 53-of-many, and the first bot-detection-graceful-degradation POST smoke the docs tree publishes lands as the load-bearing invariant on a public, non-auth-gated endpoint -- the second non-admin-tree per-source-file reference after extract-body-spec.md.
  • E2E Extract Body Spec (apps/web-e2e/tests/api/extract-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's URL-extraction proxy POST body / header smoke spec paired with apps/web-e2e/tests/api/extract-body.spec.ts, the fifty-fourth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-second under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/extract/route.ts — the first non-admin-tree per-source-file reference the docs tree publishes (every prior per-source-file e2e reference covers an apps/web/app/api/admin/** route; this spec covers the extraction-proxy at apps/web/app/api/extract/route.ts -- a non-admin proxy that forwards to the Ever Works Platform API), the first non-admin-gated POST smoke the docs tree publishes that pins a "feature disabled" graceful-degradation branch (when process.env.PLATFORM_API_URL is missing, the handler returns a 200 -- NOT 401, NOT 503 -- with the envelope { success: false, featureDisabled: true, message: 'URL extraction feature is not available. This feature requires PLATFORM_API_URL to be configured.' } -- no prior smoke spec covers a featureDisabled: true envelope shape), and the first POST smoke that uses Zod safeParse + result.error.issues[0].message (NOT flatten() like the admin-tree POST smokes such as admin/items/import) to surface the FIRST validation issue as the 400 envelope's error field. The POST handler combines a feature-disabled gate (if (!platformApiUrl) → 200 with the featureDisabled: true envelope), a JSON body parse AFTER the feature-disabled gate, a Zod safeParse with single-issue surfacing (extractSchema.safeParse(body) → 400 { success: false, error: '<first issue>' }; schema requires url: z.string().url() and optional existingCategories: z.array(z.string())), an external fetch proxy that builds the extraction endpoint URL by trimming trailing slashes and POSTs to the Platform API with optional Authorization: Bearer <PLATFORM_API_SECRET_TOKEN> header, an upstream-error pass-through (tries to parse upstream error JSON for errorData.message, falls back to response.statusText), a success pass-through (returns the upstream payload verbatim), and outer catch console.error + 500 { success: false, error: 'Internal server error during extraction' }. In the e2e test environment PLATFORM_API_URL is NOT configured, so EVERY POST request lands on the feature-disabled branch and gets a 200 response regardless of body shape -- making the spec a pinning of the feature-disabled envelope as the load-bearing invariant. Documents the at-a-glance scenario tree (a ~12-header bulk-loop walk + a ~15-body bulk-loop walk all asserting < 500; a 200-with-feature-disabled-envelope assertion { success: false, featureDisabled: true, message: '...' }; a strict envelope-shape assertion Object.keys(body).sort() === ['featureDisabled', 'message', 'success']; a no-error-key assertion on the feature-disabled branch; a feature-disabled-before-post-feature-disabled invariant pinning that NONE of the three candidate post-gate messages must appear in any feature-disabled response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a malformed-JSON-body invariance walk; a Zod-validation-chain-not-entered invariance walk pinning that the response must NEVER echo 'Invalid URL format'; an external-fetch-proxy-not-entered invariance walk pinning that the response must always include featureDisabled: true in the test environment). Cross-references the Zod-flatten() siblings admin-items-import-validate-body-spec.md and admin-twenty-crm-config-save-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 54-of-N and the tests/api/ per-spec-file sub-rollout extends to 52-of-many, and the first non-admin-tree per-source-file reference lands -- expanding the rollout beyond the admin/** route family for the first time and pinning a feature-disabled graceful-degradation envelope shape no prior smoke covers.
  • E2E Admin Location Index Manage Body Spec (apps/web-e2e/tests/api/admin-location-index-manage-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin location-index manage POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-location-index-manage-body.spec.ts, the fifty-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fifty-first under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/location-index/route.ts — the first POST smoke the docs tree publishes that uses the checkAdminAuth() helper from @/lib/auth/admin-guard.ts (the GET-sibling admin-location-index-query-spec.md already covers the helper for the query endpoint; this is the first POST smoke that does the same), and the first action-enum-dispatched POST smoke that branches on a body.action === 'rebuild' | 'clear' enum into TWO distinct destructive operations on the same path: 'rebuild' calls itemRepository.findAll() + service.rebuildIndex(items) -- the heaviest service call across the entire admin tree (re-indexes EVERY item with location data); 'clear' calls clearLocationIndex() -- a destructive table-wipe that drops every row from the location_index table. The POST handler combines a checkAdminAuth() helper call that folds three branches into one helper (!session?.user → 401 'Unauthorized', !session.user.id → 401 'User ID not found', !userIsAdmin → 403 'Insufficient permissions') -- for an unauthenticated request the FIRST branch fires returning 401 { success: false, error: 'Unauthorized' } (canonical envelope with success: false AND short 'Unauthorized' message), a JSON body parse AFTER the gate (inside the try block), the action enum dispatch with three branches ('rebuild' → 200 { success: true, data: <rebuildResult> }; 'clear' → 200 { success: true, data: { cleared: <count> } }; else → 400 { success: false, error: 'Invalid action. Use "rebuild" or "clear".' }), and outer catch console.error + 500 { success: false, error: 'Internal server error' }. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~12-body bulk-loop walk all asserting < 500; a canonical-envelope bare-message 401 assertion { success: false, error: 'Unauthorized' }; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a success-branch-key non-disclosure assertion that NONE of data, cleared keys must appear in any unauth response and success must be false; a gate-before-post-auth invariant pinning that NONE of the four candidate static messages ('Invalid action. Use "rebuild" or "clear".', 'Internal server error', 'User ID not found', 'Insufficient permissions') must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; an action-enum-dispatch-not-entered invariance walk; a rebuild-and-clear-destructive-paths-not-entered invariance walk pinning that the unauth response must NEVER echo a data key from the rebuild result or the cleared count from the destructive table-wipe). Cross-references the GET-sibling admin-location-index-query-spec.md, the dashboard-stats sibling admin-clients-dashboard-query-spec.md (the second checkAdminAuth() helper consumer for query endpoints), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 53-of-N and the tests/api/ per-spec-file sub-rollout extends to 51-of-many, and the first destructive-action-enum-dispatch POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same checkAdminAuth()-gated location-index route.
  • E2E Admin Navigation Update Method Spec (apps/web-e2e/tests/api/admin-navigation-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin navigation-update PATCH body / header smoke spec paired with apps/web-e2e/tests/api/admin-navigation-update-method.spec.ts, the fifty-second per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fiftieth under apps/web-e2e/tests/api/. Pairs with the PATCH export of apps/web/app/api/admin/navigation/route.ts — the second admin-tree smoke the docs tree publishes that uses getCachedApiSession(req) instead of auth() (after admin/settings PATCH), and the first PATCH-only admin-tree smoke that pins a per-item path-format XSS-prevention validation loop via isValidNavigationPath(item.path). The PATCH handler combines a single-step !session?.user?.isAdmin gate that returns 401 { error: 'Unauthorized' } (BARE envelope, NO success key, SHORT message), a JSON body parse, a type enum check ('header' | 'footer' → 400 'Type must be "header" or "footer"'), an items array check (!Array.isArray(items) → 400 'Items must be an array'), a per-item structure validation loop (!label || !path || typeof !== 'string' → 400 'Each item must have non-empty "label" and "path" string fields'), a per-item path-format XSS-prevention validation (isValidNavigationPath(item.path) → 400 'Invalid path format. Paths must start with "/" for internal routes or "http://"/"https://" for external URLs.') -- the first PATCH smoke with a per-item XSS-prevention validation in a loop, then configManager.updateNestedKey('custom_header'|'custom_footer', items) for the load-bearing works.yml write, an update-failed branch (500 'Failed to update navigation' if falsy), success payload { success: true, type, items } with status 200 (UNIQUE: echoes both type and items from the input), and outer catch console.error + 500 'Failed to update navigation'. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~13-body bulk-loop walk all asserting < 500; a bare 401-envelope assertion; a strict envelope-shape assertion Object.keys(body) === ['error']; a success-branch-key non-disclosure assertion that NONE of success, type, items keys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the five candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (POST / PUT / DELETE); a malformed-JSON-body invariance walk; a type-enum-and-items-array-validation-not-entered invariance walk; a per-item-XSS-prevention-loop-not-entered invariance walk pinning that the unauth response must NEVER echo 'Invalid path format.'; a configManager-update-not-entered invariance walk pinning that the unauth response must NEVER echo a type or items key from the input). Cross-references the settings-update PATCH companion admin-settings-update-method-spec.md (the FIRST cached-session-lookup PATCH config-write admin-tree smoke; this navigation-update PATCH smoke is the SECOND), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 52-of-N and the tests/api/ per-spec-file sub-rollout extends to 50-of-many, and the first per-item-XSS-prevention navigation-update PATCH admin-tree smoke lands -- complementing the existing settings-update PATCH coverage of the cached-session-lookup config-write surface.
  • E2E Admin Twenty CRM Config Save Body Spec (apps/web-e2e/tests/api/admin-twenty-crm-config-save-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Twenty CRM config-save POST body / header smoke spec paired with apps/web-e2e/tests/api/admin-twenty-crm-config-save-body.spec.ts, the fifty-first per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-ninth under apps/web-e2e/tests/api/. Pairs with the POST export of apps/web/app/api/admin/twenty-crm/config/route.ts — the first admin-tree POST smoke the docs tree publishes that combines the compound single-if gate !session?.user?.isAdmin || !session.user.id (matching admin/sponsor-ads/[id]/approve and /reject POST handlers but for a CRM-config-save endpoint), a Zod-safeParse-like validation via validateTwentyCrmConfig(body) that returns a custom { success, data | error } shape and is translated to a details: [{field, message}] 400 envelope, AND a logActivity(...) side-effect that captures request.headers.get('x-forwarded-for') for the audit log -- the first POST smoke that reads a request header for an audit side-effect. The POST handler returns { success: true, message: 'Configuration saved successfully', data: <savedConfig> } with status 200 on success, and console.error + 500 { success: false, error: 'Failed to save configuration' } in the outer catch. The companion admin-twenty-crm-config-query.spec.ts covers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk including X-Forwarded-For + a ~11-body bulk-loop walk all asserting < 500; a canonical longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE of data, details, message keys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a validation-chain-not-entered invariance walk; a configRepository-saveConfig-and-logActivity-not-entered invariance walk pinning that the unauth response must NEVER echo 'Configuration saved successfully' or a data key from the saved config). Cross-references the companion admin-twenty-crm-config-query-spec.md, the sibling test-connection POST smoke at admin-twenty-crm-test-connection-body.spec.ts, and the other compound-single-if POST smokes admin-sponsor-ads-id-approve-method-spec.md and admin-sponsor-ads-id-reject-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 51-of-N and the tests/api/ per-spec-file sub-rollout extends to 49-of-many, and the first audit-logged CRM-config-save POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same Twenty-CRM-config route.
  • E2E Admin Tags All Query Spec (apps/web-e2e/tests/api/admin-tags-all-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-CMS tags-listing query-param smoke spec paired with apps/web-e2e/tests/api/admin-tags-all-query.spec.ts, the forty-ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the forty-seventh under apps/web-e2e/tests/api/. Pairs with the GET export of apps/web/app/api/admin/tags/all/route.ts -- the second Git-CMS-backed admin-tree query smoke the docs tree references (the first was the sibling admin-categories-all-query.spec.ts covered indirectly via the client-trash-page-object.md co-tenant cross-link). The route reads from the per-locale tag list stored in the Git-based content repository (cloned from DATA_REPOSITORY into .content/) via getCachedItems({ lang }) -- distinct from every other admin-tree route's drizzle / Postgres posture EXCEPT the sibling categories-all route. The handler combines auth() session lookup, a single-step !session?.user?.isAdmin gate returning 401 { success: false, error: 'Unauthorized' } (canonical envelope, bare 'Unauthorized' message -- NOT 'Unauthorized. Admin access required.' / 'Forbidden'), a ?locale= query param read AFTER the gate with searchParams.get('locale') || 'en' default coercion, a dead-branch typeof locale !== 'string' defensive narrowing that emits 400 { success: false, error: 'Invalid locale parameter' } but can never fire today (because searchParams.get(name) always returns string | null and the || 'en' default coerces null to a string before the typeof check), then getCachedItems({ lang: locale }) for the load-bearing per-locale tag list read, success payload { success: true, data: tags } with status 200, and outer catch console.error + 500 'Failed to fetch tags'. Distinct from the sibling admin-categories-all-query.spec.ts route which omits the dead-branch defensive narrowing entirely. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting < 500; a bare 401-envelope assertion; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for ?locale= / ?userId= / ?token= / ?bypass= / ?repo=&branch=&commit= key families; a gate-before-locale-narrowing invariant pinning that the dead-branch typeof locale !== 'string' narrow must never fire on the unauth branch; a gate-before-Git-CMS-read invariant pinning that the getCachedItems({ lang: locale }) Git-CMS reader must NEVER be entered on the unauth branch). Cross-references the DB-backed sibling admin-tags-query.spec.ts which covers the database-backed /api/admin/tags listing route (with pagination), the Git-CMS sibling for categories at admin-categories-all-query.spec.ts (same posture WITHOUT the dead-branch defensive narrowing), the page-object driver admin-tags-page-object.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 49-of-N and the tests/api/ per-spec-file sub-rollout extends to 47-of-many, and the first dead-branch type-narrowing Git-CMS query smoke lands -- complementing the existing per-source-file references for the categories-all sibling and the page-object driver covering the same admin-tree route's UI shell.
  • E2E Admin Featured Items [id] Method Spec (apps/web-e2e/tests/api/admin-featured-items-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-featured-item CRUD GET / PUT / DELETE method / id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-featured-items-id-method.spec.ts, the thirty-third per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirty-first under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/featured-items/[id]/route.ts — the first triple-method admin-tree smoke the docs tree publishes that combines a non-admin gate (the route gates on !session?.user?.id rather than !session?.user?.isAdmin, so any authenticated user with a tenant can hit it — a Q-010b-style auth-gate-divergence finding, the fourth admin route the smoke layer documents as effectively non-admin-restricted today) with a soft-delete DELETE (sets isActive: false rather than removing the row), a validation-less PUT (seven body fields shoved straight into db.update(...)), and a two-step !session?.user?.id!tenantId gate envelope. All three handlers share a hybrid bare-message + success: false 401 envelope ({ success: false, error: 'Unauthorized' } -- matching admin/users/[id] and admin/roles/[id]/permissions), inline Drizzle queries with tenant scoping, and a console.error + 500 catch with handler-specific messages ('Failed to fetch\|update\|remove featured item'). Each handler diverges on its post-gate surface: GET runs an inline Drizzle select() with tenant scoping returning 404 'Featured item not found' if result.length === 0 or { success: true, data: <featured-item> }; PUT parses JSON body AFTER tenant resolution and runs NO body validation (seven fields destructured: itemName, itemIconUrl, itemCategory, itemDescription, featuredOrder, featuredUntil, isActive -- shoved straight into db.update(...).set({...}).returning()), returns 404 if updatedItem.length === 0 or { success: true, data: <updatedItem>, message: 'Featured item updated successfully' }; DELETE runs soft delete via db.update(...).set({ isActive: false, updatedAt: new Date() }).returning() -- distinct from every prior admin DELETE smoke that actually removes the row -- returns 404 if updatedItem.length === 0 or { success: true, message: 'Featured item removed successfully' } (NO data key). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~16-PUT-body bulk-loop walk all asserting < 500; per-method hybrid 401-envelope assertions; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages must appear in any unauth response; an unauth-lands-on-401-not-403 invariant pinning that the FIRST gate step fires for unauth clients (the response is 401 not 403, and must NEVER echo 'Tenant not found'); a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting POST / PATCH round-trip to < 500; a malformed-JSON-body invariance walk for PUT; a Drizzle-query-not-entered invariance walk across all three handlers; a soft-delete-update-not-entered invariance walk pinning that the unauth response must NEVER echo 'Featured item removed successfully'). Cross-references the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the bare-envelope triple-method admin-clients-clientid-method-spec.md, the hybrid-envelope triple-method admin-users-id-method-spec.md, the categories-CRUD triple-method admin-categories-id-method-spec.md, the 403-on-unauth triple-method admin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-method admin-companies-id-method-spec.md, and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, and docs/questions.md (the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 33-of-N and the tests/api/ per-spec-file sub-rollout extends to 31-of-many, and the first non-admin-gated triple-method admin-tree smoke lands as a complementary surface to the canonical admin-gated triple-method smokes the sub-rollout previously published, and the fourth admin route flagged by Q-010b picks up its own per-source-file reference (joining admin-roles-query-spec.md, admin-roles-active-query-spec.md, and the broader admin-by-id.spec.ts coverage of similar tenant-only-gated routes).
  • E2E Admin Collections [id] Items Method Spec (apps/web-e2e/tests/api/admin-collections-id-items-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-items GET / POST nested-dynamic-id / body / header smoke spec paired with apps/web-e2e/tests/api/admin-collections-id-items-method.spec.ts, the twenty-seventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twenty-fifth under apps/web-e2e/tests/api/. Pairs with apps/web/app/api/admin/collections/[id]/items/route.ts — the first nested-[id]/<sub-resource> dual-method admin smoke the docs tree publishes (every prior dynamic-segment admin smoke covers a [id]/<sub-resource> route as a SINGLE-method export — [id]/review POST, [id]/history GET, [id]/read PATCH, [id]/approve|reject|cancel POST, [id]/permissions GET+PUT — but this is the first that combines GET + POST on a nested path). All three handlers share the SAME single-step inline !session?.user?.isAdmin gate and the SAME canonical longer 401 envelope, but each has its own divergent post-gate surface: GET has no body parse, calls collectionRepository.getAssignedItems(id), returns { success: true, items: [...] } (success key is items, NOT data — distinct from every prior admin GET smoke), catches with safeErrorResponse(error, 'Failed to fetch collection items'); POST parses JSON via await request.json(), validates Array.isArray(body.itemIds) → 400 'itemIds array is required', calls collectionRepository.assignItems(id, body.itemIds), then runs invalidateContentCaches() + two revalidatePath(...) calls (/collections and /collections/<slug>), returns { success: true, collection, updatedItems, message: 'Collection items updated successfully' } (FOUR success-branch keys -- success, collection, updatedItems, message -- distinct from every prior admin POST smoke which uses at most three keys), catches with safeErrorResponse(error, 'Failed to assign collection items'). Documents the at-a-glance scenario tree (a ~6-id × 2-method bulk-loop walk + a ~17-header × 2-method bulk-loop walk + a ~16-POST-body bulk-loop walk all asserting < 500; per-method canonical-longer 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion that NONE of the route-specific items, collection, updatedItems keys plus message and success: true must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the four post-auth messages must appear in any unauth response; a per-nested-id-shape status-stability comparison; a POST body-permutation status-stability comparison; a cross-method side-channel cookie / X-* header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to < 500; a malformed-JSON-body invariance walk for POST; a service-not-entered invariance walk; a side-effects-not-entered invariance walk pinning that the invalidateContentCaches() + revalidatePath(...) chain is unreachable on the unauth branch). Cross-references the full set of sibling per-spec-file references under tests/api/, including the canonical-longer-envelope triple-method admin-items-id-method-spec.md, the bare-envelope triple-method admin-clients-clientid-method-spec.md, the hybrid-envelope triple-method admin-users-id-method-spec.md, and the dual-method admin-roles-id-permissions-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 27-of-N and the tests/api/ per-spec-file sub-rollout extends to 25-of-many, and the first nested-[id]/<sub-resource> dual-method admin smoke lands as a complementary surface to the leaf-[id] triple- and dual-method smokes the sub-rollout previously published.
  • E2E Admin Items [id] History Query Spec (apps/web-e2e/tests/api/admin-items-id-history-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin item-audit-history query-param / dynamic-segment smoke spec paired with apps/web-e2e/tests/api/admin-items-id-history-query.spec.ts, the sixteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the fourteenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/items/[id]/history/route.ts -- the first admin-tree route the smoke layer covers that combines a dynamic-segment [id] GET handler with all four of (a) an auth() gate, (b) a 404 item-existence branch, (c) a query-param surface, and (d) a per-key enum-validation 400 branch. Documents the unique combination of: (1) dynamic-segment GET handler distinct from the dynamic-segment POST route covered by admin-items-id-review-body-spec.md; (2) single-step auth() chain with the canonical longer envelope { success: false, error: 'Unauthorized. Admin access required.' }; (3) canonical longer 401 message matching the canonical-envelope family; (4) success: false envelope key on the 401 branch with a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; (5) item-existence check via itemRepository.findById(itemId, true) AFTER the gate AND AFTER await params -- the first admin-tree route the smoke layer covers that has a 404 item-existence branch between the gate and the query-param parse, with the 404 envelope { success: false, error: 'Item not found' } and the boolean second argument true to findById opting the lookup into including soft-deleted items (distinct from every other admin-tree route the smoke layer covers that does not lift the soft-delete filter); (6) query params parsed AFTER the existence check -- searchParams.get('page') / searchParams.get('limit') / searchParams.get('action') are all read AFTER the 404 branch; (7) page clamping via Number.isNaN(rawPage) ? 1 : Math.max(1, rawPage) (NaN-safe, defaults to 1, clamps to >= 1); (8) limit clamping via Math.min(100, Math.max(1, Number.isNaN(rawLimit) ? 20 : rawLimit)) (NaN-safe, defaults to 20, clamps to 1..100) -- the first admin-tree route the smoke layer covers that documents the Math.min(100, Math.max(1, ...)) two-sided bound posture distinct from the wider unilateral validatePaginationParams(searchParams) posture of the sibling admin-roles / admin-sponsor-ads routes; (9) action enum-validation 400 branch with a dynamically-interpolated message Invalid action filter(s): <bad>. Valid actions are: <list> -- the first admin-tree route the smoke layer covers that emits a dynamic 400 message constructed from user-supplied bad-action strings, with the unauth branch asserting that the response body must NEVER match the /^Invalid action filter\(s\):/ regex prefix nor contain any of the six valid action names (created, updated, status_changed, reviewed, deleted, restored) via word-boundary regexes; (10) itemAuditService.getHistory(...) call AFTER all four gates with success-branch payload { success: true, data: { logs, total, page, limit, totalPages } }; (11) safeErrorResponse(error, 'Failed to fetch item history') catch matching the admin/items/[id]/review catch family; (12) method-resolution surface with GET-only export, so every other method (POST / PUT / PATCH / DELETE) must round-trip to a < 500 status. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~30-query bulk-loop walk + a ~10-id bulk-loop walk all asserting < 500; a canonical-longer 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' } exactly; a gate-before-existence-check invariant pinning that the 'Item not found' 404 message must NEVER appear in the unauth response body; a gate-before-query-parse invariant pinning that the dynamic 'Invalid action filter(s): ...' 400 message must NEVER appear in the unauth response body; an action-enum non-disclosure assertion that the six valid action names must NEVER appear in the unauth response body via word-boundary regexes; a negative-property assertion that the unauth response does NOT echo data.logs / data.total / data.totalPages keys or success: true; a gate-before-catch invariant pinning that the 'Failed to fetch item history' message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting POST / PUT / PATCH / DELETE round-trip to < 500; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a dynamic-[id]-segment invariance walk pinning that numeric / zero / 200-char-padded / Unicode / URL-encoded / soft-deleted-shape ids all round-trip to the same status as the baseline UUID; a repeated-key invariance walk pinning that ?limit=1&limit=999 / ?page=1&page=2 / ?action=created&action=invalid all round-trip to the same status; an action-enum boundary-fuzzing invariance walk pinning that empty / comma-only / mixed-valid+invalid / SQL-injection-shape / XSS-shape / null-byte-encoded action probes all round-trip to the same status). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, admin-items-bulk-body-spec.md, and admin-clients-bulk-method-spec.md (the sibling per-spec-file references), admin-items-page-object.md (the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 16-of-N and the tests/api/ per-spec-file sub-rollout extends to 14-of-many, and the first dynamic-segment-GET-with-404 admin smoke lands as a complementary surface to the dynamic-segment-POST admin/items/[id]/review smoke and to the static-path query smokes the sub-rollout previously published.
  • E2E Admin Clients Bulk Method Spec (apps/web-e2e/tests/api/admin-clients-bulk-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin clients-bulk-action method / body / header smoke spec paired with apps/web-e2e/tests/api/admin-clients-bulk-method.spec.ts, the fifteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the thirteenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/clients/bulk/route.ts -- the first admin-tree route the smoke layer covers that exports two HTTP methods on the same path (PUT for bulk update and DELETE for bulk deletion), distinct from every prior admin-tree smoke spec which covers a single-method route. Documents the unique combination of: (1) dual-method export (PUT + DELETE) -- the first admin-tree route the smoke layer covers where TWO methods are valid handlers and the cross-method probe walks exactly THREE remaining methods (GET / POST / PATCH) rather than the usual four; (2) bare 'Unauthorized' 401 message with bare { error: 'Unauthorized' } envelope (no success: false key) -- distinct from the canonical longer family of admin-items-bulk-body-spec.md, admin-categories-reorder-method-spec.md, and admin-items-id-review-body-spec.md, and the same bare-message family as admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, and admin-notifications-mark-all-read-method-spec.md; (3) single-step auth() chain with bare-message envelope -- the gate is if (!session?.user?.isAdmin) returning 401 with the bare envelope, filling the previously-empty "single-step gate × bare envelope" quadrant in the admin-tree smoke matrix (every prior single-step gate had the canonical longer envelope; every prior bare envelope had a two-step gate); (4) body parse via await request.json() AFTER the gate AND inside the per-method try block -- the gate-then-parse-then-validate-then-loop order is the load-bearing invariant of both methods; (5) single-step body validation AFTER the gate AND AFTER the body parse with one 400 message 'Invalid request: clients array is required' that fires on !Array.isArray(body.clients) || body.clients.length === 0; (6) per-client try/catch loop -- both methods iterate for (const [index, clientData] of body.clients.entries()) collecting successes into a results: { index, success: true, data | clientId }[] array and failures into a errors: { index, error, clientData }[] array, distinct from the single-array results: BulkActionResult[] shape of admin/items/bulk; (7) direct DB-helper call without a repository abstraction -- both methods call updateClientProfile / deleteClientProfile directly from @/lib/db/queries, distinct from the itemRepository.review(...) / itemRepository.delete(...) calls of admin/items/bulk; (8) per-method success-branch payload divergence -- the success branch returns a 200 with a payload shape that differs only in the message template ('Bulk update completed: ...' vs 'Bulk deletion completed: ...') and the per-result inner key (data: <clientProfile> for PUT vs clientId: <id> for DELETE); (9) per-method catch-branch envelope divergence -- each method's try/catch wraps the entire handler body and returns its own safeErrorResponse(error, '<msg>') envelope ('Failed to process bulk update' for PUT, 'Failed to process bulk deletion' for DELETE), the first admin-tree route the smoke layer covers with two distinct catch envelopes on the same path; (10) safeErrorMessage + safeErrorResponse twin-import surface -- the second admin-tree route the smoke layer covers that imports BOTH helpers (after admin/items/bulk); (11) method-resolution surface with PUT AND DELETE exports, so every other method (GET / POST / PATCH) must round-trip to a < 500 status. Documents the at-a-glance scenario tree (a ~18-header × 2-method bulk-loop walk + a ~17-body × 2-method bulk-loop walk both asserting < 500; per-method bare 401-envelope assertions pinning { error: 'Unauthorized' } exactly; a cross-method response-parity assertion that the PUT and DELETE 401 envelopes are byte-identical -- the load-bearing invariant of the dual-method smoke layer; per-method negative-property assertions that the unauth response does NOT echo results / errors / summary keys, the per-result data (PUT) / clientId (DELETE) keys, or success: true; a gate-before-body-validation invariant pinning that the 'Invalid request: clients array is required' and 'Client ID is required' messages must NEVER appear in the unauth response body for either method; a gate-before-catch invariant pinning that neither the 'Failed to process bulk update' nor the 'Failed to process bulk deletion' message appears in the unauth response body; per-method parameterised-vs-baseline status-stability comparisons; per-method side-channel cookie / X-* header walks; a cross-method probe asserting GET / POST / PATCH round-trip to < 500 -- the first admin-tree probe that walks exactly THREE remaining methods; a strict envelope-shape assertion Object.keys(body).sort() === ['error'] (no success / code keys) for both methods; a malformed-JSON-body invariance walk for both methods; a per-client-loop-not-entered invariance walk pinning that body.results / body.errors / body.summary are undefined and body.message does NOT start with either success-branch template). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, admin-items-id-review-body-spec.md, and admin-items-bulk-body-spec.md (the sibling per-spec-file references), admin-clients-page-object.md (the admin clients page-object driver paired with the same admin-clients area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 15-of-N and the tests/api/ per-spec-file sub-rollout extends to 13-of-many, and the first dual-method admin-tree smoke (PUT + DELETE on the same path with byte-identical 401 envelopes and per-method catch envelopes) lands as a complementary surface to the single-method specs the sub-rollout previously published.
  • E2E Admin Items Bulk Body Spec (apps/web-e2e/tests/api/admin-items-bulk-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-bulk-action body / header / method smoke spec paired with apps/web-e2e/tests/api/admin-items-bulk-body.spec.ts, the fourteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the twelfth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/items/bulk/route.ts -- the first admin-tree route the smoke layer covers that pairs the static-path single-step-gate posture of the admin/categories/reorder and admin/twenty-crm/test-connection family with the most-validation-step body validation chain in the admin tree (six distinct 400 messages), distinct from the immediately-preceding dynamic-segment admin-items-id-review-body-spec.md. Documents the unique combination of: (1) POST handler with a static path distinct from the dynamic [id] of admin/items/[id]/review; (2) single-step auth() chain matching the canonical longer message family (if (!session?.user?.isAdmin)), distinct from the two-step gates of admin/users/check-email / admin/users/check-username / admin/notifications/mark-all-read; (3) canonical longer 'Unauthorized. Admin access required.' 401 message matching the admin/categories/reorder, admin/items/[id]/review, and admin/twenty-crm/* family; (4) success: false envelope key matching the same family, distinct from the bare { error: 'Unauthorized' } envelope of the two-step-gated routes; (5) body parse via await request.json() AFTER the gate -- the gate-then-parse-then-validate-then-loop order is the load-bearing invariant of this route; (6) six-step body validation chain AFTER the gate AND AFTER the body parse with six distinct 400 messages ("Action must be 'approve', 'reject', or 'delete'", 'At least one item ID is required', 'Maximum 100 items per bulk action', 'All item IDs must be non-empty strings', 'Duplicate item IDs are not allowed', 'Rejection reason is required (minimum 10 characters)') -- the most validation messages of any admin-tree route the smoke layer covers, distinct from the single-step body validation of admin/items/[id]/review and the three-step body validation of admin/categories/reorder; (7) per-id try/catch loop -- the first admin-tree route the smoke layer covers where individual id failures are collected into a results: BulkActionResult[] array rather than failing the whole request, with the per-id catch using safeErrorMessage(error, 'Unknown error') to extract the per-id error string; (8) conditional repository routing on action routing each id to one of itemRepository.review(id, { status: 'approved' }, auditUser) (with fire-and-forget sendReviewNotification(item, 'approved')), itemRepository.review(id, { status: 'rejected', review_notes: trimmedReason }, auditUser) (with fire-and-forget sendReviewNotification(item, 'rejected', trimmedReason)), or itemRepository.delete(id, auditUser) (no email side-effect), with success-branch payload { success: true, message: 'Bulk <action> completed: <successful> <past-tense>, <failed> failed', results, summary }; (9) safeErrorResponse(error, 'Failed to process bulk action') catch matching the admin/categories/reorder and admin/items/[id]/review catch family; (10) safeErrorMessage + safeErrorResponse twin-import surface -- the only admin route the smoke layer covers that imports BOTH helpers (every other admin-tree route imports only safeErrorResponse); (11) method-resolution surface with POST-only export, so every other method (GET / PUT / PATCH / DELETE) must round-trip to a < 500 status. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~32-body bulk-loop walk both asserting < 500; a canonical-longer 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' } exactly; a negative-property assertion that the unauth response does NOT echo results / summary keys or success: true; a gate-before-body-validation invariant pinning that ALL six 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the 'Failed to process bulk action' message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to < 500; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk; an ids-array-length-invariance walk pinning that at-the-bound (length 100), over-the-bound (length 101), and far-over-the-bound (length 200) probes all round-trip to the same status as the no-body baseline; a per-id-loop-not-entered invariance walk pinning that body.results / body.summary are undefined and body.message does NOT match the success-branch 'Bulk <action> completed: ...' template). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-categories-reorder-method-spec.md, and admin-items-id-review-body-spec.md (the sibling per-spec-file references), admin-items-page-object.md (the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 14-of-N and the tests/api/ per-spec-file sub-rollout extends to 12-of-many, and the most-validation-step admin-tree smoke (six distinct 400 body-validation messages) lands as a complementary surface to the dynamic-segment single-step-validation admin/items/[id]/review smoke that immediately precedes it.
  • E2E Admin Items [id] Review Body Spec (apps/web-e2e/tests/api/admin-items-id-review-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin item-review body / header / method smoke spec paired with apps/web-e2e/tests/api/admin-items-id-review-body.spec.ts, the thirteenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eleventh under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/items/[id]/review/route.ts -- the first dynamic-segment admin-tree route the smoke layer covers, distinct from the prior static-path specs (admin-categories-reorder-method-spec.md, admin-notifications-mark-all-read-method-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md). Documents the unique combination of: (1) POST handler with a dynamic [id] path parameter -- the first admin-tree dynamic-segment route the smoke layer pins, with a { params: Promise<{ id: string }> } second argument resolved AFTER the gate AND AFTER the body validation; (2) single-step auth() chain matching the sibling admin/categories/reorder gate shape (if (!session?.user?.isAdmin)), distinct from the two-step gates of admin/users/check-email / admin/users/check-username / admin/notifications/mark-all-read; (3) canonical longer 'Unauthorized. Admin access required.' 401 message matching the admin/categories/reorder and admin/twenty-crm/* family; (4) success: false envelope key matching the admin/categories/reorder envelope, distinct from the bare { error: 'Unauthorized' } envelope of the two-step-gated routes; (5) body parse via await request.json() AFTER the gate -- the gate-then-parse-then-validate-then-resolve-params-then-call order is the load-bearing invariant of this route; (6) single-step body validation with the 400 message "Review status must be either 'approved' or 'rejected'" -- distinct from the three-step body validation of admin/categories/reorder and the one-key 'Email is required' requirement of admin/users/check-email; (7) itemRepository.review(id, { status, review_notes }, auditUser) call followed by a fire-and-forget EmailNotificationService.sendSubmissionDecisionEmail side-effect, with success-branch payload { success: true, data: <item>, message: 'Item <status> successfully' }; (8) safeErrorResponse(error, 'Failed to review item') catch matching the admin/categories/reorder catch family; (9) method-resolution surface with POST-only export, so every other method (GET / PUT / PATCH / DELETE) must round-trip to a < 500 status. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~19-body bulk-loop walk both asserting < 500; a canonical-longer 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' } exactly; a negative-property assertion that the unauth response does NOT echo 'Item approved successfully' / 'Item rejected successfully' or success: true; a gate-before-body-validation invariant pinning that the 400 message "Review status must be either 'approved' or 'rejected'" must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the 'Failed to review item' message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to < 500; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk; a dynamic-[id]-segment invariance walk pinning that three different [id] shapes (123, 0, a 200-char padded id) all round-trip to the same status as the baseline UUID, pinning the gate-before-params-resolve order). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, admin-notifications-mark-all-read-method-spec.md, and admin-categories-reorder-method-spec.md (the sibling per-spec-file references), admin-items-page-object.md (the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 13-of-N and the tests/api/ per-spec-file sub-rollout extends to 11-of-many, and the first dynamic-segment admin-tree smoke lands -- distinct from the static-path admin-tree smokes the sub-rollout previously published.
  • E2E Admin Categories Reorder Method Spec (apps/web-e2e/tests/api/admin-categories-reorder-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin categories-reorder method / body / header smoke spec paired with apps/web-e2e/tests/api/admin-categories-reorder-method.spec.ts, the twelfth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the tenth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/categories/reorder/route.ts -- the first PUT-method admin-tree route the smoke layer covers, distinct from the prior PATCH-method spec for admin/notifications/mark-all-read (admin-notifications-mark-all-read-method-spec.md). Documents the unique combination of: (1) PUT handler with request: NextRequest body-reading signature distinct from the bare PATCH() of admin/notifications/mark-all-read (which narrows the request surface to zero); (2) single-step auth() chain that collapses unauthenticated and authenticated-non-admin into the SAME 401 envelope, distinct from the two-step gates of admin/notifications/mark-all-read, admin/users/check-email, and admin/users/check-username -- this route's gate is if (!session?.user?.isAdmin) returning 401 with the canonical longer envelope; (3) canonical longer 'Unauthorized. Admin access required.' message matching the admin/twenty-crm/* family and admin/sponsor-ads, distinct from the bare 'Unauthorized' message of the two-step-gated routes; (4) success: false envelope key matching the admin/twenty-crm/test-connection envelope, distinct from the bare { error: 'Unauthorized' } envelope (no success key) of admin/notifications/mark-all-read; (5) body parse via await request.json() AFTER the gate distinct from the bare PATCH() / POST() of the bare-handler routes which never read the body -- the gate-then-parse-then-validate-then-call order is the load-bearing invariant of this route; (6) three-step body validation AFTER the gate AND AFTER the body parse with three distinct 400 messages ('categoryIds must be an array', 'categoryIds array cannot be empty', 'All category IDs must be strings') -- the unauth branch must NEVER reach any of the three validation steps; (7) categoryRepository.reorder(categoryIds) call followed by invalidateContentCaches(), with success-branch payload { success: true, message: 'Categories reordered successfully' } -- the unauth branch must NEVER reach the repository call; (8) safeErrorResponse(error, 'Failed to reorder categories') catch distinct from the console.error + 'Internal server error' catch of the sibling check-email / check-username routes and the bare 'Failed to test connection' catch of admin/twenty-crm/test-connection; (9) method-resolution surface with PUT-only export, so every other method (GET / POST / PATCH / DELETE) must round-trip to a 405 deterministically. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~15-body bulk-loop walk both asserting < 500; a canonical-longer 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' } exactly; a negative-property assertion that the unauth response does NOT echo the success-branch 'Categories reordered successfully' message or success: true key; a gate-before-body-validation invariant pinning that the three 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the 'Failed to reorder categories' message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / POST / PATCH / DELETE round-trip to < 500; a strict envelope-shape assertion Object.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk pinning the gate-before-body-parse order). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md, and admin-notifications-mark-all-read-method-spec.md (the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 12-of-N and the tests/api/ per-spec-file sub-rollout extends to 10-of-many, and the first PUT-method admin-tree smoke lands as the fourth HTTP-method-distinct posture the docs tree publishes (after the GET-tree query smokes, the POST-tree body smokes, and the PATCH-tree method smoke).
  • E2E Admin Notifications Mark-All-Read Method Spec (apps/web-e2e/tests/api/admin-notifications-mark-all-read-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin mark-all-notifications-read method / body / header smoke spec paired with apps/web-e2e/tests/api/admin-notifications-mark-all-read-method.spec.ts, the eleventh per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the ninth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/notifications/mark-all-read/route.ts -- the first admin-tree route the smoke layer covers that documents the unique combination of: (1) PATCH handler -- the first PATCH-only route the e2e suite exercises, distinct from every other admin-tree smoke spec which targets GET / POST; (2) bare PATCH() handler signature (no request parameter) narrowing the request surface to zero -- a regression that adds request parameter and starts reading the body would surface as a status divergence from the no-body baseline; (3) two-step auth() chain that splits 401 vs 403 on the tenantId boundary distinct from the sibling admin/users/check-email and admin/users/check-username routes' two-step gates that split on isAdmin -- this route's first step checks !session?.user?.id (returning 401 'Unauthorized') and the second step checks !tenantId (returning 403 'Tenant not found'); (4) 'Tenant not found' 403 envelope distinct from the sibling routes' bare 'Forbidden' message -- the route-specific message is the production-source convention for endpoints that require multi-tenancy context; (5) direct Drizzle DB call without a repository abstraction -- the handler imports db / notifications from @/lib/db/drizzle and @/lib/db/schema directly and runs an inline db.update(notifications).set({…}).where(and(…)).returning() Drizzle pipeline distinct from the sibling routes' repository abstractions; (6) per-tenant scope on the success branch scoping the update to (a) the authenticated user's notifications only, (b) the unread subset, and (c) the user's tenant only; (7) method-resolution surface -- the route exports ONLY PATCH, so every other method (GET / POST / PUT / DELETE) must round-trip to a 405 deterministically. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~8-body bulk-loop walk both asserting < 500; a bare 401-envelope assertion pinning { error: 'Unauthorized' } exactly; a negative-property assertion that the unauth response does NOT echo the success-branch success: true / updatedCount keys; a gate-step-ordering invariant pinning that the 403 'Tenant not found' envelope must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie / X-* header walk including fabricated X-Tenant-Id / X-User-Id / Authorization: Bearer / X-Api-Key / X-Admin-Token headers; a cross-method probe asserting GET / POST / PUT / DELETE round-trip to < 500; a strict envelope-shape assertion). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md, admin-users-check-username-body-spec.md (the sibling per-spec-file references), admin-notifications-page-object.md (the admin notifications page-object driver paired with the same admin-notifications area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 11-of-N and the tests/api/ per-spec-file sub-rollout extends to 9-of-many, and the first PATCH-method admin-tree smoke lands as the third HTTP-method-distinct posture the docs tree publishes (after the GET-tree query smokes and the POST-tree body smokes).
  • E2E Admin Users Check-Username Body Spec (apps/web-e2e/tests/api/admin-users-check-username-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin check-username request-body / header smoke spec paired with apps/web-e2e/tests/api/admin-users-check-username-body.spec.ts, the tenth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the eighth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/users/check-username/route.ts -- the sibling of apps/web/app/api/admin/users/check-email/route.ts (already covered by admin-users-check-email-body-spec.md). The two routes share an identical authorization shell (same two-step auth() chain 401 + 403, same bare 'Unauthorized' / 'Forbidden' envelopes, same await request.json()-after-gate body parse, same if (!field)-after-body-parse 400 validation, same console.error + 'Internal server error' catch, same { available, exists } success-branch payload shape), differing in exactly four respects: (1) documented field is username vs email; (2) body-validation message is 'Username is required' vs 'Email is required'; (3) repository call is userRepository.usernameExists(username, excludeId) vs userRepository.emailExists(email, excludeId); (4) catch-log prefix is 'Error in POST /api/admin/users/check-username:' vs 'Error in POST /api/admin/users/check-email:'. The unauth branch is INVARIANT to all four divergences -- both routes return the same bare 401 envelope on the first gate step. The per-spec separation surfaces three regression classes a shared spec would mask: (1) cross-route field-validation regression that swaps the if (!username) validation to if (!email) (or vice versa), (2) one-route-only auth-gate-removal regression, (3) username-shape boundary fuzzing on the unauth branch (the username field accepts shorter / narrower inputs than the email field, so the boundary-fuzzing payloads diverge: the username spec walks Unicode / RTL-override / null-byte / SQL injection / XSS / Cyrillic-homoglyph / zero-width-character / collation-sensitivity / leading/trailing-space shapes that target the usernameExists(...) repository call's collation-sensitivity surface, distinct from the email spec's MX-record / CRLF email-header / RFC-5322 boundary surface). Documents the at-a-glance scenario tree (a ~50-body bulk-loop walk asserting < 500; a bare 401-envelope assertion; a negative-property assertion; a parameterised-vs-baseline status-stability comparison; a sibling-route response-parity assertion that the bare-401 envelope of THIS route is byte-identical to the sibling admin/users/check-email route's bare-401 envelope -- the load-bearing invariant of the cross-route smoke layer; a malformed-JSON-body invariance walk; a side-channel cookie / X-* header walk; a cross-method probe; a strict envelope-shape assertion; a Username-required-validation-does-NOT-fire assertion that pins the gate-before-body-validation order). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md, admin-users-check-email-body-spec.md (the sibling per-spec-file references), admin-clients-page-object.md (the admin clients page-object driver paired with the same admin-users area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 10-of-N and the tests/api/ per-spec-file sub-rollout extends to 8-of-many, and the first cross-route response-parity assertion the docs tree publishes (between admin/users/check-email and admin/users/check-username) lands as the load-bearing invariant of the cross-route smoke layer.
  • E2E Admin Users Check-Email Body Spec (apps/web-e2e/tests/api/admin-users-check-email-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin check-email request-body / header smoke spec paired with apps/web-e2e/tests/api/admin-users-check-email-body.spec.ts, the ninth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the seventh under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/users/check-email/route.ts -- the first admin-tree route the docs tree publishes a per-source-file body-surface reference for that documents the full two-step auth() chain posture splitting 401 (no session) from 403 (session without isAdmin). Where the sibling admin-twenty-crm-test-connection-body.spec.ts walks the body surface of a POST route with a single-step gate that collapses both branches into 401 with the canonical longer message ('Unauthorized. Admin access required.'), this spec is its complement -- documenting the unique combination of (a) two-step gate that splits 401 vs 403 with the bare shorter messages ('Unauthorized' / 'Forbidden') and lacks the success: false envelope key; (b) body parse via await request.json() AFTER the gate (distinct from the bare POST() of the test-connection route which never reads the body); (c) body-validation step if (!email) AFTER the gate AND AFTER the body parse (the gate-then-parse-then-validate-then-call order is the load-bearing invariant); (d) internal-error catch with console.error (a side-channel observable via the server log) before returning the bare 'Internal server error' envelope (out of scope for the unauth branch); (e) per-user PII non-disclosure on the unauth branch (the success-branch { available, exists } keys must NEVER appear in the unauth response, which would indicate the gate was bypassed and userRepository.emailExists(email, excludeId) was reached). Documents the at-a-glance scenario tree (a ~45-body bulk-loop walk asserting < 500; a bare 401-envelope assertion pinning { error: 'Unauthorized' } exactly; a negative-property assertion that the unauth response does NOT echo the success-branch available / exists keys; a parameterised-vs-baseline status-stability comparison; a malformed-JSON-body invariance walk pinning the body-parse-after-gate order; a side-channel cookie / X-* header walk; a cross-method probe asserting GET / PUT / DELETE / PATCH round-trip to < 500; a strict envelope-shape assertion Object.keys(body).sort() === ['error'] with no success / available / exists keys). Includes email-shape boundary fuzzing on the unauth branch (null-byte injection admin@example.com\x00malicious@evil.com, CRLF email-header injection, XSS-shape email, SQL-shape email) -- a regression that runs the email validation before the gate would surface here. Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md, admin-items-export-query-spec.md (the sibling per-spec-file references), admin-clients-page-object.md (the admin clients page-object driver paired with the same admin-users area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 9-of-N and the tests/api/ per-spec-file sub-rollout extends to 7-of-many, and the first two-step auth() chain POST route in the admin tree picks up its per-source-file body-surface reference (the second body-surface reference overall, after the single-step admin-twenty-crm-test-connection-body-spec.md).
  • E2E Admin Items Export Query Spec (apps/web-e2e/tests/api/admin-items-export-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-export query-param smoke spec paired with apps/web-e2e/tests/api/admin-items-export-query.spec.ts, the eighth per-source-file reference the docs tree publishes for any file under apps/web-e2e/tests/ and the sixth under apps/web-e2e/tests/api/. Pairs with a new spec covering apps/web/app/api/admin/items/export/route.ts -- the per-tenant items dump counterpart to the sample-template route already covered by the existing apps/web-e2e/tests/api/admin-items-export-sample-query.spec.ts smoke. The two routes share an identical authorization shell (same admin gate session?.user?.isAdmin, same canonical 401 message 'Unauthorized. Admin access required.', same exportQuerySchema Zod parse with the 'csv' | 'xlsx' enum and 'csv' default, same safeErrorResponse(...) catch envelope, same Content-Disposition: attachment; filename="…" binary-stream return shape on the happy path), differing only in the post-gate service call: the sample route calls exportService.generateSampleCSV / XLSX() (a static schema-documentation template), whereas the route under test here calls exportService.exportToCSV / XLSX() (the per-tenant items dump, i.e. every item in the directory's CMS / DB) — and in the catch message ('Failed to export items' vs 'Failed to generate sample template'). The unauth branch is INVARIANT to that distinction (both routes return the same canonical 401 envelope), so the smoke walk pins the same load-bearing "401-before-any-service-call" contract; the per-spec separation surfaces three regression classes a shared spec would mask: (1) sample-route-only catch-message regression that copies the items-export route's 'Failed to export items' catch message into the sample route, (2) items-export-route-only service-call regression that swaps exportToCSV() for generateSampleCSV() (or vice versa), and (3) one-route-only auth-gate-removal regression that removes the admin gate from one route but not the other. Documents the at-a-glance scenario tree (a ~85-path bulk-loop walk asserting < 500; a canonical 401-envelope assertion pinning { success: false, error: 'Unauthorized. Admin access required.' }; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for ?format= covering the case-sensitive CSV / XLSX rejections, ?userId= / ?token= / ?bypass= covering impersonation / magic-token / admin-override keys, ?filename= covering the path-traversal ../../etc/passwd / null-byte %00malicious attack-vector pins, ?metadata= covering the #include-metadata checkbox in the AdminDataExportPage driver; an Accept header walk including the application/vnd.openxmlformats-officedocument.spreadsheetml.sheet XLSX MIME type; a side-channel cookie / X-* header walk; a repeated-key walk). Cross-references to smoke-health-spec.md, smoke-navigation-spec.md, admin-settings-map-status-query-spec.md, admin-twenty-crm-config-query-spec.md, admin-sponsor-ads-query-spec.md, admin-roles-query-spec.md, admin-roles-active-query-spec.md (the sibling per-spec-file references), admin-data-export-page-object.md (the admin data-export page-object driver paired with the same admin route's UI shell), admin-item-form-page-object.md and admin-items-page-object.md (the sibling admin-items shells), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 8-of-N and the tests/api/ per-spec-file sub-rollout extends to 6-of-many, and the sample-template / items-dump pair of admin-export routes both pick up per-source-file references documenting their identical authorization shell and divergent post-gate service calls.
  • E2E Item-Detail Page Object (apps/web-e2e/page-objects/public/item-detail.page.ts) -- Per-source-file reference for the Playwright e2e suite's item-detail page driver paired with apps/web-e2e/page-objects/public/item-detail.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and language-switcher-page-object.md documents the suite's locale-switching driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's per-item detail-page driver boundary — the smallest possible page object that lets a spec drive the public item-detail page (/items/[slug]) end-to-end (navigate either to a known slug or to the first item linked from / via a 30 s seed-data-tolerant slug-agnostic discovery primitive, click the canonical OR-of-two-aria-label "Upvote" / "Remove upvote" toggle button to upvote or un-upvote the item, read the live vote count via the [aria-live="polite"] accessibility region with a ?? '0' nullish-coalesce that pins the public return type to Promise<string> and a .first() strict-mode-correctness append, toggle the favorite via the aria-label*="favorites" substring-matched .first() button whose plural-by-design substring survives a future "Save favorite" rename without masking a real label-rename regression, locate the comments section by its OR-of-tags section, div filter on the ^comments-anchored case-insensitive regex that survives a future wrapping-tag refactor, fill the #comment textarea via the production-source HTML-form id-based accessibility wiring and click the name=/post comment/i button via the case-insensitive accessible-name match to post a comment as an authenticated user, hover any existing comment to surface the per-comment aria-label="Edit comment" / aria-label="Delete comment" buttons that the production source hides behind a :hover CSS state, and resolve the delete-comment confirmation dialog via the [role="dialog"] filter on the /delete comment/i body text re-evaluated on every read via a get-accessor instead of a stale readonly Locator field). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the export class ItemDetailPage extends BasePage single named export with the extends BasePage clause — the page-route driver posture; the eight readonly Locator fields covering heading / voteButton / voteCount / favoriteButton / commentsSection / commentTextarea / postCommentButton / signInToCommentButton; the synchronous constructor that calls super(page) first then pre-binds every per-page Locator in a single pass; the navigateToItem(slug) slug-driven primitive; the navigateToFirstItem() slug-agnostic discovery primitive with the 30 s seed-data tolerance; the clickVote() bare upvote primitive; the getVoteCount(): Promise<string> polite-aria-live region read with the ?? '0' fallback; the isVoted(): Promise<boolean> strict-equality state pin on 'Remove upvote'; the clickFavorite() bare favorite-toggle primitive; the postComment(text) composite fill-then-click primitive; the getComment(text): Locator per-comment factory; the editComment(text) / deleteComment(text) hover-then-click primitives; the get deleteCommentDialog() re-evaluating Locator getter); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming specs at apps/web-e2e/tests/public/item-detail.spec.ts (heading visible, body content visible) and apps/web-e2e/tests/public/votes-and-comments.spec.ts (vote toggle, favorite toggle, comment post / edit / delete); the "Why the class extends BasePage" walkthrough that pins the three load-bearing reasons (page-route navigation via inherited goto(), global header / footer / navLinks chrome surfaced through inherited composite getters, waitForPageReady() post-navigation stabiliser); the "Why the vote button uses an OR-of-two-aria-labels selector" walkthrough that pins the three reasons (state-agnostic resolution across the pre-vote / post-vote DOM flip, exact-match collision-signal preservation against future per-comment "Vote on comment" buttons, symmetry with isVoted()'s strict-equality check); the "Why the favorite button uses an aria-label*="favorites" substring selector" walkthrough that pins the three reasons (state-agnostic resolution, plural-by-design "favorites" survives a future "Save favorite" rename, .first() strict-mode-correctness append); the "Why getVoteCount() returns Promise<string>" walkthrough that pins the three reasons (screen-reader-contract string emission, ?? '0' fallback, locale-thousands-separator preservation); the "Why isVoted() checks the exact 'Remove upvote' label" walkthrough that pins the three reasons (OR-of-two-aria-labels collapse, substring drift avoidance, symmetry with the voteButton selector); the failure matrix covering every item-detail-page-level mistake (type-only import drop, extends BasePage removal, readonly drop on any Locator, single-aria-label re-bind on the vote button, substring aria-label*="vote" re-bind, .first() drop on voteCount, single-aria-label re-bind on the favorite button, singular *="favorite" rebind, .first() drop on the favorite, single-tag re-bind on commentsSection, non-anchored regex on commentsSection, non-id re-bind on commentTextarea, exact-match re-bind on postCommentButton, signInToCommentButton field drop, super(page) drop, pre-parse to number on getVoteCount, substring includes('Remove') re-bind on isVoted, hover-step drop on editComment / deleteComment, readonly Locator field conversion of deleteCommentDialog, non-role="dialog" re-bind, data-testid re-bind, file move, .tsx rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming specs, the production-source DOM contract under apps/web/components/item-detail/*, base-page-object.md for the inherited surface, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, fixtures-index.md for a future authenticated variant) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto vote-button-aria-label-rename, polite-region-selector-change, favorite-button-substring-drop, comments-section-heading-rename, #comment-id-change, [role="dialog"]-removal, middleware-prefix-change, and baseURL-change failures; and the item-detail.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/item-detail.spec.ts and apps/web-e2e/tests/public/votes-and-comments.spec.ts), a base-page-object.md cross-check, a production-source cross-check (the H1 role, the OR-of-two-aria-label on the vote button, the [aria-live="polite"] polite region, the aria-label*="favorites" substring on the favorite, the #comment textarea id, the "Post comment" / "Sign in to comment" button text, and the [role="dialog"] confirm-modal), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future authenticated variant would surface there), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the item-detail spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Item Detail"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Star-Rating Page Object (apps/web-e2e/page-objects/public/star-rating.page.ts) -- Per-source-file reference for the Playwright e2e suite's five-star rating-picker driver paired with apps/web-e2e/page-objects/public/star-rating.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and sort-menu-page-object.md documents the suite's listing-sort driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's per-item rating-picker driver boundary — the smallest possible page object that lets a spec drive the item-detail page's five-star rating picker end-to-end (locate the picker by its WAI-ARIA [role="radiogroup"] / aria-label="Rating" pair pinned to the first match for strict-mode safety, click any star 1–5 by its visible aria-label*="N star" substring scoped through the container Locator, and read the currently selected rating value via a reverse aria-checked="true" scan that returns the highest-numbered checked star or 0 when nothing is checked). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class StarRating single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage; the readonly container: Locator page.locator('[role="radiogroup"][aria-label="Rating"]').first() exact-match dual selector that pins to both the WAI-ARIA radiogroup role AND the exact aria-label="Rating" value to disambiguate against future sibling radio groups while deliberately avoiding a substring match because a future translation layer would localize the label and break the selector silently — see the i18n row in the failure matrix; the constructor(page: Page) that stores the page and pre-binds the single container Locator without a super(page) call; the star(n: number): Locator locator-factory that constructs a per-star Locator at call-time by interpolating the numeric n into the aria-label*="${n} star" substring match, scoped through this.container.locator(…) rather than this.page.locator(…) to survive a future "1 star = bad, 5 stars = great" legend rendered in the page footer or sidebar, with the substring match accommodating both the singular "1 star" and plural "2 stars"/"3 stars"/"4 stars"/"5 stars" shapes, returning the Locator without clicking so consuming specs can compose visibility / role / aria-checked assertions on the same Locator; the rate(n: number) composite "click the nth star" primitive that reuses the star(n) factory; the getValue(): Promise<number> accessor that reverse-iterates i = 5..1, reads await this.star(i).getAttribute('aria-checked'), returns i on the first 'true' and falls through to return 0 when nothing is checked, with the reverse-iteration load-bearing for the highest-checked-wins semantics that handle the host app's HeroUI fill-up-to-N pattern correctly and the return 0 floor turning the no-rating state into a definitive numeric sentinel that pins the public return type to Promise<number>); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/star-rating.spec.ts (picker container visibility on the item-detail page reachable from the first a[href*="/items/"] link on /discover/1, fourth-star click via rate(4) with a 500 ms settle delay before reading getValue() returns 4, all five star buttons present via per-star visibility loop — all three tests soft-skip with test.skip(true, …) when the picker container is not visible because ratings may be disabled, the comment form may not surface, or the user may not be authenticated); the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the item-detail surface, reusability on non-item-detail surfaces like a future "Rate this content" inline widget on the dashboard / per-tag rating mosaic / admin-shell rating-distribution widget, constructor parity with non-page widgets like theme-toggle.page.ts / language-switcher.page.ts / share-button.page.ts / view-toggle.page.ts / scroll-to-top.page.ts / search-bar.page.ts / sort-menu.page.ts); the "Why [role="radiogroup"][aria-label="Rating"] exact match" walkthrough that pins the three reasons for the dual selector (the radiogroup role is the WAI-ARIA-canonical primitive for a single-select radio surface vs the related-but-different [role="group"] pattern, the aria-label="Rating" anchor disambiguates against future sibling radio groups like Difficulty / Recommend / per-survey radio sets, no production-source change required because the role and label are already there for accessibility); the "Why .first() on the container Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second rating picker like a sticky-header rating-picker mirror / portal-rendered modal duplicate / Rate-average mosaic badge, strict-mode collision against an already-rendered duplicate during item-to-item navigation transitions with exit-animation overlap, strict-mode collision against a portal-rendered duplicate); the "Why star(n) returns a Locator instead of clicking" walkthrough that pins the three reasons for the locator-factory shape (composability with expect(starRating.star(3)).toBeVisible() / toHaveAttribute('aria-checked', 'true') / .hover() chains, symmetric posture with rate(n), type-narrowed Locator return for IntelliSense); the "Why aria-label*="N star" substring match (and not exact)" walkthrough that pins the three reasons for the substring posture (plural-form variance between singular "1 star" and plural "2 stars"/.../"5 stars", future locale variance with labels like "1 star (out of 5)", future a11y-label expansion with labels like "Rate as 1 star"); the "Why reverse iteration in getValue()" walkthrough that pins the three reasons for the i = 5..1 walk (highest-checked-wins semantics correctly handle the fill-up-to-N pattern that would always return 1 on a forward iteration, short-circuit on the most-likely-rating common case 4–5, symmetric to the visual left-to-right rendering); the "Why return 0 as the no-rating sentinel" walkthrough (type narrowing to Promise<number>, defensive symmetry with sibling getCurrentTheme(): Promise<string> / getValue(): Promise<string> posture, 0 as the natural null-rating value matching the host app's data-model); the failure matrix covering every star-rating-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop on page or container, aria-label-only or role-only single anchor swap, [aria-label*="Rating"] substring swap that breaks under translation, data-testid swap, .first() drop on container, aria-label="N star" exact match swap that breaks plural forms, this.page.locator(…) swap that loses container scoping, await this.star(n).click() shape swap that loses Locator composability, forward iteration in getValue(), Promise.all swap that loses short-circuit, return 0 fallback drop, file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/star-rating.spec.ts, future smoke / a11y specs that drive rate(N) for any N in 1..5 or audit per-star aria-label, the item-detail-page production-source component for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, discover-page-object.md for the /discover/[N] listing-route contract, fixtures-index.md for the clientPage fixture barrel that grants authenticated-user state) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto [role="radiogroup"] semantic-loss soft-skip, aria-label="Rating" translation soft-skip, <button><input type="radio"> element-swap, picker-label rename, item-route-change, middleware-prefix, baseURL-change, feature-flag-disable (intended soft-skip), library-swap, and clientPage fixture-removal failures; and the star-rating.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/star-rating.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (the container's [role="radiogroup"] ARIA role / aria-label="Rating" anchor / per-star aria-label*="N star" accessible-name shape / per-star aria-checked attribute), a discover-page-object.md cross-check (the /discover/[N] listing-route contract the consuming spec follows before navigating to the item-detail surface), a fixtures-index.md cross-check (the clientPage fixture for authenticated-user state), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the star-rating spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Star Rating"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Sort-Menu Page Object (apps/web-e2e/page-objects/public/sort-menu.page.ts) -- Per-source-file reference for the Playwright e2e suite's listing-sort dropdown driver paired with apps/web-e2e/page-objects/public/sort-menu.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and share-button-page-object.md documents the suite's social-share driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's listing-sort driver boundary — the smallest possible page object that lets a spec drive the public listing's sort dropdown end-to-end (open the dropdown by clicking the aria-haspopup="menu" trigger button pinned to the first match for strict-mode safety, select any sort option by visible-text regex via the dual [role="menuitemradio"], [role="menuitem"] ARIA-role selector that accommodates both single-select and free-action menu shapes, read the trigger button's current text content via textContent()?.trim() ?? '' to assert that the post-select label changed). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class SortMenu single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage AND is also used inside selectOption to construct the option Locator at call-time rather than constructor-time because the option set materialises only after open() has been called; the readonly trigger: Locator page.locator('button[aria-haspopup="menu"]').first() exact-match selector pinned to the canonical ARIA-spec value menu (not true and not listbox) for strict-mode-correctness against [role="menu"] popups; the readonly menuContent: Locator page.locator('[role="menu"]').first() deliberately-exposed dropdown Locator that lets a spec assert on the opened dropdown's properties without reaching through a method; the constructor(page: Page) that stores the page and pre-binds the two Locators in a single pass without a super(page) call; the open() minimal "open the dropdown" primitive every other method composes against; the selectOption(text: RegExp) composite "open the dropdown then click the first option matching the regex" primitive with the load-bearing RegExp parameter type that forces consuming specs to think about the case-insensitive substring contract rather than hard-coding an English-only string and the dual-role [role="menuitemradio"], [role="menuitem"] selector that accommodates both single-select and free-action option shapes; the getCurrentLabel(): Promise<string> accessor that reads the trigger button's text content via textContent()?.trim() ?? '' with the ?? '' nullish-coalesce that pins the public return type to Promise<string> and mirrors the sibling theme-toggle-page-object.md's getCurrentTheme() and search-bar-page-object.md's getValue() posture); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/sort-menu.spec.ts (trigger visibility on /discover/1 after a 2-second settle delay, dropdown opens with at least one menu item, second-option selection updates the trigger label — all three tests soft-skip with test.skip(true, …) when the trigger is not visible); the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the listing surface, reusability on non-listing surfaces like a future admin-shell sort menu / client-shell sort menu on the user submissions page / category-page sort menu, constructor parity with non-page widgets like theme-toggle.page.ts / language-switcher.page.ts / share-button.page.ts / view-toggle.page.ts / scroll-to-top.page.ts / search-bar.page.ts / star-rating.page.ts); the "Why aria-haspopup="menu" exact match and not a substring" walkthrough that pins the three reasons for the strict-equality posture (the menu value is the WAI-ARIA-spec-canonical match for a trigger that opens a [role="menu"] popup vs the related-but-different listbox / dialog patterns, strict equality survives a future aria-haspopup="dialog" Filter button regression that adds another popup trigger to the same listing, no production-source change required because the aria-haspopup attribute is already there for accessibility); the "Why [role="menu"] exact match for menuContent" walkthrough that pins the three reasons for the role-anchor posture (screen-reader-driven primitive, library invariance against HeroUI / Radix / Headless UI / shadcn-ui swaps, strict-mode resilience against future submenus); the "Why .first() on every Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second sort trigger / dropdown / portal mirror, strict-mode collision against an already-open [role="menu"] from a profile-dropdown / navigation submenu, strict-mode collision against a portal-rendered duplicate); the "Why the dual-role selector in selectOption" walkthrough that pins the three reasons for the comma-separated [role="menuitemradio"], [role="menuitem"] selector (single-select vs free-action option-shape mismatch where HeroUI writes menuitemradio for single-select but a future multi-select / reset-action sort would write menuitem, library variance because different menu libraries default to different role choices, compatibility with the consuming spec's option-count check that uses the same dual selector); the "Why text: RegExp and not text: string" walkthrough that pins the three reasons for the regex parameter type (locale invariance because selectOption(/votes/i) matches translations like "Stimmen", strict-mode resilience because the i flag tolerates casing variants, type-narrowed Locator construction that documents the case-insensitivity expectation); the "Why ?.trim() ?? '' on getCurrentLabel" rationale (type narrowing to Promise<string>, whitespace tolerance against flex / gap rendering, defensive symmetry with sibling getCurrentTheme() / getValue() posture); the failure matrix covering every sort-menu-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop on page or any of the two Locator fields, aria-haspopup*="menu" substring swap, aria-haspopup="true" legacy-ARIA swap, data-testid swap, .first() drop on trigger or menuContent, [role="menu"] ARIA-role anchor drop on menuContent, dual-role selector drop in selectOption, text: RegExptext: string parameter swap, .first() drop on the option Locator, ?.trim() drop on getCurrentLabel, ?? '' drop on getCurrentLabel, pre-construction of the option Locator in the constructor before open() materialises the option set, file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/sort-menu.spec.ts, future smoke / a11y specs that call selectOption(/votes/i) to drive specific sort flows or read menuContent for aria-orientation audits / role-count audits / screenshot diffs, the listing-page production-source component for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, discover-page-object.md for the /discover/[N] listing-route contract the consuming spec follows before reaching the sort menu) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto trigger-visibility-soft-skip, Locator not found, listbox-pattern-swap, sort-option-rename, route-change, middleware-prefix, baseURL-change, and library-removal failures; and the sort-menu.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/sort-menu.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (the trigger's aria-haspopup attribute, the [role="menu"] ARIA contract on the dropdown, the [role="menuitemradio"] or menuitem role on every option, the trigger button's text-content shape that getCurrentLabel() reads), a discover-page-object.md cross-check (the /discover/[N] listing-route contract the consuming spec follows before reaching the sort menu), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future fixture-bound sort menu would surface there), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the sort-menu spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Sort Menu"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Share-Button Page Object (apps/web-e2e/page-objects/public/share-button.page.ts) -- Per-source-file reference for the Playwright e2e suite's share-button dropdown driver paired with apps/web-e2e/page-objects/public/share-button.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and scroll-to-top-page-object.md documents the suite's scroll-position driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's social-share driver boundary — the smallest possible page object that lets a spec drive the item-detail page's social-share dropdown end-to-end (open the dropdown by clicking the trigger button whose visible text matches the case-insensitive /share/i regex pinned to the first match for strict-mode safety, click any of the four [role="menuitem"] menu items — Copy Link / Twitter (X) / Facebook / LinkedIn — to fire the per-platform share intent, and read each menu-item Locator for visibility / a11y assertions). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class ShareButton single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage; the readonly trigger: Locator page.locator('button').filter({ hasText: /share/i }).first() regex-filter selector that pins to the visible text of the share button (the host app's button has no aria-label today); the readonly copyLinkItem: Locator [role="menuitem"] ARIA-role anchor with the case-insensitive /copy link/i regex filter for the always-present Copy Link entry; the readonly twitterItem: Locator with the dual-substring /twitter|x \(/i regex that survives the X rebrand by matching either the legacy "Twitter" text or the post-rebrand "X (formerly Twitter)" shape via the x \( disambiguator; the readonly facebookItem: Locator and readonly linkedinItem: Locator siblings with the same [role="menuitem"] and i-flag posture; the constructor(page: Page) that stores the page and pre-binds the five Locators in a single pass without a super(page) call; the open() minimal "open the dropdown" primitive every other action method composes against; the copyLink() composite "open the dropdown then click Copy Link" primitive — the only end-to-end action method in the class today because it is the only deterministic per-platform action whose effect can be verified without a popup-verification harness). Documents the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/share-button.spec.ts (trigger visibility on the item-detail page reachable from /discover/1, dropdown opens with at least two [role="menuitem"] entries, both tests soft-skip with test.skip(true, …) when the trigger is not visible); the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the item-detail surface where the share button is one of several item-detail widgets, reusability on non-item-detail surfaces like a future profile / collection / per-tag share button, constructor parity with non-page widgets like theme-toggle.page.ts / language-switcher.page.ts / view-toggle.page.ts / scroll-to-top.page.ts / search-bar.page.ts / sort-menu.page.ts / star-rating.page.ts); the "Why filter({ hasText: /share/i }) and not an aria-label" walkthrough that pins the three reasons for the visible-text regex filter posture (production source carries no aria-label today so adding one would be a production-source concession to the e2e suite, visible-text invariance because the substring share survives every label evolution and the i flag tolerates case drift, strict-mode resilience against a future second share button with .first()); the "Why [role="menuitem"] and not a data-testid" walkthrough that pins the three reasons for the ARIA-role anchor posture (no production-source change required because the role is already there for accessibility and every menu library — HeroUI / Radix / Headless UI / shadcn-ui — writes it onto the dropdown rows, screen-reader-driven discoverability so the e2e suite drives the UI the same way an assistive-technology user does, strict-mode resilience against a future second [role="menuitem"] group on the same page like a profile-dropdown / navigation submenu / context menu); the "Why the Twitter regex uses /twitter|x \(/i" walkthrough that pins the three reasons for the dual-substring posture (survives the X rebrand by matching legacy "Twitter", post-rebrand "X", and transitional "X (formerly Twitter)" copy variants, the x \( fragment with the literal opening parenthesis disambiguates from incidental matches like "Export" / "Excel" / "Mr. X", the trailing space + ( survives copy variants while staying strict-mode-safe even after a future copy-team revision); the "Why .first() on every Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second share trigger like a sticky share widget / mobile-drawer share mirror / admin-only "share with team" trigger, strict-mode collision against a future nested dropdown like a profile-dropdown rendered alongside the share dropdown with its own [role="menuitem"] entries, strict-mode collision against a portal-rendered duplicate like a mobile drawer that mirrors the desktop share dropdown); the "Why the i flag on every regex" walkthrough that pins the locale-style casing drift survival, production-source casing drift survival, and per-tenant override survival; the "Why only open() and copyLink() action methods" walkthrough that pins the three reasons for the no-per-platform-method posture (copyLink() is the only deterministic action whose effect can be verified via clipboard inspection while the per-platform entries open external window.open(...) URLs that require a popup-verification harness, symmetric posture preserves a future addition the day a popup-verification harness lands, direct-Locator access discipline lets specs interact with the four item Locators for visibility / getAttribute('href') / a11y audits without reaching through a method); the failure matrix covering every share-button-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop on page or any of the five Locator fields, aria-label-based selector swap on trigger, i-flag drop on any regex filter, .first() drop on any Locator, bare /x/i swap on the Twitter regex causing strict-mode chaos, bare /twitter/i swap that stops matching post-rebrand "X" entries, [role="menuitem"] ARIA-role anchor drop on item Locators, [role="menuitem"]data-testid swap, unconditional selectTwitter() / selectFacebook() / selectLinkedIn() add without a popup-verification harness, copyLink() composite shape drop, any of the four item fields drop, open() method drop while keeping copyLink(), file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/share-button.spec.ts, future smoke / a11y specs that audit per-platform getAttribute('href') and role counts plus future per-platform action methods once popup verification lands, the item-detail-page production-source component for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, discover-page-object.md for the /discover/[N] listing-route contract the consuming spec follows before reaching the share button) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto trigger-visibility-soft-skip, menu-item-Locator not found, item-detail-route-change, and dropdown-library-swap failures; and the share-button.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/share-button.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (the trigger's visible text, the four menu-item entries' visible text, the [role="menuitem"] ARIA contract on every dropdown row, the host app's clipboard-write / window.open posture for the Copy Link / per-platform actions), a discover-page-object.md cross-check (the /discover/[N] listing-route contract the consuming spec follows before reaching the item-detail share button), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future fixture-bound share button would surface there), a per-platform popup-verification harness cross-check if a future selectTwitter() / selectFacebook() / selectLinkedIn() method is added, dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the share-button spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Share Button"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Scroll-To-Top Page Object (apps/web-e2e/page-objects/public/scroll-to-top.page.ts) -- Per-source-file reference for the Playwright e2e suite's scroll-to-top floating-button driver paired with apps/web-e2e/page-objects/public/scroll-to-top.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and view-toggle-page-object.md documents the suite's listing-view-mode driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's scroll-position driver boundary — the smallest possible page object that lets a spec drive the global "back to top" floating button end-to-end (scroll the page down by an arbitrary pixel offset to trigger the button to appear, click the button to animate the page back to the top, read the current window.scrollY value at any point during the flow to make scroll-position assertions). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class ScrollToTop single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage; the readonly button: Locator page.locator('button[aria-label="Scroll to top"]') exact-match selector with no .first() because the floating button is a single-instance fixed-position widget on every page; the constructor(page: Page) that stores the page and pre-binds the single button Locator without a super(page) call; the scrollDown(pixels = 500) primitive that runs window.scrollBy(0, pixels) inside the page context to scroll the document by pixels pixels vertically with a 500-pixel default that comfortably clears the production source's ~300-pixel threshold; the click() primitive that clicks the floating button; the getScrollY(): Promise<number> accessor that returns window.scrollY for both precondition and postcondition assertions); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/scroll-to-top.spec.ts (button hidden at the top of /, button appears after scrollDown(500), page returns to the top after click()); the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against any host surface where the scroll-to-top button is a global floating widget rendered on every page in every role tree, reusability across all role trees including future admin-shell or client-shell scroll-to-top consumers, constructor parity with non-page widgets like theme-toggle.page.ts / view-toggle.page.ts / language-switcher.page.ts / share-button.page.ts / search-bar.page.ts); the "Why exact aria-label=\"Scroll to top\" and not a substring" walkthrough that pins the three reasons for the exact-match posture (the label is the canonical accessibility primitive a screen reader announces, no collision risk requiring substring-tolerance because there is no plausible label variant, strict-mode safety from the single-button shape with future regressions surfacing as strict-mode violations); the "Why no .first() on the button Locator" walkthrough that pins the three reasons for the bare Locator (single-instance invariant, strict-mode signal preservation against future duplicate-button regressions, symmetric posture with future a11y assertions that count exactly one scroll-to-top button); the "Why page.evaluate(() => window.scrollBy(0, px), pixels) for scrollDown" walkthrough that pins the deterministic scroll distance, the no-reliance on viewport-shape for PageDown, and the threshold-test ergonomics; the "Why pixels = 500 default on scrollDown" rationale (comfortable threshold clearance, symmetric with the consuming spec's per-test override, documentation-by-default); the "Why getScrollY reads window.scrollY and not React state" walkthrough that pins the production-source-first signal, the no reach-in to React internals, and the symmetric click-and-return assertion shape; the failure matrix covering every scroll-to-top-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop on page or button, substring aria-label*= swap, data-testid swap, accidental .first() add, page.mouse.wheel swap on scrollDown with wheel-acceleration flake, page.keyboard.press('PageDown') swap with viewport-dependence, pixels = 500 default drop, React-state read on getScrollY, Promise<number> annotation drop, file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/scroll-to-top.spec.ts, future smoke / a11y specs that count button.count() === 1 and audit aria-label, the listing-page / item-detail-page production-source components for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto Locator not found, threshold-clearance failure, and JavaScript-disabled-route failures; and the scroll-to-top.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/scroll-to-top.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (the exact aria-label="Scroll to top" attribute, the fixed-position floating shape, the ~300-pixel scroll threshold, the React-state-driven visibility flip), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future fixture-bound scroll-to-top would surface there), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the scroll-to-top spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "scroll-to-top"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E View-Toggle Page Object (apps/web-e2e/page-objects/public/view-toggle.page.ts) -- Per-source-file reference for the Playwright e2e suite's public-listing view-toggle driver paired with apps/web-e2e/page-objects/public/view-toggle.page.ts, sitting inside the public/ page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and theme-toggle-page-object.md documents the suite's theme-switch driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's listing-view-mode driver boundary — the smallest possible page object that lets a spec drive the public listing's list / grid / masonry / map view-toggle end-to-end (read the four toggle buttons, switch to one of the three named view modes, observe which button is the "active" one via the scale-105 Tailwind utility class written into the button's class list). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class ViewToggle single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage; the readonly listButton: Locator page.locator('button[aria-label*="list" i]').first() case-insensitive substring selector pinning to the first match; the readonly gridButton: Locator page.locator('button[aria-label*="grid" i]').first() mirror for the grid mode; the readonly masonryButton: Locator page.locator('button[aria-label*="masonry" i]').first() mirror for the masonry mode; the readonly mapButton: Locator page.locator('button[aria-label*="map" i]').first() mirror for the map mode (with no selectMap() method today because the map-view feature is gated behind features/map-view.md); the constructor(page: Page) that stores the page and pre-binds the four button Locators in a single pass without a super(page) call; the selectList() / selectGrid() / selectMasonry() symmetric click primitives; the isActive(button: Locator) predicate that reads the supplied button's class attribute and returns whether the scale-105 Tailwind utility-class substring is present, with the ?? false nullish-coalesce that pins the public return type to Promise<boolean> and mirrors the sibling theme-toggle-page-object.md's isDarkMode() posture); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/view-toggle.spec.ts (visibility on /discover/1, grid-active flip after selectGrid(), list-active flip after selectList()); the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the listing surface, reusability on non-listing surfaces like a future admin-shell list/grid switch, constructor parity with non-page widgets like theme-toggle.page.ts / language-switcher.page.ts / share-button.page.ts / search-bar.page.ts); the "Why aria-label*=\"…\" i and not a data-testid" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required for tests, view-label invariance because the substring is invariant to phrasing variants like "View as list" / "List view" / "Show as a list", strict-mode resilience against a future second view toggle with .first()); the "Why .first() on every button Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future admin-shell view toggle, strict-mode collision against per-card "Add to list" buttons, strict-mode collision against a portal-rendered mobile drawer mirror); the "Why the i flag on every substring selector" walkthrough that pins the locale-style casing drift survival, production-source casing drift survival, and per-tenant override survival; the "Why there is no selectMap() method today" walkthrough that pins the three reasons (map-view is feature-gated behind features/map-view.md, symmetric posture preserves a future addition the day the map mode becomes always-on, the exposed mapButton field permits direct-Locator interaction); the "Why isActive() reads the scale-105 substring" walkthrough that pins the production-source-first signal, strict-mode safety against future class-list expansion, and future-proofing against aria-pressed adoption as an additive a11y signal; the "Why ?? false on the class-list scan" rationale (type narrowing to Promise<boolean>, defensive symmetry with theme-toggle-page-object.md's isDarkMode()); the failure matrix covering every view-toggle-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop on any of the five fields, exact aria-label match instead of substring, i flag drop, .first() drop on any button, aria-label*=data-testid swap, unconditional selectMap() add, mapButton field drop, React state / aria-pressed reach-in for isActive(), scale-105bg-primary substring swap with hover false-positive, ?? false drop on isActive(), file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/view-toggle.spec.ts, future smoke / a11y specs that need the mapButton field for a feature-gated map-view spec, the listing-page production-source component for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL, features/map-view.md for the map-view feature gate) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto Locator not found, isActive()-returns-false-on-active-button, and mapButton half-rendered failures; and the view-toggle.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/view-toggle.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (the aria-label shape on every button, the scale-105 Tailwind utility-class hook on the active button, the four button positions in the toggle row), a discover-page-object.md cross-check (the /discover/[N] listing-route contract the consuming spec relies on), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future fixture-bound view-toggle would surface there), a features/map-view.md cross-check (if a future selectMap() method is added), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the view-toggle spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "view-toggle"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Search-Bar Page Object (apps/web-e2e/page-objects/public/search-bar.page.ts) -- Per-source-file reference for the Playwright e2e suite's public-listing search-input driver paired with apps/web-e2e/page-objects/public/search-bar.page.ts, sitting inside the public/ page-object subtree alongside the fourteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and theme-toggle-page-object.md documents the suite's theme-switch driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's directory-search driver boundary — the smallest possible page object that lets a spec drive the public listing's search <input> end-to-end (type a search term, clear the input, read back the current value). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class SearchBar single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage; the readonly input: Locator page.locator('input[placeholder*="Search" i]').first() case-insensitive substring selector on the <input>'s placeholder attribute pinned to the first match; the readonly clearButton: Locator page.locator('button', { hasText: '×' }).first() Locator pinned to the multiplication-sign glyph U+00D7 (not the lower-case Latin x U+0078); the constructor(page: Page) that stores the page and pre-binds input and clearButton without a super(page) call; the search(term: string) method that calls Playwright's fill() for debounce-deterministic single-round-trip semantics; the clear() method that calls Playwright's clear() to handle empty-input safety regardless of the clear button's visibility; the getValue(): Promise<string> accessor with the ?? '' nullish coalesce that future-proofs against a Playwright API change to string | null); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/search.spec.ts (three flows: visibility on /, fill+read after debounce, clear+read after debounce); the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against any host surface where the search input is page-mounted not page-shaped, reusability on non-listing surfaces like a future admin-shell or client-shell search widget, constructor parity with non-page widgets like theme-toggle.page.ts / language-switcher.page.ts / share-button.page.ts / view-toggle.page.ts); the "Why placeholder*=\"Search\" i and not a data-testid" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required for tests, placeholder-text invariance because the prefix \"Search\" survives every label evolution and the i flag tolerates case drift, strict-mode resilience against a future second search input with .first()); the "Why hasText: '×' for the clear button" walkthrough that pins the three reasons for the multiplication-sign-glyph selector (glyph invariance because next-intl does not translate Unicode symbols, visual symmetry with the production source's Tailwind utility-class glyph rendering, strict-mode resilience against a stacked clear-filters button using the same glyph); the "Why .first() on input and clearButton" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second search input from an admin shell or footer newsletter, strict-mode collision against the autocomplete dropdown's input, strict-mode collision against a portal-rendered Cmd-K palette duplicate); the "Why fill() instead of pressSequentially() for search()" walkthrough that pins the debounce semantics (single React state update vs N collapsed events), the speed advantage (single round-trip vs N round-trips), and the deterministic value-readback against the React reconciler; the "Why clear() instead of clicking clearButton" walkthrough that pins the empty-input safety (the clear button is hidden when the input is empty so clicking it would flake), the clear-button-drift survival, and the production-source-first input semantics (Playwright clear() dispatches the same keyboard sequence a real user would); the "Why ?? '' on getValue()" rationale (API future-proofing against a future string | null return, type-narrowed assertion shape for consuming specs, defensive symmetry with getCurrentTheme()); the failure matrix covering every search-bar-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop, exact placeholder=\"Search\" match instead of substring, i flag drop, .first() drop on input, placeholder*=data-testid swap, hasText: 'x' Latin-x swap on clearButton, CSS class selector swap on clearButton, .first() drop on clearButton, fill()pressSequentially() swap on search(), clear()clearButton.click() swap, ?? '' drop on getValue(), file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/search.spec.ts, future smoke / a11y specs that read getValue(), the production source for the listing's search input React component for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto Locator not found, inputValue()-returns-empty, and clear-button-glyph-misses failures; and the search-bar.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/search.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (placeholder substring, × glyph, React-controlled value), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future fixture-bound search bar would surface there), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the search-bar spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep search), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Discover Page Object (apps/web-e2e/page-objects/public/discover.page.ts) -- Per-source-file reference for the Playwright e2e suite's public directory-listing driver paired with apps/web-e2e/page-objects/public/discover.page.ts, sitting inside the public/ page-object subtree alongside the fourteen other public-surface page objects (item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, theme-toggle.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and theme-toggle-page-object.md documents the suite's theme-switch driver boundary under apps/web-e2e/page-objects/public/, this page documents the suite's directory-listing driver boundary — the smallest possible page object that lets a spec drive /discover/[N] end-to-end (navigate to a page in the directory, count the items rendered, click into the first item, observe the pagination control). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the import { BasePage } from '../base.page' runtime import — the only runtime import in the file; the export class DiscoverPage extends BasePage single named export with the inherited (page: Page) constructor signature; the readonly itemLinks: Locator page.locator('a[href*="/items/"]') substring-selector matching every directory-card link; the readonly pagination: Locator page.locator('nav[aria-label*="pagination"], nav[aria-label*="Pagination"]') dual-substring selector tolerating production-source case drift; the readonly heading: Locator page.getByRole('heading', { level: 1 }) role+level resolution that survives translation churn; the constructor(page: Page) that calls super(page) and pre-binds the three Locators above; the navigate(pageNum = 1) method that wraps the inherited goto with the canonical /discover/[N] route and defaults pageNum to 1; the getItemCount() method that returns Playwright's count() over the itemLinks Locator without throwing on an empty fixture; the clickFirstItem() method that clicks the first directory-card link with .first() for strict-mode safety); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming specs at apps/web-e2e/tests/public/discover.spec.ts (three flows) and apps/web-e2e/tests/public/map.spec.ts (precondition seeding); the "Why the class extends BasePage" walkthrough that pins the three load-bearing reasons (the route is a navigable page in the URL sense so goto / waitForPageReady are useful for free, shared page-shell Locators are useful for "directory page renders the global header / footer" assertions, constructor parity with sibling page-shaped page objects across the four role trees); the "Why a[href*="/items/"] for itemLinks" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required, locale invariance against /en/items/ / /fr/items/ / /de/items/ / /es/items/ / /zh/items/ / /ar/items/, slug invariance against every data-source change); the "Why dual-substring aria-label* for pagination" walkthrough that pins the production-source case drift tolerance, the strict-mode safety from the unique landmark, and the zero-false-positive substring narrowness; the "Why getByRole('heading', { level: 1 }) for heading" walkthrough that pins the locale invariance, the single accessible-name source of truth, and the production-source-first discipline; the "Why pageNum = 1 default on navigate" rationale that pins the most-common call site shortest, the explicit-page-number documentation for pagination tests, and the type-narrowed Promise<void> posture; the "Why .first() on clickFirstItem" walkthrough that pins the three failure modes of dropping it (strict-mode collision against multiple matching links, render-order independence with URL-substring assertions, future highlighted-item-at-position-0 support); the failure matrix covering every discover-page-level mistake (type-only import drop, extends BasePage drop, readonly drop, a[href^="/items/"] prefix-only swap, data-testid swap, dual-substring drop on pagination, data-testid swap on pagination, h1 tag-selector swap on heading, pageNum = 1 default drop, template-literal-to-concat swap, .first() drop on clickFirstItem, hard-coded slug, file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/discover.spec.ts, the precondition consumer apps/web-e2e/tests/public/map.spec.ts, the production source at apps/web/app/[locale]/discover/[page]/page.tsx for the DOM contract, base-page-object.md for the inherited primitives, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto getItemCount() returns 0, Locator not found, and navigate timeout failures; and the discover.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/discover.spec.ts and apps/web-e2e/tests/public/map.spec.ts), a base-page-object.md cross-check, a production-source cross-check (the directory-card link shape, the pagination landmark shape, the H1 contract), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future fixture-bound directory driver would surface there), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the discover and map spec subsets (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "discover|map"), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Theme-Toggle Page Object (apps/web-e2e/page-objects/public/theme-toggle.page.ts) -- Per-source-file reference for the Playwright e2e suite's header theme-switch driver paired with apps/web-e2e/page-objects/public/theme-toggle.page.ts, sitting inside the public/ page-object subtree alongside the fourteen other public-surface page objects (discover.page.ts, item-detail.page.ts, language-switcher.page.ts, map.page.ts, newsletter.page.ts, profile-dropdown.page.ts, public-pages.page.ts, scroll-to-top.page.ts, search-bar.page.ts, share-button.page.ts, sort-menu.page.ts, star-rating.page.ts, view-toggle.page.ts). Where base-page-object.md documents the page-object inheritance root and signin-page-object.md documents the suite's sign-in surface boundary under apps/web-e2e/page-objects/auth/, this page documents the suite's theme-switch driver boundary — the smallest possible page object that lets a spec drive the header theme-switch dropdown end-to-end (open the dropdown, select the light or dark option, observe the canonical aria-label shape, observe the dark class flip on the <html> element). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the export class ThemeToggle single named export with no extends clause — the public-tree widget-driver posture; the readonly page: Page field that the standalone class restates because it does not inherit from BasePage; the readonly toggleButton: Locator page.locator('button[aria-label*="Current theme"]').first() substring-selector pinning to the first match; the constructor(page: Page) that stores the page and pre-binds toggleButton without a super(page) call; the getCurrentTheme() method that reads the toggle button's aria-label and returns 'light' / 'dark' / 'unknown'; the open() minimal "open the dropdown" primitive every other method composes against; the selectLight() and selectDark() methods that compose open() + role+regex-name resolution + click; the isDarkMode() method that reads the <html> class list and returns whether the dark substring is present); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec at apps/web-e2e/tests/public/theme-toggle.spec.ts; the "Why the class does not extend BasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the header surface, reusability on non-page surfaces like a future admin-shell theme switch, constructor parity with non-page widgets like language-switcher.page.ts / share-button.page.ts / view-toggle.page.ts); the "Why aria-label*=\"Current theme\" and not a data-testid" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required for tests, theme-label invariance because the prefix \"Current theme\" survives every theme rename, strict-mode resilience against a future second theme switch with .first()); the "Why .first() on the toggle button" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future admin-shell theme switch, strict-mode collision against the dropdown's per-option labels, strict-mode collision against a portal-rendered duplicate like a mobile drawer); the "Why parse the aria-label substring instead of querying state" walkthrough that pins the three reasons for the black-box discipline (no React-internals reach-in, storage drift survival from localStorage → cookie → Drizzle row migration, theme-set extensibility because a future fifth theme returns 'unknown' instead of breaking); the "Why role+regex name for the option buttons" walkthrough that pins the locale-invariance posture and the strict-mode resilience against an off-dropdown "light theme" CTA banner; the "Why isDarkMode() reads <html>'s class" walkthrough that pins the Tailwind darkMode: 'class' posture, the server-render parity, and the no-flicker guarantee documented in rendering-hydration-no-flicker.md; the failure matrix covering every theme-toggle-page-level mistake (type-only import drop, accidental extends BasePage add, readonly drop, exact aria-label match instead of substring, .first() drop on the toggle button, aria-label*=data-testid swap, CSS attribute selector swap on the option-button locator, text-only locator swap on the option button, .first() drop on the option button, React state / localStorage reach-in for getCurrentTheme(), <body> / <main> swap on isDarkMode(), 'unknown' branch drop on getCurrentTheme(), file move, rename, .tsx extension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec at apps/web-e2e/tests/public/theme-toggle.spec.ts, future smoke / a11y specs that read getCurrentTheme(), the production source at apps/web/components/header/theme-switch.tsx for the DOM contract, e2e-tsconfig.md for the include glob, playwright-config.md for the baseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto Locator not found and isDarkMode()-returns-false-in-dark-mode failures; and the theme-toggle.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/public/theme-toggle.spec.ts), a base-page-object.md cross-check (if the new shape inherits, document why), a production-source cross-check (aria-label shape, dropdown option-button shape, <html>-class hook), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob), a playwright-config.md cross-check (the baseURL posture), a fixtures-index.md cross-check (a future fixture-bound theme-toggle would surface there), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the theme-toggle spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep theme), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Sign-in Page Object (apps/web-e2e/page-objects/auth/signin.page.ts) -- Per-source-file reference for the Playwright e2e suite's sole auth/-tree page object paired with apps/web-e2e/page-objects/auth/signin.page.ts, sitting at the root of the auth/ page-object subtree the same way base-page-object.md sits at the root of the page-objects tree as a whole, fixtures-index.md sits at the root of the fixtures tree, and e2e-test-data.md sits at the root of the helpers tree. Where base-page-object.md documents the page-object inheritance root and auth-fixture.md documents the suite's authenticated-fixture boundary that turns the persisted storage states minted at pre-flight into per-test isolated contexts, this page documents the suite's sign-in surface boundary — the smallest possible page object that lets a spec drive /auth/signin end-to-end (fill the email, fill the password, submit, optionally wait for the post-sign-in redirect, observe the success / error alerts). Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only Playwright import that mirrors the base-class discipline; the import { BasePage } from '../base.page' runtime import — the only runtime import in the file; the export class SignInPage extends BasePage single named export with the inherited (page: Page) constructor signature; the readonly emailInput: Locator form-scoped #email input; the readonly passwordInput: Locator form-scoped #password input; the readonly submitButton: Locator page.getByRole('button', { name: /sign in/i }) role-based regex-name lookup that survives translation churn and refactors that move the button outside the form; the readonly forgotPasswordLink: Locator form-scoped a[href*="forgot-password"] substring selector that is invariant to the localePrefix: 'as-needed' middleware posture; the readonly errorAlert: Locator page.locator('.bg-red-50').first() first-banner pinning that mirrors the base class's header.first() / footer.first() posture; the readonly successAlert: Locator symmetric .bg-green-50.first() pinning; the constructor(page: Page) that calls super(page) and pre-binds the seven Locators above using a local authForm Locator that filters page.locator('form') to the form containing #email; the navigate() method that wraps the inherited goto with the canonical /auth/signin route; the signIn(email, password) form-fill kernel that finishes with passwordInput.press('Enter') to submit via the default form-submission path instead of clicking the button; the signInAndWaitForRedirect(email, password, expectedUrl) happy-path wrapper that delegates to signIn() and awaits page.waitForURL(expectedUrl, { timeout: 60_000 })); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the auth-flow spec set under apps/web-e2e/tests/auth/; the "Why scope every form Locator to authForm" walkthrough that pins the three failure modes of unscoped selection (newsletter / search drawer collision injecting a second #email, auth-shell drift from header newsletter widgets, sign-up vs sign-in form parity since /auth/signup reuses the same IDs) against the form-scoping posture; the "Why role+regex name for submitButton" walkthrough that pins the three rejected alternatives (form-scoped role lookup that breaks on a button-floats-outside-form refactor, CSS attribute selector button[type="submit"] that collides with social-login button rows, text selector :has-text("Sign In") that fails on every non-English locale) against the role+name regex with the i flag; the "Why Enter-key submission" walkthrough that pins the three reasons for the keyboard-driven flow (mirrors real-user form submission, avoids button-state flakes when the button is hidden during submit, keeps signIn() and signInAndWaitForRedirect() semantics consistent with no click-driven intermediate); the "Why .bg-red-50.first() and .bg-green-50.first()" walkthrough that pins the three failure modes of dropping .first() (strict-mode violations on stacked banners, future "all errors" assertions still allowed via .locator('.bg-red-50') directly, dark-mode / theme drift survives because the locator matches the Tailwind class present in both light and dark variants); the "Why signInAndWaitForRedirect exists alongside signIn" rationale that pins the kernel-vs-wrapper split (the kernel does not wait so failure-path specs can assert on errorAlert immediately, the wrapper waits for the URL change so happy-path specs land on the post-sign-in route, the 60s timeout is generous because cold-start CI runs can take 30+ seconds for the NextAuth handshake + cookie set + redirect + server render chain); the failure matrix that maps each signin.page.ts mistake (drop import type for Page / Locator → bundle cost and circular-import risk, drop the extends BasePage clause → loses inherited goto / gotoLocalized / waitForPageReady / getTitle and inherited header / footer / navLinks Locators, drop readonly from any field → cross-test state leak risk, drop the form-scoping on emailInput / passwordInput → strict-mode collision with footer newsletter #email / sign-up modal #email, switch submitButton to a CSS attribute selector → multiple submit buttons collide under strict-mode, switch submitButton to a text-only locator → localised pages fail to resolve the button, switch forgotPasswordLink from href*= to href= → localised forgot-password URLs fail to match, drop .first() from errorAlert / successAlert → stacked alerts collide under strict-mode, switch signIn() from Enter-key to button click → loses real-user submission semantics and flakes when the button is hidden during submit, tighten signInAndWaitForRedirect's timeout below 30s → cold-start CI flakes on the post-sign-in redirect chain, raise the timeout above 60s → genuinely-broken redirects hang the suite, add a new field that captures global state → cross-test leakage on serial runs, move the file out of apps/web-e2e/page-objects/auth/Cannot find module on every importing spec, rename SignInPage → every importer needs a matching rename, switch the file extension to .tsx → falls out of the include: ["./**/*.ts"] glob and every importing spec breaks, drop the trailing newline → Prettier diff, ship the file with CRLF → same as above) onto the layer that surfaces each one; the per-line walkthrough table that pins each line of the 37-line file to its purpose; the read / write surface summary that maps every caller (apps/web-e2e/tests/auth/** specs, the BasePage parent class, auth.fixture.ts which today bypasses the form by pre-loading storage states, apps/web/components/auth/** production source for the DOM contract, e2e-tsconfig.json, playwright.config.ts) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto strict-mode and Cannot find module failures; and the signin.page.ts-change checklist that ties any change to a spec audit (every spec under apps/web-e2e/tests/auth/), a base-page-object.md cross-check (inherited methods and Locators), a production-source cross-check (the seven Locator strings must match production sign-in form components and the route under apps/web/app/[locale]/auth/signin/), an auth-fixture.md cross-check (the authenticated fixtures bypass the form today; a future fresh-sign-in fixture would cross-reference this file), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob picks up this file), an e2e-package-manifest.md cross-check (Playwright major bumps may change the getByRole overload set), a playwright-config.md cross-check (the baseURL resolves the relative /auth/signin path), a global-setup.md cross-check (the pre-flight global setup also drives /auth/signin to mint storage states; the form's identifiers must stay in sync between this file and the global setup's selectors), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run targeting the auth-spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep auth), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Base Page Object (apps/web-e2e/page-objects/base.page.ts) -- Per-source-file reference for the Playwright e2e suite's foundational page-object class paired with apps/web-e2e/page-objects/base.page.ts, the page-object inheritance root sitting at apps/web-e2e/page-objects/base.page.ts the same way fixtures-index.md sits at the root of the fixtures tree and e2e-test-data.md sits at the root of the helpers tree. Where fixtures-index.md documents the directory-level fixture-export boundary and e2e-test-data.md documents the suite's shared-data boundary, this page documents the page-object inheritance root — the smallest possible class every concrete page object under apps/web-e2e/page-objects/admin/, apps/web-e2e/page-objects/auth/, apps/web-e2e/page-objects/client/, and apps/web-e2e/page-objects/public/ extends. Documents the at-a-glance summary table of every load-bearing element (the import type { Page, Locator } from '@playwright/test' type-only import that stays out of the runtime bundle and prevents a runtime require of Playwright's runner internals at module-load for every consumer; the export class BasePage single named export inherited by 30+ subclasses today across the four role trees; the readonly page: Page field that gives every subclass the Playwright Page handle for ad-hoc locator construction; the readonly header: Locator pre-bound page.locator('header').first() Locator pinned to the first <header> because Next 16 layouts can stack a global header above a section header; the readonly footer: Locator symmetric page.locator('footer').first() pinning; the readonly navLinks: Locator header.getByRole('link') header-scoped link enumeration so footer links and in-page links do not pollute navigation assertions; the constructor(page: Page) that stores the page handle and pre-binds the three structural Locators above; the goto(path: string) suite-wide navigation primitive with the waitUntil: 'domcontentloaded' override of Playwright's default 'load' posture; the gotoLocalized(path: string, locale: string) locale-aware navigation that special-cases 'en' to bare paths and prefixes every other locale with /${locale}, encoding the host app's localePrefix: 'as-needed' middleware posture; the waitForPageReady() re-await primitive for post-navigation interactions that uses the same 'domcontentloaded' load state as goto(); the getTitle(): Promise<string> shortcut for page.title() so subclasses do not import page.title from inside their own assertions); the full file annotated chunk-by-chunk; the "Why import type and not a runtime import" walkthrough that pins the three failure modes of a runtime import (circular-import risk between this file and the runner's test function, bundle-size cost on type-only consumers, fixture-vs-runner ambient drift between the type-only page-object tree and the runtime-bearing fixtures barrel) against the type-only shape; the "Why page.locator('header').first() and not a plain header" walkthrough that pins the three failure modes of unscoped selection (strict-mode violations on item-detail pages, cross-page assertion drift, header / footer asymmetry) against the first() posture; the "Why header.getByRole('link') for navLinks" walkthrough that pins the three differences between header and footer link inventories (primary vs secondary navigation, auth-state divergence, modal / drawer pollution risk) against the header-scoped enumeration; the "Why goto() uses waitUntil: 'domcontentloaded'" walkthrough that pins the three reasons for the override (Next 16 streaming <Suspense> boundaries pending the load event, image-heavy pages adding ~3-5s per spec on load, networkidle being a footgun on pages with polling useEffect like analytics heartbeats) against the deliberate single-load-state choice; the "Why gotoLocalized() special-cases 'en'" walkthrough that pins the three rejected alternatives (always-prefix triggers a 308 redirect on en, never-prefix lands on the wrong page for non-English locales, branch-on-'en'-at-every-call-site duplicates the prefix logic across page objects) against the encoded localePrefix: 'as-needed' posture; the "Why waitForPageReady() is a thin re-state of goto's wait" rationale (post-goto interaction patterns: pagination button clicks, next/link client-side navigations, modal dismissals); the "Why getTitle() exists" rationale (subclass discoverability, future title-sanitisation override site, type-narrowed Promise<string> return type); the failure matrix that maps each base.page.ts mistake (drop the import type and switch to a runtime import → bundle-size cost and circular-import risk, drop readonly from any field → cross-test state leak risk, drop .first() from header or footer → strict-mode locator violation on stacked-header pages, switch navLinks from header.getByRole('link') to page.getByRole('link') → footer / in-page links pollute the inventory, switch goto() from domcontentloaded to load → ~3-5s slower per spec on image-heavy pages, switch goto() from domcontentloaded to networkidle → spec timeouts on pages with analytics heartbeats, drop the 'en' special-case from gotoLocalized() → 308 redirect on every English call, hard-code /${locale}${path} for every locale → same as above, switch waitForPageReady() to a different load state → asymmetry with goto(), drop getTitle() → subclass drift, move the file from apps/web-e2e/page-objects/base.page.ts → mass Cannot find module failures at TS gate, rename BasePage → every extends BasePage clause breaks, add a public field that holds shared state across tests → cross-test leakage, add a protected static cache → same as above, make goto() return a Promise<Response | null> → subclass drift, make the constructor accept anything other than page: Page → every super(page) breaks, drop the trailing newline → Prettier diff, ship the file with a CRLF line ending → same as above, switch the file extension to .tsx → falls out of the include: ["./**/*.ts"] glob) onto the layer that surfaces each one; the per-line walkthrough table that pins each line of the 32-line file to its purpose; and the base.page.ts-change checklist that ties any change to a subclass audit (every page object under the four role trees), a fixtures-index.md cross-check (today nothing in the page-object tree is re-exported through the fixtures barrel; a future "default page object" addition cross-references this file), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob picks up this file), an e2e-package-manifest.md cross-check (the package's devDependencies.@playwright/test underwrites the Page and Locator types this file uses; a Playwright major bump may change the type signatures), a playwright-config.md cross-check (the baseURL is what page.goto(path) resolves relative to), an auth-fixture.md cross-check (every authenticated page object instantiated from the auth fixture's storage-state-bearing context still goes through this base class), dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run against a representative spec from each subclass tree (admin, auth, client, public), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass.
  • E2E Test Data Helpers (apps/web-e2e/helpers/test-data.ts) -- Per-source-file reference for the Playwright e2e suite's central test-data and constants module paired with apps/web-e2e/helpers/test-data.ts, the shared-data companion to global-setup.md (which destructures TEST_DATA, ADMIN_STATE_FILE, CLIENT_STATE_FILE, AUTH_STATE_DIR, and REQUIRED_ENV_VARS from this module) and to global-teardown.md (which will consume the same constants when the no-op placeholder grows into a real cleanup sequence). Where global-setup.md documents the suite's pre-flight boundary and global-teardown.md documents the suite's post-flight boundary, this page documents the suite's shared-data boundary — every constant, generator, env-var name, route path, auth-state file path, and seeded credential the rest of the suite reads through. Documents the at-a-glance summary table of every export (requireEnv(name) private fail-fast helper that throws on missing OR empty env-vars with a contributor-actionable message naming the missing key and the file .env.local it must be set in; TEST_DATA.ADMIN_EMAIL / TEST_DATA.ADMIN_PASSWORD lazy getters calling requireEnv('SEED_ADMIN_EMAIL') / requireEnv('SEED_ADMIN_PASSWORD') so the env-var read happens at access time, not module load — specs that never touch admin credentials never trigger the throw; TEST_DATA.CLIENT_PASSWORD static 'TestClient123!' because per-run uniqueness comes from the email so the password is irrelevant to identity and a static value makes failed sign-ups immediately reproducible from the trace; TEST_DATA.generateClientEmail() returning e2e-client-${Date.now()}-${randomSuffix}@test.local with a 6-char base-36 random suffix giving ~36⁶ ≈ 2.2B values per millisecond and the @test.local TLD reserved by RFC 6761 to prevent accidental real-world delivery; TEST_DATA.generateItemName() returning E2E Test Item ${Date.now()}-${randomSuffix} with a 4-char suffix; TEST_DATA.generateItemUrl() returning https://e2e-test-${Date.now()}.example.com with no random suffix because URL hostnames have syntactic constraints and example.com is reserved by IANA RFC 2606 to prevent accidental real-world traffic; REQUIRED_ENV_VARS as const whitelist ['SEED_ADMIN_EMAIL', 'SEED_ADMIN_PASSWORD'] consumed by global-setup.md's promptForMissingEnv() step; PUBLIC_ROUTES 13-row as const table of every public route the navigation shell links to (/, /discover/1, /categories, /tags, /collections, /pricing, /about, /help, /privacy-policy, /terms-of-service, /cookies, /auth/signin, /auth/register) consumed by every public-shell smoke spec; AUTH_STATE_DIR literal 'auth-states', ADMIN_STATE_FILE template-composed ${AUTH_STATE_DIR}/admin.json, CLIENT_STATE_FILE template-composed ${AUTH_STATE_DIR}/client.json so a future move from auth-states/ to .auth/ is a one-line edit); the full file annotated chunk-by-chunk; the "Why the lazy-getter pattern for the admin credentials" walkthrough that pins the three failure modes of property assignment (process.env read at module load punishes specs that do not need credentials, ?? '' fallback turns the error into silent '' propagation 30 seconds in, ?? removes the type-check safety net) against the lazy-getter shape (access-time read, fail-fast throw with contributor-actionable message, narrow string return type); the "Why the generators use Date.now() + base-36 random" walkthrough with the three rejected alternatives (crypto.randomUUID() introduces a Node-specific import in an otherwise import-free module, @faker-js/faker's faker.internet.email() generates real-world TLDs that risk accidental delivery, per-worker static emails collide with prior runs and require a per-run cleanup step that does not exist) against the current shape (no imports, RFC-reserved TLD, ~2.2B values per millisecond); the "Why PUBLIC_ROUTES is a readonly array of objects" rationale (the name field is the test description so the report shows Home / Categories / Sign In not slugs, the name survives a route rename so the report does not become unreadable after a /auth/signin/login move, the as const posture lets specs type-check against literal values for compile-time typo safety); the failure matrix that maps each test-data.ts mistake (drop the requireEnv() empty-string check → ?email='' 30 seconds in instead of fail-fast, switch the getters to property assignments → punishes specs that never touch admin credentials, add a ?? '' fallback → silent '' propagation, switch the client TLD from @test.local to @example.com → MX records exist on example.com so a leaked test account could receive a real email, switch the URL apex from example.com to a real domain → accidental traffic to that site, drop the Date.now() prefix from a generator → ~2.2B values across the suite's lifetime instead of per millisecond, add a required env-var without updating REQUIRED_ENV_VARS → pre-flight prompt does not ask for it, drop as const from a literal array → typos become silent test failures instead of TypeScript errors, add a public route to the navigation without adding it to PUBLIC_ROUTES → ships without smoke coverage, remove a public route without removing it from PUBLIC_ROUTES → smoke matrix asserts on a 404 the route legitimately produces, hard-code 'auth-states' in global-setup.ts instead of importing → multi-file rename becomes lossy, switch the CLIENT_PASSWORD from a static string to a generator → failed sign-ups become harder to reproduce from trace, move the file from apps/web-e2e/helpers/test-data.tsCannot find module on every import, export requireEnv and call from another module → multiple opinions of "missing env-var" semantics drift) onto the layer that surfaces each one; the per-line walkthrough table; and the test-data.ts-change checklist that ties any export change to a global-setup.md cross-check, a global-teardown.md cross-check, a playwright-config.md cross-check (the webServer.cwd resolves the relative paths in ADMIN_STATE_FILE / CLIENT_STATE_FILE), an e2e-tsconfig.md cross-check, the .gitignore cross-check (AUTH_STATE_DIR must match the gitignore entry), the public-routes smoke spec cross-check under apps/web-e2e/tests/public/, the apps/web/.env.example and workspace README propagation for new required env-vars, dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset Playwright run that confirms the pre-flight prompt walks the new env-var and auth-states/ still contains both admin.json and client.json after the run, a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept, and a reviewer pass.
  • Playwright Global Teardown (apps/web-e2e/global-teardown.ts) -- Per-source-file reference for the Playwright e2e suite's per-run global teardown paired with apps/web-e2e/global-teardown.ts, the post-flight companion to global-setup.md. Where the setup mints the two persisted authentication storage states by driving a real Chromium browser against the host web app's /auth/signin and /auth/register screens, this page documents the suite's post-flight boundary — what the runner does once after the last test, in what order, with what failure modes — even when, today, the answer is nothing: the file is a deliberate no-op placeholder (async function globalTeardown() { /* Placeholder for future cleanup (e.g., test database reset) */ } export default globalTeardown;) wired into playwright-config.md via the always-resolved globalTeardown: field. Documents the at-a-glance summary table of every load-bearing element (async function globalTeardown() with the empty parameter list because the no-op does not use Playwright's FullConfig, the single marker comment that prevents the file from being deleted as dead code, the default export shape Playwright's runner imports as (await import(...)).default, and the absence of imports because there is nothing to clean up today); the full file annotated chunk-by-chunk; the "Why a no-op placeholder instead of dropping the file" walkthrough that pins the lowest-coupling rationale against the three rejected alternatives (drop the file and the config field together, point the field at a real noop.ts that does not communicate intent, or keep both with a self-descriptive empty stub); the five concrete cleanup buckets the placeholder reserves the slot for (per-run auth-states/ directory cleanup that would force every CI run to re-mint admin / client state, per-run client account deletion via TEST_DATA.generateClientEmail() against a hypothetical DELETE /api/admin/users?email=... to keep the seeded-DB row count stable, per-run Stripe / Polar / LemonSqueezy sandbox fixture cleanup via per-provider customers.del(...) / subscriptions.cancel(...), apps/web-e2e/test-results/ directory cleanup on success that walks files older than the run's start time to prevent crashed-worker trace.zip / video.webm / screenshot.png cross-pollution, and test-database snapshot reset for any future seeded fixture); the "Why the parameter list is empty today" rationale that pins the (config: FullConfig) => Promise<void> | void Playwright contract against the future-friendly addition of (config: FullConfig) for a teardown that needs the resolved baseURL / project list / outputDir; the "Why globalTeardown is not allowed to throw" rationale that pins the recommended per-bucket try / catch + console.error('[global-teardown] bucket #N cleanup failed:', err) pattern (so one failing bucket does not skip the others, the run's real test outcomes are not overwritten in the reporter, and CI reruns do not re-execute the whole suite for a non-fatal cleanup error); the "Why globalTeardown runs once, not per-worker" rationale that pins the global-shared cleanup buckets (auth states / client account / payment-provider sandbox / test-results/) against the race-condition cost of pushing cleanup down to project / file / test level; the failure matrix that maps each global-teardown.ts mistake (drop the file → ENOENT on every run before globalSetup, drop the globalTeardown field → silent skip with no error, switch to a named export → TypeError: undefined is not a function at run end, make the function synchronous and throw → "tests passed but the run failed" log noise, leave the body empty without the marker comment → contributor deletes the file as dead code, move the file to apps/web-e2e/setup/global-teardown.tsENOENT from the hard-coded path.resolve(__dirname, './global-teardown.ts'), add a process.exit(0) → empty playwright-report/ directory, hard-code an await on a database client → failure on minimal local-dev configurations, add a setTimeout / long-running async wait → 60-s end-of-run blocker that produces false "run timed out" results) onto the layer that surfaces each one; the per-line walkthrough table; and the global-teardown.ts-change checklist that ties any flip back to a global-setup.md cross-check (anything the setup mints is the natural target for the teardown), a playwright-config.md cross-check (the globalTeardown: field's path resolution is the only thing pointing the runner at this file), a apps/web-e2e/helpers/test-data.ts cross-check (AUTH_STATE_DIR, ADMIN_STATE_FILE, CLIENT_STATE_FILE, and TEST_DATA.generateClientEmail() are the constants the teardown will use), an e2e-tsconfig.md cross-check (the include: ["./**/*.ts"] glob picks up this file), the dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset run that confirms the runner starts (no ENOENT), exits cleanly (the teardown returns within the per-test timeout), and writes the HTML report (playwright-report/index.html exists), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link if the teardown gains a real cleanup bucket, and a reviewer pass.
  • Playwright Global Setup (apps/web-e2e/global-setup.ts) -- Per-source-file reference for the Playwright e2e suite's per-run pre-flight hook paired with apps/web-e2e/global-setup.ts, the pre-flight companion to playwright-config.md. Where the config locks the suite's runtime boundary (which directory the runner walks, how many workers, which browsers, the use-defaults, the webServer block), this page documents the suite's pre-flight boundary — what the runner does once before the first test, in what order, with what failure modes. Documents the at-a-glance summary table of every load-bearing step (promptForMissingEnv() first walking REQUIRED_ENV_VARS = ['SEED_ADMIN_EMAIL', 'SEED_ADMIN_PASSWORD'] with the process.env.CI short-circuit that prevents CI hangs and the TTY-only readline/promises prompt with the empty-answer guard, baseURL resolution from config.projects[0]?.use?.baseURL ?? 'http://localhost:3000' with the defensive fallback, recursive mkdirSync(auth-states/) because Playwright's storageState({ path }) does not auto-create the parent directory, the __dirname-anchored absolute path resolution that survives webServer.cwd: '../..', single shared chromium.launch() reused by both auth flows for the boot-cost / memory-footprint win, the admin sign-in flow against /auth/signin with stable #email / #password IDs and the role-tolerant waitForURL(/\/(admin|client\/dashboard)/) redirect regex, the per-run client sign-up flow with TEST_DATA.generateClientEmail() returning a unique e2e-client-${Date.now()}-${randomSuffix}@test.local address that prevents parallel-worker collisions, the press('Enter') keyboard submit that avoids button-text dependencies, the waitForURL(/\/client\/dashboard/, { timeout: 120_000, waitUntil: 'domcontentloaded' }) slow-path-tolerant client-redirect wait, the per-flow try / catch that closes the browser on failure, the single await browser.close() that runs only on the happy path, and the export default globalSetup Playwright contract); the full file annotated chunk-by-chunk; the "Why promptForMissingEnv is the first call" walkthrough that pins the fail-fast posture against the locator('#email').fill(undefined) failure mode 30 seconds in; the "Why one Chromium, two contexts" cost / benefit rationale (~500 ms × 1 vs ~500 ms × 2 boot, ~150 MB × 1 vs ~150 MB × 2 memory); the "Why storageState({ path }) instead of cookies-only" rationale (captures both NextAuth cookies and any localStorage set by the auth callback); the "Why the admin flow accepts both /admin and /client/dashboard" role-tolerance rationale; the "Why the client flow uses domcontentloaded instead of load" rationale (analytics pixels and fonts would push wall-clock past the budget); the "Why the auth-states/ directory is per-suite, not per-worker" rationale (workers share the global setup's output by design, so per-worker auth states would multiply the Chromium boot cost without buying isolation the suite does not need); the failure matrix that maps each global-setup.ts mistake (drop promptForMissingEnv() → cryptic fill(undefined) 30 s in, drop the process.env.CI branch → CI hangs forever, drop the empty-answer guard → silent failure later, hard-code baseURLBASE_URL= override stops working, drop the ?? '...' fallback → goto(undefined) TypeError, drop mkdir auth-states/ENOENT, use process.cwd() → broken paths under webServer.cwd: '../..', two chromium.launch() calls → doubled wall-clock and memory, drop try / catch → leaked Chromium processes, hard-code admin email → suite breaks on seed rotation, hard-code client email → parallel-worker collisions, use real-world TLD on client email → accidental delivery risk, use .click() instead of press('Enter') on register → button-text dependency, use waitUntil: 'load' on client redirect → analytics pixel wall-clock blow-up, use 30-s client timeout → cold-render flakes, drop storageState({ path }) → every authenticated test re-runs sign-in, tighten admin redirect regex to /admin only → breaks on demoted seeded admin, loosen admin redirect regex to / → succeeds when sign-in fails, remove per-success console.log → silent CI on success, drop AUTH_STATE_DIR / ADMIN_STATE_FILE / CLIENT_STATE_FILE constants → path drift across files) onto the layer that surfaces each one; the per-line walkthrough table; and the global-setup.ts-change checklist that ties any flip back to a playwright-config.md cross-check, a apps/web-e2e/helpers/test-data.ts cross-check, dual pnpm tsc --noEmit runs (e2e + workspace root), a smoke-subset run that proves both auth states land in apps/web-e2e/auth-states/, the per-CI-vs-local both-modes verification (set CI=1 to exercise the no-prompt branch), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link, and a reviewer pass.
  • Playwright Runner Configuration (apps/web-e2e/playwright.config.ts) -- Per-source-file reference for the Playwright e2e suite's runner configuration paired with apps/web-e2e/playwright.config.ts, the runtime companion to e2e-tsconfig.md. Where the tsconfig locks the suite's type-checking posture (which files the type-checker walks, which preset they extend), this page documents the suite's runtime boundary — which directory the runner discovers tests from, how many workers run them, which browsers they execute against, what the use-defaults are, and how the host app is booted before the first test. Documents the at-a-glance summary table of every load-bearing field (the dotenv.config({ path: '../web/.env.local' }) cross-app env loading that keeps a single source of truth between the suite and the booted host app, the BASE_URL override hatch with the 'http://localhost:3000' default that lets CI / staging point the suite at deployed previews, the isCI = !!process.env.CI boolean gate, the testDir: './tests' and outputDir: './test-results' artefact boundaries, fullyParallel: true for spec interleaving, workers: isCI ? 2 : 1 for CI throughput vs local determinism, retries: isCI ? 2 : 0 for CI flake auto-recovery, the per-environment reporter set with ['html', { open: 'never' }] + ['github'] + ['list'] on CI vs ['html', { open: 'on-failure' }] + ['list'] locally, the 60_000 per-test and 30_000 expect() timeouts, the globalSetup / globalTeardown paths, the use-block defaults (baseURL, trace: isCI ? 'on-first-retry' : 'retain-on-failure', screenshot: 'only-on-failure', video: isCI ? 'on-first-retry' : 'off', navigationTimeout: 60_000, actionTimeout: 30_000, locale: 'en-US', timezoneId: 'America/New_York'), the three browser projects (Chromium, Firefox, WebKit), and the webServer block with the per-environment command (pnpm --filter @ever-works/web build && start on CI, pnpm --filter @ever-works/web dev locally), cwd: '../..' monorepo-root anchor, reuseExistingServer: !isCI, the per-environment timeouts (300_000 CI, 120_000 local), and the stdout: 'pipe' / stderr: 'pipe' self-diagnosing posture); the full file annotated line-by-line; the "Why dotenv.config({ path: '../web/.env.local' })" walkthrough that pins the single-source-of-truth posture against drift, the impossibility of two-file divergence, the contributor cost of the convention, and the trade-off (env overrides only-for-the-suite must be set in the shell); the "Why BASE_URL is the only env-var override surface" rationale (locked posture for everything else, future contributors must add explicit fields rather than read fresh env vars inline); the per-CI-vs-local branch matrix that maps each isCI ? X : Y branch (workers, retries, reporter, use.trace, use.video, webServer.command, webServer.reuseExistingServer, webServer.timeout) to its trade-off (CI optimises for throughput / reliability, local optimises for fast iteration / clear failure signals); the "Why the three browser projects" cost / benefit matrix (Chromium catches the largest cross-section, Firefox catches Gecko-specific bugs, WebKit catches Safari-only divergence especially around Stripe Apple Pay / IDN URLs / cookie SameSite); the "Why webServer.cwd is the monorepo root" rationale (pnpm --filter resolves the workspace alias from the pnpm-workspace.yaml anchor); the "Why stdout: 'pipe' and stderr: 'pipe'" self-diagnosing rationale (the default 'ignore' swallows host-app compile errors and produces cryptic timeout failures); the failure matrix that maps each playwright.config.ts mistake (dropped dotenv.config(...) → cryptic 500s in DB-touching specs, separate apps/web-e2e/.env.local → drift between suite and booted server, BASE_URL fallback dropped → cannot target deployed previews, fullyParallel: false → ~3× wall-clock on file-size-heavy specs, workers > 2 on CI → resource contention flakes against the booted server, retries: 0 on CI → un-mergeable flake amplification across a 50-spec suite, github reporter dropped → no inline annotations on the GitHub Actions run page, html reporter dropped → unreproducible flakes because traces / screenshots / videos live in the HTML report, open: 'always' on CI → CI hangs trying to launch a browser on no-display, timeout reduction → cold-render flakes on never-warmed routes, globalSetup dropped → unseeded specs failing erratically, use.trace: 'off' on CI → un-diagnosable CI-only flakes, use.locale: 'en-GB' → date-format breakage on en-US-asserting specs, use.timezoneId: 'UTC' → timestamp-render breakage, project drop → engine-specific regressions slip past CI until a real user files an issue, project add without matrix bump → wall-clock blow-up, next start without build step on CI → cold-checkout failure, webServer.cwd: __dirnameERR_PNPM_NO_WORKSPACE_FOUND, reuseExistingServer: false locally → EADDRINUSE on every invocation when a next dev is already running, stdout: 'ignore' → silent host-app errors and cryptic timeouts) onto the layer that surfaces each one; the per-line walkthrough table; and the playwright.config.ts-change checklist that ties any field change to a e2e-tsconfig.md cross-check, a pnpm tsc --noEmit run, a smoke-subset run (pnpm test:e2e:chromium -- --grep '@smoke'), the per-CI-vs-local both-modes verification (run with CI unset and CI=true set), a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link, and a reviewer pass.
  • Workspace Root Manifest (package.json) -- Per-source-file reference for the monorepo's workspace-coordination manifest paired with package.json at the repo root, the third root-level config reference after pnpm-workspace.md and turbo-config.md. Where pnpm-workspace.md documents which folders become workspace members and turbo-config.md documents what tasks those members can run, in what order, with what inputs, this page documents the workspace-coordination posture itself — the top-level scripts every contributor and CI runner invokes (pnpm build, pnpm dev, pnpm dev:web, pnpm dev:docs, pnpm lint, pnpm test:e2e, pnpm clean, pnpm format, pnpm build:web, pnpm build:docs, pnpm build:docs:en), the runtime / package-manager floor every environment must meet (engines.node >=20.19.0, packageManager exact pin pnpm@10.31.0 enforced by Corepack), the version-pinning posture for transitive dependencies via pnpm.overrides (@types/react, @types/react-dom, esbuild, esbuild-register, @opentelemetry/api), the public-hoist rule for @opentelemetry/* that protects OTel's global registration model, the 11-entry pnpm.onlyBuiltDependencies allow-list (@vercel/speed-insights, @heroui/shared-utils, @parcel/watcher, @scarf/scarf, @sentry/cli, @swc/core, core-js, core-js-pure, esbuild, protobufjs, sharp) that gates which packages can run postinstall hooks during pnpm install, and the workspace-wide Prettier formatting baseline (printWidth: 120, singleQuote, semi, useTabs with tabWidth: 4, arrowParens: 'always', trailingComma: 'none', plus the two language-specific overrides for *.scss and *.yml that switch to 2-space indents because YAML cannot use tab characters at the syntax level). Documents the at-a-glance summary table of every load-bearing field with its value and why-it-matters note; the file-contents walk-through (the full JSON file); the per-field walkthrough — name as the workspace-root label (not a package identifier), version: '0.1.0' as symbolic-only because private: true blocks publishing, private: true as the hard-block on accidental pnpm publish, license: AGPL-3.0 as the inheritance root for every workspace member, packageManager: pnpm@10.31.0 as the exact-pin Corepack reads to download the right pnpm, engines.node: '>=20.19.0' as the floor required by Next.js 16 / apps/web/scripts/check-env.js's ESM APIs / node:test parity, the eleven scripts.* entries with their turbo run <task> delegations and the --filter=@ever-works/<name> shortcut rationale for dev:web / dev:docs / build:web / build:docs / build:docs:en, the two devDependencies.turbo/prettier ranges with their exact-version rationales (Turborepo 2.x cache-key semantics + $schema enforcement + persistent: true honouring dependsOn; Prettier 3.x's overrides matcher syntax), the pnpm.publicHoistPattern: ['@opentelemetry/*'] rule and why it must coexist with the @opentelemetry/api override (two resolved copies break OTel's global-registration model), the pnpm.overrides field with the per-entry rationale for each pin (React typings to lock the React 19 narrowed ReactNode, esbuild to align Next.js / Drizzle Kit / Trigger.dev bundler output, esbuild-register to keep TS syntax features parsing identically across the workspace, @opentelemetry/api for OTel singleton enforcement), the pnpm.onlyBuiltDependencies allow-list as pnpm 10's deny-by-default postinstall hardening with a per-entry table of why each package needs to run a build step, the prettier block as the single-source-of-truth for formatting rules (no .prettierrc at the repo root by intent), and the two language-specific overrides for SCSS conventions and YAML's tab-disallow syntax constraint; the deliberately-absent-fields matrix covering top-level dependencies, workspaces (because pnpm reads pnpm-workspace.yaml), main / exports / types / module, bin, type, repository / homepage / bugs, peerDependencies, engineStrict / os / cpu with the reason each is omitted; the "Why this file lives at the repo root" rationale (pnpm, Corepack, Turborepo, Vercel, GitHub Actions, Renovate, and editors all walk upward and stop at the first package.json); the consumer table mapping each reader (Corepack, pnpm, Turborepo CLI, Prettier CLI, Vercel build runner, GitHub Actions, editors, Renovate / Dependabot, contributors) to the fields it consumes; the failure matrix that maps each manifest-level mistake (ERR_PNPM_UNSUPPORTED_ENGINE from a Node-floor regression, Wrong package manager from a Corepack drift, OTel span loss from a hoist or override drop, pnpm install ignored build script warnings from a missing allow-list entry, React 19 typings clash from a missing override, YAML re-formatted with tabs from a missing override, --filter shortcut breakage from a renamed package name, Couldn't find a turbo binary from a dropped devDependency, Vercel's pnpm: command not found from disabled Corepack) onto the layer that surfaces each one; and the public-surface change checklist that ties any field change to a Spec 001 plan cross-check, a CI workflow Corepack-enable check, a .github/workflows/*.yml propagation check, an apps/*/vercel.json propagation check, a workspace-wide pnpm format round-trip, a docs/log.md entry, and the Constitution-Check note in the PR description for Article III (Public-Surface Stability) and Article IX (Test Coverage Bar).
  • @ever-works/plugin-sdk -- Capability interfaces, slot ids, manifest types, and the defineDirectoryPlugin factory.
  • @ever-works/plugin-runtime -- PluginRegistry, config loader, and the <SlotHost /> React component.
  • @ever-works/plugin-demo -- Reference plugin used by the test suite and as a teaching example.

Use Cases

This template project is perfect for:

  • Tool directories (like ProductHunt for tools)
  • Service marketplaces
  • Resource catalogs
  • Professional directories
  • Product showcases
  • Community platforms

Ever Works Platform

The Template can be used standalone or paired with the Ever Works Platform for AI-powered content generation. For Platform documentation, visit docs.ever.works. See Platform vs Template for a detailed comparison.

Need Help?