Skip to content

fix: TextField tests, API base validation, CSP headers, events sanitization (#18 #24 #25 #26)#34

Merged
mikewheeleer merged 1 commit into
Agentpay-Org:mainfrom
Senorespecial:fix/security-test-batch-oss
Jun 19, 2026
Merged

fix: TextField tests, API base validation, CSP headers, events sanitization (#18 #24 #25 #26)#34
mikewheeleer merged 1 commit into
Agentpay-Org:mainfrom
Senorespecial:fix/security-test-batch-oss

Conversation

@Senorespecial

Copy link
Copy Markdown
Contributor

Summary

Resolves the four GrantFox OSS campaign issues assigned to @Senorespecial in a single focused PR.

Closes

What changed

#18 - TextField

  • Added JSDoc to src/components/TextField.tsx.
  • aria-invalid now always emits true/false for a stable screen-reader signal.
  • New src/components/__tests__/TextField.test.tsx (8 tests, 100% component coverage) using getByRole("textbox", { name }) so queries work regardless of label nesting.

#24 - API base validation

  • New src/lib/resolveApiBase.ts (resolveApiBase()) trims whitespace, strips trailing slashes, parses URLs, refuses unsupported protocols, warns (dev) or throws (prod) on non-https non-localhost origins, and exports DEFAULT_API_BASE.
  • src/lib/apiClient.ts, src/app/export/page.tsx, and src/app/usage/page.tsx now use the validator instead of reading process.env inline.
  • Tests at src/lib/__tests__/apiClient.test.ts cover resolveApiBase plus the apiFetch wrapper (success / 204 / 4xx Error shaping / 429 with full ApiError fields / fetch rejection). Coverage: 100%.

#25 - Security headers

  • New src/lib/securityHeaders.ts exports buildCsp({ apiBase, isDev? }) and defaultSecurityHeaders(...). CSP derives connect-src from the API origin, includes 'unsafe-inline' in script-src so the inline Next.js bootstrap still works when configured via headers() (no nonce pipeline), and adds 'unsafe-eval' only in dev for Fast Refresh. HSTS is emitted only in production.
  • Wired into every response via next.config.ts headers().
  • New src/__tests__/securityHeaders.test.ts covers prod / dev / prod connect-src derivation / HSTS dev guard / permissions policy contents. Coverage: 100%.

#26 - Event log sanitization

  • New safeStringify(value, maxChars) in src/lib/format.ts defends against circular references ([Circular]), BigInt ([BigInt:N]), symbols ([Symbol]), functions ([Function]), top-level undefined ([undefined]), and truncates at EVENT_PAYLOAD_MAX_CHARS = 5000 with a visible …(truncated) marker. Never throws.
  • New safeFormatTimestamp(value, fallback = "—") returns ISO for finite numbers / numeric strings, and falls back for nullish / NaN / non-numeric input.
  • src/app/events/page.tsx now renders events through both helpers and uses ${i}-${String(e.id ?? "")} for stable React keys.
  • Tests at src/lib/__tests__/format.test.ts cover every branch. Coverage: 100% statements/functions/lines.

Supporting

  • src/lib/apiClient.ts: reordered the fetch options spread so the default Content-Type: application/json is preserved when callers supply init.headers; split the throw line so the v8 coverage engine correctly marks the throw branch as covered.
  • jest.setup.ts: minimal Response polyfill for jsdom so the new new Response(...) mocks work in the test environment.
  • README.md: documented NEXT_PUBLIC_AGENTPAY_API_BASE (and that it is a NEXT_PUBLIC_* variable), the security header set, and the event-log payload truncation / fallback behaviour.

Verification (run locally before push)

  • npm run typecheck — exit 0 (no errors)
  • npx jest — 14/14 suites, 96/96 tests pass
  • Per-module coverage on changed files: TextField.tsx 100%, apiClient.ts 100%, resolveApiBase.ts 100%, securityHeaders.ts 100%, format.ts 100% statements/functions/lines (92.7% branch on deliberately defensive guard branches).
  • npm run build and npm run lint were not run by the author — see note below.

Note on lint

Five pre-existing lint errors live on main and are out of scope for this PR (the affected files are not in this diff):

  • react-hooks/set-state-in-effect in src/app/search/page.tsx:17, src/components/ThemeToggle.tsx:11, src/lib/useApi.ts:18, src/lib/useLocalState.ts:18
  • react-hooks/purity in src/components/TimeAgo.tsx:29

Recommend a follow-up PR to address them. They do not affect the security or correctness of anything in this PR.

Security check

  • No secrets, .env files, wallet seeds, or production credentials are included.
  • All API base URL handling now goes through resolveApiBase() so misconfigured origins surface during build instead of silently leaking traffic to the wrong host.

…ent log sanitization

Resolves the four GrantFox OSS campaign issues assigned to @Senorespecial:

- Agentpay-Org#18: Add JSDoc to TextField.tsx and cover label / aria / description /
  error wiring with role-based queries in
  src/components/__tests__/TextField.test.tsx (8 tests, 100% component
  coverage). aria-invalid now always emits true|false so screen readers
  see a stable signal.

- Agentpay-Org#24: Introduce src/lib/resolveApiBase.ts that validates
  NEXT_PUBLIC_AGENTPAY_API_BASE: trims whitespace, falls back to
  http://localhost:3001, strips trailing slashes, requires https in
  production unless the host is localhost / 127.0.0.1, and warns in
  development for non-https production-like hosts. Adopted by
  src/lib/apiClient.ts, src/app/export/page.tsx, and
  src/app/usage/page.tsx. Co-located tests in
  src/lib/__tests__/apiClient.test.ts hit 100% statement + branch
  coverage.

- Agentpay-Org#25: Build a CSP + hardening-header map in src/lib/securityHeaders.ts
  (buildCsp + defaultSecurityHeaders) and wire it into every response
  via next.config.ts headers(). The CSP tracks the API origin for
  connect-src and includes 'unsafe-inline' in script-src so Next.js
  hydration still works with the static headers() pipeline. HSTS is
  emitted only in production. Tests in src/__tests__/securityHeaders.test.ts
  cover prod / dev / fallback paths at 100%.

- Agentpay-Org#26: Add safeStringify + safeFormatTimestamp to src/lib/format.ts.
  safeStringify handles circular references, BigInt, symbols,
  functions, undefined, oversized payloads (capped at
  EVENT_PAYLOAD_MAX_CHARS = 5000 with a visible \u2026(truncated) marker)
  and never throws. safeFormatTimestamp falls back to '\u2014' for
  nullish / non-finite values. Adopted by src/app/events/page.tsx
  along with a safe String() coercion for the React key to avoid
  duplicate-key warnings. Tests in src/lib/__tests__/format.test.ts
  cover all branches.

Additional fixes:
- jest.setup.ts adds a minimal Response polyfill so the new
  apiClient tests using  work in jest-environment-jsdom.
- src/lib/apiClient.ts: reordered the fetch options spread so the
  default Content-Type: application/json is preserved when callers
  supply init.headers.
- src/lib/apiClient.ts: split the throw line into two statements so
  the v8 coverage engine correctly marks the throw branch as covered.

Verification:
- npm run typecheck: exit 0
- npx jest: 14/14 suites, 96/96 tests pass
- Coverage on new modules: 100% statements / functions / lines
  (applies to TextField.tsx, apiClient.ts, resolveApiBase.ts,
  securityHeaders.ts; format.ts is 100% stmt/func/line and 92.7%
  branch on intentionally-hard-to-hit defensive guards).

Note: 5 pre-existing lint errors (react-hooks/set-state-in-effect
in search/page.tsx, ThemeToggle.tsx, useApi.ts, useLocalState.ts and
react-hooks/purity in TimeAgo.tsx) live on main and are out of scope
for this PR; they are documented in the PR description for the
maintainers.
@mikewheeleer

Copy link
Copy Markdown
Contributor

nice batch — TextField tests, API base validation, CSP headers and event sanitization all in one solid pass. merging 🚀

@mikewheeleer mikewheeleer merged commit fd9f648 into Agentpay-Org:main Jun 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants