Conversation
- 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
| }, | ||
|
|
||
| async headers() { | ||
| const isDev = process.env.NODE_ENV === "development"; |
There was a problem hiding this comment.
note that isDev is actually false in the local emulator (instead, the local emulator envvar is set). use isLocalEmulatorEnabled instead
| const isDev = process.env.NODE_ENV === "development"; | |
| const isLocalEmulator = isLocalEmulatorEnabled(); |
There was a problem hiding this comment.
to test this, run pnpm dev with NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true
There was a problem hiding this comment.
tnx, dont be reviewin tis rn, its all ai slop atm
N2D4
left a comment
There was a problem hiding this comment.
i like the indicator, some comments on how to implement it
| // 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: "", |
There was a problem hiding this comment.
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)
| // 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" : "", |
| urlMap={(t) => { | ||
| if (t == null) { | ||
| throw new Error("SelectedTeamSwitcher urlMap expected a non-null team"); | ||
| } | ||
| return `/team/${t.id}`; | ||
| }} |
| "@types/react-dom": ">=18.3.0", | ||
| "react-dom": ">=18.3.0", | ||
| "react": ">=18.3.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "@types/react-dom": { | ||
| "optional": true | ||
| }, |
There was a problem hiding this comment.
we shouldn't rely on react DOM in the react pkg if we can, eg. react native doesn't have it
| "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 |
There was a problem hiding this comment.
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).
| {/* 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 */} |
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
note: we cannot use process.env in the SDKs (eg. Vite does not support it)
packages/template/src/index.ts
Outdated
| 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"; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
| "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 | ||
| }, |
There was a problem hiding this comment.
with the discussed changes we can revert all of this i believe
| declare module "react-dom" { | ||
| import type { ReactNode, ReactPortal } from "react"; | ||
|
|
||
| export function createPortal( | ||
| children: ReactNode, | ||
| container: Element | DocumentFragment, | ||
| key?: null | string, | ||
| ): ReactPortal; | ||
| } |
There was a problem hiding this comment.
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.
|
@greptile-ai review |
Greptile SummaryThis PR introduces a Stack Auth Dev Tool — a floating overlay panel that is automatically injected into any app using Key changes:
Issues found:
Confidence Score: 4/5Safe 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
Important Files Changed
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "Merge branch 'dev' into feat/dev-tool" | Re-trigger Greptile |
| } | ||
| if (seen.has(value)) { | ||
| return value; | ||
| } | ||
| seen.add(value); | ||
| const result: Record<string, unknown> = {}; | ||
| for (const [key, entry] of Object.entries(value)) { |
There was a problem hiding this comment.
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.
| } | |
| 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]"; | |
| } |
| // Show path only for cleaner logs | ||
| displayUrl = u.pathname + (u.search || ''); | ||
| } catch { | ||
| // keep full url | ||
| } | ||
|
|
||
| const startTime = Date.now(); | ||
|
|
||
| try { |
There was a problem hiding this comment.
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.
| // 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'); |
| } catch (err) { | ||
| const duration = Date.now() - startTime; | ||
| addApiLogRef.current({ | ||
| id: nextId(), | ||
| timestamp: startTime, | ||
| method: method.toUpperCase(), | ||
| url: displayUrl, | ||
| duration, |
There was a problem hiding this comment.
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:
| } 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) { |
| 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?" }, |
There was a problem hiding this comment.
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).
| 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` | ||
| */ |
There was a problem hiding this comment.
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.
| src={src} | ||
| title={title} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
StackProviderClientand adding styling/context/logging. - Adds AI SDK dependencies (
@ai-sdk/react,ai) acrosspackages/template,packages/react, andpackages/stack. - Adjusts dashboard security headers in development to allow iframe embedding (omits
X-Frame-Optionsin 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.
| 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; |
There was a problem hiding this comment.
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).
| 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"); |
| const setState: React.Dispatch<React.SetStateAction<DevToolState>> = useCallback((action) => { | ||
| setStateRaw((prev) => { | ||
| const next = typeof action === 'function' ? action(prev) : action; | ||
| saveState(next); | ||
| return next; | ||
| }); | ||
| }, []); |
There was a problem hiding this comment.
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.
|
|
||
| // 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); |
There was a problem hiding this comment.
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.
| // 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); | |
| } |
| 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; |
There was a problem hiding this comment.
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.
| 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; |
| urlMap={(t) => { | ||
| if (t == null) { | ||
| throw new Error("SelectedTeamSwitcher urlMap expected a non-null team"); | ||
| } | ||
| return `/team/${t.id}`; | ||
| }} |
There was a problem hiding this comment.
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.
| 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")} |
| // 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).'); | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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.
| // Link | ||
| parts.push( | ||
| <a key={match.index} className="sdt-ai-link" href={match[7]} target="_blank" rel="noopener noreferrer"> | ||
| {match[6]} | ||
| </a> | ||
| ); |
There was a problem hiding this comment.
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).
| /** | ||
| * 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"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (10)
packages/template/src/dev-tool/iframe-tab.tsx (1)
73-80:onErroris 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: UseencodeURIComponent()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`` orencodeURIComponent()` 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
- 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` : `/`}'); }
- 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`` orencodeURIComponent()` 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 forduration.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-clocktimestamp, but measure elapsed time withperformance.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 theanyescape hatch here or narrow it.
CatalogEntryis exported, soReact.ComponentType<any>becomes the registry contract for every previewed component. If heterogeneous props makeanyunavoidable, 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
anytype; 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. Althoughuseris 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: Avoidcatch (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 theanycast on globalThis.The coding guidelines require comments explaining
anyusage. 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: Avoidanyin map callback.The
anytype in(p: any) => p.idcan 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (31)
apps/backend/src/app/api/latest/ai/query/[mode]/route.tsapps/dashboard/next.config.mjsclaude/CLAUDE-KNOWLEDGE.mddocs/src/components/stack-auth/stack-team-switcher.tsxexamples/docs-examples/src/app/team/[teamId]/page.tsxpackages/react/package.jsonpackages/stack/package.jsonpackages/template/package-template.jsonpackages/template/package.jsonpackages/template/src/components-page/sign-in.tsxpackages/template/src/dev-tool/component-catalog.tsxpackages/template/src/dev-tool/dev-tool-context.tsxpackages/template/src/dev-tool/dev-tool-indicator.tsxpackages/template/src/dev-tool/dev-tool-panel.tsxpackages/template/src/dev-tool/dev-tool-styles.tspackages/template/src/dev-tool/dev-tool-tab-bar.tsxpackages/template/src/dev-tool/dev-tool-trigger.tsxpackages/template/src/dev-tool/hooks/use-component-registry.tsxpackages/template/src/dev-tool/hooks/use-dev-tool-state.tsxpackages/template/src/dev-tool/iframe-tab.tsxpackages/template/src/dev-tool/index.tsxpackages/template/src/dev-tool/tabs/ai-tab.tsxpackages/template/src/dev-tool/tabs/components-tab.tsxpackages/template/src/dev-tool/tabs/console-tab.tsxpackages/template/src/dev-tool/tabs/dashboard-tab.tsxpackages/template/src/dev-tool/tabs/docs-tab.tsxpackages/template/src/dev-tool/tabs/overview-tab.tsxpackages/template/src/dev-tool/tabs/support-tab.tsxpackages/template/src/index.tspackages/template/src/providers/stack-provider-client.tsxpackages/template/src/react-dom.d.ts
| "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" |
There was a problem hiding this comment.
🧩 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")
PYRepository: 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}")
PYRepository: 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]}")
PYRepository: 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.
| "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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/template/src/dev-tool/dev-tool-indicator.tsx | head -150Repository: 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.tsxRepository: stack-auth/stack-auth
Length of output: 573
Normalize Request inputs and use performance.now() for elapsed time measurement.
Two issues:
-
Request normalization: The interceptor only inspects
init?.headersandinit?.method, sofetch(new Request(...))skips the Request's headers entirely and defaults the method to 'GET' regardless of what the Request specifies. Merge headers/method frominputfirst, then applyinitoverrides. -
Elapsed time measurement: Lines 65 and 70 (and line 111 in the catch block) use
Date.now()for duration calculation. Useperformance.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.
| 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]); |
There was a problem hiding this comment.
🧩 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.tsxRepository: 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 fRepository: 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 -100Repository: 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.tsxRepository: 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.tsxRepository: 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.
| /** 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| /** 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.
| const retry = useCallback(() => { | ||
| setLoading(true); | ||
| setError(false); | ||
| }, []); |
There was a problem hiding this comment.
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.
| function getOverride(): boolean | null { | ||
| try { | ||
| const val = localStorage.getItem(OVERRIDE_KEY); | ||
| if (val === 'true') return true; | ||
| if (val === 'false') return false; | ||
| } catch {} | ||
| return null; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd packages/template/src/dev-tool && wc -l index.tsxRepository: stack-auth/stack-auth
Length of output: 78
🏁 Script executed:
cd packages/template/src/dev-tool && head -100 index.tsx | cat -nRepository: 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.
| onClick={() => { | ||
| navigator.clipboard.writeText(text).then(() => { | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1500); | ||
| }).catch(() => {}); |
There was a problem hiding this comment.
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.
| useEffect(() => { | ||
| const opts = (app as any)[stackAppInternalsSymbol]?.getConstructorOptions?.() ?? {}; |
There was a problem hiding this comment.
🛠️ 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.
| 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]); |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
Release Notes
New Features
Documentation
Chores