Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
863e1d3
Update pnpm-lock.yaml and enhance dashboard dev tools
mantrakp04 Mar 20, 2026
d4a17f4
Update pnpm-lock.yaml and enhance dev tool components
mantrakp04 Mar 27, 2026
01cbe39
Merge branch 'dev' into feat/dev-tool
mantrakp04 Mar 27, 2026
7bfde19
Refactor dashboard configuration and clean up template dependencies
mantrakp04 Mar 27, 2026
ce13ce2
Refactor dev tool components and enhance URL handling
mantrakp04 Mar 30, 2026
40c7d79
Merge branch 'dev' into feat/dev-tool
mantrakp04 Mar 30, 2026
e3feccc
Enhance feedback handling and introduce new internal API routes
mantrakp04 Mar 31, 2026
754f296
Refactor component version handling and improve feedback tests
mantrakp04 Mar 31, 2026
3c5b938
Merge branch 'dev' into feat/dev-tool
mantrakp04 Mar 31, 2026
fd4ade1
Refactor dev tool components and enhance functionality
mantrakp04 Mar 31, 2026
86f480c
Refactor feedback tests for improved clarity and consistency
mantrakp04 Apr 1, 2026
4a9272c
Remove deprecated dev tool components and consolidate functionality
mantrakp04 Apr 1, 2026
b2a73d2
Add request logging functionality to StackClientInterface
mantrakp04 Apr 1, 2026
38f6ddf
Merge branch 'dev' into feat/dev-tool
mantrakp04 Apr 4, 2026
aea74a3
Merge branch 'dev' into feat/dev-tool
mantrakp04 Apr 6, 2026
0c178c5
Refactor component version handling and add tests for API endpoint
mantrakp04 Apr 6, 2026
9f9000e
Updated component prompts
N2D4 Apr 9, 2026
4180be2
Merge branch 'dev' into feat/dev-tool
mantrakp04 Apr 9, 2026
5f8debe
Add "restricted users redirected to onboarding" logic to sign in and …
N2D4 Apr 9, 2026
37a37a6
Refactor dev environment scripts and add new demo pages for dev tools
mantrakp04 Apr 9, 2026
2607576
Merge branch 'dev' into feat/dev-tool
mantrakp04 Apr 9, 2026
633acc6
Implement trigger position snapping and management in dev tools
mantrakp04 Apr 11, 2026
a365ac4
Merge branch 'dev' into feat/dev-tool
N2D4 Apr 12, 2026
b1780b4
Update prompt
N2D4 Apr 12, 2026
8be613c
Merge branch 'dev' into feat/dev-tool
mantrakp04 Apr 13, 2026
3bbdfc9
Enhance AI Proxy Integration and Dev Tool UI
mantrakp04 Apr 13, 2026
e51d459
Refactor Dev Tool Core Functions and Enhance URL Target Tests
mantrakp04 Apr 13, 2026
01982ff
Refactor Dev Tool Tab Functions to Return Structured Results
mantrakp04 Apr 13, 2026
359e6d1
Refactor Panel Closing Logic in Dev Tool Core
mantrakp04 Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/backend/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ STACK_OPENAI_API_KEY=mock_openai_api_key
STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey
STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret
STACK_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION
STACK_FEEDBACK_MODE=FORWARD_TO_PRODUCTION
# Email monitor configuration for tests
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:8101/handler/email-verification
STACK_EMAIL_MONITOR_PROJECT_ID=internal
Expand Down
19 changes: 10 additions & 9 deletions apps/backend/src/app/api/latest/ai/query/[mode]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,6 @@ export const POST = createSmartRouteHandler({
const apiKey = getEnvVariable("STACK_OPENROUTER_API_KEY");


if (apiKey === "FORWARD_TO_PRODUCTION") {
const prodResponse = await forwardToProduction(mode, body);
return {
statusCode: prodResponse.status,
bodyType: "response" as const,
body: prodResponse,
};
}

const isAuthenticated = fullReq.auth != null;
const { quality, speed, systemPrompt: systemPromptId, tools: toolNames, messages, projectId } = body;

Expand All @@ -60,6 +51,16 @@ export const POST = createSmartRouteHandler({
}
}

if (apiKey === "FORWARD_TO_PRODUCTION") {
// Strip projectId before forwarding — production infers it from the API key
const prodResponse = await forwardToProduction(mode, { quality, speed, systemPrompt: systemPromptId, tools: toolNames, messages });
return {
statusCode: prodResponse.status,
bodyType: "response" as const,
body: prodResponse,
};
}

const model = selectModel(quality, speed, isAuthenticated);
const systemPrompt = getFullSystemPrompt(systemPromptId);
const tools = await getTools(toolNames, { auth: fullReq.auth, targetProjectId: projectId });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getLatestPageVersions } from "@stackframe/stack";

export const GET = createSmartRouteHandler({
metadata: {
hidden: true,
},
request: yupObject({
method: yupString().oneOf(["GET"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
versions: yupRecord(yupString().defined(), yupObject({
version: yupNumber().defined(),
changelogs: yupRecord(yupString().defined(), yupString().defined()).defined(),
}).defined()).defined(),
}).defined(),
}),
handler: async () => {
return {
statusCode: 200,
bodyType: "json" as const,
body: {
versions: getLatestPageVersions(),
},
};
},
});
56 changes: 45 additions & 11 deletions apps/backend/src/app/api/latest/internal/feedback/route.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { sendSupportFeedbackEmail } from "@/lib/internal-feedback-emails";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, clientOrHigherAuthTypeSchema, emailSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
import { adaptSchema, emailSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";

/**
* Unified feedback endpoint used by both the dashboard and the dev tool.
*
* Auth is optional: when the user is signed in (dashboard), user info is
* included in the email. When unauthenticated (dev tool), feedback is sent
* without user context.
*
* In the local emulator, feedback is forwarded to production Stack Auth (same
* pattern as the AI query endpoint). Set STACK_FEEDBACK_MODE=FORWARD_TO_PRODUCTION
* in .env.development to enable this.
*/
export const POST = createSmartRouteHandler({
metadata: {
summary: "Submit support feedback",
description: "Send a support feedback message to the internal Stack Auth inbox",
description: "Send a support feedback message to the internal Stack Auth inbox. Auth is optional — works from both the dashboard (authenticated) and the dev tool (unauthenticated).",
tags: ["Internal"],
},
request: yupObject({
auth: yupObject({
type: clientOrHigherAuthTypeSchema,
tenancy: adaptSchema.defined(),
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
tenancy: adaptSchema.optional(),
user: adaptSchema.optional(),
Comment thread
mantrakp04 marked this conversation as resolved.
}).nullable().optional(),
body: yupObject({
name: yupString().optional().max(100),
email: emailSchema.defined().nonEmpty(),
message: yupString().defined().nonEmpty().max(5000),
feedback_type: yupString().oneOf(["feedback", "bug"]).optional(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
Expand All @@ -32,12 +43,35 @@ export const POST = createSmartRouteHandler({
}).defined(),
}),
async handler({ auth, body }) {
// Forward to production in local emulator (same pattern as AI query endpoint)
const feedbackMode = getEnvVariable("STACK_FEEDBACK_MODE", "email");
if (feedbackMode === "FORWARD_TO_PRODUCTION") {
const prodResponse = await fetch("https://api.stack-auth.com/api/latest/internal/feedback", {
method: "POST",
headers: { "content-type": "application/json", "accept-encoding": "identity" },
body: JSON.stringify(body),
});
if (!prodResponse.ok) {
throw new StatusError(prodResponse.status, "Failed to forward feedback to production");
}
return {
statusCode: 200,
bodyType: "json" as const,
body: { success: true as const },
};
}

// Use the authenticated tenancy if available, otherwise fall back to the
// internal project tenancy (for unauthenticated dev tool submissions).
const tenancy = auth?.tenancy ?? await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);

await sendSupportFeedbackEmail({
tenancy: auth.tenancy,
user: auth.user,
tenancy,
user: auth?.user ?? null,
name: body.name ?? null,
email: body.email,
message: body.message,
feedbackType: body.feedback_type,
});

return {
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/app/api/latest/internal/projects/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const adminUserProjectsCrudHandlers = createLazyProxy(() => createCrudHan
}),
onPrepare: async ({ auth }) => {
if (!auth.user) {
throw new KnownErrors.UserAuthenticationRequired;
throw new KnownErrors.UserAuthenticationRequired();
}
if (auth.project.id !== "internal") {
throw new KnownErrors.ExpectedInternalProject();
Expand Down
38 changes: 28 additions & 10 deletions apps/backend/src/lib/internal-feedback-emails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createTemplateComponentFromHtml } from "@/lib/email-rendering";
import { normalizeEmail, sendEmailToMany } from "@/lib/emails";
import { isLocalEmulatorEnabled } from "@/lib/local-emulator";
import { getNotificationCategoryByName } from "@/lib/notification-categories";
import { Tenancy } from "@/lib/tenancies";
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
Expand Down Expand Up @@ -82,24 +83,41 @@ async function sendInternalOperationsEmail(options: {

export async function sendSupportFeedbackEmail(options: {
tenancy: Tenancy,
user: UsersCrud["Admin"]["Read"],
user: UsersCrud["Admin"]["Read"] | null,
name: string | null,
email: string,
message: string,
feedbackType?: string,
}) {
const displayName = options.name ?? options.user.display_name ?? "Not provided";
const displayName = options.name ?? options.user?.display_name ?? "Not provided";
const feedbackLabel = options.feedbackType === "bug" ? "Bug Report" : "Support";

const fields: Array<{ label: string, value: string }> = [
{ label: "Sender name", value: displayName },
{ label: "Sender email", value: options.email },
];

if (options.user) {
fields.push(
{ label: "Stack Auth user ID", value: options.user.id },
{ label: "Stack Auth display name", value: options.user.display_name ?? "Not provided" },
);
}

if (options.feedbackType) {
fields.push({ label: "Type", value: feedbackLabel });
}

if (isLocalEmulatorEnabled()) {
fields.push({ label: "Environment", value: "Local Emulator" });
}

await sendInternalOperationsEmail({
tenancy: options.tenancy,
subject: `[Support] ${options.email}`,
subject: `[${feedbackLabel}] ${options.email}`,
htmlContent: buildInternalEmailHtml({
heading: "Support feedback submission",
fields: [
{ label: "Sender name", value: displayName },
{ label: "Sender email", value: options.email },
{ label: "Stack Auth user ID", value: options.user.id },
{ label: "Stack Auth display name", value: options.user.display_name ?? "Not provided" },
],
heading: `${feedbackLabel} feedback submission`,
fields,
contentLabel: "Message",
contentBody: options.message,
}),
Expand Down
5 changes: 4 additions & 1 deletion apps/dashboard/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const nextConfig = {
},

async headers() {
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
return [
{
source: "/(.*)",
Expand Down Expand Up @@ -118,7 +119,9 @@ const nextConfig = {
},
{
key: "Content-Security-Policy",
value: "",
// Note: *.localhost requires Chrome 117+ and may not work in Firefox
// without network.dns.localDomains configuration. Fine for dev tool purposes.
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" : "",
},
],
},
Expand Down
41 changes: 30 additions & 11 deletions apps/dashboard/src/components/feedback-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from "@/components/ui";
import { SelectField } from "@/components/form-fields";
import { getPublicEnvVar } from "@/lib/env";
import { getInternalProjectHeaders } from "@/lib/internal-project-headers";
import { CheckCircleIcon, EnvelopeIcon, GithubLogoIcon, WarningCircleIcon } from "@phosphor-icons/react";
import { useUser } from "@stackframe/stack";
import { emailSchema } from "@stackframe/stack-shared/dist/schema-fields";
Expand Down Expand Up @@ -34,25 +34,44 @@ export function FeedbackForm() {
.max(5000)
.label("Message")
.meta({ type: "textarea" }),
feedback_type: yup.string()
.oneOf(["feedback", "bug"] as const)
.defined()
.label("Type")
.default("feedback")
.meta({
stackFormFieldRender: (props: any) => (
<SelectField
{...props}
options={[
{ value: "feedback", label: "Feedback" },
{ value: "bug", label: "Bug Report" },
]}
/>
),
}),
});

const handleSubmit = async (values: yup.InferType<typeof domainFormSchema>) => {
setSubmitStatus('idle');
setErrorMessage('');

try {
if (user == null) {
throw new Error("Please sign in again and retry sending feedback.");
// Auth headers are sent when available so the backend can include user
// context in the email, but the endpoint accepts unauthenticated requests.
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (user) {
const authJson = await user.getAuthJson();
headers["X-Stack-Access-Type"] = "client";
headers["X-Stack-Project-Id"] = "internal";
headers["X-Stack-Publishable-Client-Key"] = getPublicEnvVar("NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY") ?? "";
Comment thread
mantrakp04 marked this conversation as resolved.
if (authJson.accessToken) {
headers["X-Stack-Access-Token"] = authJson.accessToken;
}
}
const authJson = await user.getAuthJson();
const response = await fetch(`${baseUrl}/api/v1/internal/feedback`, {
method: "POST",
headers: {
...getInternalProjectHeaders({
accessToken: authJson.accessToken,
contentType: "application/json",
}),
},
headers,
body: JSON.stringify(values),
});

Expand Down Expand Up @@ -139,7 +158,7 @@ export function FeedbackForm() {
form="feedback-form"
className="w-full"
loading={submitting}
disabled={submitting || user == null}
disabled={submitting}
>
Send Feedback
</Button>
Expand Down
Loading
Loading