Merge branch 'dev' into external-db-sync

This commit is contained in:
BilalG1 2026-01-30 16:42:54 -08:00 committed by GitHub
commit 85bb8933ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 65 additions and 24 deletions

View File

@ -84,6 +84,7 @@ STACK_QSTASH_NEXT_SIGNING_KEY=
# Email monitor
STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=# enter the resend poller api key here
STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=# enter the resend domain that should receive the emails
STACK_EMAIL_MONITOR_PROJECT_ID=# enter the project id for the project that the email monitor will attempt to sign up for
STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=# enter the publishable client key for email monitor to use when attempting a sign up
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=# enter a valid verification callback url for the project that the email monitor will attempt to sign up for
STACK_EMAIL_MONITOR_INBUCKET_API_URL=# enter a valid inbucket api url for the email monitor to check emails from in test mode

View File

@ -59,6 +59,7 @@ STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret
# Email monitor configuration for tests
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:8101/handler/email-verification
STACK_EMAIL_MONITOR_PROJECT_ID=internal
STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=stack-generated.example.com
STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=this-is-a-fake-key

View File

@ -69,7 +69,7 @@ const performSignUp = async (email: string, password: string) => {
"Content-Type": "application/json",
"X-Stack-Access-Type": "client",
"X-Stack-Publishable-Client-Key": getEnvVariable("STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY"),
"X-Stack-Project-Id": "internal",
"X-Stack-Project-Id": getEnvVariable("STACK_EMAIL_MONITOR_PROJECT_ID"),
},
body: JSON.stringify({
email,

View File

@ -620,15 +620,18 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
}
const BLOCKED_PROJECT_ID = "2397ef60-a33e-4efb-ad9b-300da67ee29e";
const BLOCKED_DOMAIN = "gsmoal.com";
const BLOCKED_DOMAINS = ["gsmoal.com", "virgilian.com"];
if (context.tenancy.project.id === BLOCKED_PROJECT_ID) {
for (const email of resolution.emails) {
const emailDomain = email.split("@")[1]?.toLowerCase();
if (emailDomain === BLOCKED_DOMAIN || emailDomain.endsWith(`.${BLOCKED_DOMAIN}`)) {
console.warn(`[email-queue] Blocked email to ${email} from project ${BLOCKED_PROJECT_ID} — domain @${BLOCKED_DOMAIN} (or subdomain) is blocked for this project`);
const blockedDomain = emailDomain
? BLOCKED_DOMAINS.find((domain) => emailDomain === domain || emailDomain.endsWith(`.${domain}`))
: undefined;
if (blockedDomain) {
console.warn(`[email-queue] Blocked email to ${email} from project ${BLOCKED_PROJECT_ID} — domain @${blockedDomain} (or subdomain) is blocked for this project`);
await markSkipped(row, EmailOutboxSkippedReason.LIKELY_NOT_DELIVERABLE, {
reason: "domain_blocked_for_project",
blockedDomain: BLOCKED_DOMAIN,
blockedDomain,
email,
});
return;

View File

@ -1,5 +1,6 @@
import "../polyfills";
import { recordRequestStats } from "@/lib/dev-request-stats";
import * as Sentry from "@sentry/nextjs";
import { EndpointDocumentation } from "@stackframe/stack-shared/dist/crud";
import { KnownError, KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
@ -10,7 +11,6 @@ import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/pro
import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry";
import { NextRequest } from "next/server";
import * as yup from "yup";
import { recordRequestStats } from "@/lib/dev-request-stats";
import { DeepPartialSmartRequestWithSentinel, MergeSmartRequest, SmartRequest, createSmartRequest, validateSmartRequest } from "./smart-request";
import { SmartResponse, createResponse, validateSmartResponse } from "./smart-response";
@ -103,7 +103,8 @@ export function handleApiRequest(handler: (req: NextRequest, options: any, reque
}
// request duration warning
if (req.nextUrl.pathname !== "/api/latest/internal/email-queue-step") {
const allowedLongRequestPaths = ["/api/latest/internal/email-queue-step", "/api/latest/internal/analytics/query", "/health/email", "/api/latest/internal/metrics"];
if (!allowedLongRequestPaths.includes(req.nextUrl.pathname)) {
const warnAfterSeconds = 12;
runAsynchronously(async () => {
await wait(warnAfterSeconds * 1000);

View File

@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import { classifyClickHouseSqlVsPrompt } from "./classify-query";
describe("classifyClickHouseSqlVsPrompt", () => {

View File

@ -11,7 +11,9 @@ export function classifyClickHouseSqlVsPrompt(input: unknown): {
const raw = (input ?? "").toString();
const s = raw.trim();
if (!s) return { kind: "prompt", confidence: 0.5, reasons: ["empty"] };
if (!s) {
return { kind: "prompt", confidence: 0.5, reasons: ["empty"] };
}
// Strip code fences if someone pasted markdown
const unfenced = s.replace(/^```[\w-]*\n([\s\S]*?)\n```$/m, "$1").trim();
@ -25,15 +27,28 @@ export function classifyClickHouseSqlVsPrompt(input: unknown): {
const reasons: string[] = [];
// 1) Strong starts (high signal)
const startsWithSql = /^(with|select|insert|update|delete|alter|create|drop|truncate|show|describe|desc|explain|use|set)\b/i.test(unfenced);
if (startsWithSql) { sql += 3; reasons.push("starts-with-sql-keyword"); }
const startsWithSqlKeyword = /^(with|select|insert|update|delete|alter|create|drop|truncate|describe|desc|explain|use|set)\b/i.test(unfenced);
const showEnglishLead = /^show\s+(me|us|my|our|your|them|him|her)\b/i.test(unfenced);
const showHasSqlTarget = /^show\s+(tables?|databases?|columns?|create|processlist|functions?|settings|grants|roles|quotas|dictionary|dictionaries|clusters|indexes|partitions|privileges|users?)\b/i.test(unfenced);
const startsWithShowSql = /^show\b/i.test(unfenced) && showHasSqlTarget && !showEnglishLead;
const startsWithSql = startsWithSqlKeyword || startsWithShowSql;
if (startsWithSql) {
sql += 3;
reasons.push("starts-with-sql-keyword");
}
// 2) Structural patterns (very strong)
const hasSelectFrom = /\bselect\b[\s\S]{0,300}\bfrom\b/i.test(unfenced);
if (hasSelectFrom) { sql += 4; reasons.push("select-from-structure"); }
if (hasSelectFrom) {
sql += 4;
reasons.push("select-from-structure");
}
const hasInsertInto = /\binsert\b[\s\S]{0,80}\binto\b/i.test(unfenced);
if (hasInsertInto) { sql += 4; reasons.push("insert-into-structure"); }
if (hasInsertInto) {
sql += 4;
reasons.push("insert-into-structure");
}
// 3) Common SQL clauses
const clauseHits = [
@ -49,44 +64,64 @@ export function classifyClickHouseSqlVsPrompt(input: unknown): {
"prewhere", "final", "sample", "array", "engine", "partition", "ttl", "distributed", "merge", "replacing", "collapsing",
"materialized", "view", "database", "table", "cluster"
].filter(k => wordSet.has(k));
if (chHits.length) { sql += 2; reasons.push("clickhouse-ish:" + chHits.join(",")); }
if (chHits.length) {
sql += 2;
reasons.push("clickhouse-ish:" + chHits.join(","));
}
// 5) Operator / punctuation density
const opCount = (unfenced.match(/(<=|>=|!=|=|<|>|\b(in|like|ilike|between|and|or)\b)/gi) ?? []).length;
if (opCount >= 2) { sql += 2; reasons.push("many-operators"); }
else if (opCount === 1) { sql += 1; reasons.push("some-operators"); }
if (opCount >= 2) {
sql += 2;
reasons.push("many-operators");
} else if (opCount === 1) {
sql += 1;
reasons.push("some-operators");
}
const punct = (unfenced.match(/[(),;*]/g) ?? []).length;
const punctRatio = punct / Math.max(1, unfenced.length);
if (punctRatio > 0.03) { sql += 1; reasons.push("sql-punctuation-density"); }
if (punctRatio > 0.03) {
sql += 1;
reasons.push("sql-punctuation-density");
}
// 6) Identifier-ish things
if (/`[^`]+`/.test(unfenced) || /"[^"]+"\."[^"]+"/.test(unfenced)) {
sql += 1; reasons.push("quoted-identifiers");
sql += 1;
reasons.push("quoted-identifiers");
}
if (/\b[a-z_]+\.[a-z_]+\b/i.test(unfenced)) {
sql += 1; reasons.push("dot-identifiers");
sql += 1;
reasons.push("dot-identifiers");
}
if (/--|\/\*/.test(unfenced)) {
sql += 1; reasons.push("sql-comments");
sql += 1;
reasons.push("sql-comments");
}
// Prompt-ish features
if (/[?]\s*$/.test(unfenced)) { prompt += 2; reasons.push("ends-with-question-mark"); }
if (/[?]\s*$/.test(unfenced)) {
prompt += 2;
reasons.push("ends-with-question-mark");
}
if (/\b(please|could you|can you|what|why|how|explain|help)\b/i.test(unfenced)) {
prompt += 2; reasons.push("prompt-words");
prompt += 2;
reasons.push("prompt-words");
}
// If it's mostly letters/spaces and barely any operators, lean prompt.
const symbolChars = (unfenced.match(/[^a-z0-9_\s]/gi) ?? []).length;
const symbolRatio = symbolChars / Math.max(1, unfenced.length);
if (symbolRatio < 0.06 && opCount === 0 && !startsWithSql) {
prompt += 2; reasons.push("low-symbol-low-operator");
prompt += 2;
reasons.push("low-symbol-low-operator");
}
// Avoid the classic false positive: "select ..." in English without any SQL structure
if (/^select\b/i.test(unfenced) && !hasSelectFrom && clauseHits.length === 0 && opCount === 0) {
prompt += 3; reasons.push("english-select-false-positive-guard");
prompt += 3;
reasons.push("english-select-false-positive-guard");
}
const margin = sql - prompt;