Summary
Let the repo owner choose which URL paths the widget is active on. Today the script is all-or-nothing: if it is in the layout, every page gets the FAB — including /admin, checkout, preview deploys, and client “please don’t show this to users yet” routes.
Path is already captured (pagePath on WidgetFeedback, sent to the agent). It is not gated. Pro marketing even lists “Page path context tracking” (lib/plan-features.ts) — that is context-for-the-agent, not an allowlist. This issue is the allowlist/blocklist.
Current behavior
- Parent embed always mounts the iframe (
buildParentEmbedScript in lib/widget-embed.ts) and sends pathname to the iframe.
POST /f stores pagePath (max 2048, forced to start with /) in app/f/route.ts.
- Authorize is domain-only (
RepositoryConfig.authorizedDomains, lib/widget-resolve.ts / lib/widget-origin.ts). No path check.
- Configure UI (
configure/page.tsx) has domains, embed snippet, email, agent instructions — no paths.
pagePath is client-supplied. Anyone who can load the widget can POST an arbitrary path. Domain allowlist is the real security boundary; path rules are a product control (where the FAB appears + where we accept submissions).
Problem
Agencies embed one snippet in a shared layout (Next.js app/layout.tsx, WordPress footer, etc.). They still need:
- Widget on marketing pages, not on
/wp-admin, /checkout, /app/*
- Staging vs production is already handled by domains; paths handle the rest
- “Launch the widget only on
/blog this week” without a code deploy
If we only hide the FAB in CSS but still accept POST /f from those paths, a curious user (or scraped widget id) can still submit. Hide in the parent script and reject on the server.
Proposed design
Config (per RepositoryConfig)
widgetPathMode: all | allowlist | denylist // default all (today’s behavior)
widgetPathRules: string[] // path prefixes, e.g. /blog, /pricing
Matching rules (keep them dumb so owners cannot shoot themselves):
- Compare against
location.pathname only (ignore query/hash).
- Prefix match:
/blog matches /blog and /blog/post, not /blogging.
- Normalize: ensure leading
/, strip trailing / except /.
- Max ~20 rules, each max 256 chars.
- Empty allowlist in
allowlist mode = widget never shows (fail closed) + configure warning.
UI on configure, next to Authorized Domains:
- Default: “All pages on authorized domains”
- “Only these paths…” / “Everywhere except…”
- Textarea or tag input, one prefix per line, with live examples.
Runtime
Parent script (best UX, no iframe flash):
The parent currently has no config besides widgetId. Options:
- Inline config in the snippet — bad, requires re-copying the snippet on every path change.
- Tiny bootstrap: parent script already knows
apiOrigin. Before mounting iframe, GET /h or /api/widget/hello with w + parentOrigin + pathname, cache in sessionStorage for a few minutes.
- Hello already exists (
app/api/widget/hello/route.ts / lib/widget-hello.ts) but only returns { ok, message, widgetId }. Extend hello (or the existing /h route) to return { mount: boolean, theme, … }.
Recommendation: (3) — one authenticated-by-widget-id call from the parent. If hello fails, fail closed (do not mount) when we know the owner configured restrictions; if hello 5xx, current behavior (mount) is OK for mode=all to avoid a total outage.
app/h/route.ts is already a short widget endpoint — check it when implementing.
Server POST /f: if path is not allowed, 403 with a generic message (do not leak the rule list).
Caching
/widget/{wid} is max-age=300. Path rules must not be baked into that JS, or owners wait 5 minutes after saving. Put rules behind hello with Cache-Control: private, max-age=60 or no-store.
Assumptions
- This is not a substitute for authorized domains. Paths are evaluated only after origin checks.
- Free tier included. Hiding the widget on
/admin is hygiene, not a Pro upsell. (We can still upsell “Page path context tracking” as agent context — already implemented.)
- Query strings do not participate (
/pricing?ref=ad counts as /pricing).
- Glob stars (
/app/**) are v2; prefixes are enough.
- Localhost page URLs are already stripped from storage (
isLocalDevPageUrl); path rules still apply on localhost if they authorized localhost.
Acceptance criteria
Out of scope
- Per-path different widgets / different repos.
- Time-based schedules (“only weekdays”).
- Matching on hash routers (
/#/settings) — document that SPA hash routes need the host to pass a logical path later if demanded.
Implementation notes
prisma/schema.prisma RepositoryConfig
app/dashboard/[owner]/[repo]/configure/page.tsx + new fields component (mirror AuthorizedDomainsFields)
lib/widget-embed.ts parent bootstrap
lib/widget-hello.ts / app/h/route.ts / app/api/widget/hello/route.ts — pick one config endpoint and use it
app/f/route.ts shared matcher helper isPathAllowed(pathname, mode, rules)
See also
- Widget theme (same hello/config channel)
- Element picker (must not start pick mode if widget is not mounted)
- Use-case landings (agencies care about this in sales copy)
Summary
Let the repo owner choose which URL paths the widget is active on. Today the script is all-or-nothing: if it is in the layout, every page gets the FAB — including
/admin, checkout, preview deploys, and client “please don’t show this to users yet” routes.Path is already captured (
pagePathonWidgetFeedback, sent to the agent). It is not gated. Pro marketing even lists “Page path context tracking” (lib/plan-features.ts) — that is context-for-the-agent, not an allowlist. This issue is the allowlist/blocklist.Current behavior
buildParentEmbedScriptinlib/widget-embed.ts) and sendspathnameto the iframe.POST /fstorespagePath(max 2048, forced to start with/) inapp/f/route.ts.RepositoryConfig.authorizedDomains,lib/widget-resolve.ts/lib/widget-origin.ts). No path check.configure/page.tsx) has domains, embed snippet, email, agent instructions — no paths.pagePathis client-supplied. Anyone who can load the widget can POST an arbitrary path. Domain allowlist is the real security boundary; path rules are a product control (where the FAB appears + where we accept submissions).Problem
Agencies embed one snippet in a shared layout (Next.js
app/layout.tsx, WordPress footer, etc.). They still need:/wp-admin,/checkout,/app/*/blogthis week” without a code deployIf we only hide the FAB in CSS but still accept
POST /ffrom those paths, a curious user (or scraped widget id) can still submit. Hide in the parent script and reject on the server.Proposed design
Config (per
RepositoryConfig)Matching rules (keep them dumb so owners cannot shoot themselves):
location.pathnameonly (ignore query/hash)./blogmatches/blogand/blog/post, not/blogging./, strip trailing/except/.allowlistmode = widget never shows (fail closed) + configure warning.UI on configure, next to Authorized Domains:
Runtime
Parent script (best UX, no iframe flash):
The parent currently has no config besides
widgetId. Options:apiOrigin. Before mounting iframe,GET /hor/api/widget/hellowithw+parentOrigin+pathname, cache insessionStoragefor a few minutes.app/api/widget/hello/route.ts/lib/widget-hello.ts) but only returns{ ok, message, widgetId }. Extend hello (or the existing/hroute) to return{ mount: boolean, theme, … }.Recommendation: (3) — one authenticated-by-widget-id call from the parent. If hello fails, fail closed (do not mount) when we know the owner configured restrictions; if hello 5xx, current behavior (mount) is OK for
mode=allto avoid a total outage.app/h/route.tsis already a short widget endpoint — check it when implementing.Server
POST /f: if path is not allowed,403with a generic message (do not leak the rule list).Caching
/widget/{wid}ismax-age=300. Path rules must not be baked into that JS, or owners wait 5 minutes after saving. Put rules behind hello withCache-Control: private, max-age=60or no-store.Assumptions
/adminis hygiene, not a Pro upsell. (We can still upsell “Page path context tracking” as agent context — already implemented.)/pricing?ref=adcounts as/pricing)./app/**) are v2; prefixes are enough.isLocalDevPageUrl); path rules still apply on localhost if they authorizedlocalhost.Acceptance criteria
POST /fwith a disallowedpagePathis rejected even if someone calls the API directly.Out of scope
/#/settings) — document that SPA hash routes need the host to pass a logical path later if demanded.Implementation notes
prisma/schema.prismaRepositoryConfigapp/dashboard/[owner]/[repo]/configure/page.tsx+ new fields component (mirrorAuthorizedDomainsFields)lib/widget-embed.tsparent bootstraplib/widget-hello.ts/app/h/route.ts/app/api/widget/hello/route.ts— pick one config endpoint and use itapp/f/route.tsshared matcher helperisPathAllowed(pathname, mode, rules)See also