Skip to content

dev tool indicator#1272

Open
mantrakp04 wants to merge 3 commits intodevfrom
feat/dev-tool
Open

dev tool indicator#1272
mantrakp04 wants to merge 3 commits intodevfrom
feat/dev-tool

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented Mar 20, 2026

  • Updated package versions for '@supabase/*' libraries to 2.99.2 and '@supabase/ssr' to 0.9.0.
  • Added new devDependencies for 'rimraf' and 'framer-motion' in the pnpm-lock file.
  • Modified Next.js configuration to conditionally omit 'X-Frame-Options' in development mode for better integration with Stack Auth dev tools.
  • Refactored component exports in the template package to include tracking for dev tools.
  • Introduced new dev tool components and context for improved logging and state management.
  • Added styles for the dev tool indicator and panel, ensuring a consistent dark theme.
  • Implemented fetch interception to log API calls and user authentication events in the dev tool.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive developer tools with interactive panels including Overview, Components, Console, AI Chat, Documentation, and Dashboard tabs.
    • Added AI chat integration within the developer tools for assistance and queries.
    • Improved team switcher to handle null team values gracefully.
  • Documentation

    • Added FAQ entries covering common troubleshooting scenarios and configuration guidance.
  • Chores

    • Added AI SDK and related runtime dependencies.
    • Updated response header handling for development environments.

- Updated package versions for '@supabase/*' libraries to 2.99.2 and '@supabase/ssr' to 0.9.0.
- Added new devDependencies for 'rimraf' and 'framer-motion' in the pnpm-lock file.
- Modified Next.js configuration to conditionally omit 'X-Frame-Options' in development mode for better integration with Stack Auth dev tools.
- Refactored component exports in the template package to include tracking for dev tools.
- Introduced new dev tool components and context for improved logging and state management.
- Added styles for the dev tool indicator and panel, ensuring a consistent dark theme.
- Implemented fetch interception to log API calls and user authentication events in the dev tool.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Mar 27, 2026 9:37pm
stack-backend Ready Ready Preview, Comment Mar 27, 2026 9:37pm
stack-dashboard Ready Ready Preview, Comment Mar 27, 2026 9:37pm
stack-demo Ready Ready Preview, Comment Mar 27, 2026 9:37pm
stack-docs Ready Ready Preview, Comment Mar 27, 2026 9:37pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive development tool for Stack Auth with multiple tabs for logging, component previewing, and AI assistance. It adds AI SDK dependencies across packages, improves null-safety in team switcher components, and adjusts backend/frontend configuration to support development workflows.

Changes

Cohort / File(s) Summary
Backend AI Query Route
apps/backend/src/app/api/latest/ai/query/[mode]/route.ts
Destructures body to remove projectId before forwarding to production, changing the forwardToProduction call signature from passing the full body to passing mode and a filtered forwardBody object.
Frontend Configuration
apps/dashboard/next.config.mjs
Adds conditional X-Frame-Options: SAMEORIGIN response header logic based on NODE_ENV, omitting the header in development to allow iframe embedding while preserving it in production.
Documentation
claude/CLAUDE-KNOWLEDGE.md
Expands FAQ with five new entries covering ESM bundle failures with createRequire, Stack dev tool Config tab API URL resolution, translation provider context errors, component catalog behavior, and Next.js resolution errors with tsdown.
Component Examples
docs/src/components/stack-auth/stack-team-switcher.tsx, examples/docs-examples/src/app/team/[teamId]/page.tsx
Updates SelectedTeamSwitcher urlMap prop to handle null team values, either returning fallback route ('/') or throwing explicit errors instead of assuming non-null teams.
Package Dependencies
packages/react/package.json, packages/stack/package.json, packages/template/package.json, packages/template/package-template.json
Adds @ai-sdk/react (^3.0.72) and ai (^6.0.0) as runtime dependencies; also adds react-dom and @types/react-dom as peer dependencies in react package with optional type definitions.
Dev Tool Entry Point
packages/template/src/dev-tool/index.tsx, packages/template/src/dev-tool/dev-tool-trigger.tsx, packages/template/src/dev-tool/dev-tool-indicator.tsx
Implements dev tool visibility gate restricted to localhost (overrideable via localStorage), renders a trigger button and indicator that intercepts API calls (logging method, URL, status, duration), tracks auth events, and manages component registration.
Dev Tool Core Infrastructure
packages/template/src/dev-tool/dev-tool-context.tsx, packages/template/src/dev-tool/dev-tool-panel.tsx, packages/template/src/dev-tool/dev-tool-tab-bar.tsx
Establishes TypeScript-typed context for dev tool state (active tab, panel sizing, support prefill), provides resizable panel UI with animated tab bar and content layering, and exports state management hooks and utilities for URL resolution and key masking.
Dev Tool Styling
packages/template/src/dev-tool/dev-tool-styles.ts
Defines comprehensive 2415-line CSS stylesheet scoped under .stack-devtool with theme-aware variables, animations (panel entry/exit, tab transitions, AI thinking pulses), and detailed styling for Overview, Components, Console, Support, and AI Chat tabs.
Dev Tool Tab Components
packages/template/src/dev-tool/tabs/overview-tab.tsx, packages/template/src/dev-tool/tabs/components-tab.tsx, packages/template/src/dev-tool/tabs/ai-tab.tsx, packages/template/src/dev-tool/tabs/console-tab.tsx, packages/template/src/dev-tool/tabs/support-tab.tsx, packages/template/src/dev-tool/tabs/dashboard-tab.tsx, packages/template/src/dev-tool/tabs/docs-tab.tsx
Implements seven tab views: Overview (auth status, changelog, quick sign-in), Components (catalog browser with live preview), AI Chat (streaming endpoint integration with markdown rendering), Console (merged API/event logs plus config table with export), Support (feedback form and feature request iframe), Dashboard (localhost-only embedded dashboard), and Docs (external documentation iframe).
Dev Tool Utilities & Helpers
packages/template/src/dev-tool/component-catalog.tsx, packages/template/src/dev-tool/hooks/use-component-registry.tsx, packages/template/src/dev-tool/hooks/use-dev-tool-state.tsx, packages/template/src/dev-tool/iframe-tab.tsx
Exports component catalog metadata structure (CatalogEntry, COMPONENT_CATALOG, CATALOG_NAMES), preview provider context for avoiding duplicate registry entries, hook re-export convenience layer, and reusable iframe tab component with loading/error states and retry logic.
Provider Integration & Re-exports
packages/template/src/providers/stack-provider-client.tsx, packages/template/src/index.ts, packages/template/src/react-dom.d.ts, packages/template/src/components-page/sign-in.tsx
Wraps dev tool and translation provider in StackProviderClient for client-only rendering, removes redundant export aliases, adds TypeScript createPortal type declaration for react-dom, and marks sign-in component as client-side with 'use client' directive.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Whiskers twitch with dev tools here,
Logs and previews bright and clear,
AI chat flows in real-time streams,
Localhost dreams come alive it seems!
A rabbit's gift for builders true, 🚀
Stack Auth now guides them through and through!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'dev tool indicator' refers to a real component added in this PR but does not capture the full scope of changes, which include a comprehensive dev tool system with multiple tabs, context management, styling, and API integration.
Description check ✅ Passed The description covers main changes including dev tool components, fetch interception, and Next.js config updates, but is somewhat high-level and does not fully align with the extensive file-by-file changes in the PR summary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dev-tool

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 and usage tips.

},

async headers() {
const isDev = process.env.NODE_ENV === "development";
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.

note that isDev is actually false in the local emulator (instead, the local emulator envvar is set). use isLocalEmulatorEnabled instead

Suggested change
const isDev = process.env.NODE_ENV === "development";
const isLocalEmulator = isLocalEmulatorEnabled();

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.

to test this, run pnpm dev with NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

tnx, dont be reviewin tis rn, its all ai slop atm

Copy link
Copy Markdown
Contributor

@N2D4 N2D4 left a comment

Choose a reason for hiding this comment

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

i like the indicator, some comments on how to implement it

Comment on lines +116 to 124
// In development, omit X-Frame-Options so the Stack Auth dev tool
// indicator can embed the dashboard in an iframe.
...(!isDev ? [{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
}] : []),
{
key: "Content-Security-Policy",
value: "",
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.

we can be more precise about this one and allow only localhost URLs for max security (to prevent clickjacking the local emulator from untrusted URLs)

Suggested change
// In development, omit X-Frame-Options so the Stack Auth dev tool
// indicator can embed the dashboard in an iframe.
...(!isDev ? [{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
}] : []),
{
key: "Content-Security-Policy",
value: "",
{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
{
key: "Content-Security-Policy",
value: isLocalEmulator ? "frame-ancestors 'self' http://localhost:* https://localhost:* http://127.0.0.1:* https://127.0.0.1:* http://[::1]:* https://[::1]:* http://*.localhost https://*.localhost" : "",

Comment on lines +16 to +21
urlMap={(t) => {
if (t == null) {
throw new Error("SelectedTeamSwitcher urlMap expected a non-null team");
}
return `/team/${t.id}`;
}}
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.

accident?

Comment on lines +86 to +93
"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0",
"react": ">=18.3.0"
},
"peerDependenciesMeta": {
"@types/react-dom": {
"optional": true
},
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.

we shouldn't rely on react DOM in the react pkg if we can, eg. react native doesn't have it

Comment on lines +1 to +29
"use client";

import React, { lazy, Suspense } from "react";

// IF_PLATFORM react-like

const DevToolIndicatorLazy = lazy(() =>
import("./dev-tool-indicator").then((mod) => ({ default: mod.DevToolIndicator }))
);

/**
* Dev Tool Indicator entry point.
* - Only renders in development mode (process.env.NODE_ENV === 'development')
* - Uses React.lazy + Suspense for zero production bundle impact
* - Renders as a floating overlay pill in bottom-right corner
*/
export function DevToolEntry() {
if (process.env.NODE_ENV !== 'development') {
return null;
}

return (
<Suspense fallback={null}>
<DevToolIndicatorLazy />
</Suspense>
);
}

// END_PLATFORM
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.

ohh, i see — we're only doing the dev indicator in React currently

i think we should support the dev indicator for any JS sdk. whenever @stackframe/js (or another SDK) is loaded, and we determine local development (maybe just by looking at URL and comparing for localhost, possibly same allowlist as the CSP from next.config.js) it should also load and show the devtool indicator. this means we'll have to use vanilla JS without a framework to do the devtool itself, which is probably fine though as AI is quite good at that (we can use web-components actually).

Comment on lines +30 to +35
{/* IF_PLATFORM react-like */}
{/* Same translation context as StackProvider-wrapped UI; dev tool previews render Stack components that use useTranslation(). */}
<TranslationProvider lang={undefined}>
<DevToolEntry />
</TranslationProvider>
{/* END_PLATFORM */}
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.

i don't think we need to translate the devtool

name: string,
Component: React.ComponentType<P>,
): (props: P) => React.ReactElement | null {
if (process.env.NODE_ENV !== 'development') {
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.

note: we cannot use process.env in the SDKs (eg. Vite does not support it)

Comment on lines +31 to +48
export const EmailVerification = withDevToolTracking("EmailVerification", _EmailVerification);
export const ForgotPassword = withDevToolTracking("ForgotPassword", _ForgotPassword);
export const PasswordReset = withDevToolTracking("PasswordReset", _PasswordReset);
export const SignIn = withDevToolTracking("SignIn", _SignIn);
export const SignUp = withDevToolTracking("SignUp", _SignUp);
export const CredentialSignIn = withDevToolTracking("CredentialSignIn", _CredentialSignIn);
export const CredentialSignUp = withDevToolTracking("CredentialSignUp", _CredentialSignUp);
export { UserAvatar } from "./components/elements/user-avatar";
export { MagicLinkSignIn as MagicLinkSignIn } from "./components/magic-link-sign-in";
export const MagicLinkSignIn = withDevToolTracking("MagicLinkSignIn", _MagicLinkSignIn);
export { MessageCard } from "./components/message-cards/message-card";
export { OAuthButton } from "./components/oauth-button";
export { OAuthButtonGroup } from "./components/oauth-button-group";
export { SelectedTeamSwitcher } from "./components/selected-team-switcher";
export { TeamSwitcher } from "./components/team-switcher";
export { UserButton } from "./components/user-button";
export const OAuthButton = withDevToolTracking("OAuthButton", _OAuthButton);
export const OAuthButtonGroup = withDevToolTracking("OAuthButtonGroup", _OAuthButtonGroup);
export const SelectedTeamSwitcher = withDevToolTracking("SelectedTeamSwitcher", _SelectedTeamSwitcher);
export const TeamSwitcher = withDevToolTracking("TeamSwitcher", _TeamSwitcher);
export const UserButton = withDevToolTracking("UserButton", _UserButton);
export {
registerDevToolComponentCatalog,
} from "./dev-tool/hooks/use-component-registry";
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.

i have some thoughts on how we can implement this — let's chat about this in person. for now, just revert this file, and we can alr merge this PR and then do that as a follow-up PR

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.

as it stands, it would fail to track if eg. the component is used on a different page than the current one, also it's a bit odd that importing the component from a different path will yield a different component with diff behavior (even if identical in prod)

Comment on lines 123 to 140
"peerDependencies": {
"@types/react": ">=18.3.0",
"//": "IF_PLATFORM next",
"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0",
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
"//": "END_PLATFORM",
"react": ">=18.3.0"
"@types/react": ">=18.3.0"
,"//": "IF_PLATFORM react-like"
,"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0"
,"//": "END_PLATFORM",
,"//": "IF_PLATFORM next",
,"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0"
,"//": "END_PLATFORM",
,"react": ">=18.3.0"
},
"//": "END_PLATFORM",
"//": "IF_PLATFORM react-like",
"peerDependenciesMeta": {
"//": "IF_PLATFORM next",
"//": "IF_PLATFORM react-like",
"@types/react-dom": {
"optional": true
},
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.

with the discussed changes we can revert all of this i believe

Comment on lines +1 to +9
declare module "react-dom" {
import type { ReactNode, ReactPortal } from "react";

export function createPortal(
children: ReactNode,
container: Element | DocumentFragment,
key?: null | string,
): ReactPortal;
}
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.

shouldn't be necessary anymore with the changes from the other comments

- Updated package versions for '@supabase/*' libraries to 2.100.0 and added new dependencies for '@ai-sdk/react' and 'ai'.
- Refactored API route to forward request body correctly in the backend.
- Improved team switcher component to handle null team cases in URL mapping.
- Introduced new AI tab in the dev tool for enhanced user interaction and logging.
- Updated dev tool styles and components for better theme integration and user experience.
- Added support for new features in the dev tool, including export functionality and improved state management.
@mantrakp04
Copy link
Copy Markdown
Collaborator Author

@greptile-ai review

@mantrakp04 mantrakp04 marked this pull request as ready for review March 27, 2026 21:31
Copilot AI review requested due to automatic review settings March 27, 2026 21:31
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR introduces a Stack Auth Dev Tool — a floating overlay panel that is automatically injected into any app using StackProviderClient and is visible by default only on localhost. The panel includes tabs for an overview (quick sign-in, project info), a component inspector, an AI assistant, docs/dashboard/support iframes, and a console that captures fetch-intercepted API calls and auth events.

Key changes:

  • packages/template/src/dev-tool/ — ~1 800 lines of new React components, context, hooks, and CSS-in-JS styles that make up the dev tool UI.
  • packages/template/src/providers/stack-provider-client.tsxDevToolEntry is now automatically rendered inside every StackContext.Provider.
  • apps/dashboard/next.config.mjsX-Frame-Options: SAMEORIGIN is conditionally omitted in NODE_ENV=development so the dashboard can be embedded in the dev tool iframe.
  • apps/backend/…/ai/query/[mode]/route.tsprojectId is now stripped from the body before forwarding to the production AI endpoint, preventing an unexpected field error.
  • packages/template/src/index.ts — minor cleanup of redundant re-export aliases.

Issues found:

  • sanitizeForPreview in components-tab.tsx returns the raw object when a circular reference is detected, unlike its sibling sanitizeForPrompt which returns \"[Circular]\". This can throw TypeError: Converting circular structure to JSON downstream.
  • The AI tab's useAIHeaders hook initialises the headers useRef as {} and populates it in an effect; a very fast first message could be dispatched before the effect runs, omitting required auth headers.
  • The fetch interceptor's sign-out detection uses init?.method === 'DELETE' (case-sensitive) instead of the already-normalised method variable, potentially missing lowercase method strings.
  • The array-format header case in the fetch interceptor uses a strict string equality check (key === 'X-Stack-Project-Id') rather than case-insensitive comparison.
  • iframe-tab.tsx combines allow-scripts and allow-same-origin in the sandbox attribute, which defeats iframe sandboxing; acceptable for the current trusted first-party URLs but worth a comment.

Confidence Score: 4/5

Safe to merge after addressing the circular-reference and AI-headers timing issues; all other findings are minor.

All findings are P2 or lower — no confirmed production data loss or security exploits. However, the sanitizeForPreview circular-reference bug is a real correctness issue that will throw on JSON.stringify, and the AI headers timing issue can produce auth-failure errors on first use. Both are straightforward one-line fixes and should be addressed before shipping.

packages/template/src/dev-tool/tabs/components-tab.tsx (circular-reference handling), packages/template/src/dev-tool/tabs/ai-tab.tsx (headers init), and packages/template/src/dev-tool/dev-tool-indicator.tsx (fetch interceptor case-sensitivity).

Important Files Changed

Filename Overview
packages/template/src/dev-tool/dev-tool-context.tsx New file: global log store, DevToolProvider context, and URL derivation helpers. Minor heuristic fragility in deriveDashboardBaseUrl port mapping.
packages/template/src/dev-tool/dev-tool-indicator.tsx New file: fetch interceptor and auth-event tracker. Case-sensitive header check in array branch and raw init?.method sign-out detection may silently miss some events.
packages/template/src/dev-tool/tabs/components-tab.tsx New file: component inspector tab. sanitizeForPreview returns circular-reference objects as-is (unlike sanitizeForPrompt) which can throw on downstream JSON.stringify calls.
packages/template/src/dev-tool/tabs/ai-tab.tsx New file: AI chat tab using DefaultChatTransport. Auth headers ref initialized empty and populated in effect, creating a window where the first message could be sent without credentials.
packages/template/src/dev-tool/index.tsx New file: DevToolEntry entry point with localStorage override, useSyncExternalStore visibility tracking, and lazy-loaded indicator. Clean localhost-only default with console toggle support.
apps/dashboard/next.config.mjs X-Frame-Options SAMEORIGIN header is conditionally omitted only in development to allow the dev tool to iframe the dashboard. No production impact.
packages/template/src/dev-tool/iframe-tab.tsx New file: shared iframe wrapper with load/error states. sandbox="allow-scripts allow-same-origin" combination defeats iframe sandboxing; acceptable for trusted first-party URLs but worth documenting.
packages/template/src/providers/stack-provider-client.tsx DevToolEntry is now rendered inside StackProviderClient wrapped in TranslationProvider, making the dev tool available automatically in all Stack-powered apps.
apps/backend/src/app/api/latest/ai/query/[mode]/route.ts Strips projectId from the forwarded body when forwarding to production; fixes a potential extra-field issue on the production endpoint.

Sequence Diagram

sequenceDiagram
    participant App as User App
    participant SPC as StackProviderClient
    participant DTE as DevToolEntry
    participant DTP as DevToolProvider
    participant FI as Fetch Interceptor
    participant GLS as GlobalLogStore
    participant Panel as DevToolPanel / Tabs

    App->>SPC: render children
    SPC->>DTE: render (inside TranslationProvider)
    DTE->>DTE: useSyncExternalStore — localhost check
    DTE-->>Panel: lazy-load DevToolIndicator (only on localhost)
    Panel->>DTP: wrap with DevToolProvider + context
    DTP->>GLS: subscribe log listener

    Note over FI: Mounted once inside DevToolIndicator
    FI->>FI: patch window.fetch

    App->>FI: any fetch() call
    FI->>FI: check X-Stack-Project-Id header
    alt Stack Auth call
        FI->>GLS: addApiLog(entry)
        GLS->>Panel: notify → re-render ConsoleTab
    else other call
        FI->>App: pass through to originalFetch
    end

    Panel->>Panel: AI tab → POST /api/latest/ai/query/stream
    Panel->>Panel: Dashboard/Docs/Support tabs → IframeTab
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Reviews (1): Last reviewed commit: "Merge branch 'dev' into feat/dev-tool" | Re-trigger Greptile

Comment on lines +72 to +78
}
if (seen.has(value)) {
return value;
}
seen.add(value);
const result: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
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.

P2 sanitizeForPreview returns circular reference as-is

When a circular reference is detected, sanitizeForPreview returns the original object rather than a safe placeholder. Compare this to sanitizeForPrompt (line ~98) which correctly returns "[Circular]". If the output of sanitizeForPreview is ever passed to JSON.stringify (e.g. when building a prompt from a live-previewed component) it will throw TypeError: Converting circular structure to JSON, silently crashing the prompt generation flow.

Suggested change
}
if (seen.has(value)) {
return value;
}
seen.add(value);
const result: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
if (seen.has(value)) {
return "[Circular]";
}

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +59 to +67
// Show path only for cleaner logs
displayUrl = u.pathname + (u.search || '');
} catch {
// keep full url
}

const startTime = Date.now();

try {
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.

P2 Case-sensitive header key check for array-format headers

The Array.isArray(headers) branch compares header keys with strict equality (key === 'X-Stack-Project-Id'). HTTP headers are case-insensitive, so if any layer passes the header in lowercase (x-stack-project-id) the interceptor will miss it and treat the request as a non-Stack call, silently skipping the log entry.

Suggested change
// Show path only for cleaner logs
displayUrl = u.pathname + (u.search || '');
} catch {
// keep full url
}
const startTime = Date.now();
try {
} else if (Array.isArray(headers)) {
isStackCall = headers.some(([key]) => key.toLowerCase() === 'x-stack-project-id');

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +110 to +117
} catch (err) {
const duration = Date.now() - startTime;
addApiLogRef.current({
id: nextId(),
timestamp: startTime,
method: method.toUpperCase(),
url: displayUrl,
duration,
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.

P2 Sign-out detection reads raw init?.method instead of normalised method variable

The earlier declaration let method = init?.method || 'GET' normalises the method to uppercase. However, the sign-out check on this line reads init?.method === 'DELETE' directly, which is a case-sensitive comparison. If the SDK ever issues a 'delete' (lowercase) request, the sign-out event will silently go unlogged.

Use the already-normalised variable for consistency:

Suggested change
} catch (err) {
const duration = Date.now() - startTime;
addApiLogRef.current({
id: nextId(),
timestamp: startTime,
method: method.toUpperCase(),
url: displayUrl,
duration,
if (displayUrl.includes('/auth/sessions') && method === 'DELETE' && response.ok) {

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +232 to +248
h["X-Stack-Publishable-Client-Key"] = opts.publishableClientKey;
}
headers.current = h;
}, [app]);

return headers;
}

// ---------------------------------------------------------------------------
// Suggested questions
// ---------------------------------------------------------------------------

const SUGGESTED_QUESTIONS = [
{ icon: "\u{1F512}", text: "How do I protect a Next.js route?" },
{ icon: "\u{1F465}", text: "How do teams and permissions work?" },
{ icon: "\u{1F517}", text: "How do I add OAuth providers?" },
{ icon: "\u2709\uFE0F", text: "How do I customize auth emails?" },
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.

P2 AI request headers initialised as empty {} — first message could be sent without auth headers

The headers ref starts as {} and is populated only inside a useEffect (which fires after the first render commit). In React 18 concurrent mode, if a user clicks "Send" before the effect has had a chance to run, headersRef.current will be {}, so the first AI request will be missing X-Stack-Project-Id and X-Stack-Publishable-Client-Key, causing an auth failure.

Initialize the ref eagerly instead:

const headers = useRef<Record<string, string>>({
  "X-Stack-Access-Type": "client",
  "X-Stack-Project-Id": app.projectId,
});

Then keep the useEffect only to pick up the publishable client key (or merge it in on the first call).

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +218 to +240
return (
<DevToolContext.Provider value={value}>
{children}
</DevToolContext.Provider>
);
}

export function useDevToolContext() {
const context = useContext(DevToolContext);
if (!context) {
throw new Error('useDevToolContext must be used within a DevToolProvider');
}
return context;
}

/**
* Derives the dashboard base URL from the resolved Stack Auth API base URL.
*
* Mapping:
* - Production API `https://api.stack-auth.com` → `https://app.stack-auth.com`
* - Local dev API `http://localhost:8102` → `http://localhost:8101` (port XX02 → XX01)
* - Self-hosted `https://api.myapp.com` → `https://app.myapp.com`
*/
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.

P2 deriveDashboardBaseUrl port heuristic may mismap uncommon self-hosted ports

The expression port.endsWith('02') maps any port ending in 02 to the corresponding 01 port (e.g. 8102 → 8101, 1002 → 1001, 802 → 801). This works correctly for the default Stack Auth dev-server pair, but for a self-hosted setup that uses, for example, port 3002 for an unrelated service, the dev tool would silently redirect to 3001 rather than surfacing a helpful "could not determine dashboard URL" message.

Consider scoping the heuristic more tightly to the known local dev pair, or exposing a dashboardBaseUrl option that operators can set explicitly.

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +74 to +75
src={src}
title={title}
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.

P2 allow-scripts + allow-same-origin together defeats the iframe sandbox

The HTML spec explicitly warns that combining these two tokens lets the framed content access parent.location and read/write the embedding document. For the current trusted sources (dashboard, docs, support) this is acceptable, but if the src prop ever receives an untrusted URL the protection is gone with no defensive layer.

Consider adding a comment explaining why both tokens are intentionally required, and/or validating the src against an allowlist before rendering the frame.

Fix in Claude Code Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a Stack Auth “dev tool indicator” overlay (panel + tabs) to the template/SDK packages to improve local development workflows, including embedded dashboard/docs, component previews, logging/export, support submission, and an AI helper. It also updates dependencies and adjusts the dashboard’s dev headers to support iframe embedding.

Changes:

  • Introduces a new dev-tool UI (indicator + panel + tabs), wiring it into StackProviderClient and adding styling/context/logging.
  • Adds AI SDK dependencies (@ai-sdk/react, ai) across packages/template, packages/react, and packages/stack.
  • Adjusts dashboard security headers in development to allow iframe embedding (omits X-Frame-Options in dev).

Reviewed changes

Copilot reviewed 30 out of 32 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pnpm-lock.yaml Updates lockfile to reflect new deps (AI SDK, rolldown optional dep, etc.).
packages/template/src/react-dom.d.ts Adds minimal react-dom typing for createPortal.
packages/template/src/providers/stack-provider-client.tsx Renders DevToolEntry under the same translation context as Stack UI.
packages/template/src/index.ts Simplifies/reformats exports for some components.
packages/template/src/dev-tool/tabs/support-tab.tsx Adds Support tab (feedback form + feature requests iframe).
packages/template/src/dev-tool/tabs/overview-tab.tsx Adds Overview tab (identity/actions/config/changelog).
packages/template/src/dev-tool/tabs/docs-tab.tsx Adds Docs iframe tab.
packages/template/src/dev-tool/tabs/dashboard-tab.tsx Adds Dashboard iframe tab with localhost-only embedding guard.
packages/template/src/dev-tool/tabs/console-tab.tsx Adds Console tab (API/auth logs, export/share dialog, config view).
packages/template/src/dev-tool/tabs/components-tab.tsx Adds Components tab (catalog + live previews + page iframes).
packages/template/src/dev-tool/tabs/ai-tab.tsx Adds AI tab (chat UI + lightweight markdown renderer).
packages/template/src/dev-tool/index.tsx Adds dev tool entrypoint, visibility logic, and global override commands.
packages/template/src/dev-tool/iframe-tab.tsx Shared iframe wrapper with loading/error UX and sandboxing.
packages/template/src/dev-tool/hooks/use-dev-tool-state.tsx Convenience re-export for useDevToolContext.
packages/template/src/dev-tool/hooks/use-component-registry.tsx Adds preview context for component registry behavior.
packages/template/src/dev-tool/dev-tool-trigger.tsx Adds floating “DEV” trigger pill.
packages/template/src/dev-tool/dev-tool-tab-bar.tsx Adds reusable animated tab bar (bar + pill variants).
packages/template/src/dev-tool/dev-tool-styles.ts Adds full CSS theme for dev tool indicator/panel/tabs.
packages/template/src/dev-tool/dev-tool-panel.tsx Adds panel container (tabs, resizing, iframe reload, export dialog overlay).
packages/template/src/dev-tool/dev-tool-indicator.tsx Adds portal-mounted indicator, fetch interception, auth event tracking, keyboard shortcut.
packages/template/src/dev-tool/dev-tool-context.tsx Adds dev tool context + persisted state + global log store + URL helpers.
packages/template/src/dev-tool/component-catalog.tsx Defines the built-in component catalog used by the Components tab.
packages/template/src/components-page/sign-in.tsx Marks SignIn page as a client component.
packages/template/package.json Adds AI SDK dependencies to the template package.
packages/template/package-template.json Adds AI deps and adjusts peerDependencies macros for generated packages.
packages/stack/package.json Adds AI SDK dependencies to @stackframe/stack.
packages/react/package.json Adds AI SDK deps + adds react-dom to peer deps (and optional typing meta).
examples/docs-examples/src/app/team/[teamId]/page.tsx Updates SelectedTeamSwitcher urlMap typing/behavior.
docs/src/components/stack-auth/stack-team-switcher.tsx Adjusts docs example to handle `team
claude/CLAUDE-KNOWLEDGE.md Adds internal notes about dev tool-related pitfalls/architecture.
apps/dashboard/next.config.mjs Omits X-Frame-Options in dev to allow iframe embedding.
apps/backend/src/app/api/latest/ai/query/[mode]/route.ts Strips projectId when forwarding AI requests to production.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +42
const prefillApplied = useRef(false);

// Apply prefill when it changes (e.g. navigating from share dialog)
useEffect(() => {
if (prefill && !prefillApplied.current) {
setFeedbackType(prefill.feedbackType);
setMessage(prefill.message);
setStatus("idle");
prefillApplied.current = true;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

prefillApplied prevents applying prefill after the first time the component sees a prefill. If the user opens the export dialog multiple times (setting a new supportPrefill), the new prefill will be ignored and the textarea/type won't update. Consider removing prefillApplied or resetting it when prefill changes (e.g., track the last prefill value and re-apply when it differs).

Suggested change
const prefillApplied = useRef(false);
// Apply prefill when it changes (e.g. navigating from share dialog)
useEffect(() => {
if (prefill && !prefillApplied.current) {
setFeedbackType(prefill.feedbackType);
setMessage(prefill.message);
setStatus("idle");
prefillApplied.current = true;
// Apply prefill when it changes (e.g. navigating from share dialog)
useEffect(() => {
if (prefill) {
setFeedbackType(prefill.feedbackType ?? "feedback");
setMessage(prefill.message ?? "");
setStatus("idle");

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +187
const setState: React.Dispatch<React.SetStateAction<DevToolState>> = useCallback((action) => {
setStateRaw((prev) => {
const next = typeof action === 'function' ? action(prev) : action;
saveState(next);
return next;
});
}, []);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

saveState(next) persists the entire dev-tool state to localStorage on every update. This currently includes ephemeral and potentially large fields like supportPrefill (which can contain exported logs/config) and showExportDialog. Persisting those can bloat localStorage, slow updates, and leak debug data across sessions. Consider persisting only durable UI prefs (e.g., panel size + last active tab) and explicitly omitting transient fields before writing.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +199

// Wire up fetch interceptor and auth event tracking
useFetchInterceptor(addApiLog, addEventLog);
useAuthEventTracker(addEventLog);

useEffect(() => {
// Create a portal container attached to document.body
const container = document.createElement('div');
container.id = '__stack-dev-tool-root';
document.body.appendChild(container);
setPortalContainer(container);

return () => {
document.body.removeChild(container);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The portal container is always created with a fixed id and appended to document.body without checking for an existing element. If DevToolIndicator mounts more than once (multi-root apps, HMR edge cases), this can create duplicate roots and the cleanup may remove the wrong node. Consider reusing an existing #__stack-dev-tool-root if present, and only removing it on unmount if this instance created it.

Suggested change
// Wire up fetch interceptor and auth event tracking
useFetchInterceptor(addApiLog, addEventLog);
useAuthEventTracker(addEventLog);
useEffect(() => {
// Create a portal container attached to document.body
const container = document.createElement('div');
container.id = '__stack-dev-tool-root';
document.body.appendChild(container);
setPortalContainer(container);
return () => {
document.body.removeChild(container);
const createdPortalRef = useRef(false);
// Wire up fetch interceptor and auth event tracking
useFetchInterceptor(addApiLog, addEventLog);
useAuthEventTracker(addEventLog);
useEffect(() => {
// Reuse an existing portal container if present, otherwise create one
let container = document.getElementById('__stack-dev-tool-root') as HTMLElement | null;
if (!container) {
container = document.createElement('div');
container.id = '__stack-dev-tool-root';
document.body.appendChild(container);
createdPortalRef.current = true;
}
setPortalContainer(container);
return () => {
if (createdPortalRef.current && container && container.parentNode === document.body) {
document.body.removeChild(container);
}

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +45
const headers = init?.headers;
let isStackCall = false;
let method = init?.method || 'GET';

if (headers) {
if (headers instanceof Headers) {
isStackCall = headers.has('X-Stack-Project-Id');
} else if (Array.isArray(headers)) {
isStackCall = headers.some(([key]) => key === 'X-Stack-Project-Id');
} else {
isStackCall = 'X-Stack-Project-Id' in headers;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

useFetchInterceptor only checks init.headers (and does case-sensitive key matching for array/object forms). fetch callers can supply headers via a Request object (or different casing), which will bypass logging even though the request includes X-Stack-Project-Id. Consider normalizing header names case-insensitively and also inspecting input when it's a Request to decide whether to log.

Suggested change
const headers = init?.headers;
let isStackCall = false;
let method = init?.method || 'GET';
if (headers) {
if (headers instanceof Headers) {
isStackCall = headers.has('X-Stack-Project-Id');
} else if (Array.isArray(headers)) {
isStackCall = headers.some(([key]) => key === 'X-Stack-Project-Id');
} else {
isStackCall = 'X-Stack-Project-Id' in headers;
const STACK_HEADER_NAME = 'x-stack-project-id';
const hasStackProjectHeader = (headersLike: HeadersInit | undefined | null): boolean => {
if (!headersLike) return false;
if (headersLike instanceof Headers) {
// Headers.has is case-insensitive by spec
return headersLike.has(STACK_HEADER_NAME);
}
if (Array.isArray(headersLike)) {
return headersLike.some(([key]) => key.toLowerCase() === STACK_HEADER_NAME);
}
// Treat as a dictionary-like object
return Object.keys(headersLike as Record<string, unknown>).some(
(key) => key.toLowerCase() === STACK_HEADER_NAME,
);
};
let isStackCall = false;
// Prefer explicit method from init, then from Request, default to GET
let method = init?.method;
if (!method && input instanceof Request) {
method = input.method;
}
method = method || 'GET';
// Check headers from init first
if (init?.headers && hasStackProjectHeader(init.headers)) {
isStackCall = true;
}
// If not found yet, and input is a Request, also inspect its headers
if (!isStackCall && input instanceof Request) {
if (hasStackProjectHeader(input.headers)) {
isStackCall = true;

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +21
urlMap={(t) => {
if (t == null) {
throw new Error("SelectedTeamSwitcher urlMap expected a non-null team");
}
return `/team/${t.id}`;
}}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

urlMap now throws when t is null. The docs example and the updated docs component both allow team | null in urlMap, so this throw can crash the page if selectedTeam is unset. Prefer returning a safe fallback (or make the function accept only non-null by construction) instead of throwing at runtime.

Suggested change
urlMap={(t) => {
if (t == null) {
throw new Error("SelectedTeamSwitcher urlMap expected a non-null team");
}
return `/team/${t.id}`;
}}
urlMap={(t) => (t ? `/team/${t.id}` : "/team")}

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +75
// Expose console commands: StackDevTool.enable() / StackDevTool.disable() / StackDevTool.reset()
if (typeof window !== 'undefined') {
(window as any).StackDevTool = {
enable() {
localStorage.setItem(OVERRIDE_KEY, 'true');
notify();
console.log('[Stack DevTool] Enabled. Refresh if the panel does not appear.');
},
disable() {
localStorage.setItem(OVERRIDE_KEY, 'false');
notify();
console.log('[Stack DevTool] Disabled.');
},
reset() {
localStorage.removeItem(OVERRIDE_KEY);
notify();
console.log('[Stack DevTool] Reset to default (visible on localhost only).');
},
};
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

DevToolEntry exposes window.StackDevTool.enable/disable/reset() unconditionally (and the comment says it works in production). This makes it possible for any end-user on a production site to enable the dev tool UI (including AI + logging + support submission), which has security/cost/abuse implications. Consider gating the global and the override mechanism behind NODE_ENV === "development" (or an explicit opt-in flag) so it cannot be enabled in production builds.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +51
// Link
parts.push(
<a key={match.index} className="sdt-ai-link" href={match[7]} target="_blank" rel="noopener noreferrer">
{match[6]}
</a>
);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The markdown link renderer uses the raw model-provided URL as an <a href> without protocol validation. Since content comes from an AI response, this can allow javascript: (or other unsafe) URLs and becomes an XSS vector on click. Please sanitize/validate href (e.g., allow only http/https/mailto, otherwise render as plain text).

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +16
/**
* Same Web3Forms endpoint & public access key used by the dashboard's
* FeedbackForm in the Stack Companion — keeps the submission logic DRY.
*/
const WEB3FORMS_ENDPOINT = "https://api.web3forms.com/submit";
const WEB3FORMS_ACCESS_KEY = "4f0fc468-c066-4e45-95c1-546fd652a44a";
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The Web3Forms access key is hard-coded in a client-side SDK file. Even if it's a "public" key, shipping it broadly enables third parties to automate spam submissions against that endpoint and pollute your support inbox. Consider moving submission behind a Stack backend endpoint with rate limiting / abuse protection (and keep any keys server-side), or otherwise add bot mitigation.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (10)
packages/template/src/dev-tool/iframe-tab.tsx (1)

73-80: onError is unreliable for detecting iframe load failures.

Many iframe loading failures (CSP blocks, X-Frame-Options denials, network issues) don't fire onError. The iframe may display blank content without triggering the error state. Consider adding a timeout fallback or documenting this limitation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/iframe-tab.tsx` around lines 73 - 80, The
iframe's onError is unreliable; add a timeout fallback that starts whenever src
changes (store an iframe ref like iframeRef) and clears on handleLoad and
handleError; if the timeout elapses (e.g., 5–10s) check the iframeRef.current
(try accessing contentDocument/contentWindow safely) and if it's empty or
cross-origin-inaccessible treat it as a load failure and set the same error
state used by handleError/loading, and also document this limitation in comments
next to the iframe, referencing the iframe element, handleLoad, handleError,
loading, src and title so future readers know why the timer exists.
examples/docs-examples/src/app/team/[teamId]/page.tsx (1)

16-21: Use encodeURIComponent() for URL path segment.

The defensive null check is appropriate given the underlying type-safety gap in TeamSwitcher. However, the URL construction should use proper encoding for consistency with coding guidelines.

♻️ Suggested fix
         urlMap={(t) => {
           if (t == null) {
             throw new Error("SelectedTeamSwitcher urlMap expected a non-null team");
           }
-          return `/team/${t.id}`;
+          return `/team/${encodeURIComponent(t.id)}`;
         }}

As per coding guidelines: "Use urlString`` or encodeURIComponent()` instead of normal string interpolation for URLs for consistency."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/docs-examples/src/app/team/`[teamId]/page.tsx around lines 16 - 21,
The urlMap function constructs a raw path using string interpolation; update url
creation in the urlMap arrow function to encode the team id (use
encodeURIComponent(t.id) or the project's urlString helper) while keeping the
existing null-check that throws if t is null—change the return for urlMap to
produce an encoded path like /team/<encoded-id> so ids with unsafe characters
are properly escaped.
docs/src/components/stack-auth/stack-team-switcher.tsx (1)

235-235: Code example doesn't match actual implementation.

The generateCodeExample() at line 125 shows (team: { id: string }) => ... without null handling, but the actual implementation here accepts { id: string } | null. Users copying the code example won't get the defensive null check.

Additionally, consider using encodeURIComponent(team.id) for URL consistency.

♻️ Suggested fixes
  1. Update the code example generator (around line 125) to match:
     if (props.urlMap) {
-      propsArray.push('urlMap={(team: { id: string }) => `/teams/${team.id}/dashboard`}');
+      propsArray.push('urlMap={(team: { id: string } | null) => team ? `/teams/${team.id}/dashboard` : `/`}');
     }
  1. Encode the URL path:
-                urlMap={props.urlMap ? (team: { id: string } | null) => team ? `/teams/${team.id}/dashboard` : '/' : undefined}
+                urlMap={props.urlMap ? (team: { id: string } | null) => team ? `/teams/${encodeURIComponent(team.id)}/dashboard` : '/' : undefined}

As per coding guidelines: "Use urlString`` or encodeURIComponent()` instead of normal string interpolation for URLs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/src/components/stack-auth/stack-team-switcher.tsx` at line 235, The code
example produced by generateCodeExample() must match the actual urlMap prop
signature used in stack-team-switcher.tsx: update generateCodeExample() to show
the parameter as (team: { id: string } | null) => team ?
`/teams/${encodeURIComponent(team.id)}/dashboard` : '/' (or equivalent using
urlString/encodeURIComponent) so the example includes the null defensive check
and encodes team.id; search for generateCodeExample and the urlMap prop to apply
the change and replace plain string interpolation of team.id with
encodeURIComponent(team.id) (or the project urlString helper) for URL safety.
packages/template/src/dev-tool/dev-tool-indicator.tsx (1)

65-70: Use a monotonic clock for duration.

These timings are currently derived from Date.now(), so a system clock adjustment can make the dev-tool latency jump or even go negative. Keep the wall-clock timestamp, but measure elapsed time with performance.now().

As per coding guidelines, don't use Date.now() for measuring elapsed (real) time, instead use performance.now().

Also applies to: 111-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/dev-tool-indicator.tsx` around lines 65 - 70,
The code currently uses Date.now() (startTime) to compute request duration in
the fetch wrapper (around the fetch call that uses originalFetch), which is
susceptible to system clock changes; change it to record a monotonic start via
performance.now() (e.g., startMonotonic = performance.now()) and compute
duration as performance.now() - startMonotonic while keeping the wall-clock
timestamp from Date.now() if you still need a timestamp; update the other
analogous occurrence that computes elapsed time elsewhere in this file (the
second startTime/duration pair) the same way to use performance.now() for
elapsed measurements.
packages/template/src/dev-tool/component-catalog.tsx (1)

31-33: Explain the any escape hatch here or narrow it.

CatalogEntry is exported, so React.ComponentType<any> becomes the registry contract for every previewed component. If heterogeneous props make any unavoidable, please add the required inline note about why it's safe and where prop misuse is still caught.

As per coding guidelines, try to avoid the any type; whenever you need it, leave a comment explaining why you're using it and how you can be certain that errors would still be flagged.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/component-catalog.tsx` around lines 31 - 33,
The exported type CatalogEntry currently uses React.ComponentType<any>, which
leaks an any into the public registry contract; either make CatalogEntry generic
(e.g., CatalogEntry<P = unknown> with component: React.ComponentType<P>) or
switch to React.ComponentType<unknown> to avoid any, and update callers to
provide the concrete prop type, or if heterogeneous props truly make any
unavoidable add a concise inline comment on the CatalogEntry.type explaining why
any is required, where runtime/compile-time prop validation still occurs (e.g.,
preview wrappers or story files), and add a TODO to tighten the type when
components share a common prop shape; refer to the exported symbol CatalogEntry
and its component field when making the change.
packages/template/src/dev-tool/tabs/overview-tab.tsx (2)

131-131: Prefer explicit null check over non-null assertion.

Per coding guidelines, prefer ?? throwErr(...) over non-null assertions. Although user is checked in the JSX conditional, the async function scope doesn't carry that narrowing.

Suggested fix
-      await user!.signOut();
+      if (user == null) {
+        throw new Error('Cannot sign out: no user is signed in');
+      }
+      await user.signOut();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/tabs/overview-tab.tsx` at line 131, The
non-null assertion on user in the async call await user!.signOut() should be
replaced with an explicit null-check that throws if user is missing; update the
sign-out call in the same scope (the function that calls user.signOut) to use
the null-coalescing pattern (user ?? throwErr("user is undefined when attempting
signOut")) before invoking signOut, or otherwise guard with an if and throw so
the function no longer relies on the non-null assertion operator.

89-91: Avoid catch (e: any) pattern.

The coding guidelines prohibit using any. For error handling, consider using a type guard or utility to safely extract the error message.

Suggested approach
-    } catch (e: any) {
-      setStatus({ type: 'error', message: e.message || 'Unknown error' });
+    } catch (e) {
+      setStatus({ type: 'error', message: e instanceof Error ? e.message : 'Unknown error' });
     }

Apply the same pattern to the other catch blocks at lines 121-123 and 133-135.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/tabs/overview-tab.tsx` around lines 89 - 91,
Replace the unsafe "catch (e: any)" usages with "catch (err: unknown)" and use a
small type-guard/helper to extract a safe message before calling setStatus; for
example, add a getErrorMessage(err: unknown): string utility that checks for err
being an Error or having a message property and returns a string, then change
the three catch blocks that call setStatus to use getErrorMessage(err) when
creating the status ({ type: 'error', message: getErrorMessage(err) }); update
all occurrences (the catch currently using setStatus at the top and the two
other similar catch blocks) to use this helper.
packages/template/src/dev-tool/dev-tool-context.tsx (2)

130-132: Document the any cast on globalThis.

The coding guidelines require comments explaining any usage. This global attachment is for cross-module interop with the fetch interceptor.

Suggested fix
 // Expose globally so the fetch interceptor (which may be installed once) can
 // always reach the latest store even after HMR / remounts.
 if (typeof globalThis !== 'undefined') {
+  // Using `any` because globalThis doesn't have a typed slot for our store.
+  // This is safe as we control both the writer (here) and readers (dev-tool-indicator.tsx).
   (globalThis as any).__STACK_DEV_TOOL_LOG_STORE__ = globalLogStore;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/dev-tool-context.tsx` around lines 130 - 132,
Add a short inline comment explaining the use of "any" on globalThis when
assigning __STACK_DEV_TOOL_LOG_STORE__ to globalLogStore: state that the cast is
intentional to attach a cross-module global used for the fetch
interceptor/cross-module interop, and that TypeScript typing is intentionally
bypassed here because globalThis has no typed property for this runtime-only
debugging store; keep the comment adjacent to the cast that sets (globalThis as
any).__STACK_DEV_TOOL_LOG_STORE__ = globalLogStore and mention the related
symbol names (__STACK_DEV_TOOL_LOG_STORE__, globalLogStore, fetch interceptor)
so future readers know why the any is necessary.

72-74: Add comment explaining why errors are silently ignored.

Per coding guidelines, errors shouldn't be silently swallowed. For localStorage operations, silent failure is often acceptable (quota exceeded, private browsing, etc.), but the reasoning should be documented.

Suggested fix
   } catch {
-    // ignore
+    // localStorage may be unavailable (private browsing, quota exceeded, etc.)
+    // Failing silently is acceptable here as state persistence is non-critical
   }

Apply the same to the catch block at lines 83-85.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/dev-tool-context.tsx` around lines 72 - 74,
The empty catch blocks in dev-tool-context (around the localStorage access in
the code surrounding the anonymous catch { // ignore }) silently swallow errors;
update both catch blocks (the one shown and the similar one later) to include a
brief comment explaining why errors are intentionally ignored (e.g.,
localStorage may be unavailable in private mode or exceed quota, so failures are
non-fatal for dev UX) and keep no-op behavior; ensure the comment references the
localStorage/read or write operation performed (so future maintainers know the
rationale) and leave behavior unchanged.
packages/template/src/dev-tool/tabs/console-tab.tsx (1)

161-162: Avoid any in map callback.

The any type in (p: any) => p.id can be replaced with a more specific type or type assertion that documents the expected shape.

Suggested fix
     if (key === 'oauthProviders' && Array.isArray(value)) {
-      lines.push(`${key}: ${value.map((p: any) => p.id).join(', ') || 'None'}`);
+      lines.push(`${key}: ${value.map((p: { id: string }) => p.id).join(', ') || 'None'}`);
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/tabs/console-tab.tsx` around lines 161 - 162,
The map callback uses an unsafe any: replace (p: any) => p.id with a typed shape
or assertion for the expected provider object (e.g., define or use an
OAuthProvider type/interface with an id property and cast value to
OAuthProvider[] before mapping, or change the callback to (p: OAuthProvider) =>
p.id) so the compiler knows the structure when mapping in the oauthProviders
branch (the check key === 'oauthProviders' and the value.map call should use the
new typed shape).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/template/package-template.json`:
- Around line 127-136: The peerDependencies JSON contains duplicated commas
because the marker entries ("//") and the following real dependency entries both
include commas; fix package-template.json by ensuring only one comma separates
items—remove the trailing comma from marker lines or the leading comma from the
subsequent dependency lines so each JSON property is properly comma-separated;
specifically update the peerDependencies block around the symbols
"@types/react", "//" marker entries, "@types/react-dom", "react-dom", "next",
and "react" so marker lines do not introduce extra leading/trailing commas and
the resulting JSON parses correctly.

In `@packages/template/src/dev-tool/dev-tool-indicator.tsx`:
- Around line 35-46: The fetch interceptor currently only reads init?.headers
and init?.method, causing fetch(new Request(...)) to lose the Request's headers
and method, and it measures durations with Date.now(); fix by first normalizing
the Request input: if input is a Request object, extract its headers and method
into local variables (merge into the existing headers and method variables),
then apply init overrides so init headers/method take precedence; use the
existing symbols (input, init, headers, method, isStackCall) to implement this
merge logic. Also replace all duration uses of Date.now() (start/end/duration
calculations) with performance.now() so elapsed time is measured with a
monotonic clock.

In `@packages/template/src/dev-tool/dev-tool-tab-bar.tsx`:
- Around line 25-42: The active-indicator measurement only runs on activeTab
changes; update the useEffect that reads barRef/current and sets setStyle so it
also installs a ResizeObserver on barRef.current to call the same measurement
logic whenever the tab bar resizes (recompute btn via
bar.querySelector(`[data-tab-id="${activeTab}"]`) and call setStyle with
offsetLeft/offsetWidth/offsetHeight/transition as before), ensure you
store/close the observer on cleanup, and keep the existing initialRef.current
handling so the initial transition behavior is preserved.

In `@packages/template/src/dev-tool/hooks/use-component-registry.tsx`:
- Around line 7-16: The DevToolComponentPreviewContext is created and a provider
(DevToolComponentPreviewProvider) is exported but there is no consumer hook; add
and export a hook (e.g., useDevToolComponentPreview) that calls
React.useContext(DevToolComponentPreviewContext) and returns the boolean value
so components can detect preview mode; ensure the hook handles missing provider
gracefully (return false or throw with a clear message) and reference
DevToolComponentPreviewContext and DevToolComponentPreviewProvider so callers
can locate the provider/hook pair.

In `@packages/template/src/dev-tool/iframe-tab.tsx`:
- Around line 37-40: The retry handler (retry useCallback) only toggles state
(setLoading/setError) and does not force the iframe to remount, so the iframe
src remains unchanged; modify the retry flow to force a remount by updating a
unique key (e.g., iframeKey state) or appending a cache-busting query param to
the src used by the iframe component (the element that renders the iframe) and
ensure retry increments/changes that key or param so React recreates the iframe;
keep setLoading/setError behavior and update the code paths in retry and the
iframe render to use the new iframeKey/src-with-param.

In `@packages/template/src/dev-tool/index.tsx`:
- Around line 19-25: The getOverride() path currently swallows errors and the
write paths (enable(), disable(), reset()) and global exposure of StackDevTool
are unsafe; update getOverride() to catch and return null only after logging or
wrapping the storage error (or better: implement a small typed storage helper
used by getOverride(), enable(), disable(), reset() that returns Result/throws
explicit errors), add try/catch around localStorage.setItem/removeItem with
clear error handling (e.g., log and fall back to in-memory state), and remove
the use of (window as any) by declaring StackDevTool on the global Window
interface (declare global { interface Window { StackDevTool: typeof StackDevTool
} }) so the tool is exposed type-safely; reference OVERRIDE_KEY, getOverride,
enable, disable, reset, and StackDevTool when making these changes.

In `@packages/template/src/dev-tool/tabs/ai-tab.tsx`:
- Around line 69-73: The onClick handler currently swallows errors from
navigator.clipboard.writeText which hides failures; update the promise catch to
surface feedback to the user instead of an empty catch: capture the error from
navigator.clipboard.writeText, log it (or send to a logger), and update UI state
(e.g., setCopied(false) and set a new setCopyError/setCopyFailed state or call
your existing toast/notification helper) so the component (ai-tab) can display a
friendly "copy failed" message; ensure you still clear any transient success
state (setCopied) and avoid unhandled promise rejections.
- Around line 225-226: You're using an unsafe cast (app as any) to access the
internal symbol; remove the cast and ensure the variable `app` is correctly
typed (e.g., as StackClientApp or a narrowed type that includes the
[stackAppInternalsSymbol] property) so you can call
`[stackAppInternalsSymbol]?.getConstructorOptions?.()` directly inside the
`useEffect`; alternatively add a small type guard that verifies `app && (app as
unknown)[stackAppInternalsSymbol]` then call `getConstructorOptions` — reference
`useEffect`, `app`, `stackAppInternalsSymbol`, and `getConstructorOptions` when
making the change.

In `@packages/template/src/dev-tool/tabs/support-tab.tsx`:
- Around line 34-44: The current useEffect early-exits on prefillApplied.current
which is never reset, so subsequent prefill props are ignored; change the effect
in support-tab.tsx (the useEffect that references prefillApplied,
setFeedbackType, setMessage, setStatus) to apply whenever the prefill prop
changes by removing or abandoning the prefillApplied flag and simply running the
setFeedbackType/setMessage/setStatus when prefill is truthy; ensure the effect
dependency remains [prefill] so each new prefill payload is applied.

---

Nitpick comments:
In `@docs/src/components/stack-auth/stack-team-switcher.tsx`:
- Line 235: The code example produced by generateCodeExample() must match the
actual urlMap prop signature used in stack-team-switcher.tsx: update
generateCodeExample() to show the parameter as (team: { id: string } | null) =>
team ? `/teams/${encodeURIComponent(team.id)}/dashboard` : '/' (or equivalent
using urlString/encodeURIComponent) so the example includes the null defensive
check and encodes team.id; search for generateCodeExample and the urlMap prop to
apply the change and replace plain string interpolation of team.id with
encodeURIComponent(team.id) (or the project urlString helper) for URL safety.

In `@examples/docs-examples/src/app/team/`[teamId]/page.tsx:
- Around line 16-21: The urlMap function constructs a raw path using string
interpolation; update url creation in the urlMap arrow function to encode the
team id (use encodeURIComponent(t.id) or the project's urlString helper) while
keeping the existing null-check that throws if t is null—change the return for
urlMap to produce an encoded path like /team/<encoded-id> so ids with unsafe
characters are properly escaped.

In `@packages/template/src/dev-tool/component-catalog.tsx`:
- Around line 31-33: The exported type CatalogEntry currently uses
React.ComponentType<any>, which leaks an any into the public registry contract;
either make CatalogEntry generic (e.g., CatalogEntry<P = unknown> with
component: React.ComponentType<P>) or switch to React.ComponentType<unknown> to
avoid any, and update callers to provide the concrete prop type, or if
heterogeneous props truly make any unavoidable add a concise inline comment on
the CatalogEntry.type explaining why any is required, where runtime/compile-time
prop validation still occurs (e.g., preview wrappers or story files), and add a
TODO to tighten the type when components share a common prop shape; refer to the
exported symbol CatalogEntry and its component field when making the change.

In `@packages/template/src/dev-tool/dev-tool-context.tsx`:
- Around line 130-132: Add a short inline comment explaining the use of "any" on
globalThis when assigning __STACK_DEV_TOOL_LOG_STORE__ to globalLogStore: state
that the cast is intentional to attach a cross-module global used for the fetch
interceptor/cross-module interop, and that TypeScript typing is intentionally
bypassed here because globalThis has no typed property for this runtime-only
debugging store; keep the comment adjacent to the cast that sets (globalThis as
any).__STACK_DEV_TOOL_LOG_STORE__ = globalLogStore and mention the related
symbol names (__STACK_DEV_TOOL_LOG_STORE__, globalLogStore, fetch interceptor)
so future readers know why the any is necessary.
- Around line 72-74: The empty catch blocks in dev-tool-context (around the
localStorage access in the code surrounding the anonymous catch { // ignore })
silently swallow errors; update both catch blocks (the one shown and the similar
one later) to include a brief comment explaining why errors are intentionally
ignored (e.g., localStorage may be unavailable in private mode or exceed quota,
so failures are non-fatal for dev UX) and keep no-op behavior; ensure the
comment references the localStorage/read or write operation performed (so future
maintainers know the rationale) and leave behavior unchanged.

In `@packages/template/src/dev-tool/dev-tool-indicator.tsx`:
- Around line 65-70: The code currently uses Date.now() (startTime) to compute
request duration in the fetch wrapper (around the fetch call that uses
originalFetch), which is susceptible to system clock changes; change it to
record a monotonic start via performance.now() (e.g., startMonotonic =
performance.now()) and compute duration as performance.now() - startMonotonic
while keeping the wall-clock timestamp from Date.now() if you still need a
timestamp; update the other analogous occurrence that computes elapsed time
elsewhere in this file (the second startTime/duration pair) the same way to use
performance.now() for elapsed measurements.

In `@packages/template/src/dev-tool/iframe-tab.tsx`:
- Around line 73-80: The iframe's onError is unreliable; add a timeout fallback
that starts whenever src changes (store an iframe ref like iframeRef) and clears
on handleLoad and handleError; if the timeout elapses (e.g., 5–10s) check the
iframeRef.current (try accessing contentDocument/contentWindow safely) and if
it's empty or cross-origin-inaccessible treat it as a load failure and set the
same error state used by handleError/loading, and also document this limitation
in comments next to the iframe, referencing the iframe element, handleLoad,
handleError, loading, src and title so future readers know why the timer exists.

In `@packages/template/src/dev-tool/tabs/console-tab.tsx`:
- Around line 161-162: The map callback uses an unsafe any: replace (p: any) =>
p.id with a typed shape or assertion for the expected provider object (e.g.,
define or use an OAuthProvider type/interface with an id property and cast value
to OAuthProvider[] before mapping, or change the callback to (p: OAuthProvider)
=> p.id) so the compiler knows the structure when mapping in the oauthProviders
branch (the check key === 'oauthProviders' and the value.map call should use the
new typed shape).

In `@packages/template/src/dev-tool/tabs/overview-tab.tsx`:
- Line 131: The non-null assertion on user in the async call await
user!.signOut() should be replaced with an explicit null-check that throws if
user is missing; update the sign-out call in the same scope (the function that
calls user.signOut) to use the null-coalescing pattern (user ?? throwErr("user
is undefined when attempting signOut")) before invoking signOut, or otherwise
guard with an if and throw so the function no longer relies on the non-null
assertion operator.
- Around line 89-91: Replace the unsafe "catch (e: any)" usages with "catch
(err: unknown)" and use a small type-guard/helper to extract a safe message
before calling setStatus; for example, add a getErrorMessage(err: unknown):
string utility that checks for err being an Error or having a message property
and returns a string, then change the three catch blocks that call setStatus to
use getErrorMessage(err) when creating the status ({ type: 'error', message:
getErrorMessage(err) }); update all occurrences (the catch currently using
setStatus at the top and the two other similar catch blocks) to use this helper.
🪄 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: ebf7daf6-ae8b-4d42-8a82-43789b5a9072

📥 Commits

Reviewing files that changed from the base of the PR and between 9cf0d43 and 01cbe39.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • apps/backend/src/app/api/latest/ai/query/[mode]/route.ts
  • apps/dashboard/next.config.mjs
  • claude/CLAUDE-KNOWLEDGE.md
  • docs/src/components/stack-auth/stack-team-switcher.tsx
  • examples/docs-examples/src/app/team/[teamId]/page.tsx
  • packages/react/package.json
  • packages/stack/package.json
  • packages/template/package-template.json
  • packages/template/package.json
  • packages/template/src/components-page/sign-in.tsx
  • packages/template/src/dev-tool/component-catalog.tsx
  • packages/template/src/dev-tool/dev-tool-context.tsx
  • packages/template/src/dev-tool/dev-tool-indicator.tsx
  • packages/template/src/dev-tool/dev-tool-panel.tsx
  • packages/template/src/dev-tool/dev-tool-styles.ts
  • packages/template/src/dev-tool/dev-tool-tab-bar.tsx
  • packages/template/src/dev-tool/dev-tool-trigger.tsx
  • packages/template/src/dev-tool/hooks/use-component-registry.tsx
  • packages/template/src/dev-tool/hooks/use-dev-tool-state.tsx
  • packages/template/src/dev-tool/iframe-tab.tsx
  • packages/template/src/dev-tool/index.tsx
  • packages/template/src/dev-tool/tabs/ai-tab.tsx
  • packages/template/src/dev-tool/tabs/components-tab.tsx
  • packages/template/src/dev-tool/tabs/console-tab.tsx
  • packages/template/src/dev-tool/tabs/dashboard-tab.tsx
  • packages/template/src/dev-tool/tabs/docs-tab.tsx
  • packages/template/src/dev-tool/tabs/overview-tab.tsx
  • packages/template/src/dev-tool/tabs/support-tab.tsx
  • packages/template/src/index.ts
  • packages/template/src/providers/stack-provider-client.tsx
  • packages/template/src/react-dom.d.ts

Comment on lines 127 to +136
"peerDependencies": {
"@types/react": ">=18.3.0",
"//": "IF_PLATFORM next",
"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0",
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
"//": "END_PLATFORM",
"react": ">=18.3.0"
"@types/react": ">=18.3.0"
,"//": "IF_PLATFORM react-like"
,"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0"
,"//": "END_PLATFORM",
,"//": "IF_PLATFORM next",
,"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0"
,"//": "END_PLATFORM",
,"react": ">=18.3.0"
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path
import json

path = Path("packages/template/package-template.json")
text = path.read_text()
json.loads(text)
print(f"{path} parses as JSON")
PY

Repository: stack-auth/stack-auth

Length of output: 710


🏁 Script executed:

#!/bin/bash
# Read the actual file content around the problem area
cat -n packages/template/package-template.json | sed -n '125,140p'

Repository: stack-auth/stack-auth

Length of output: 632


🏁 Script executed:

#!/bin/bash
# Test if the proposed fix (removing trailing commas from marker lines) resolves the parse error
python - <<'PY'
from pathlib import Path
import json
import re

path = Path("packages/template/package-template.json")
text = path.read_text()

# Apply the proposed fix: remove trailing commas after marker lines
# Pattern: lines ending with `"END_PLATFORM",` or `"IF_PLATFORM next",` should lose the trailing comma
fixed_text = re.sub(r'("END_PLATFORM|IF_PLATFORM [^"]+)",\s*\n', r'\1"\n', text)

# Try to parse the fixed version
try:
    json.loads(fixed_text)
    print("✓ Fixed version parses as valid JSON")
except json.JSONDecodeError as e:
    print(f"✗ Fixed version still has parse error: {e}")
    print(f"  Location: line {e.lineno}, column {e.colno}")
PY

Repository: stack-auth/stack-auth

Length of output: 183


🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path
import json

path = Path("packages/template/package-template.json")
text = path.read_text()

# The proposed fix: remove trailing commas from the marker lines (END_PLATFORM and IF_PLATFORM)
# Lines 132, 133, and 135 end with `",` but shouldn't - the leading comma on the next line serves as the separator

fixed_text = text.replace(
    '    ,"//": "END_PLATFORM",\n    ,"//": "IF_PLATFORM next",',
    '    ,"//": "END_PLATFORM"\n    ,"//": "IF_PLATFORM next"'
).replace(
    '    ,"//": "END_PLATFORM",\n    ,"react":',
    '    ,"//": "END_PLATFORM"\n    ,"react":'
)

# Try to parse the fixed version
try:
    json.loads(fixed_text)
    print("✓ Proposed fix resolves the syntax error - JSON parses successfully")
except json.JSONDecodeError as e:
    print(f"✗ Proposed fix incomplete: {e}")
    print(f"  Location: line {e.lineno}, column {e.colno}")
    
    # Show what the diff actually needs to fix
    print("\nDebugging: showing problematic section in fixed text:")
    lines = fixed_text.split('\n')
    for i in range(130, 137):
        print(f"Line {i+1}: {lines[i]}")
PY

Repository: stack-auth/stack-auth

Length of output: 132


Fix the double commas in peerDependencies.

The new marker layout introduces both a trailing comma on the marker lines and a leading comma on the following line, causing JSON parse errors at line 133. This breaks tools like Biome and prevents proper tooling validation.

🐛 Proposed fix
     "@types/react": ">=18.3.0"
     ,"//": "IF_PLATFORM react-like"
     ,"@types/react-dom": ">=18.3.0",
     "react-dom": ">=18.3.0"
-    ,"//": "END_PLATFORM",
-    ,"//": "IF_PLATFORM next",
+    ,"//": "END_PLATFORM"
+    ,"//": "IF_PLATFORM next"
     ,"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0"
-    ,"//": "END_PLATFORM",
+    ,"//": "END_PLATFORM"
     ,"react": ">=18.3.0"
📝 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
"peerDependencies": {
"@types/react": ">=18.3.0",
"//": "IF_PLATFORM next",
"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0",
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
"//": "END_PLATFORM",
"react": ">=18.3.0"
"@types/react": ">=18.3.0"
,"//": "IF_PLATFORM react-like"
,"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0"
,"//": "END_PLATFORM",
,"//": "IF_PLATFORM next",
,"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0"
,"//": "END_PLATFORM",
,"react": ">=18.3.0"
"peerDependencies": {
"@types/react": ">=18.3.0"
,"//": "IF_PLATFORM react-like"
,"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0"
,"//": "END_PLATFORM"
,"//": "IF_PLATFORM next"
,"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0"
,"//": "END_PLATFORM"
,"react": ">=18.3.0"
🧰 Tools
🪛 Biome (2.4.9)

[error] 133-133: Expected a property but instead found ','.

(parse)


[error] 134-134: Expected a property but instead found ','.

(parse)


[error] 136-136: Expected a property but instead found ','.

(parse)


[error] 129-129: The key // was already declared.

(lint/suspicious/noDuplicateObjectKeys)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/package-template.json` around lines 127 - 136, The
peerDependencies JSON contains duplicated commas because the marker entries
("//") and the following real dependency entries both include commas; fix
package-template.json by ensuring only one comma separates items—remove the
trailing comma from marker lines or the leading comma from the subsequent
dependency lines so each JSON property is properly comma-separated; specifically
update the peerDependencies block around the symbols "@types/react", "//" marker
entries, "@types/react-dom", "react-dom", "next", and "react" so marker lines do
not introduce extra leading/trailing commas and the resulting JSON parses
correctly.

Comment on lines +35 to +46
const headers = init?.headers;
let isStackCall = false;
let method = init?.method || 'GET';

if (headers) {
if (headers instanceof Headers) {
isStackCall = headers.has('X-Stack-Project-Id');
} else if (Array.isArray(headers)) {
isStackCall = headers.some(([key]) => key === 'X-Stack-Project-Id');
} else {
isStackCall = 'X-Stack-Project-Id' in headers;
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/template/src/dev-tool/dev-tool-indicator.tsx | head -150

Repository: stack-auth/stack-auth

Length of output: 5973


🏁 Script executed:

# Verify the specific lines for Date.now() usage
sed -n '65,71p; 111,120p' packages/template/src/dev-tool/dev-tool-indicator.tsx

Repository: stack-auth/stack-auth

Length of output: 573


Normalize Request inputs and use performance.now() for elapsed time measurement.

Two issues:

  1. Request normalization: The interceptor only inspects init?.headers and init?.method, so fetch(new Request(...)) skips the Request's headers entirely and defaults the method to 'GET' regardless of what the Request specifies. Merge headers/method from input first, then apply init overrides.

  2. Elapsed time measurement: Lines 65 and 70 (and line 111 in the catch block) use Date.now() for duration calculation. Use performance.now() instead, which provides monotonic clock semantics suitable for measuring elapsed time.

♻️ Suggested fixes
-      const headers = init?.headers;
-      let isStackCall = false;
-      let method = init?.method || 'GET';
-
-      if (headers) {
-        if (headers instanceof Headers) {
-          isStackCall = headers.has('X-Stack-Project-Id');
-        } else if (Array.isArray(headers)) {
-          isStackCall = headers.some(([key]) => key === 'X-Stack-Project-Id');
-        } else {
-          isStackCall = 'X-Stack-Project-Id' in headers;
-        }
-      }
+      const request = input instanceof Request ? input : undefined;
+      const headers = new Headers(request?.headers);
+      if (init?.headers != null) {
+        new Headers(init.headers).forEach((value, key) => {
+          headers.set(key, value);
+        });
+      }
+      const method = init?.method ?? request?.method ?? 'GET';
+      const isStackCall = headers.has('X-Stack-Project-Id');

-      const startTime = Date.now();
+      const startTime = performance.now();

-        const duration = Date.now() - startTime;
+        const duration = performance.now() - startTime;

-        const duration = Date.now() - startTime;
+        const duration = performance.now() - startTime;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/dev-tool-indicator.tsx` around lines 35 - 46,
The fetch interceptor currently only reads init?.headers and init?.method,
causing fetch(new Request(...)) to lose the Request's headers and method, and it
measures durations with Date.now(); fix by first normalizing the Request input:
if input is a Request object, extract its headers and method into local
variables (merge into the existing headers and method variables), then apply
init overrides so init headers/method take precedence; use the existing symbols
(input, init, headers, method, isStackCall) to implement this merge logic. Also
replace all duration uses of Date.now() (start/end/duration calculations) with
performance.now() so elapsed time is measured with a monotonic clock.

Comment on lines +25 to +42
useEffect(() => {
const bar = barRef.current;
if (!bar) return;

const btn = bar.querySelector<HTMLElement>(`[data-tab-id="${activeTab}"]`);
if (!btn) return;

// Use offset* instead of getBoundingClientRect so the measurement isn't
// affected by CSS transforms (e.g. the panel's scale-in animation).
setStyle({
transform: `translateX(${btn.offsetLeft}px)`,
width: `${btn.offsetWidth}px`,
height: `${btn.offsetHeight}px`,
opacity: 1,
transition: initialRef.current ? 'none' : undefined,
});
initialRef.current = false;
}, [activeTab, barRef]);
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's read the complete file to understand the full context
cat -n packages/template/src/dev-tool/dev-tool-tab-bar.tsx

Repository: stack-auth/stack-auth

Length of output: 3678


🏁 Script executed:

# Search for ResizeObserver usage
rg "ResizeObserver" --type ts --type tsx

# Search for resize event handlers
rg "addEventListener.*resize" --type ts --type tsx

# Look for other files in dev-tool directory
fd . packages/template/src/dev-tool --type f

Repository: stack-auth/stack-auth

Length of output: 1073


🏁 Script executed:

# Search more broadly for resize
rg "resize" -i packages/template/src/dev-tool/

# Check the panel file
cat -n packages/template/src/dev-tool/dev-tool-panel.tsx | head -100

Repository: stack-auth/stack-auth

Length of output: 7453


🏁 Script executed:

# Get the full dev-tool-panel.tsx file
wc -l packages/template/src/dev-tool/dev-tool-panel.tsx

Repository: stack-auth/stack-auth

Length of output: 118


🏁 Script executed:

# Read the full dev-tool-panel.tsx file to see the resize handler
cat -n packages/template/src/dev-tool/dev-tool-panel.tsx

Repository: stack-auth/stack-auth

Length of output: 9847


🏁 Script executed:

# Double-check: search for any ResizeObserver or mutation observer in useTabIndicator or related hooks
rg "Observer|mutation" packages/template/src/dev-tool/

Repository: stack-auth/stack-auth

Length of output: 196


Re-measure the active indicator when the tab bar resizes.

The measurement only reruns when activeTab changes. Since this panel is resizable, offsetLeft/offsetWidth can change while the active tab stays the same, leaving the indicator stuck in the old position until the user switches tabs. Add a ResizeObserver to the tab bar to trigger re-measurement whenever its dimensions change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/dev-tool-tab-bar.tsx` around lines 25 - 42,
The active-indicator measurement only runs on activeTab changes; update the
useEffect that reads barRef/current and sets setStyle so it also installs a
ResizeObserver on barRef.current to call the same measurement logic whenever the
tab bar resizes (recompute btn via
bar.querySelector(`[data-tab-id="${activeTab}"]`) and call setStyle with
offsetLeft/offsetWidth/offsetHeight/transition as before), ensure you
store/close the observer on cleanup, and keep the existing initialRef.current
handling so the initial transition behavior is preserved.

Comment on lines +7 to +16
/** When true, component previews inside the dev tool do not duplicate registry entries. */
const DevToolComponentPreviewContext = React.createContext(false);

export function DevToolComponentPreviewProvider({ children }: { children: React.ReactNode }) {
return (
<DevToolComponentPreviewContext.Provider value={true}>
{children}
</DevToolComponentPreviewContext.Provider>
);
}
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.

⚠️ Potential issue | 🟡 Minor

Context created but no consumer hook exported.

DevToolComponentPreviewContext is defined and DevToolComponentPreviewProvider is exported, but there's no exported hook for consuming the context value. Components that need to check whether they're inside a preview won't be able to read this context.

🔧 Suggested addition
 export function DevToolComponentPreviewProvider({ children }: { children: React.ReactNode }) {
   return (
     <DevToolComponentPreviewContext.Provider value={true}>
       {children}
     </DevToolComponentPreviewContext.Provider>
   );
 }
+
+export function useIsDevToolComponentPreview(): boolean {
+  return React.useContext(DevToolComponentPreviewContext);
+}
📝 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
/** When true, component previews inside the dev tool do not duplicate registry entries. */
const DevToolComponentPreviewContext = React.createContext(false);
export function DevToolComponentPreviewProvider({ children }: { children: React.ReactNode }) {
return (
<DevToolComponentPreviewContext.Provider value={true}>
{children}
</DevToolComponentPreviewContext.Provider>
);
}
/** When true, component previews inside the dev tool do not duplicate registry entries. */
const DevToolComponentPreviewContext = React.createContext(false);
export function DevToolComponentPreviewProvider({ children }: { children: React.ReactNode }) {
return (
<DevToolComponentPreviewContext.Provider value={true}>
{children}
</DevToolComponentPreviewContext.Provider>
);
}
export function useIsDevToolComponentPreview(): boolean {
return React.useContext(DevToolComponentPreviewContext);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/hooks/use-component-registry.tsx` around lines
7 - 16, The DevToolComponentPreviewContext is created and a provider
(DevToolComponentPreviewProvider) is exported but there is no consumer hook; add
and export a hook (e.g., useDevToolComponentPreview) that calls
React.useContext(DevToolComponentPreviewContext) and returns the boolean value
so components can detect preview mode; ensure the hook handles missing provider
gracefully (return false or throw with a clear message) and reference
DevToolComponentPreviewContext and DevToolComponentPreviewProvider so callers
can locate the provider/hook pair.

Comment on lines +37 to +40
const retry = useCallback(() => {
setLoading(true);
setError(false);
}, []);
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.

⚠️ Potential issue | 🟡 Minor

Retry doesn't actually reload the iframe.

Setting loading=true and error=false won't cause the iframe to reload since the src prop hasn't changed. React will preserve the existing iframe element.

🔧 Suggested fix using a key to force remount
+"use client";
+
+import React, { useCallback, useState } from "react";
+
+// IF_PLATFORM react-like
+
 export function IframeTab({
   src,
   title,
   loadingMessage = "Loading…",
   errorMessage = "Unable to load content",
   errorDetail,
 }: {
   src: string;
   title: string;
   loadingMessage?: string;
   errorMessage?: string;
   errorDetail?: string;
 }) {
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(false);
+  const [retryKey, setRetryKey] = useState(0);
 
   // ... handlers ...
 
   const retry = useCallback(() => {
     setLoading(true);
     setError(false);
+    setRetryKey(k => k + 1);
   }, []);
 
   // ... error UI ...
 
   return (
     <div className="sdt-iframe-container">
       {loading && (
         <div className="sdt-iframe-loading">{loadingMessage}</div>
       )}
       <iframe
+        key={retryKey}
         src={src}
         title={title}
         onLoad={handleLoad}
         onError={handleError}
         sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
         style={{ display: loading ? 'none' : 'block' }}
       />
     </div>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/iframe-tab.tsx` around lines 37 - 40, The
retry handler (retry useCallback) only toggles state (setLoading/setError) and
does not force the iframe to remount, so the iframe src remains unchanged;
modify the retry flow to force a remount by updating a unique key (e.g.,
iframeKey state) or appending a cache-busting query param to the src used by the
iframe component (the element that renders the iframe) and ensure retry
increments/changes that key or param so React recreates the iframe; keep
setLoading/setError behavior and update the code paths in retry and the iframe
render to use the new iframeKey/src-with-param.

Comment on lines +19 to +25
function getOverride(): boolean | null {
try {
const val = localStorage.getItem(OVERRIDE_KEY);
if (val === 'true') return true;
if (val === 'false') return false;
} catch {}
return null;
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd packages/template/src/dev-tool && wc -l index.tsx

Repository: stack-auth/stack-auth

Length of output: 78


🏁 Script executed:

cd packages/template/src/dev-tool && head -100 index.tsx | cat -n

Repository: stack-auth/stack-auth

Length of output: 3476


Make the override control path explicit and properly typed.

The read path silently swallows errors via catch {} (line 24), violating the "fail loud" principle. The write paths (enable(), disable(), reset() at lines 60, 65, 70) attempt to write to localStorage without handling potential failures (quota exceeded, private browsing mode, etc.). Additionally, StackDevTool is exposed via (window as any) (line 58), bypassing the type system. Replace the silent catch with either explicit error handling or a type-safe storage helper, and declare StackDevTool on the global Window interface instead of using as any.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/index.tsx` around lines 19 - 25, The
getOverride() path currently swallows errors and the write paths (enable(),
disable(), reset()) and global exposure of StackDevTool are unsafe; update
getOverride() to catch and return null only after logging or wrapping the
storage error (or better: implement a small typed storage helper used by
getOverride(), enable(), disable(), reset() that returns Result/throws explicit
errors), add try/catch around localStorage.setItem/removeItem with clear error
handling (e.g., log and fall back to in-memory state), and remove the use of
(window as any) by declaring StackDevTool on the global Window interface
(declare global { interface Window { StackDevTool: typeof StackDevTool } }) so
the tool is exposed type-safely; reference OVERRIDE_KEY, getOverride, enable,
disable, reset, and StackDevTool when making these changes.

Comment on lines +69 to +73
onClick={() => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}).catch(() => {});
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.

⚠️ Potential issue | 🟡 Minor

Don't silently swallow clipboard errors.

Per coding guidelines, errors should never be silently swallowed. Consider showing feedback to the user when copy fails.

Suggested fix
       onClick={() => {
-        navigator.clipboard.writeText(text).then(() => {
-          setCopied(true);
-          setTimeout(() => setCopied(false), 1500);
-        }).catch(() => {});
+        navigator.clipboard.writeText(text).then(
+          () => {
+            setCopied(true);
+            setTimeout(() => setCopied(false), 1500);
+          },
+          () => {
+            // Clipboard API failed - could be permissions or unsupported
+            // For a dev tool, logging is acceptable
+            console.warn('Failed to copy to clipboard');
+          }
+        );
       }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/tabs/ai-tab.tsx` around lines 69 - 73, The
onClick handler currently swallows errors from navigator.clipboard.writeText
which hides failures; update the promise catch to surface feedback to the user
instead of an empty catch: capture the error from navigator.clipboard.writeText,
log it (or send to a logger), and update UI state (e.g., setCopied(false) and
set a new setCopyError/setCopyFailed state or call your existing
toast/notification helper) so the component (ai-tab) can display a friendly
"copy failed" message; ensure you still clear any transient success state
(setCopied) and avoid unhandled promise rejections.

Comment on lines +225 to +226
useEffect(() => {
const opts = (app as any)[stackAppInternalsSymbol]?.getConstructorOptions?.() ?? {};
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.

🛠️ Refactor suggestion | 🟠 Major

Avoid any cast to access internal symbols.

The coding guidelines state: "Do NOT use as/any/type casts or anything else to bypass the type system." The StackClientApp type already includes the [stackAppInternalsSymbol] property based on the context snippet, so the cast shouldn't be necessary.

Suggested fix
   useEffect(() => {
-    const opts = (app as any)[stackAppInternalsSymbol]?.getConstructorOptions?.() ?? {};
+    const opts = app[stackAppInternalsSymbol].getConstructorOptions();
     const h: Record<string, string> = {
       "X-Stack-Access-Type": "client",
       "X-Stack-Project-Id": app.projectId,
     };
-    if ("publishableClientKey" in opts && opts.publishableClientKey) {
+    if (opts.publishableClientKey) {
       h["X-Stack-Publishable-Client-Key"] = opts.publishableClientKey;
     }
     headers.current = h;
   }, [app]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/tabs/ai-tab.tsx` around lines 225 - 226,
You're using an unsafe cast (app as any) to access the internal symbol; remove
the cast and ensure the variable `app` is correctly typed (e.g., as
StackClientApp or a narrowed type that includes the [stackAppInternalsSymbol]
property) so you can call `[stackAppInternalsSymbol]?.getConstructorOptions?.()`
directly inside the `useEffect`; alternatively add a small type guard that
verifies `app && (app as unknown)[stackAppInternalsSymbol]` then call
`getConstructorOptions` — reference `useEffect`, `app`,
`stackAppInternalsSymbol`, and `getConstructorOptions` when making the change.

Comment on lines +34 to +44
const prefillApplied = useRef(false);

// Apply prefill when it changes (e.g. navigating from share dialog)
useEffect(() => {
if (prefill && !prefillApplied.current) {
setFeedbackType(prefill.feedbackType);
setMessage(prefill.message);
setStatus("idle");
prefillApplied.current = true;
}
}, [prefill]);
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.

⚠️ Potential issue | 🟠 Major

Later support prefills never apply.

After the first prefill, prefillApplied.current stays true for the lifetime of this mounted form. A second "report this issue" flow in the same session will keep showing the old message/type instead of the new payload. Reapply whenever prefill changes, not once per component lifetime.

🐛 Proposed fix
-  const prefillApplied = useRef(false);
-
   // Apply prefill when it changes (e.g. navigating from share dialog)
   useEffect(() => {
-    if (prefill && !prefillApplied.current) {
+    if (prefill != null) {
       setFeedbackType(prefill.feedbackType);
       setMessage(prefill.message);
       setStatus("idle");
-      prefillApplied.current = true;
+      setErrorMessage("");
     }
   }, [prefill]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/dev-tool/tabs/support-tab.tsx` around lines 34 - 44,
The current useEffect early-exits on prefillApplied.current which is never
reset, so subsequent prefill props are ignored; change the effect in
support-tab.tsx (the useEffect that references prefillApplied, setFeedbackType,
setMessage, setStatus) to apply whenever the prefill prop changes by removing or
abandoning the prefillApplied flag and simply running the
setFeedbackType/setMessage/setStatus when prefill is truthy; ensure the effect
dependency remains [prefill] so each new prefill payload is applied.


lines.push(`--- Environment ---`);
lines.push(`SDK Version: ${typeof window !== 'undefined' ? (window as any).__STACK_VERSION__ || 'Unknown' : 'Unknown'}`);
lines.push(`Environment: ${process.env.NODE_ENV}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
lines.push(`Environment: ${process.env.NODE_ENV}`);
lines.push(`Environment: ${typeof process !== 'undefined' && process.env?.NODE_ENV ? process.env.NODE_ENV : 'Unknown'}`);

Direct access to process.env.NODE_ENV causes runtime error in Vite environments where process is undefined

Fix on Vercel

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.

3 participants