mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Merge branch 'dev' into external-db-sync
This commit is contained in:
commit
85bb8933ec
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classifyClickHouseSqlVsPrompt } from "./classify-query";
|
||||
|
||||
describe("classifyClickHouseSqlVsPrompt", () => {
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user