Skip to content

feat(blog): add shoppable product block sections#1603

Open
yuriassuncx wants to merge 54 commits into
deco-cx:mainfrom
yuriassuncx:feat/autonomous-blog-compatibility
Open

feat(blog): add shoppable product block sections#1603
yuriassuncx wants to merge 54 commits into
deco-cx:mainfrom
yuriassuncx:feat/autonomous-blog-compatibility

Conversation

@yuriassuncx

@yuriassuncx yuriassuncx commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds three shoppable product blocks to the blog app for autonomous-blog content, with a searchable admin product picker backed by the site storefront integration.

  • ProductCard — compact card with image, name, price, and CTA
  • ProductHighlight — full-width featured product layout
  • ProductShelf — horizontally scrollable row of products

Supporting changes

  • blog/loaders/options/productsByTerm.ts — dynamic-options picker for the Deco admin
  • blog/utils/productReference.ts — canonical refs (platform:kind:id)
  • blog/utils/productResolver.ts — multi-platform resolve/search via ctx.invoke (VTEX, Shopify, Wake, WAP)
  • blog/utils/productData.ts — image, price, and link display helpers
  • blog/manifest.gen.ts — registers loader and sections

Test plan

  • Install blog app on a site with a commerce integration (VTEX, Shopify, Wake, or WAP)
  • Add Product Card, Product Highlight, and Product Shelf blocks to a blog post
  • Confirm the product picker searches and returns labeled options
  • Preview: blocks render image, formatted price, and product link
  • deno task check passes on CI

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR integrates Spire as a live blog post source alongside native Deco posts. It adds webhook authentication to receive Spire events, a gate resolution action for admin approvals, UI components for sync status, conversion utilities for Spire posts and blocks, hybrid loaders that fetch native posts first with Spire fallback, and data loaders for authors, tags, and filtered posts. All loaders cache responses for 60 seconds to enable near-real-time updates while maintaining performance.

Native Blog Loaders with Spire Fallback

Layer / File(s) Summary
Type Contracts and API Shapes
spire/types.ts, blog/types.ts, spire/utils/client.ts
Adds SpireAuthorFull, SpireTagWithCount, and SpirePostTag types; extends BlogPost with optional spirePostId; and wires SpireApi with new author and tag GET endpoints and response types.
Spire Post Conversion and Block HTML Compilation
blog/utils/spireImport.ts
Exports spirePostToBlogPost and spirePostSummaryToBlogPost mappers, block content type, escapeHtml/safeJsonParse helpers, and compileBlocksToHtml that renders modular blocks into sanitized semantic HTML.
Webhook Handler and Gate Resolution Action
blog/actions/webhook.ts, blog/actions/resolveSpireGate.ts
Webhook authenticates events via HMAC-SHA256 or bearer token with tenant isolation and slug validation; gate action validates admin caller and sends gate identifiers to Spire with timeout protection.
Blog App State and Configuration
blog/mod.ts
Extends State with Spire config (allowedBlogSlug, spireWebhookSecret, spireUrl), introduces internal BlogState with optional spireApi client, and configures App to derive Spire URL and conditionally create typed HTTP client.
Spire Sync UI Components
blog/components/SpireSyncPreviewTab.tsx, blog/sections/SpireSync.tsx
Renders SpireSyncPreviewTab in preview tab and SpireSync section in admin UI, both showing connection status and dashboard link with conditional configuration messaging.
Native Loaders with Spire Fallback
blog/loaders/BlogPostItem.ts, blog/loaders/BlogPostPage.ts, blog/loaders/BlogpostList.ts, blog/loaders/BlogpostListing.ts
BlogPostItem, BlogPostPage, BlogpostList, and BlogpostListing fetch native posts first and conditionally fall back to Spire API, with shared caching, slug-based deduplication, and error handling.
Spire Data Loaders
spire/loaders/GetAuthors.ts, spire/loaders/GetTags.ts, spire/loaders/BlogPostItem.ts, spire/loaders/BlogsByAuthor.ts, spire/loaders/BlogsByTag.ts
Exports cached loaders for Spire authors (with avatar mapping), tags (with count), single posts, posts by author, and posts by tag, each with deterministic cache keys scoped by account and optional slug parameters.
Loader Optimization and Manifest
spire/loaders/BlogPostPage.ts, spire/loaders/BlogpostList.ts, spire/loaders/BlogpostListing.ts, blog/manifest.gen.ts, spire/manifest.gen.ts, spire/mod.ts
Reduces blog and Spire loader cache TTLs to 60 seconds for near-real-time updates, regenerates manifests to register new actions/sections/loaders, and updates Spire module to accept optional apiKey prop for private blogs.

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • rafavalls
  • guitavano
  • aka-sacci-ccr

🐇 Spire now blooms in Deco's garden fair,
With webhooks synced and posts laid bare,
Native and live in harmony they blend,
On cache's swift wing through browsers they send!

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The title mentions shoppable product blocks, but this PR actually centers on Spire webhook and blog sync changes. Retitle it to match the main change, e.g. "feat(blog): add Spire webhook integration and live blog sync".
Description check ⚠️ Warning The description does not follow the required template and is missing the Issue Link, Loom Video, and Demonstration Link sections. Rewrite it using the repo template and add the issue link, Loom video link, and demo or branch link.
✅ Passed checks (2 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.
✨ 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.

@github-actions

Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.154.1 update
  • 🎉 for Minor 0.155.0 update
  • 🚀 for Major 1.0.0 update

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
spire/sections/SpirePendingApprovals.tsx (1)

1-190: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run deno fmt for this file before merge.

CI is currently failing on formatting for this module.

🤖 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 `@spire/sections/SpirePendingApprovals.tsx` around lines 1 - 190, Run the
formatter (deno fmt) on this file to fix CI formatting errors; specifically
format the module containing the loader function and the default export
SpirePendingApprovals so imports, JSX attributes, spacing, and trailing commas
match the project's style — after running deno fmt, re-run the tests/CI and
commit the updated file.
spire/sections/SpireDashboard.tsx (1)

1-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run deno fmt for this file before merge.

CI is currently failing on formatting for this module.

🤖 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 `@spire/sections/SpireDashboard.tsx` around lines 1 - 65, Run the formatter on
this file (e.g., run `deno fmt`) to fix CI formatting errors for the
SpireDashboard component; ensure the import and JSX/TSX formatting, indentation,
and trailing semicolons in the SpireDashboard function (including the useId
import, Props interface, default props, the conditional early-return block, and
the embeddedUrl/iframe JSX) are normalized by the formatter before merging.
blog/actions/importSpirePost.ts (1)

1-197: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run deno fmt for this file before merge.

CI is currently failing on formatting for this module.

🤖 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 `@blog/actions/importSpirePost.ts` around lines 1 - 197, The file fails
formatting; run the formatter and commit the changes: execute `deno fmt` (or
your repo's formatting script) to format this module, ensuring functions like
importSpirePost and compileBlocksToHtml (and their surrounding imports/exports)
are consistently formatted, then stage and commit the resulting changes so the
CI formatting check passes.
🤖 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 `@blog/actions/importSpirePost.ts`:
- Around line 148-152: The JSON.parse of serialized block arrays (e.g., when
building items in importSpirePost.ts) is unguarded and can throw, aborting
webhook processing; wrap each JSON.parse usage (the assignment to items and the
similar parses at the other mentioned spots) in a try/catch and on failure fall
back to an empty array (or safe default) and optionally log the error via the
existing logger, so parsing errors do not throw—update the parsing sites (the
items variable and the other two parse locations noted) to validate typeof
content.items === "string" then try JSON.parse(...) inside try/catch and use the
safe fallback on catch.
- Around line 44-45: Validate and sanitize postSlug before using it to build
filesystem paths: ensure postSlug contains only allowed characters (e.g.,
alphanumerics, hyphens/underscores) and does not include path separators or
segments like ".."; if validation fails, throw an error. Replace direct uses of
postSlug when constructing blocksDir/filePath (and the other paths referenced
around lines 89-92) with a validated/sanitizedSlug (or use path.basename after
validation) so no crafted slug can escape the intended posts directory;
reference variables to update: postSlug, blocksDir, filePath and any other
path-building that uses postSlug. Ensure the validation runs early in the
importSpirePost flow so all subsequent file operations use the safe slug.
- Around line 30-40: The handler currently allows unauthenticated requests when
expectedSecret is falsy (the expectedSecret check/log block in
importSpirePost.ts), which is unsafe; change the logic so that if expectedSecret
is not set you return a failure response (e.g., success: false with
"Unauthorized: webhook secret not configured.") instead of logging a warning and
allowing the request, and keep the existing Authorization header check
(authHeader !== `Bearer ${expectedSecret}`) intact to reject mismatches; locate
the block referencing expectedSecret and req.headers.get("Authorization") and
replace the else-branch to return an unauthorized response rather than
permitting the request.
- Line 59: The fetch call that builds the Spire API URL with raw
blogSlug/postSlug and no timeout is brittle; URL-encode blogSlug and postSlug
(via encodeURIComponent) before interpolating into the path and enforce a
request timeout using an AbortController: create controller, start a timer
(e.g., 5s) that calls controller.abort(), pass controller.signal into fetch (the
existing response = await fetch(...) call), and clear the timer after fetch
completes; also handle the abort/timeout error path where the request can throw.
Use the variables blogSlug, postSlug, response and the fetch invocation in
importSpirePost.ts to locate and update the code.
- Around line 137-193: The map over sorted blocks interpolates many content
fields directly (see sorted.map callback and SpireBlockContent) causing stored
XSS; replace direct interpolation by running all user-provided strings
(content.text, content.html, content.items entries, content.quote,
content.attribution, content.title, content.body, content.caption, content.alt,
content.href, content.code, etc.) through a canonical sanitizer/escaper before
embedding, e.g., use a shared escapeHtml(value) for plain-text fields and a
vetted HTML sanitizer (e.g., DOMPurify or sanitizeHtml) for content.html if raw
HTML must be allowed; update each case branch
("paragraph","heading","list","quote","callout","checklist","steps","image","video","code","cta",
and the default) to call the appropriate sanitizer/escaper on the referenced
fields instead of interpolating them directly.

In `@spire/sections/SpireDashboard.tsx`:
- Around line 44-55: The iframe URL currently builds embeddedUrl using blogSlug
raw, which can produce malformed routes; update the construction of embeddedUrl
(used for the iframe src in SpireDashboard) to encode the slug by applying
encodeURIComponent(blogSlug) so the path/query are safe and preserved when
embedded; locate where embeddedUrl is defined and replace the raw blogSlug usage
with the encoded value before passing embeddedUrl to the iframe.

In `@spire/sections/SpirePendingApprovals.tsx`:
- Around line 47-68: The catch block in SpirePendingApprovals.tsx currently
returns synthetic mockGates (PendingGate[]), which can surface false approvals;
remove the mockGates fallback and instead handle fetch failures safely by
logging the error and returning a non-misleading result (e.g., return {
...props, gates: [] } or rethrow the error so the caller can handle it). Update
the catch in the function that fetches gates (the try/catch that references
mockGates and returns { ...props, gates: mockGates }) to eliminate the mock data
path; if you still want local developer convenience, gate that mock-only
behavior behind an explicit DEV flag (e.g., process.env.NODE_ENV ===
'development') so production never receives synthetic approvals.
- Line 41: The fetch call in SpirePendingApprovals.tsx (where `response` is
assigned) must URL-encode the blog slug and use a timeout via AbortController;
replace the raw template `${spireUrl}/api/blog/${blogSlug}/gates/pending` with a
safe URL built using encodeURIComponent(blogSlug) (or new URL()) and wrap the
fetch with an AbortController whose signal is passed to fetch and which is
aborted after a short timeout (e.g. 3–10s) using setTimeout; also add an
explicit UTF-8 header like Accept-Charset: 'utf-8' (or ensure headers include
'Accept': 'application/json; charset=utf-8') so the loader won’t hang or break
on special characters.

---

Outside diff comments:
In `@blog/actions/importSpirePost.ts`:
- Around line 1-197: The file fails formatting; run the formatter and commit the
changes: execute `deno fmt` (or your repo's formatting script) to format this
module, ensuring functions like importSpirePost and compileBlocksToHtml (and
their surrounding imports/exports) are consistently formatted, then stage and
commit the resulting changes so the CI formatting check passes.

In `@spire/sections/SpireDashboard.tsx`:
- Around line 1-65: Run the formatter on this file (e.g., run `deno fmt`) to fix
CI formatting errors for the SpireDashboard component; ensure the import and
JSX/TSX formatting, indentation, and trailing semicolons in the SpireDashboard
function (including the useId import, Props interface, default props, the
conditional early-return block, and the embeddedUrl/iframe JSX) are normalized
by the formatter before merging.

In `@spire/sections/SpirePendingApprovals.tsx`:
- Around line 1-190: Run the formatter (deno fmt) on this file to fix CI
formatting errors; specifically format the module containing the loader function
and the default export SpirePendingApprovals so imports, JSX attributes,
spacing, and trailing commas match the project's style — after running deno fmt,
re-run the tests/CI and commit the updated file.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2d2c9cb-0831-414a-8e43-f486b1eecdc8

📥 Commits

Reviewing files that changed from the base of the PR and between fbcb3f7 and ce4570f.

📒 Files selected for processing (7)
  • blog/actions/importSpirePost.ts
  • blog/manifest.gen.ts
  • blog/mod.ts
  • blog/types.ts
  • spire/manifest.gen.ts
  • spire/sections/SpireDashboard.tsx
  • spire/sections/SpirePendingApprovals.tsx

Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread blog/actions/webhook.ts Outdated
Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread spire/sections/SpireDashboard.tsx Outdated
Comment thread spire/sections/SpirePendingApprovals.tsx Outdated
Comment thread spire/sections/SpirePendingApprovals.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 issues found across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread spire/sections/SpireDashboard.tsx Outdated
Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread spire/sections/SpirePendingApprovals.tsx Outdated
Comment thread blog/actions/importSpirePost.ts Outdated
Comment thread spire/sections/SpireDashboard.tsx Outdated
Comment thread blog/actions/importSpirePost.ts Outdated

@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: 6

🤖 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 `@blog/actions/importSpirePost.ts`:
- Around line 269-271: The heading level and variant values (content.level and
content.variant) are interpolated directly into tag/attribute contexts which can
enable markup injection; validate these tokens against strict whitelists before
use (e.g., allow only digits 1-6 for level and a fixed set of variant CSS
names), normalize/fallback to safe defaults (e.g., "2" for level, "default" for
variant) if validation fails, and use the validated variables in the template
generation where the current string interpolation occurs (the lines building
`<h${level}>...</h${level}>` and the variant-related template at 293-296) so no
unvalidated user input is placed into tag names or attributes.
- Around line 241-245: safeJsonParse<T> currently returns whatever JSON.parse
yields and callers assume specific shapes (e.g., arrays), causing runtime errors
when Spire returns malformed JSON; change safeJsonParse to accept an optional
runtime validator (a (v: unknown) => v is T) or a flag for arrays, run
JSON.parse inside the try and if the validator (or Array.isArray for array
expectations) returns false, return the fallback instead, and update all call
sites that expect arrays (the locations using .map) to pass a validator or check
Array.isArray on the result before mapping so malformed shapes safely fall back.

In `@blog/actions/resolveSpireGate.ts`:
- Around line 40-55: The POST to Spire in resolveSpireGate should use an
AbortController with a timeout (e.g., 8000ms) so the request cannot hang
indefinitely: create an AbortController, pass controller.signal into fetch in
resolveSpireGate, set a setTimeout to call controller.abort() after the timeout
and clear that timer after fetch completes; in the catch block detect an
AbortError and handle it (return a specific error/result or throw a clearer
error) so callers (and SpirePendingApprovals.tsx which uses an 8s loader)
receive a timely failure instead of waiting forever.

In `@blog/mod.ts`:
- Around line 111-124: The fetch call that constructs `response` to POST to
`${spireUrl}/api/blog/posts/sync-manual` lacks timeout/abort handling; wrap the
request with an AbortController, pass `controller.signal` into the fetch
options, start a timer (e.g., using setTimeout) to call `controller.abort()`
after a reasonable timeout, and clear the timeout when the fetch completes or
fails; ensure you handle the abort error case (thrown when aborted) and
propagate or log it appropriately while still including the existing headers and
JSON body built from `post`, `expectedSecret`, and `spireUrl`.

In `@spire/sections/SpirePendingApprovals.tsx`:
- Around line 217-227: The Quick Approve <button> element (identified by class
"gate-approve-btn" and data attributes like data-gate-id) is missing an explicit
type and fails the Deno lint; add type="button" to that button element in
SpirePendingApprovals.tsx so it does not default to "submit" inside forms.
- Around line 234-282: The inline script in SpirePendingApprovals.tsx unsafely
injects blogSlug into the template string, creating an XSS risk; fix it by
serializing/escaping blogSlug before embedding (e.g. use
JSON.stringify(blogSlug) when constructing the script string) so the value
assigned to the local blogSlug variable is safely escaped, then keep the rest of
the handler logic (querySelectorAll('.gate-approve-btn') and the fetch to
"/live/action/blog/actions/resolveSpireGate") unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43363455-1387-4f1f-a6ed-9bb5b4c0f48b

📥 Commits

Reviewing files that changed from the base of the PR and between ce4570f and 210abec.

📒 Files selected for processing (5)
  • blog/actions/importSpirePost.ts
  • blog/actions/resolveSpireGate.ts
  • blog/mod.ts
  • spire/sections/SpireDashboard.tsx
  • spire/sections/SpirePendingApprovals.tsx

Comment thread blog/actions/webhook.ts Outdated
Comment thread blog/actions/webhook.ts Outdated
Comment thread blog/actions/resolveSpireGate.ts Outdated
Comment thread blog/mod.ts Outdated
Comment thread spire/sections/SpirePendingApprovals.tsx Outdated
Comment thread spire/sections/SpirePendingApprovals.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

10 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="spire/sections/SpirePendingApprovals.tsx">

<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>

<file name="blog/mod.ts">

<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>

<violation number="2" location="blog/mod.ts:94">
P1: Unbounded watcher restart loop will spam logs and churn CPU if `Deno.watchFs` fails permanently (missing permission, unsupported runtime, etc.). The catch block unconditionally retries after 5 seconds with no max retry count or backoff.</violation>
</file>

<file name="blog/actions/importSpirePost.ts">

<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>

<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread spire/sections/SpirePendingApprovals.tsx Outdated
Comment thread blog/actions/resolveSpireGate.ts Outdated
Comment thread spire/sections/SpirePendingApprovals.tsx Outdated
Comment thread blog/mod.ts Outdated
Comment thread blog/mod.ts Outdated
Comment thread blog/mod.ts Outdated
Comment thread blog/actions/webhook.ts Outdated
Comment thread spire/sections/SpirePendingApprovals.tsx Outdated
Comment thread blog/mod.ts Outdated
Comment thread blog/actions/webhook.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="blog/actions/resolveSpireGate.ts">

<violation number="1" location="blog/actions/resolveSpireGate.ts:24">
P1: Authorization relies on spoofable Origin/Referer headers instead of authenticated user context, enabling possible privilege bypass.</violation>
</file>

<file name="spire/sections/SpirePendingApprovals.tsx">

<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>

<file name="blog/mod.ts">

<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>
</file>

<file name="blog/actions/importSpirePost.ts">

<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>

<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread blog/actions/resolveSpireGate.ts Outdated
- blog/webhook.ts: HMAC auth via canonical payload reconstruction,
  Bearer fallback, tenant isolation (allowedBlogSlug), path-traversal guard
- blog/utils/spireImport.ts: shared importSpirePost + compileBlocksToHtml
  (full block type support: card-group, stat, comparison, etc.)
- blog/actions/syncAllPosts.ts: admin-only bulk sync with pagination
- blog/sections/SpireSync.tsx: sync dashboard section (registered in manifest)
- blog/components/SpireSyncPreviewTab.tsx: interactive preview tab shown in
  Deco Studio app settings (shows connection status + Sync All Posts button)
- blog/mod.ts: preview() now receives state and renders SpireSyncPreviewTab;
  activeSyncs Set exported; Deno.watchFs guard for serverless environments
- spire/types.ts: SpirePost.tags → SpirePostTag[], added SpireAuthorFull,
  SpireTagWithCount; full block type union
- spire/utils/client.ts: added authors, tags, authors/:slug, tags/:slug endpoints
- spire/loaders: BlogPostPage tags→categories; new GetAuthors, GetTags,
  BlogsByAuthor, BlogsByTag loaders
- spire/sections/SpirePendingApprovals.tsx: fixed route /live/invoke/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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 (3)
blog/actions/webhook.ts (1)

79-82: 💤 Low value

Consider timing-safe comparison for bearer token verification.

The direct === comparison for the bearer token (line 81) may be vulnerable to timing attacks. While modern JS engines often optimize string comparisons, using a constant-time comparison is a security best practice for secret verification.

Proposed fix using crypto.subtle.timingSafeEqual
+function timingSafeEqual(a: string, b: string): boolean {
+  const encoder = new TextEncoder();
+  const aBytes = encoder.encode(a);
+  const bBytes = encoder.encode(b);
+  if (aBytes.length !== bBytes.length) return false;
+  return crypto.subtle.timingSafeEqual(aBytes, bBytes);
+}
+
 ...
     if (!isAuthorized) {
       const token = authHeader?.replace("Bearer ", "") || querySecret;
-      isAuthorized = !!token && token === expectedSecret;
+      isAuthorized = !!token && timingSafeEqual(token, expectedSecret);
     }
🤖 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 `@blog/actions/webhook.ts` around lines 79 - 82, Replace the direct ===
comparison for bearer token verification with a timing-safe comparison: import
Node's crypto, build Buffers from token and expectedSecret (or empty Buffers if
missing), check lengths first and only call crypto.timingSafeEqual when lengths
match, and set isAuthorized to the boolean result of that check (falling back to
false for missing values); update the conditional around
isAuthorized/authHeader/querySecret to use this timing-safe routine (refer to
the symbols isAuthorized, authHeader, querySecret, expectedSecret, and token).
blog/components/SpireSyncPreviewTab.tsx (1)

245-254: 💤 Low value

Consider escaping server response values before innerHTML assignment.

While data.message and error strings originate from the controlled syncAllPosts action, the errors array may include content from external API responses (e.g., Spire error messages). Using textContent or escaping HTML entities before innerHTML assignment provides defense-in-depth against potential XSS if upstream responses change.

Example text-escaping helper
function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, function(m) {
    return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&`#39`;' }[m];
  });
}
// Then use: escapeHtml(data.message)
🤖 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 `@blog/components/SpireSyncPreviewTab.tsx` around lines 245 - 254, Replace
unsafe innerHTML assignments in SpireSyncPreviewTab (where resultEl is updated)
with safe text assignments: use resultEl.textContent (or apply an escapeHtml
helper) when setting messages for success ('✓ ' + ...), failure ('✗ ' + ...),
and the catch block ('Error: ' + ...). Ensure you convert data.message and err
to strings before assigning (e.g., String(data.message) / String(err)) and use
the same symbols/prefixes but never insert unescaped HTML into innerHTML.
blog/sections/SpireSync.tsx (1)

185-196: 💤 Low value

Same text-escaping consideration as SpireSyncPreviewTab.

The error details are rendered in a <pre> tag via innerHTML. If any error message contains HTML characters, they'll be interpreted as markup. Escaping would be a minor hardening measure.

🤖 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 `@blog/sections/SpireSync.tsx` around lines 185 - 196, The code in
SpireSync.tsx builds HTML into resultEl.innerHTML using data.message and
data.errors (and mirrors the same concern as SpireSyncPreviewTab); to fix, stop
injecting raw strings and either escape HTML entities in data.message and each
entry of data.errors (replace &, <, >, ", ' ) before concatenation or, better,
create DOM nodes/text nodes programmatically (createElement for <span>,
<details>, <summary>, <pre> and append createTextNode(error) for each error) so
error text is never interpreted as markup; update the code paths that set
resultEl.innerHTML (both success and error branches and the catch block) to use
the sanitized text or DOM-node approach referencing resultEl, data.message and
data.errors.
🤖 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 `@blog/utils/spireImport.ts`:
- Around line 265-279: In the "steps" case of the switch in spireImport.ts (the
block using type Step, safeJsonParse, escapeHtml and sanitizeHtml), move the
heading out of the ordered list so the <h3> is not a direct child of <ol>;
render the title (escapeHtml(content.title)) above the <ol> when content.title
exists and then render the <ol class="steps"> containing only <li> elements
created from steps.map(...). Ensure the HTML string concatenation in the "steps"
case produces "<h3>...</h3>" (when present) followed by "<ol
class=\"steps\">...li items...</ol>" rather than embedding the <h3> inside the
<ol>.
- Around line 252-263: The checklist branch currently injects the checklist
title inside the <ul>, producing invalid HTML; update the "checklist" case (in
blog/utils/spireImport.ts) to render the title outside the list: if
content.title exists output the escaped title (use escapeHtml) before opening
the <ul class="checklist">, then render the items as <li> entries (using
sanitizeHtml for each item) inside the <ul>, joining them as before; ensure the
overall return concatenates title + ul rather than placing the <h3> inside the
ul.

---

Nitpick comments:
In `@blog/actions/webhook.ts`:
- Around line 79-82: Replace the direct === comparison for bearer token
verification with a timing-safe comparison: import Node's crypto, build Buffers
from token and expectedSecret (or empty Buffers if missing), check lengths first
and only call crypto.timingSafeEqual when lengths match, and set isAuthorized to
the boolean result of that check (falling back to false for missing values);
update the conditional around isAuthorized/authHeader/querySecret to use this
timing-safe routine (refer to the symbols isAuthorized, authHeader, querySecret,
expectedSecret, and token).

In `@blog/components/SpireSyncPreviewTab.tsx`:
- Around line 245-254: Replace unsafe innerHTML assignments in
SpireSyncPreviewTab (where resultEl is updated) with safe text assignments: use
resultEl.textContent (or apply an escapeHtml helper) when setting messages for
success ('✓ ' + ...), failure ('✗ ' + ...), and the catch block ('Error: ' +
...). Ensure you convert data.message and err to strings before assigning (e.g.,
String(data.message) / String(err)) and use the same symbols/prefixes but never
insert unescaped HTML into innerHTML.

In `@blog/sections/SpireSync.tsx`:
- Around line 185-196: The code in SpireSync.tsx builds HTML into
resultEl.innerHTML using data.message and data.errors (and mirrors the same
concern as SpireSyncPreviewTab); to fix, stop injecting raw strings and either
escape HTML entities in data.message and each entry of data.errors (replace &,
<, >, ", ' ) before concatenation or, better, create DOM nodes/text nodes
programmatically (createElement for <span>, <details>, <summary>, <pre> and
append createTextNode(error) for each error) so error text is never interpreted
as markup; update the code paths that set resultEl.innerHTML (both success and
error branches and the catch block) to use the sanitized text or DOM-node
approach referencing resultEl, data.message and data.errors.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad158b97-f66a-425b-a6b5-eb1f58755699

📥 Commits

Reviewing files that changed from the base of the PR and between 210abec and 33603e2.

📒 Files selected for processing (17)
  • blog/actions/resolveSpireGate.ts
  • blog/actions/syncAllPosts.ts
  • blog/actions/webhook.ts
  • blog/components/SpireSyncPreviewTab.tsx
  • blog/manifest.gen.ts
  • blog/mod.ts
  • blog/sections/SpireSync.tsx
  • blog/utils/spireImport.ts
  • spire/loaders/BlogPostPage.ts
  • spire/loaders/BlogsByAuthor.ts
  • spire/loaders/BlogsByTag.ts
  • spire/loaders/GetAuthors.ts
  • spire/loaders/GetTags.ts
  • spire/manifest.gen.ts
  • spire/sections/SpirePendingApprovals.tsx
  • spire/types.ts
  • spire/utils/client.ts
✅ Files skipped from review due to trivial changes (2)
  • blog/manifest.gen.ts
  • spire/manifest.gen.ts

Comment thread blog/actions/syncAllPosts.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

10 issues found across 17 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="blog/actions/resolveSpireGate.ts">

<violation number="1" location="blog/actions/resolveSpireGate.ts:24">
P1: Authorization relies on spoofable Origin/Referer headers instead of authenticated user context, enabling possible privilege bypass.</violation>
</file>

<file name="spire/sections/SpirePendingApprovals.tsx">

<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>

<file name="blog/mod.ts">

<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>
</file>

<file name="blog/actions/importSpirePost.ts">

<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>

<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>

<file name="blog/sections/SpireSync.tsx">

<violation number="1" location="blog/sections/SpireSync.tsx:185">
P1: DOM XSS via unescaped innerHTML: server response fields (data.message, data.errors) are injected into innerHTML without escaping, creating a XSS vector in the admin sync panel.</violation>
</file>

<file name="blog/utils/spireImport.ts">

<violation number="1" location="blog/utils/spireImport.ts:92">
P1: Result-contract violation: `response.json()`, `Deno.mkdir`, and `Deno.writeTextFile` can throw unhandled exceptions instead of resolving to the documented `{ success: false, message }` structured failure.</violation>

<violation number="2" location="blog/utils/spireImport.ts:256">
P2: Invalid HTML: `<h3>` is rendered as a direct child of `<ul>`, which only allows `<li>` children. Move the title element before the `<ul>` to produce valid markup.</violation>

<violation number="3" location="blog/utils/spireImport.ts:270">
P2: Invalid HTML: `<h3>` is rendered as a direct child of `<ol>`, which only allows `<li>` children. Move the title element before the `<ol>` to produce valid markup.</violation>

<violation number="4" location="blog/utils/spireImport.ts:320">
P1: Runtime crash risk in `comparison` block renderer: `safeJsonParse` is called without a validator for `left`/`right`, and `(s.items ?? [])` does not protect against malformed non-array `items` values (e.g., a string or object). If Spire sends malformed JSON, calling `.map()` on a non-array throws `TypeError` and aborts the entire HTML compilation/post import.</violation>
</file>

<file name="spire/loaders/BlogPostPage.ts">

<violation number="1" location="spire/loaders/BlogPostPage.ts:81">
P1: Potential runtime crash if Spire API response omits `tags` field</violation>
</file>

<file name="spire/loaders/BlogsByTag.ts">

<violation number="1" location="spire/loaders/BlogsByTag.ts:26">
P2: Cache key uses raw account/slug concatenation, which can cause ambiguous collisions and high-cardinality cache growth.</violation>
</file>

<file name="blog/actions/webhook.ts">

<violation number="1" location="blog/actions/webhook.ts:102">
P2: `activeSyncs` loop-prevention flag is keyed only by `postSlug` without a `blogSlug` qualifier, which can cause cross-blog collisions and incorrect sync suppression when `allowedBlogSlug` is unset or misconfigured.</violation>
</file>

<file name="blog/actions/syncAllPosts.ts">

<violation number="1" location="blog/actions/syncAllPosts.ts:94">
P2: HTTP listing failures are not counted as failures, causing aborted sync runs to report success</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread blog/sections/SpireSync.tsx Outdated
Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated
Comment thread spire/loaders/BlogPostPage.ts Outdated
Comment thread blog/actions/syncAllPosts.ts Outdated
Comment thread spire/loaders/BlogsByTag.ts Outdated
Comment thread blog/actions/webhook.ts Outdated
Comment thread blog/actions/syncAllPosts.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated
Replace isAdmin() origin check with ctx.spireWebhookSecret validation so
the Sync All Posts button in the preview tab works when a webhook secret
is configured — the preview iframe runs under the site's own origin, not
an admin domain. Passes the resolved secret through SpireSyncPreviewTab
props into the inline fetch as Authorization: Bearer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="blog/actions/resolveSpireGate.ts">

<violation number="1" location="blog/actions/resolveSpireGate.ts:24">
P1: Authorization relies on spoofable Origin/Referer headers instead of authenticated user context, enabling possible privilege bypass.</violation>
</file>

<file name="spire/sections/SpirePendingApprovals.tsx">

<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>

<file name="blog/mod.ts">

<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>
</file>

<file name="blog/actions/importSpirePost.ts">

<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>

<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>

<file name="spire/loaders/BlogPostPage.ts">

<violation number="1" location="spire/loaders/BlogPostPage.ts:81">
P1: Potential runtime crash if Spire API response omits `tags` field</violation>
</file>

<file name="blog/actions/webhook.ts">

<violation number="1" location="blog/actions/webhook.ts:102">
P2: `activeSyncs` loop-prevention flag is keyed only by `postSlug` without a `blogSlug` qualifier, which can cause cross-blog collisions and incorrect sync suppression when `allowedBlogSlug` is unset or misconfigured.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread blog/components/SpireSyncPreviewTab.tsx Outdated
yuriassuncx and others added 2 commits May 27, 2026 19:02
- Remove webhook secret from client HTML; use X-Requested-With header
  for preview-tab auth (non-simple CORS header blocks cross-origin forgery)
- Escape innerHTML injections in SpireSync and SpireSyncPreviewTab (XSS)
- Move checklist/steps <h3> title outside <ul>/<ol> (invalid HTML spec)
- Guard comparison block renderSide against non-array items (crash fix)
- Wrap importSpirePost response.json/mkdir/writeTextFile in try/catch
- Count HTTP listing failures in syncAllPosts (was silently returning success)
- Add missing-fields guard in webhook action (catches empty test calls)
- Fix BlogsByTag cache key collision (use :: separator with encodeURIComponent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ield

Aligns spireImport with how Deco Admin stores CMS blocks so synced posts,
categories, and authors appear correctly in the Studio collections browser:

- Use URL-encoded flat filename convention (.deco/blocks/collections%2Fblog%2F...)
  instead of nested subdirectories — matches files created by Deco Admin
- Add mandatory "name" field (decoded collection path) to every written block
- Upsert separate Category blocks (insert-only, never overwrite admin edits)
- Upsert separate Author blocks (insert-only, slugified name as identifier)
- Migrate: remove legacy subdirectory-format file after writing new one
- webhook removePostBlock now uses postBlockPath() from spireImport

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

5 issues found across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread blog/actions/syncAllPosts.ts Outdated
Comment thread blog/components/SpireSyncPreviewTab.tsx Outdated
Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/sections/SpireSync.tsx Outdated
Comment thread blog/utils/spireImport.ts Outdated
yuriassuncx and others added 2 commits May 27, 2026 19:47
- date: slice ISO timestamp to YYYY-MM-DD to satisfy @Format date JSON Schema
  validation (was causing 'Invalid Date format' and 'must match anyOf' errors)
- spireWarning: change from string to boolean — shows as a checkbox in the
  admin instead of a confusing optional text field; set to true on sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move paginated Spire sync loop into shared syncBlogPosts() in spireImport.ts,
  used by both syncAllPosts action and the new background auto-sync paths
- Add SyncResult interface to spireImport.ts (removed duplicate from syncAllPosts)
- On cold start, trigger a background sync 5s after App() initialises to catch any
  posts that arrived before the webhook was configured (fires once per process)
- Register Deno.cron("spire-auto-sync", every 6h) when Deno Deploy cron is available
  as a periodic reconciliation safety-net for missed webhooks
- All background syncs are fire-and-forget; they never block the server

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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

🤖 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 `@blog/utils/spireImport.ts`:
- Around line 78-86: The diacritic-stripping regex in function toId is using
literal combining characters (`/[̀-ͯ]/g`) which is error-prone; replace it with
the Unicode escape range for the Combining Diacritical Marks block
(`\u0300-\u036f`) so after .normalize("NFD") you call .replace(/\u0300-\u036f/g,
"") (use a proper regex with the escape range like /\u0300-\u036f/g) to reliably
remove accents, keeping the rest of the pipeline (.toLowerCase(),
.replace(/[^a-z0-9]+/g, "-"), .replace(/^-+|-+$/g, "")) unchanged in the toId
function.
- Around line 306-309: The JSON parse of listResponse in
blog/utils/spireImport.ts (the const { posts, pagination } = await
listResponse.json() ...) can throw and crash the sync loop; wrap the await
listResponse.json() call in a try/catch, mirror the individual post import error
handling by catching JSON/parse errors, log the error with context (include
listResponse.status/url), and then gracefully return or set posts = [] and
pagination = { totalPages: 0 } (or abort this sync iteration without throwing)
so the sync loop continues instead of terminating.
- Around line 58-61: The empty catch around Deno.stat(filePath) is too broad and
hides permission/other errors; change it to catch the thrown error as e and only
treat a Deno.errors.NotFound (or e?.name === "NotFound") as the "file not
present" case (so continue and create the file), but rethrow or surface any
other errors (e.g., permission denied) instead of swallowing them; update the
block that currently references Deno.stat and filePath to explicitly inspect the
caught error and conditionally handle NotFound vs rethrow.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a310506-1f65-4558-b583-c788c6263af4

📥 Commits

Reviewing files that changed from the base of the PR and between 33603e2 and cfc8b3b.

📒 Files selected for processing (8)
  • blog/actions/syncAllPosts.ts
  • blog/actions/webhook.ts
  • blog/components/SpireSyncPreviewTab.tsx
  • blog/mod.ts
  • blog/sections/SpireSync.tsx
  • blog/types.ts
  • blog/utils/spireImport.ts
  • spire/loaders/BlogsByTag.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • blog/types.ts
  • spire/loaders/BlogsByTag.ts
  • blog/actions/webhook.ts
  • blog/mod.ts
  • blog/components/SpireSyncPreviewTab.tsx

Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

4 issues found across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/utils/spireImport.ts Outdated
Comment thread blog/mod.ts Outdated
Comment thread blog/mod.ts Outdated
New architecture: blog loaders transparently merge native Deco posts with
live Spire API posts when allowedBlogSlug is configured. No sync machinery,
no file writes, no GitHub commits — just real-time API integration with
60-second CDN caching.

blog app:
- Remove block-sync machinery (syncAllPosts, SpirePost loader, github.ts,
  spireImport write utilities, startup/cron sync, file watcher, activeSyncs)
- Simplify mod.ts State (githubRepo/githubToken removed); derive typed
  spireApi client in App() when allowedBlogSlug is set — single config field
- Update all four loaders to merge native .deco/blocks with Spire API posts:
  BlogpostList, BlogpostListing, BlogPostPage, BlogPostItem
- Fix category filtering: use GET /tags/:slug so Spire posts appear on
  category pages with category metadata injected for handlePosts filtering
- Add cache { maxAge: 60 } + cacheKey to all loaders (prevents per-request
  API calls; key includes allowedBlogSlug + all filter params)
- Simplify webhook action to auth-only (HMAC-SHA256 + Bearer + tenant guard
  + slug path-traversal prevention); no storage side-effects
- Remove spireWarning field; keep spirePostId @hide/@readonly in types
- Simplify SpireSync section and SpireSyncPreviewTab to status display only
- Regenerate manifest via deno task bundle

spire app:
- Add BlogPostItem.ts loader (single post without full page SEO metadata)
- Reduce cache TTL 24h → 60s across all loaders for near-real-time freshness
- Add apiKey?: Secret to Props for private blog support
- Remove SpireDashboard.tsx, SpirePendingApprovals.tsx (unused)

Security: HMAC-SHA256 preserved, tenant isolation, slug path-traversal guard,
sanitizeHtml/sanitizeHref on all compiled block HTML.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
yuriassuncx and others added 4 commits June 2, 2026 20:08
Declares the shared HMAC secret as @hide true / @readonly in Props
so Deco Studio preserves the field on edits without showing it.
Written by admin's bulkSync during integration setup; validated
by handleWebhook for local HMAC authentication without a round-trip
to autonomous-blog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread blog/types.ts Outdated
Comment thread blog/actions/submitRating.ts Outdated
Comment thread blog/mod.ts Outdated
yuriassuncx and others added 10 commits June 3, 2026 19:28
…etadata now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s util

- SpireBlogPost was defined but never used anywhere in the blog app.
- utils/blocksToSections.ts was defined but never imported — dead code.
- spirePostId stays in BlogPost (used by Blogpost.ts + BlogPostItem.ts loaders).
- spireBlogSlug stays in mod.ts State (config field for the blog app).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keeps only what's necessary per Tavano's review:
- NEW: ProductCard, ProductHighlight, ProductShelf block sections
- RENAME: Cta.tsx → CallToAction.tsx
- blog/mod.ts: add spireBlogSlug + fix logo (weather→blog) + better description
- blog/types.ts: add spirePostId to BlogPost + typo fixes + remove SpireBlogPost
- blog/utils/blocksToSections.ts: deleted (orphaned code)
- blog/manifest.gen.ts: regenerated
- fmt: reformat airtable+slack HTML templates (pre-commit requirement)

Reverted to main: loaders, actions, core, utils/dataURI.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blog/mod.ts: revert logo back to weather URL (as in main)
- deno.json: exclude **/ui-templates/ from fmt/lint/check
  so those HTML files never show as changed vs main
- airtable/slack templates: reset to exact main content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moving from global exclude to fmt.exclude prevents the bundle script
from potentially skipping files during manifest generation, which was
causing uncommitted changes to appear after the Bundle Apps CI step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move multi-platform product resolution into blog/utils, cache platform
detection per request, and wire ProductCard/Highlight/Shelf to dynamic
options with inline Image usage. Restore Cta block and add shared
product display helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@yuriassuncx yuriassuncx changed the title feat: add Spire integration for Blog CMS feat(blog): shoppable product blocks with multi-platform dynamic picker Jul 2, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

7 issues found across 25 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread blog/sections/blocks/ProductCard.tsx
Comment thread blog/sections/blocks/ProductShelf.tsx
Comment thread blog/utils/productResolver.ts Outdated
Restore records guards, BlogpostList cache/cacheKey, legacy product block
fallbacks, site-scoped platform detection, and BlogpostListing JSDoc.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="blog/loaders/BlogpostList.ts">

<violation number="1" location="blog/loaders/BlogpostList.ts:66">
P1: The new `cacheKey` omits `AppContext` (`_ctx`) entirely, but the loader result depends on it through `getRecordsByPath(ctx, ...)`. If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from `ctx` in the cache key, or confirming that the framework automatically scopes loader caches.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

/**
* @description Overrides the query term at url
*/
query?: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The new cacheKey omits AppContext (_ctx) entirely, but the loader result depends on it through getRecordsByPath(ctx, ...). If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from ctx in the cache key, or confirming that the framework automatically scopes loader caches.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At blog/loaders/BlogpostList.ts, line 66:

<comment>The new `cacheKey` omits `AppContext` (`_ctx`) entirely, but the loader result depends on it through `getRecordsByPath(ctx, ...)`. If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from `ctx` in the cache key, or confirming that the framework automatically scopes loader caches.</comment>

<file context>
@@ -48,6 +48,25 @@ export interface Props {
+    props.sortBy ?? url.searchParams.get("sortBy") ?? "date_desc",
+  );
+  const query = String(props.query ?? url.searchParams.get("q") ?? "");
+  const slugs = JSON.stringify(props.postSlugs ?? []);
+  return `blog-list-p${page}-c${count}-s${slug}-${sort}-q${query}-${slugs}`;
+};
</file context>

Comment thread blog/utils/legacyProductBlock.ts Outdated
Comment thread blog/loaders/BlogpostList.ts Outdated
Add ProductCard, ProductHighlight, and ProductShelf with dynamic product
picker and multi-platform resolution (VTEX, Shopify, Wake, WAP).

Co-authored-by: Cursor <cursoragent@cursor.com>
@yuriassuncx yuriassuncx changed the title feat(blog): shoppable product blocks with multi-platform dynamic picker feat(blog): add shoppable product block sections Jul 3, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="blog/loaders/BlogpostList.ts">

<violation number="1" location="blog/loaders/BlogpostList.ts:66">
P1: The new `cacheKey` omits `AppContext` (`_ctx`) entirely, but the loader result depends on it through `getRecordsByPath(ctx, ...)`. If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from `ctx` in the cache key, or confirming that the framework automatically scopes loader caches.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread blog/sections/blocks/ProductShelf.tsx Outdated
yuriassuncx and others added 2 commits July 3, 2026 11:05
Remove unused blocksToSections util and short-circuit ProductShelf
loader before platform detection when products list is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
Accept string refs, loader-ref shapes, and pre-resolved Products so
Spire/admin posts and Studio-edited blocks both render without format loss.

Co-authored-by: Cursor <cursoragent@cursor.com>
Skip platform probes when the commerce app is not installed and require
VTEX suggestions to return products so Shopify sites are not misdetected.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="blog/utils/productResolver.ts">

<violation number="1" location="blog/utils/productResolver.ts:161">
P1: `probePlatform` now requires non-empty product results from VTEX, Wake, and WAP (`(res?.products?.length ?? 0) > 0`), and an array result from Shopify (`Array.isArray(res)`), to consider a platform "installed." This creates false negatives for correctly configured integrations with empty catalogs or where probe queries return no matches, and the indefinite `platformBySite` cache in `getPlatform` then persists the misclassification for the process lifetime.

A safer approach is to keep the original contract — if `hasInvokePlatform` passes and the loader invocation succeeds without throwing, the platform is available. Platform presence should not depend on whether the hardcoded probe query `"a"` happens to return products.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

query: "a",
count: 1,
});
return (res?.products?.length ?? 0) > 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: probePlatform now requires non-empty product results from VTEX, Wake, and WAP ((res?.products?.length ?? 0) > 0), and an array result from Shopify (Array.isArray(res)), to consider a platform "installed." This creates false negatives for correctly configured integrations with empty catalogs or where probe queries return no matches, and the indefinite platformBySite cache in getPlatform then persists the misclassification for the process lifetime.

A safer approach is to keep the original contract — if hasInvokePlatform passes and the loader invocation succeeds without throwing, the platform is available. Platform presence should not depend on whether the hardcoded probe query "a" happens to return products.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At blog/utils/productResolver.ts, line 161:

<comment>`probePlatform` now requires non-empty product results from VTEX, Wake, and WAP (`(res?.products?.length ?? 0) > 0`), and an array result from Shopify (`Array.isArray(res)`), to consider a platform "installed." This creates false negatives for correctly configured integrations with empty catalogs or where probe queries return no matches, and the indefinite `platformBySite` cache in `getPlatform` then persists the misclassification for the process lifetime.

A safer approach is to keep the original contract — if `hasInvokePlatform` passes and the loader invocation succeeds without throwing, the platform is available. Platform presence should not depend on whether the hardcoded probe query `"a"` happens to return products.</comment>

<file context>
@@ -141,26 +151,34 @@ async function probePlatform(
         count: 1,
       });
-      return true;
+      return (res?.products?.length ?? 0) > 0;
     }
     if (platform === "shopify") {
</file context>

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