Skip to content

feat(cow-fi): add Resources section for programmatic CMS content - #7846

Open
riboflavin wants to merge 10 commits into
cowprotocol:developfrom
riboflavin:feat/cow-fi-resources
Open

feat(cow-fi): add Resources section for programmatic CMS content#7846
riboflavin wants to merge 10 commits into
cowprotocol:developfrom
riboflavin:feat/cow-fi-resources

Conversation

@riboflavin

@riboflavin riboflavin commented Jul 13, 2026

Copy link
Copy Markdown

Summary

  • Adds a cow.fi Resources section for programmatic CMS content, separate from Learn / Articles.
  • Routes:
    • /resources — campaign hub
    • /resources/[campaign] — campaign listing
    • /resources/[campaign]/[slug] — detail (redirects if campaign mismatches)
  • Footer Help link: Resources/resources (no main nav).
  • Extends revalidation, sitemap lastmod, middleware tracking-param stripping, and cache headers for /resources.
  • Instantiates the CMS client in cow-fi (avoids pulling @cowprotocol/core trading code into content pages).
  • Includes a small BigInt harden in maxAmountSpend so Webpack/Babel does not crash content pages that import @cowprotocol/common-utils.

Depends on

  • cowprotocol/cms#94 — Resource collection + OpenAPI/types
  • After that merges and @cowprotocol/cms is published, bump apps/cow-fi + libs/core off 0.11.0 to the new version (typed /resources endpoints). Until then, typecheck against Resource schemas may fail.

Summary by CodeRabbit

  • New Features

    • Added a Resources hub with campaign listings and published page counts.
    • Added campaign-specific resource pages with breadcrumbs, descriptions, rich-text content, publish dates, and sharing options.
    • Added resource metadata, sitemap support, caching, and automatic page regeneration.
    • Added a Resources link to the footer Help menu.
  • Bug Fixes

    • Tracking parameters are now removed from resource URLs.
    • Improved CMS image URL handling and production compatibility.

Add typed fetch helpers for resources, campaign summaries, and slug lookup
without touching the existing Article/Learn pipeline.
Introduce /resources hub, campaign listings, and detail pages for
programmatic CMS content.
Expose the new /resources hub from the Help section without adding main nav.
Keep programmatic pages fresh and discoverable alongside existing Learn routes.
Point cow-fi at the locally built CMS client package until the published
@cowprotocol/cms release with /resources is available.
Use a concrete font-size so campaign hub and listing pages render.
Avoid BigInt exponentiation that Webpack turns into Math.pow, and
instantiate the CMS client locally so content pages do not import the
trading/wallet core barrel.
permanentRedirect throws a Next control-flow error; catching it turned
wrong-campaign URLs into soft failures instead of redirects.
Use BigInt('...') instead of a BigInt literal, and require browsers that
support bigint so maxAmountSpend no longer crashes Resources pages.
Local file: linking was only for development. Bump after cowprotocol/cms
Resource collection is published.
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

@riboflavin is attempting to deploy a commit to the cow-dev Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds CMS-backed resources hub, campaign, and detail pages with ISR, metadata, rich-text rendering, sitemap timestamps, cache refresh, tracking cleanup, footer navigation, and CMS image configuration. It also replaces selected BigInt literals with constructor calls for production build compatibility.

Changes

Resources feature

Layer / File(s) Summary
CMS resource data and contracts
apps/cow-fi/services/cms/...
Adds resource types, CMS configuration, population rules, client setup, paginated queries, campaign summaries, and slug lookup helpers.
Resource route generation and rendering
apps/cow-fi/app/(main)/resources/..., apps/cow-fi/components/..., apps/cow-fi/const/resources.ts
Adds hub, campaign, and detail pages with ISR, metadata, static parameters, redirects, not-found handling, campaign labels, links, and rich-text rendering.
Resource delivery and discovery
apps/cow-fi/app/api/revalidate/route.ts, apps/cow-fi/middleware.ts, apps/cow-fi/next-sitemap.config.js, apps/cow-fi/next.config.ts, apps/cow-fi/components/CmsImage/index.tsx, libs/ui/src/containers/Footer/footer.constants.ts
Adds resource cache refresh, tracking-parameter cleanup, sitemap lastmod data, cache headers, CMS image URL configuration, and footer navigation.

BigInt compatibility

Layer / File(s) Summary
BigInt-safe amount construction
libs/common-utils/src/maxAmountSpend.ts, apps/cow-fi/package.json
Uses BigInt(...) construction for native-currency thresholds and updates production browser targeting to support BigInt.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ResourceRoute
  participant CmsService
  participant ResourcePageComponent
  Browser->>ResourceRoute: request campaign and slug route
  ResourceRoute->>CmsService: getResourceBySlug(slug)
  CmsService-->>ResourceRoute: resource attributes
  ResourceRoute->>ResourcePageComponent: render resource
  ResourcePageComponent-->>Browser: resource content and share controls
Loading

Possibly related PRs

Suggested reviewers: alfetopito, fairlighteth, kernelwhisperer

Poem

A rabbit hops through pages bright,
With CMS carrots tucked just right.
Campaign trails and slugs align,
Rich-text burrows render fine.
Cache paths fresh, links in view—
BigInt hops safely too!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The summary is solid, but the required To Test section is missing and the template’s Background section is not addressed. Add a To Test section with reproducible QA steps and expected results, and include a short Background section if helpful.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the main change: adding a new Resources section for cow.fi CMS content.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/cow-fi/components/ResourcePageComponent.tsx (1)

51-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused content variable.

content is computed by mapping blocks and joining bodies, but is never referenced in the JSX. The blocks are rendered directly on lines 92-94. This is dead code that adds unnecessary computation on every render.

♻️ Proposed fix
   const description = attributes?.description || ''
-  const content =
-    blocks?.map((block: SharedRichTextComponent) => (isRichTextComponent(block) ? block.body : '')).join(' ') || ''
   const pathname = usePathname()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cow-fi/components/ResourcePageComponent.tsx` around lines 51 - 52,
Remove the unused content variable and its associated blocks mapping/joining
computation from ResourcePageComponent; keep the existing direct blocks
rendering unchanged.
apps/cow-fi/services/cms/index.ts (1)

362-389: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

getAllResourceSlugs fetches only the first page — resources beyond 100 items are silently missed.

getAllResourceSlugs issues a single CMS request with pagination[pageSize] = DEFAULT_PAGE_SIZE (100) and no pagination loop. If more than 100 resources exist, generateStaticParams won't pre-render those pages and getCampaignSummaries will undercount or omit campaigns. The sitemap's getAllResourceSlugsWithDates correctly loops through all pages — consider applying the same pattern here.

♻️ Proposed fix: paginate through all pages
 export async function getAllResourceSlugs(): Promise<ResourceSlugParam[]> {
   try {
+    let allSlugs: ResourceSlugParam[] = []
+    let page = 1
+    let hasMore = true
+
+    while (hasMore) {
     const { data, error, response } = await client.GET('/resources', {
       params: {
         query: {
           fields: ['slug', 'campaign'],
-          'pagination[pageSize]': DEFAULT_PAGE_SIZE,
+          'pagination[page]': page,
+          'pagination[pageSize]': DEFAULT_PAGE_SIZE,
         },
       },
       querySerializer,
       ...clientAddons,
     })
 
     if (error) {
       console.error(`Error ${response.status} getting resource slugs: ${response.url}`, error)
       throw error
     }
 
-    return data.data
-      .filter((resource: Resource) => resource.attributes?.slug && resource.attributes?.campaign)
-      .map((resource: Resource) => ({
-        campaign: resource.attributes!.campaign!,
-        slug: resource.attributes!.slug!,
-      }))
+      const pageSlugs = data.data
+        .flatMap((resource: Resource) => {
+          const { slug, campaign } = resource.attributes ?? {}
+          if (!slug || !campaign) return []
+          return [{ campaign, slug }]
+        })
+      allSlugs = allSlugs.concat(pageSlugs)
+      hasMore = data.meta.pagination.page < data.meta.pagination.pageCount
+      page++
+    }
+
+    return allSlugs
   } catch (error) {
     return handleCmsBuildFailure('getAllResourceSlugs', error, [])
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cow-fi/services/cms/index.ts` around lines 362 - 389, Update
getAllResourceSlugs to paginate through every CMS results page instead of
issuing only the initial request with DEFAULT_PAGE_SIZE. Reuse the pagination
approach from getAllResourceSlugsWithDates, accumulating valid slug/campaign
pairs until no further pages remain, while preserving the existing error
handling through handleCmsBuildFailure.
apps/cow-fi/components/ResourcesHubComponent.tsx (1)

9-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use import type for type-only imports from services/cms in client components.

CampaignSummary is only used as a type annotation (campaigns: CampaignSummary[]), but the regular import risks pulling server-side CMS code into the client bundle.

♻️ Proposed fix
-import { CampaignSummary } from '../services/cms'
+import type { CampaignSummary } from '../services/cms'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cow-fi/components/ResourcesHubComponent.tsx` at line 9, Update the
CampaignSummary import in ResourcesHubComponent to use a type-only import, since
it is only referenced in the campaigns type annotation and must not include
services/cms runtime code in the client bundle.
apps/cow-fi/components/ResourcesCampaignComponent.tsx (1)

9-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use import type for type-only imports from services/cms in client components.

Resource is only used as a type annotation (resources: Resource[]), but the regular import risks pulling the CMS client instantiation and server-side code from services/cms into the client bundle. This aligns with the PR objective of avoiding importing server/trading code into content pages.

♻️ Proposed fix
-import { Resource } from '../services/cms'
+import type { Resource } from '../services/cms'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cow-fi/components/ResourcesCampaignComponent.tsx` at line 9, Change the
Resource import in ResourcesCampaignComponent to a type-only import, preserving
its use in the resources: Resource[] annotation and preventing services/cms
runtime code from entering the client bundle.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cow-fi/app/`(main)/resources/[campaign]/[slug]/page.tsx:
- Around line 90-97: Update the redirect condition in the resource page flow to
handle a missing resource.attributes.campaign before comparing it with campaign,
preventing redirects to an undefined campaign path. Preserve the existing
permanentRedirect behavior for valid campaign values that differ from the URL
campaign.

In `@apps/cow-fi/next-sitemap.config.js`:
- Around line 138-139: Update both getAllResourceSlugsWithDates and
getAllArticleSlugsWithDates to resolve the CMS base URL through the shared
CMS_BASE_URL configuration, or preserve the same precedence by adding
REACT_APP_CMS_BASE_URL between NEXT_PUBLIC_CMS_BASE_URL and the hardcoded
default. Ensure both sitemap fetchers use the configured URL when only
REACT_APP_CMS_BASE_URL is set.

In `@apps/cow-fi/services/cms/index.ts`:
- Around line 383-384: Replace the non-null assertions in the campaign mapping
with values narrowed by the filter callback: extract and validate attributes,
campaign, and slug within that callback, then reuse those validated values when
constructing the result. Preserve the existing truthy filtering behavior while
removing every `!` assertion from the affected `resource.attributes` accesses.

---

Nitpick comments:
In `@apps/cow-fi/components/ResourcePageComponent.tsx`:
- Around line 51-52: Remove the unused content variable and its associated
blocks mapping/joining computation from ResourcePageComponent; keep the existing
direct blocks rendering unchanged.

In `@apps/cow-fi/components/ResourcesCampaignComponent.tsx`:
- Line 9: Change the Resource import in ResourcesCampaignComponent to a
type-only import, preserving its use in the resources: Resource[] annotation and
preventing services/cms runtime code from entering the client bundle.

In `@apps/cow-fi/components/ResourcesHubComponent.tsx`:
- Line 9: Update the CampaignSummary import in ResourcesHubComponent to use a
type-only import, since it is only referenced in the campaigns type annotation
and must not include services/cms runtime code in the client bundle.

In `@apps/cow-fi/services/cms/index.ts`:
- Around line 362-389: Update getAllResourceSlugs to paginate through every CMS
results page instead of issuing only the initial request with DEFAULT_PAGE_SIZE.
Reuse the pagination approach from getAllResourceSlugsWithDates, accumulating
valid slug/campaign pairs until no further pages remain, while preserving the
existing error handling through handleCmsBuildFailure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b100484d-cdf9-4ba7-af8b-963aac0a4239

📥 Commits

Reviewing files that changed from the base of the PR and between 1ab0190 and 4f15eb3.

📒 Files selected for processing (18)
  • apps/cow-fi/app/(main)/resources/[campaign]/[slug]/page.tsx
  • apps/cow-fi/app/(main)/resources/[campaign]/page.tsx
  • apps/cow-fi/app/(main)/resources/page.tsx
  • apps/cow-fi/app/api/revalidate/route.ts
  • apps/cow-fi/components/CmsImage/index.tsx
  • apps/cow-fi/components/ResourcePageComponent.tsx
  • apps/cow-fi/components/ResourcesCampaignComponent.tsx
  • apps/cow-fi/components/ResourcesHubComponent.tsx
  • apps/cow-fi/const/resources.ts
  • apps/cow-fi/middleware.ts
  • apps/cow-fi/next-sitemap.config.js
  • apps/cow-fi/next.config.ts
  • apps/cow-fi/package.json
  • apps/cow-fi/services/cms/config.ts
  • apps/cow-fi/services/cms/helpers.ts
  • apps/cow-fi/services/cms/index.ts
  • libs/common-utils/src/maxAmountSpend.ts
  • libs/ui/src/containers/Footer/footer.constants.ts

Comment on lines +90 to +97
if (!resource?.attributes) {
return notFound()
}

// Keep outside try/catch — permanentRedirect throws a control-flow error Next must handle
if (resource.attributes.campaign !== campaign) {
permanentRedirect(`/resources/${resource.attributes.campaign}/${slug}`)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing null check for resource.attributes.campaign before redirect comparison.

If campaign is undefined or null, the condition resource.attributes.campaign !== campaign evaluates to true (since campaign from the URL is always a string), causing a redirect to /resources/undefined/${slug}. The downstream ResourcePageComponent explicitly guards against falsy campaign (if (!campaign || !attributes?.slug) return null), confirming this case is reachable.

🐛 Proposed fix: guard against missing campaign before redirect
   if (!resource?.attributes) {
     return notFound()
   }

+  if (!resource.attributes.campaign) {
+    return notFound()
+  }
+
   // Keep outside try/catch — permanentRedirect throws a control-flow error Next must handle
   if (resource.attributes.campaign !== campaign) {
     permanentRedirect(`/resources/${resource.attributes.campaign}/${slug}`)
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!resource?.attributes) {
return notFound()
}
// Keep outside try/catch — permanentRedirect throws a control-flow error Next must handle
if (resource.attributes.campaign !== campaign) {
permanentRedirect(`/resources/${resource.attributes.campaign}/${slug}`)
}
if (!resource?.attributes) {
return notFound()
}
if (!resource.attributes.campaign) {
return notFound()
}
// Keep outside try/catch — permanentRedirect throws a control-flow error Next must handle
if (resource.attributes.campaign !== campaign) {
permanentRedirect(`/resources/${resource.attributes.campaign}/${slug}`)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cow-fi/app/`(main)/resources/[campaign]/[slug]/page.tsx around lines 90
- 97, Update the redirect condition in the resource page flow to handle a
missing resource.attributes.campaign before comparing it with campaign,
preventing redirects to an undefined campaign path. Preserve the existing
permanentRedirect behavior for valid campaign values that differ from the URL
campaign.

Comment on lines +138 to +139
async function getAllResourceSlugsWithDates() {
const cmsBaseUrl = process.env.NEXT_PUBLIC_CMS_BASE_URL || 'https://cms.cow.fi/api'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Missing REACT_APP_CMS_BASE_URL fallback in sitemap fetcher.

getAllResourceSlugsWithDates resolves the CMS base URL with only NEXT_PUBLIC_CMS_BASE_URL and a hardcoded default, omitting the REACT_APP_CMS_BASE_URL fallback that config.ts includes. If only REACT_APP_CMS_BASE_URL is configured, the sitemap fetcher would use the wrong CMS URL. Note: the existing getAllArticleSlugsWithDates (line 96) has the same gap, so this is a pre-existing pattern — but it's worth fixing for consistency.

🔧 Proposed fix: import CMS_BASE_URL from config
-  const cmsBaseUrl = process.env.NEXT_PUBLIC_CMS_BASE_URL || 'https://cms.cow.fi/api'
+  const cmsBaseUrl = CMS_BASE_URL

This requires importing CMS_BASE_URL from the config at the top of the file. If that import is not feasible in the sitemap config context, add the REACT_APP_CMS_BASE_URL fallback:

-  const cmsBaseUrl = process.env.NEXT_PUBLIC_CMS_BASE_URL || 'https://cms.cow.fi/api'
+  const cmsBaseUrl = process.env.NEXT_PUBLIC_CMS_BASE_URL || process.env.REACT_APP_CMS_BASE_URL || 'https://cms.cow.fi/api'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function getAllResourceSlugsWithDates() {
const cmsBaseUrl = process.env.NEXT_PUBLIC_CMS_BASE_URL || 'https://cms.cow.fi/api'
async function getAllResourceSlugsWithDates() {
const cmsBaseUrl = process.env.NEXT_PUBLIC_CMS_BASE_URL || process.env.REACT_APP_CMS_BASE_URL || 'https://cms.cow.fi/api'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cow-fi/next-sitemap.config.js` around lines 138 - 139, Update both
getAllResourceSlugsWithDates and getAllArticleSlugsWithDates to resolve the CMS
base URL through the shared CMS_BASE_URL configuration, or preserve the same
precedence by adding REACT_APP_CMS_BASE_URL between NEXT_PUBLIC_CMS_BASE_URL and
the hardcoded default. Ensure both sitemap fetchers use the configured URL when
only REACT_APP_CMS_BASE_URL is set.

Comment on lines +383 to +384
campaign: resource.attributes!.campaign!,
slug: resource.attributes!.slug!,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Non-null assertions violate coding guidelines.

Lines 383-384 use resource.attributes!.campaign! and resource.attributes!.slug!. The coding guidelines for **/*.{ts,tsx,js,jsx} state: "MUST NOT use any or non-null assertions (!) in production code." The filter on line 381 guarantees truthiness at runtime, but TypeScript cannot narrow through the callback.

🔧 Proposed fix: extract values in the filter callback
     return data.data
-      .filter((resource: Resource) => resource.attributes?.slug && resource.attributes?.campaign)
-      .map((resource: Resource) => ({
-        campaign: resource.attributes!.campaign!,
-        slug: resource.attributes!.slug!,
-      }))
+      .flatMap((resource: Resource) => {
+        const { slug, campaign } = resource.attributes ?? {}
+        if (!slug || !campaign) return []
+        return [{ campaign, slug }]
+      })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
campaign: resource.attributes!.campaign!,
slug: resource.attributes!.slug!,
.flatMap((resource: Resource) => {
const { slug, campaign } = resource.attributes ?? {}
if (!slug || !campaign) return []
return [{ campaign, slug }]
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cow-fi/services/cms/index.ts` around lines 383 - 384, Replace the
non-null assertions in the campaign mapping with values narrowed by the filter
callback: extract and validate attributes, campaign, and slug within that
callback, then reuse those validated values when constructing the result.
Preserve the existing truthy filtering behavior while removing every `!`
assertion from the affected `resource.attributes` accesses.

Source: Coding guidelines

@fairlighteth fairlighteth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ AI Review (Codex GPT-5, worked 28m): resource path revalidation is broken; valid campaign labels can crash

Finding: [BLOCKING] Resource-specific revalidation requests are rejected

  • Location: apps/cow-fi/app/api/revalidate/route.ts:39
  • This one is important: an authenticated payload containing /resources/<campaign>/<slug> is rejected by the /learn-only allowlist before revalidateTag or revalidatePath runs. I reproduced Unsupported revalidation path "/resources/tokens/example".
  • The new dynamic calls at lines 56–57 also omit the page/layout argument; Next 15.5.18 warns that dynamic patterns without it have no effect by default.
  • Receipt: PR #7618 deliberately introduced the narrow allowlist as a Medium-security remediation, so it should be extended precisely rather than bypassed.

Suggested fix

  • Allow exactly /resources, /resources/<campaign>, and /resources/<campaign>/<slug> using the canonical CMS slug grammar.
  • Revalidate the accepted concrete path, and either pass 'page' for dynamic patterns or invalidate the /resources layout.
  • Add validator and authenticated route-handler tests for resource payloads.

Finding: [NON-BLOCKING] A schema-valid constructor campaign crashes metadata generation

  • Location: apps/cow-fi/const/resources.ts:6
  • CMS PR #94 permits lowercase campaign slugs, so constructor is valid. The plain-object lookup returns the inherited Object constructor instead of a string; generateMetadata then throws at label.toLowerCase().
  • I reproduced the returned value as a function and the resulting label.toLowerCase is not a function.
  • This is separate from the existing missing-campaign guard comment because the campaign is present and schema-valid.

Suggested fix

  • Use a Map<string, string> or an own-property check before returning a configured label.
  • Add coverage for known, unknown, hyphenated, and constructor campaign names.
Review scope and related context
  • Reviewed current head 4f15eb3 against base 1ab0190.
  • Harness used: DeepSec PR mode over the exact 18 changed files. Source: DeepSec output verified against current code. Command used pnpm dlx deepsec@2.0.12 process --files-from … --comment-out /tmp/deepsec-comment-cowswap-7846-head.md; exit 1, run 20260714151805-29e774a8c9a75754.
  • Security result: no verified-vulnerability findings. DeepSec produced three BUG candidates; two became the correctness findings above, while the native-symbol ERC-20 candidate predates this PR.
  • Automated verification: cow-fi:lint passed; the focused CMS validation/Markdown suites passed, 18/18 tests.
  • Targeted typecheck is not green: @cowprotocol/cms 0.11.0 lacks ResourceListResponseDataItem and /resources, matching the dependency already disclosed in the PR body. Re-run after the CMS release and dependency bump.
  • Existing CodeRabbit feedback already covers the missing campaign guard, sitemap environment fallback, prohibited non-null assertions, unused content, type-only imports, and first-page slug pagination.
  • When resolving that pagination feedback, also cover ResourcesCampaignPage -> getResources; otherwise the rendered campaign listing still stops at 100 items.
  • No author replies or resolved threads required follow-up verification.
🤖 Prompt for AI agents
Verify these findings against the current PR head and keep the fixes minimal.

1. Extend the existing revalidation allowlist with exact resource route patterns without weakening the security boundary introduced in PR #7618.
2. Ensure dynamic revalidatePath calls pass the required page/layout type, or invalidate the resources layout.
3. Replace inherited-property-prone campaign label lookup with a Map or own-property-safe lookup.
4. Add focused tests for resource revalidation payloads and the "constructor" campaign case.
5. While addressing the existing pagination feedback, verify that the campaign listing itself can render beyond DEFAULT_PAGE_SIZE.

Generated using the pr-review and security-review skills from the CoW Protocol skills repo.

@kernelwhisperer
kernelwhisperer self-requested a review July 30, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants