Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions infra/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
const ENTERPRISE_SALES_INBOX_EMAIL = new sst.Secret("ENTERPRISE_SALES_INBOX_EMAIL")
// OpenCode API key for Jev spam scoring of enterprise form submissions.
const ENTERPRISE_SPAM_API_KEY = new sst.Secret("ENTERPRISE_SPAM_API_KEY")
// hCaptcha guards the enterprise form; the site key is public and baked into the client bundle.
const HCAPTCHA_SITE_KEY = new sst.Secret("HCAPTCHA_SITE_KEY")
const HCAPTCHA_SECRET_KEY = new sst.Secret("HCAPTCHA_SECRET_KEY")

const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID")
const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET")
Expand All @@ -43,6 +46,7 @@ new sst.cloudflare.x.SolidStart("Console", {
AWS_SES_SECRET_ACCESS_KEY,
ENTERPRISE_SALES_INBOX_EMAIL,
ENTERPRISE_SPAM_API_KEY,
HCAPTCHA_SECRET_KEY,
SALESFORCE_CLIENT_ID,
SALESFORCE_CLIENT_SECRET,
SALESFORCE_INSTANCE_URL,
Expand All @@ -53,6 +57,7 @@ new sst.cloudflare.x.SolidStart("Console", {
//VITE_DOCS_URL: web.url.apply((url) => url!),
//VITE_API_URL: gateway.url.apply((url) => url!),
VITE_CONSOLE_MARKETING_ENABLED: $app.stage === "dev" ? "true" : "false",
VITE_HCAPTCHA_SITE_KEY: HCAPTCHA_SITE_KEY.value,
},
transform: {
server: {
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "تنسيق البريد الإلكتروني غير صالح.",
"enterprise.form.error.invalidInferenceSpend": "اختر نطاقًا صالحًا للإنفاق على الاستدلال.",
"enterprise.form.error.internalServer": "خطأ داخلي في الخادم.",
"enterprise.form.error.captcha": "فشل التحقق من captcha. يرجى المحاولة مرة أخرى.",
"enterprise.faq.title": "الأسئلة الشائعة",
"enterprise.faq.q1": "ما هو OpenCode Enterprise؟",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Formato de e-mail inválido.",
"enterprise.form.error.invalidInferenceSpend": "Selecione uma faixa de gasto com inferência válida.",
"enterprise.form.error.internalServer": "Erro interno do servidor.",
"enterprise.form.error.captcha": "A verificação captcha falhou. Tente novamente.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "O que é OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Ugyldigt e-mailformat.",
"enterprise.form.error.invalidInferenceSpend": "Vælg et gyldigt interval for inferensforbrug.",
"enterprise.form.error.internalServer": "Intern serverfejl.",
"enterprise.form.error.captcha": "Captcha-verificering mislykkedes. Prøv igen.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Hvad er OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Ungültiges E-Mail-Format.",
"enterprise.form.error.invalidInferenceSpend": "Wählen Sie einen gültigen Bereich für die Inferenzausgaben.",
"enterprise.form.error.internalServer": "Interner Serverfehler.",
"enterprise.form.error.captcha": "Captcha-Überprüfung fehlgeschlagen. Bitte versuchen Sie es erneut.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Was ist OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Invalid email format.",
"enterprise.form.error.invalidInferenceSpend": "Select a valid inference spend range.",
"enterprise.form.error.internalServer": "Internal server error.",
"enterprise.form.error.captcha": "Captcha verification failed. Please try again.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "What is OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Formato de correo inválido.",
"enterprise.form.error.invalidInferenceSpend": "Selecciona un rango de gasto en inferencia válido.",
"enterprise.form.error.internalServer": "Error interno del servidor.",
"enterprise.form.error.captcha": "La verificación captcha falló. Inténtalo de nuevo.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "¿Qué es OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Format d'e-mail invalide.",
"enterprise.form.error.invalidInferenceSpend": "Sélectionnez une tranche de dépenses d'inférence valide.",
"enterprise.form.error.internalServer": "Erreur interne du serveur.",
"enterprise.form.error.captcha": "La vérification captcha a échoué. Veuillez réessayer.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Qu'est-ce que OpenCode Enterprise ?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Formato email non valido.",
"enterprise.form.error.invalidInferenceSpend": "Seleziona una fascia di spesa per l'inferenza valida.",
"enterprise.form.error.internalServer": "Errore interno del server.",
"enterprise.form.error.captcha": "Verifica captcha non riuscita. Riprova.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Cos'è OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "無効なメール形式です。",
"enterprise.form.error.invalidInferenceSpend": "有効な推論費用の範囲を選択してください。",
"enterprise.form.error.internalServer": "内部サーバーエラー。",
"enterprise.form.error.captcha": "CAPTCHA認証に失敗しました。もう一度お試しください。",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "OpenCode Enterpriseとは?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "유효하지 않은 이메일 형식입니다.",
"enterprise.form.error.invalidInferenceSpend": "유효한 추론 비용 범위를 선택하세요.",
"enterprise.form.error.internalServer": "내부 서버 오류입니다.",
"enterprise.form.error.captcha": "캡차 인증에 실패했습니다. 다시 시도해 주세요.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "OpenCode 엔터프라이즈란 무엇인가요?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/no.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Ugyldig e-postformat.",
"enterprise.form.error.invalidInferenceSpend": "Velg et gyldig intervall for inferensforbruk.",
"enterprise.form.error.internalServer": "Intern serverfeil.",
"enterprise.form.error.captcha": "Captcha-verifiseringen mislyktes. Prøv igjen.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Hva er OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Nieprawidłowy format adresu e-mail.",
"enterprise.form.error.invalidInferenceSpend": "Wybierz prawidłowy przedział wydatków na inferencję.",
"enterprise.form.error.internalServer": "Wewnętrzny błąd serwera.",
"enterprise.form.error.captcha": "Weryfikacja captcha nie powiodła się. Spróbuj ponownie.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Czym jest OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Неверный формат email.",
"enterprise.form.error.invalidInferenceSpend": "Выберите допустимый диапазон расходов на инференс.",
"enterprise.form.error.internalServer": "Внутренняя ошибка сервера.",
"enterprise.form.error.captcha": "Не удалось пройти проверку captcha. Попробуйте ещё раз.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Что такое OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "รูปแบบอีเมลไม่ถูกต้อง",
"enterprise.form.error.invalidInferenceSpend": "เลือกช่วงค่าใช้จ่ายด้าน inference ที่ถูกต้อง",
"enterprise.form.error.internalServer": "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์",
"enterprise.form.error.captcha": "การยืนยัน captcha ล้มเหลว โปรดลองอีกครั้ง",
"enterprise.faq.title": "คำถามที่พบบ่อย",
"enterprise.faq.q1": "OpenCode Enterprise คืออะไร?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Geçersiz e-posta formatı.",
"enterprise.form.error.invalidInferenceSpend": "Geçerli bir çıkarım harcaması aralığı seçin.",
"enterprise.form.error.internalServer": "İç sunucu hatası.",
"enterprise.form.error.captcha": "Captcha doğrulaması başarısız oldu. Lütfen tekrar deneyin.",
"enterprise.faq.title": "SSS",
"enterprise.faq.q1": "OpenCode Enterprise nedir?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "Недійсний формат email.",
"enterprise.form.error.invalidInferenceSpend": "Виберіть допустимий діапазон витрат на інференс.",
"enterprise.form.error.internalServer": "Внутрішня помилка сервера.",
"enterprise.form.error.captcha": "Не вдалося пройти перевірку captcha. Спробуйте ще раз.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Що таке OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "邮箱格式无效。",
"enterprise.form.error.invalidInferenceSpend": "请选择有效的推理支出范围。",
"enterprise.form.error.internalServer": "内部服务器错误。",
"enterprise.form.error.captcha": "验证码验证失败,请重试。",
"enterprise.faq.title": "常见问题",
"enterprise.faq.q1": "什么是 OpenCode 企业版?",
"enterprise.faq.a1":
Expand Down
1 change: 1 addition & 0 deletions packages/console/app/src/i18n/zht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ export const dict = {
"enterprise.form.error.invalidEmailFormat": "無效的電子郵件格式。",
"enterprise.form.error.invalidInferenceSpend": "請選擇有效的推論支出範圍。",
"enterprise.form.error.internalServer": "內部伺服器錯誤。",
"enterprise.form.error.captcha": "驗證碼驗證失敗,請再試一次。",
"enterprise.faq.title": "常見問題",
"enterprise.faq.q1": "什麼是 OpenCode Enterprise?",
"enterprise.faq.a1":
Expand Down
3 changes: 2 additions & 1 deletion packages/console/app/src/lib/enterprise-spam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ const submit = async (fetch: (request: Request) => Promise<Response>) => {
new Request("http://localhost/api/enterprise", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...submission, alias: "" }),
body: JSON.stringify({ ...submission, alias: "", captcha: "test-token" }),
}),
{
spam: { inferenceUrl: "https://opencode.test/inference", apiKey: "test-key", fetch },
captcha: { secret: "test-secret", fetch: async () => Response.json({ success: true }) },
salesforceSubmit: async (payload) => {
leads.push(payload)
return true
Expand Down
15 changes: 15 additions & 0 deletions packages/console/app/src/lib/enterprise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createLead } from "~/lib/salesforce"
import { inferenceSpendOptions } from "~/lib/inference-spend"
import { lookupConsoleAccount, type ConsoleAccount } from "~/lib/console-account"
import { ENTERPRISE_SPAM_THRESHOLD, enterpriseSpamProbability } from "~/lib/enterprise-spam"
import { verifyHcaptcha } from "~/lib/hcaptcha"

interface EnterpriseFormData {
name: string
Expand All @@ -16,6 +17,7 @@ interface EnterpriseFormData {
inferenceSpend?: string
alias?: string
message: string
captcha?: string
}

const EMAIL_OCTOPUS_LIST_ID = "1b381e5e-39bd-11f1-ba4a-cdd4791f0c43"
Expand Down Expand Up @@ -98,6 +100,7 @@ export async function handleEnterpriseRequest(
notifications?: boolean
accountLookup?: (email: string) => Promise<ConsoleAccount | null>
spam?: Parameters<typeof enterpriseSpamProbability>[1]
captcha?: Parameters<typeof verifyHcaptcha>[1]
} = {},
) {
const dict = i18n(localeFromRequest(request))
Expand All @@ -123,6 +126,18 @@ export async function handleEnterpriseRequest(
return Response.json({ error: dict["enterprise.form.error.invalidInferenceSpend"] }, { status: 400 })
}

if (typeof body.captcha !== "string" || !body.captcha) {
return Response.json({ error: dict["enterprise.form.error.captcha"] }, { status: 400 })
}
// Fail open when hCaptcha itself is unavailable; only a rejected token blocks the lead.
const human = await verifyHcaptcha(body.captcha, {
remoteip: request.headers.get("cf-connecting-ip") ?? undefined,
...options.captcha,
})
if (human === false) {
return Response.json({ error: dict["enterprise.form.error.captcha"] }, { status: 400 })
}

// Fail open: a missing score never blocks a lead. Confident spam gets the same
// response as the honeypot and never reaches Salesforce, email, or EmailOctopus.
const spam = await enterpriseSpamProbability(body, options.spam)
Expand Down
99 changes: 99 additions & 0 deletions packages/console/app/src/lib/hcaptcha.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { afterEach, expect, mock, spyOn, test } from "bun:test"
import { verifyHcaptcha } from "./hcaptcha"
import { handleEnterpriseRequest } from "./enterprise"

afterEach(() => mock.restore())

test("verifies a token with hCaptcha siteverify", async () => {
const requests: Request[] = []
const result = await verifyHcaptcha("token-123", {
secret: "test-secret",
remoteip: "203.0.113.7",
fetch: async (request) => {
requests.push(request)
return Response.json({ success: true })
},
})

expect(result).toBe(true)
expect(requests[0].url).toBe("https://api.hcaptcha.com/siteverify")
const body = new URLSearchParams(await requests[0].text())
expect(body.get("secret")).toBe("test-secret")
expect(body.get("response")).toBe("token-123")
expect(body.get("remoteip")).toBe("203.0.113.7")
})

test("rejects tokens hCaptcha says are invalid", async () => {
spyOn(console, "warn").mockImplementation(() => {})
const result = await verifyHcaptcha("bad", {
secret: "test-secret",
fetch: async () => Response.json({ success: false, "error-codes": ["invalid-input-response"] }),
})
expect(result).toBe(false)
})

test("returns undefined when hCaptcha is unreachable or misconfigured", async () => {
spyOn(console, "error").mockImplementation(() => {})
const failures = [
async () => new Response("down", { status: 503 }),
async () => {
throw new Error("network")
},
async () => Response.json({ success: false, "error-codes": ["invalid-input-secret"] }),
(request: Request) =>
new Promise<Response>((_, reject) =>
request.signal.addEventListener("abort", () => reject(request.signal.reason)),
),
]
for (const fetch of failures) {
expect(await verifyHcaptcha("token", { secret: "test-secret", timeoutMs: 20, fetch })).toBeUndefined()
}
})

const submit = async (captcha: string | undefined, verify: () => Promise<Response>) => {
const leads: Record<string, unknown>[] = []
const response = await handleEnterpriseRequest(
new Request("http://localhost/api/enterprise", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Fake Visitor",
role: "Engineering lead",
email: "fake-visitor@example.test",
message: "We need SSO for 200 developers.",
captcha,
}),
}),
{
captcha: { secret: "test-secret", fetch: verify },
spam: { inferenceUrl: "" },
salesforceSubmit: async (payload) => {
leads.push(payload)
return true
},
notifications: false,
accountLookup: async () => null,
},
)
return { status: response.status, leads }
}

test("the enterprise form rejects missing or invalid captcha tokens before creating a lead", async () => {
spyOn(console, "warn").mockImplementation(() => {})
const verified: string[] = []
const missing = await submit(undefined, async () => {
verified.push("called")
return Response.json({ success: true })
})
expect(missing).toEqual({ status: 400, leads: [] })
expect(verified).toHaveLength(0)

const invalid = await submit("bad", async () => Response.json({ success: false }))
expect(invalid).toEqual({ status: 400, leads: [] })
})

test("the enterprise form accepts verified tokens and fails open when hCaptcha is down", async () => {
spyOn(console, "error").mockImplementation(() => {})
expect((await submit("good", async () => Response.json({ success: true }))).leads).toHaveLength(1)
expect((await submit("good", async () => new Response("down", { status: 503 }))).leads).toHaveLength(1)
})
44 changes: 44 additions & 0 deletions packages/console/app/src/lib/hcaptcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Resource } from "@opencode/console-resource"

const TIMEOUT_MS = 3000
const CONFIG_ERRORS = new Set(["missing-input-secret", "invalid-input-secret", "sitekey-secret-mismatch"])

// Returns undefined when hCaptcha is unreachable so the caller can fail open.
export async function verifyHcaptcha(
token: string,
options: {
fetch?: (request: Request) => Promise<Response>
secret?: string
remoteip?: string
timeoutMs?: number
} = {},
): Promise<boolean | undefined> {
try {
const body = new URLSearchParams({ secret: options.secret ?? Resource.HCAPTCHA_SECRET_KEY.value, response: token })
if (options.remoteip) body.set("remoteip", options.remoteip)
const response = await (options.fetch ?? fetch)(
new Request("https://api.hcaptcha.com/siteverify", {
method: "POST",
body,
signal: AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS),
}),
)
if (!response.ok) {
console.error("hCaptcha verification failed:", response.status)
return undefined
}
const data = (await response.json()) as { success?: unknown; "error-codes"?: unknown }
if (data.success === true) return true
const errors = Array.isArray(data["error-codes"]) ? data["error-codes"] : []
// A broken secret or site key is our misconfiguration, not a bot, so it fails open too.
if (errors.some((code) => CONFIG_ERRORS.has(code))) {
console.error("hCaptcha is misconfigured:", errors)
return undefined
}
console.warn("hCaptcha rejected token", { errors })
return false
} catch (error) {
console.error("hCaptcha verification unavailable:", error instanceof Error ? error.name : "unknown")
return undefined
}
}
2 changes: 2 additions & 0 deletions packages/console/app/src/lib/salesforce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ test("the local Enterprise request handler applies the updated mapping to a form
inferenceSpend: "50k-100k",
message: " How are you bro!\nThis is a local form test. ",
alias: "",
captcha: "test-token",
}),
}),
{
captcha: { secret: "test-secret", fetch: async () => Response.json({ success: true }) },
salesforceSubmit: async (payload) => {
const response = await fetch(`${server.url.origin}/services/data/v59.0/sobjects/Lead`, {
method: "POST",
Expand Down
Loading
Loading