diff --git a/infra/console.ts b/infra/console.ts index 0b35732f2290..370bde42f869 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -1,20 +1,6 @@ import { domain } from "./stage" import { EMAILOCTOPUS_API_KEY } from "./app" -// Preview stages have no new Console or inference deployment to forward to. -const migrationDomain = - $app.stage === "production" ? "opencode.ai" : $app.stage === "dev" ? "dev.opencode.ai" : undefined -const consoleMigration = new sst.Linkable("ConsoleMigration", { - properties: { - consoleUrl: migrationDomain ? `https://${migrationDomain}/console` : "", - inferenceUrl: migrationDomain ? `https://${migrationDomain}/inference` : "", - }, -}) - -//////////////// -// CONSOLE -//////////////// - // Archived Zen request/response dumps (writer removed May 2026). Retained on purpose; nothing reads or writes them. new sst.cloudflare.Bucket("ZenData") new sst.cloudflare.Bucket("ZenDataNew") @@ -29,16 +15,11 @@ 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") const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL") -const CONSOLE_CRM_TOKEN = new sst.Secret("CONSOLE_CRM_TOKEN") -const CONSOLE_CRM_LOOKUP_URL = new sst.Linkable("CONSOLE_CRM_LOOKUP_URL", { - properties: { value: `https://${domain}/console/api/internal/console-account` }, -}) new sst.cloudflare.x.SolidStart("Console", { domain, path: "packages/console/app", link: [ - consoleMigration, EMAILOCTOPUS_API_KEY, AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY, @@ -47,8 +28,6 @@ new sst.cloudflare.x.SolidStart("Console", { SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET, SALESFORCE_INSTANCE_URL, - CONSOLE_CRM_TOKEN, - CONSOLE_CRM_LOOKUP_URL, ], environment: { //VITE_DOCS_URL: web.url.apply((url) => url!), @@ -65,12 +44,6 @@ new sst.cloudflare.x.SolidStart("Console", { ...(flags ?? []), "global_fetch_strictly_public", ]) - // Same-zone fetch to inference re-enters as a public request and loses the client IP. - if (migrationDomain) - args.bindings = $resolve(args.bindings).apply((bindings) => [ - ...(bindings ?? []), - { type: "service", name: "INFERENCE", service: `opencode-inference-${$app.stage}` }, - ]) }, }, }, diff --git a/packages/console/app/script/dev-enterprise.ts b/packages/console/app/script/dev-enterprise.ts index 6666541ce80b..0c009d072474 100644 --- a/packages/console/app/script/dev-enterprise.ts +++ b/packages/console/app/script/dev-enterprise.ts @@ -1,19 +1,10 @@ import { handleEnterpriseRequest } from "../src/lib/enterprise" import { z } from "zod" import { SalesforceCliError, salesforceCliResponse } from "./salesforce-cli-response" -import { handleConsoleSignup } from "../src/lib/console-signup" -import { consoleAccountSchema } from "../src/lib/console-account" const org = process.env.SF_TARGET_ORG || "opencode" -const crmToken = process.env.CONSOLE_CRM_TOKEN -if (!crmToken) throw new Error("Set CONSOLE_CRM_TOKEN to the shared local Console CRM token") -const accountUrl = process.env.CONSOLE_CRM_LOOKUP_URL || "http://localhost:3100/console/api/internal/console-account" -async function authenticatedRequest( - path: string, - payload?: Record, - method: "GET" | "POST" | "PATCH" = payload ? "POST" : "GET", -) { +async function authenticatedRequest(path: string, payload?: Record) { const child = Bun.spawn( [ "sf", @@ -25,7 +16,7 @@ async function authenticatedRequest( org, "--json", "--method", - method, + payload ? "POST" : "GET", ...(payload ? ["--body", "-", "--header", "Content-Type:application/json"] : []), ], { @@ -52,11 +43,6 @@ const server = Bun.serve({ if (path === "/health" && request.method === "GET") { return Response.json({ handler: "local-sst-enterprise", salesforceOrg: org }) } - if (path === "/api/console-signup" && request.method === "POST") { - return handleConsoleSignup(request, crmToken, (path, method, payload) => - authenticatedRequest(path, payload, method), - ) - } if (path !== "/api/enterprise") return new Response("Not found", { status: 404 }) if (request.method !== "POST") return new Response("Method not allowed", { status: 405 }) const failures: string[] = [] @@ -73,16 +59,6 @@ const server = Bun.serve({ return z.object({ success: z.literal(true) }).safeParse(result).success }, notifications: false, - accountLookup: async (email) => { - const response = await fetch(accountUrl, { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${crmToken}` }, - body: JSON.stringify({ email }), - signal: AbortSignal.timeout(8000), - }) - if (!response.ok) throw new Error(`Local Console account lookup failed (${response.status})`) - return z.object({ account: consoleAccountSchema.nullable() }).parse(await response.json()).account - }, }) if (failures.length > 0) { return Response.json( diff --git a/packages/console/app/src/lib/console-account.ts b/packages/console/app/src/lib/console-account.ts deleted file mode 100644 index 7636c5e35f7a..000000000000 --- a/packages/console/app/src/lib/console-account.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Resource } from "@opencode/console-resource" -import { z } from "zod" - -export const consoleAccountSchema = z.object({ - id: z.string().min(1).max(255), - email: z.email(), - name: z.string().nullable(), - createdAt: z.iso.datetime(), -}) -export type ConsoleAccount = z.infer - -export async function lookupConsoleAccount(email: string): Promise { - const response = await fetch(`${Resource.CONSOLE_CRM_LOOKUP_URL.value}`, { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${Resource.CONSOLE_CRM_TOKEN.value}` }, - body: JSON.stringify({ email }), - signal: AbortSignal.timeout(3000), - }) - if (!response.ok) throw new Error(`Console account lookup failed (${response.status})`) - const result = z.object({ account: consoleAccountSchema.nullable() }).parse(await response.json()) - return result.account -} - -export function consoleAccountFields(account: ConsoleAccount | null) { - if (!account) return { Has_Console_Account__c: false } - return { - Has_Console_Account__c: true, - Console_Account_ID__c: account.id, - Console_Signup_Date__c: account.createdAt, - } -} diff --git a/packages/console/app/src/lib/console-signup.test.ts b/packages/console/app/src/lib/console-signup.test.ts deleted file mode 100644 index e0b6511efcd8..000000000000 --- a/packages/console/app/src/lib/console-signup.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { afterEach, expect, test } from "bun:test" -import { syncConsoleAccount, type SalesforceRequest } from "./salesforce" -import { handleConsoleSignup } from "./console-signup" -import { z } from "zod" - -const account = { - id: "user_console_test", - email: "person@example.test", - name: "Test Person", - createdAt: "2026-10-01T10:00:00.000Z", -} -const servers = new Set>() -afterEach(async () => { - await Promise.all([...servers].map((server) => server.stop(true))) - servers.clear() -}) - -function fixture( - initial: { object: "Lead" | "Contact"; Id: string; LeadSource: string; Company?: string; AccountId?: string }[] = [], - email = account.email, -) { - const people: Record[] = initial.map((person) => ({ ...person })) - const writes: { method: string; path: string; payload: Record }[] = [] - const server = Bun.serve({ - hostname: "127.0.0.1", - port: 0, - async fetch(request) { - const url = new URL(request.url) - if (url.pathname.endsWith("/query")) { - const query = url.searchParams.get("q") ?? "" - expect(query).toContain("Console_Account_ID__c") - expect(query).toContain(`Email = '${email.toLowerCase()}'`) - const object = query.includes("FROM Contact") ? "Contact" : "Lead" - if (object === "Lead") expect(query).toContain("IsConverted = false") - return Response.json({ - done: true, - records: people.filter((person) => person.object === object).map((person) => ({ Id: person.Id })), - }) - } - const payload = z.record(z.string(), z.unknown()).parse(await request.json()) - writes.push({ method: request.method, path: url.pathname, payload }) - if (request.method === "PATCH") { - const person = people.find((person) => person.Id === url.pathname.split("/").at(-1)) - if (!person) return Response.json({ error: "Not found" }, { status: 404 }) - Object.assign(person, payload) - return new Response(null, { status: 204 }) - } - const id = "00Q000000000001AAA" - people.push({ object: "Lead", Id: id, ...payload }) - return Response.json({ success: true, id }, { status: 201 }) - }, - }) - servers.add(server) - const request: SalesforceRequest = async (path, method = "GET", payload) => { - const response = await fetch(new URL(path, server.url), { - method, - ...(payload ? { body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } } : {}), - }) - if (!response.ok) throw new Error("CRM request failed") - return response.status === 204 ? null : response.json() - } - return { people, writes, request } -} - -test("a new signup creates one Console lead and an exact retry updates it", async () => { - const crm = fixture() - await syncConsoleAccount(account, crm.request) - await syncConsoleAccount(account, crm.request) - expect(crm.people).toHaveLength(1) - expect(crm.writes.filter((write) => write.method === "POST")).toHaveLength(1) - expect(crm.people[0]).toMatchObject({ - Company: "example.test", - LeadSource: "console signup", - Has_Console_Account__c: true, - Console_Account_ID__c: account.id, - Console_Signup_Date__c: account.createdAt, - }) - expect(crm.writes[1].payload).not.toHaveProperty("LeadSource") - expect(crm.writes[1].payload).not.toHaveProperty("Company") -}) - -test.each([ - ["hello@acme.com", "acme.com"], - ["jane@company.co.uk", "company.co.uk"], - ["HELLO@ACME.COM", "acme.com"], - ["person@gmail.com", "Individual"], - ["person+signup@GOOGLEMAIL.COM", "Individual"], - ["person@outlook.com", "Individual"], - ["person@hotmail.co.uk", "Individual"], - ["person@yahoo.com", "Individual"], - ["person@icloud.com", "Individual"], - ["person@proton.me", "Individual"], - ["person@protonmail.com", "Individual"], - ["person@pm.me", "Individual"], - ["person@aol.com", "Individual"], - ["person@gmail.com.example.test", "gmail.com.example.test"], -])("sets Company for a new signup with %s to %s", async (email, company) => { - const crm = fixture([], email) - await syncConsoleAccount({ ...account, email }, crm.request) - expect(crm.people).toHaveLength(1) - expect(crm.people[0]).toMatchObject({ Company: company, Email: email.toLowerCase(), LeadSource: "console signup" }) -}) - -test("matching Leads and Contacts keep their source and receive the Console fields", async () => { - const crm = fixture([ - { object: "Contact", Id: "003000000000001AAA", LeadSource: "Partner", AccountId: "001000000000001AAA" }, - { object: "Lead", Id: "00Q000000000002AAA", LeadSource: "enterprise website form", Company: "Visitor's Company" }, - ]) - await syncConsoleAccount(account, crm.request) - expect(crm.writes.every((write) => write.method === "PATCH")).toBe(true) - expect(crm.people.map((person) => person.LeadSource)).toEqual(["Partner", "enterprise website form"]) - expect(crm.people[0].AccountId).toBe("001000000000001AAA") - expect(crm.people[1].Company).toBe("Visitor's Company") - for (const write of crm.writes) { - expect(write.payload).not.toHaveProperty("Company") - expect(write.payload).not.toHaveProperty("AccountId") - } - for (const person of crm.people) - expect(person).toMatchObject({ - Has_Console_Account__c: true, - Console_Account_ID__c: account.id, - Console_Signup_Date__c: account.createdAt, - }) -}) - -test("a converted person's Contact is updated without creating another Lead", async () => { - const crm = fixture([{ object: "Contact", Id: "003000000000001AAA", LeadSource: "Website" }]) - await syncConsoleAccount(account, crm.request) - expect(crm.people).toHaveLength(1) - expect(crm.writes[0].path).toContain("/Contact/") - expect(crm.people[0].LeadSource).toBe("Website") -}) - -test("the signup endpoint requires its server credential before touching Salesforce", async () => { - const crm = fixture() - const response = await handleConsoleSignup( - new Request("http://localhost/api/console-signup", { method: "POST", body: JSON.stringify(account) }), - "test-shared-token", - crm.request, - ) - expect(response.status).toBe(401) - expect(crm.writes).toHaveLength(0) -}) - -test("the authenticated signup endpoint reports confirmed delivery", async () => { - const crm = fixture() - const response = await handleConsoleSignup( - new Request("http://localhost/api/console-signup", { - method: "POST", - headers: { Authorization: "Bearer test-shared-token" }, - body: JSON.stringify(account), - }), - "test-shared-token", - crm.request, - ) - expect(response.status).toBe(200) - expect(await response.json()).toMatchObject({ success: true }) - expect(crm.people).toHaveLength(1) -}) diff --git a/packages/console/app/src/lib/console-signup.ts b/packages/console/app/src/lib/console-signup.ts deleted file mode 100644 index f27a47d33ea3..000000000000 --- a/packages/console/app/src/lib/console-signup.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { timingSafeEqual } from "node:crypto" -import { consoleAccountSchema } from "~/lib/console-account" -import { salesforceRequest, syncConsoleAccount, type SalesforceRequest } from "~/lib/salesforce" - -export async function handleConsoleSignup(request: Request, token: string, salesforce?: SalesforceRequest) { - const expected = Buffer.from(`Bearer ${token}`) - const actual = Buffer.from(request.headers.get("authorization") ?? "") - if (!token || expected.length !== actual.length || !timingSafeEqual(expected, actual)) - return new Response(null, { status: 401 }) - const data: unknown = await request.json().catch(() => null) - const account = consoleAccountSchema.safeParse(data) - if (!account.success) return Response.json({ error: "Invalid Console account" }, { status: 400 }) - return Promise.resolve(salesforce) - .then(async (client) => syncConsoleAccount(account.data, client ?? (await salesforceRequest()))) - .then( - (result) => Response.json(result), - () => Response.json({ error: "Salesforce sync unavailable" }, { status: 503 }), - ) -} diff --git a/packages/console/app/src/lib/enterprise.ts b/packages/console/app/src/lib/enterprise.ts index f5d7e5c5c7ca..9720bd2d1568 100644 --- a/packages/console/app/src/lib/enterprise.ts +++ b/packages/console/app/src/lib/enterprise.ts @@ -4,7 +4,6 @@ import { i18n } from "~/i18n" import { localeFromRequest } from "~/lib/language" import { createLead } from "~/lib/salesforce" import { inferenceSpendOptions } from "~/lib/inference-spend" -import { lookupConsoleAccount, type ConsoleAccount } from "~/lib/console-account" import { verifyHcaptcha } from "~/lib/hcaptcha" interface EnterpriseFormData { @@ -97,7 +96,6 @@ export async function handleEnterpriseRequest( options: { salesforceSubmit?: (payload: Record) => Promise notifications?: boolean - accountLookup?: (email: string) => Promise captcha?: Parameters[1] } = {}, ) { @@ -136,11 +134,6 @@ export async function handleEnterpriseRequest( return Response.json({ error: dict["enterprise.form.error.captcha"] }, { status: 400 }) } - const account = await (options.accountLookup ?? lookupConsoleAccount)(body.email).catch((err) => { - console.error("Failed to look up Console account:", err) - return undefined - }) - const emailContent = enterpriseEmailContent(body, inferenceSpend?.salesforceValue) const [lead, mail, octopus] = await Promise.all([ @@ -153,7 +146,6 @@ export async function handleEnterpriseRequest( phone: body.phone, inferenceSpend: inferenceSpend?.salesforceValue, message: body.message, - consoleAccount: account, }, options.salesforceSubmit, ).catch((err) => { @@ -179,10 +171,6 @@ export async function handleEnterpriseRequest( ]) if (!lead && !mail && !octopus) { - if (import.meta.env.DEV) { - console.warn("Enterprise inquiry accepted in dev mode without integrations", { email: body.email }) - return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 }) - } console.error("Enterprise inquiry delivery failed", { email: body.email }) return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 }) } diff --git a/packages/console/app/src/lib/hcaptcha.test.ts b/packages/console/app/src/lib/hcaptcha.test.ts index ab6ef9bad32a..4e24e356c37e 100644 --- a/packages/console/app/src/lib/hcaptcha.test.ts +++ b/packages/console/app/src/lib/hcaptcha.test.ts @@ -71,7 +71,6 @@ const submit = async (captcha: string | undefined, verify: () => Promise null, }, ) return { status: response.status, leads } diff --git a/packages/console/app/src/lib/inference-proxy.ts b/packages/console/app/src/lib/inference-proxy.ts deleted file mode 100644 index b0a2ec0cb99e..000000000000 --- a/packages/console/app/src/lib/inference-proxy.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Resource } from "@opencode/console-resource" - -const paths: Record = { - "POST /zen/v1/chat/completions": "/openai/v1/chat/completions", - "POST /zen/v1/responses": "/openai/v1/responses", - "POST /zen/v1/messages": "/anthropic/v1/messages", - "POST /zen/v1/systemone": "/systemone/v1/systemone", - "POST /zen/go/v1/chat/completions": "/go/openai/v1/chat/completions", - "POST /zen/go/v1/responses": "/go/openai/v1/responses", - "POST /zen/go/v1/messages": "/go/anthropic/v1/messages", - "POST /zen/go/v1/systemone": "/go/systemone/v1/systemone", - "GET /zen/v1/models": "/v1/models", - "GET /zen/go/v1/models": "/go/v1/models", - "GET /zen/go/v1/usage": "/go/v1/usage", -} - -// New inference owns authentication, model availability, free-tier access, and billing. -export async function proxyInference(request: Request) { - const url = new URL(request.url) - const path = - paths[`${request.method} ${url.pathname}`] ?? - (request.method === "POST" && - /^\/zen\/v1\/models\/[^/]+:(?:generateContent|streamGenerateContent)$/.test(url.pathname) - ? url.pathname.replace("/zen/v1/models/", "/google/v1beta/models/") - : undefined) - if (!path) return new Response(null, { status: 404 }) - - const key = url.pathname.endsWith("/messages") - ? request.headers.get("x-api-key") - : path.startsWith("/google/") - ? request.headers.get("x-goog-api-key") - : request.headers.get("authorization")?.split(" ")[1] - - const destination = new URL(Resource.ConsoleMigration.inferenceUrl) - destination.pathname = `${destination.pathname.replace(/\/$/, "")}${path}` - destination.search = url.search - destination.hash = "" - - const forwarded = new Request(destination, request) - for (const name of ["host", "content-length"]) forwarded.headers.delete(name) - // Keyless requests must reach new inference without credentials; `public` is not an API key there. - const anonymous = !key || key === "public" - if (anonymous) for (const name of ["authorization", "x-api-key", "x-goog-api-key"]) forwarded.headers.delete(name) - if (!anonymous) forwarded.headers.set("authorization", `Bearer ${key}`) - const ip = request.headers.get("cf-connecting-ip") - if (ip) forwarded.headers.set("x-real-ip", ip) - const requestID = request.headers.get("x-opencode-request-id") ?? request.headers.get("x-opencode-request") - if (requestID) forwarded.headers.set("x-opencode-request-id", requestID) - - const response = await (Resource.INFERENCE as { fetch: typeof fetch }) - .fetch(forwarded, { redirect: "manual" }) - .catch(() => undefined) - if (!response) - return Response.json( - { error: { type: "api_error", message: "Inference routing is unavailable. Please retry later." } }, - { status: 503, headers: { "Cache-Control": "no-store" } }, - ) - // SolidStart replaces 404 responses with its own route-not-found body, hiding the inference error. - if (response.status !== 404) return response - return new Response(response.body, { status: 400, statusText: "Bad Request", headers: response.headers }) -} diff --git a/packages/console/app/src/lib/salesforce.test.ts b/packages/console/app/src/lib/salesforce.test.ts index 36cf5419e84b..1f0a8c4df8a9 100644 --- a/packages/console/app/src/lib/salesforce.test.ts +++ b/packages/console/app/src/lib/salesforce.test.ts @@ -91,12 +91,6 @@ test("the local Enterprise request handler applies the updated mapping to a form return response.ok }, notifications: false, - accountLookup: async () => ({ - id: "user_test", - email: "fake-local-visitor@example.test", - name: null, - createdAt: "2026-10-01T12:00:00.000Z", - }), }, ) @@ -107,8 +101,5 @@ test("the local Enterprise request handler applies the updated mapping to a form Description: " How are you bro!\nThis is a local form test. ", Current_Monthly_Inference_Spend__c: "$50K–$100K", LeadSource: "enterprise website form", - Has_Console_Account__c: true, - Console_Account_ID__c: "user_test", - Console_Signup_Date__c: "2026-10-01T12:00:00.000Z", }) }) diff --git a/packages/console/app/src/lib/salesforce.ts b/packages/console/app/src/lib/salesforce.ts index 6ffd395c8269..73c7b87e3997 100644 --- a/packages/console/app/src/lib/salesforce.ts +++ b/packages/console/app/src/lib/salesforce.ts @@ -1,54 +1,4 @@ import { Resource } from "@opencode/console-resource" -import { z } from "zod" -import { consoleAccountFields, type ConsoleAccount } from "./console-account" - -// Match provider domains exactly; business domains hosted by these providers remain business addresses. -const personalEmailDomains = new Set([ - "gmail.com", - "googlemail.com", - "outlook.com", - "outlook.co.uk", - "hotmail.com", - "hotmail.co.uk", - "hotmail.fr", - "hotmail.de", - "hotmail.it", - "live.com", - "live.co.uk", - "live.ca", - "live.com.au", - "msn.com", - "yahoo.com", - "yahoo.co.uk", - "yahoo.co.in", - "yahoo.com.au", - "yahoo.ca", - "yahoo.fr", - "yahoo.de", - "yahoo.it", - "yahoo.co.jp", - "ymail.com", - "rocketmail.com", - "icloud.com", - "me.com", - "mac.com", - "proton.me", - "protonmail.com", - "pm.me", - "aol.com", - "aim.com", - "gmx.com", - "gmx.de", - "gmx.net", - "mail.com", - "fastmail.com", - "fastmail.fm", - "hey.com", - "tuta.com", - "tuta.io", - "tutanota.com", - "tutanota.de", -]) async function login() { const url = Resource.SALESFORCE_INSTANCE_URL.value.replace(/\/$/, "") @@ -96,7 +46,6 @@ export interface SalesforceLeadInput { phone?: string inferenceSpend?: string message: string - consoleAccount?: ConsoleAccount | null } export async function createLead( @@ -112,7 +61,6 @@ export async function createLead( Description: input.message, Current_Monthly_Inference_Spend__c: input.inferenceSpend, LeadSource: "enterprise website form", - ...(input.consoleAccount === undefined ? {} : consoleAccountFields(input.consoleAccount)), } if (submit) return submit(payload) const auth = await login() @@ -138,65 +86,3 @@ export async function createLead( return true } - -export type SalesforceRequest = ( - path: string, - method?: "GET" | "POST" | "PATCH", - payload?: Record, -) => Promise - -export async function salesforceRequest(): Promise { - const auth = await login() - if (!auth) throw new Error("Salesforce authentication unavailable") - return async (path, method = "GET", payload) => { - const response = await fetch(`${auth.url}${path}`, { - method, - headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json" }, - ...(payload ? { body: JSON.stringify(payload) } : {}), - signal: AbortSignal.timeout(8000), - }) - if (!response.ok) throw new Error(`Salesforce request failed (${response.status})`) - if (response.status === 204) return null - return response.json() - } -} - -/** Update existing people without changing their acquisition source or sales notes. */ -export async function syncConsoleAccount(account: ConsoleAccount, request: SalesforceRequest) { - const quote = (value: string) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'") - const matches = async (object: "Contact" | "Lead") => { - const query = `SELECT Id FROM ${object} WHERE ${object === "Lead" ? "IsConverted = false AND " : ""}(Console_Account_ID__c = '${quote(account.id)}' OR Email = '${quote(account.email.toLowerCase())}')` - const records: { Id: string }[] = [] - let path: string | undefined = `/services/data/v59.0/query?q=${encodeURIComponent(query)}` - while (path) { - const result = z - .object({ - records: z.array(z.object({ Id: z.string().regex(/^[a-zA-Z0-9]{15,18}$/) })), - done: z.boolean(), - nextRecordsUrl: z.string().optional(), - }) - .parse(await request(path)) - records.push(...result.records) - if (result.done) break - if (!result.nextRecordsUrl?.startsWith("/services/data/")) throw new Error("Salesforce query pagination missing") - path = result.nextRecordsUrl - } - return records.map((record) => ({ object, id: record.Id })) - } - const people = [...(await matches("Contact")), ...(await matches("Lead"))] - const fields = consoleAccountFields(account) - for (const person of people) - await request(`/services/data/v59.0/sobjects/${person.object}/${person.id}`, "PATCH", fields) - if (people.length > 0) return { success: true, created: false, updated: people.length } - const domain = account.email.slice(account.email.lastIndexOf("@") + 1).toLowerCase() - const result = z.object({ success: z.literal(true), id: z.string() }).parse( - await request("/services/data/v59.0/sobjects/Lead", "POST", { - LastName: (account.name?.trim() || account.email).slice(0, 80), - Company: personalEmailDomains.has(domain) ? "Individual" : domain, - Email: account.email.toLowerCase(), - LeadSource: "console signup", - ...fields, - }), - ) - return { success: true, created: true, id: result.id } -} diff --git a/packages/console/app/src/middleware.ts b/packages/console/app/src/middleware.ts index 8591165a531f..195f6ec5adb8 100644 --- a/packages/console/app/src/middleware.ts +++ b/packages/console/app/src/middleware.ts @@ -1,5 +1,4 @@ import { createMiddleware } from "@solidjs/start/middleware" -import { Resource } from "@opencode/console-resource" import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language" import { sanitizeServerActionRequest } from "~/lib/server-action" @@ -36,10 +35,10 @@ export default createMiddleware({ return new Response(null, { status: 301, headers: { Location: locale ? `/${locale}/go` : "/go" } }) // The legacy dashboard and its sign-in moved to the new Console. - if (/^\/(auth|workspace)(\/|$)/.test(url.pathname) && Resource.ConsoleMigration.consoleUrl) + if (/^\/(auth|workspace)(\/|$)/.test(url.pathname)) return new Response(null, { status: 302, - headers: { Location: `${Resource.ConsoleMigration.consoleUrl}/login`, "Cache-Control": "no-store" }, + headers: { Location: "https://opencode.ai/console/login", "Cache-Control": "no-store" }, }) }, }) diff --git a/packages/console/app/src/routes/api/console-signup.ts b/packages/console/app/src/routes/api/console-signup.ts deleted file mode 100644 index ca80d8052197..000000000000 --- a/packages/console/app/src/routes/api/console-signup.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { Resource } from "@opencode/console-resource" -import { handleConsoleSignup } from "~/lib/console-signup" - -// SolidStart keeps only the method export from API routes, so the handler lives in lib. -export async function POST(event: APIEvent) { - return handleConsoleSignup(event.request, Resource.CONSOLE_CRM_TOKEN.value) -} diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts index 1ecd1582474a..c2048552fcde 100644 --- a/packages/console/app/src/routes/api/enterprise.ts +++ b/packages/console/app/src/routes/api/enterprise.ts @@ -1,7 +1,16 @@ import type { APIEvent } from "@solidjs/start/server" +import { Resource } from "@opencode/console-resource" +import { i18n } from "~/i18n" import { handleEnterpriseRequest } from "~/lib/enterprise" +import { localeFromRequest } from "~/lib/language" // SolidStart keeps only the method export from API routes, so the handler lives in lib. export async function POST(event: APIEvent) { + // Outside production, accept submissions without reaching spam scoring, Salesforce, email, or EmailOctopus. + if (import.meta.env.DEV || Resource.App.stage !== "production") + return Response.json({ + success: true, + message: i18n(localeFromRequest(event.request))["enterprise.form.success.submitted"], + }) return handleEnterpriseRequest(event.request) } diff --git a/packages/console/app/src/routes/zen/go/v1/chat/completions.ts b/packages/console/app/src/routes/zen/go/v1/chat/completions.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/go/v1/chat/completions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/go/v1/messages.ts b/packages/console/app/src/routes/zen/go/v1/messages.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/go/v1/messages.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/go/v1/models.ts b/packages/console/app/src/routes/zen/go/v1/models.ts deleted file mode 100644 index b9fe76d85cd2..000000000000 --- a/packages/console/app/src/routes/zen/go/v1/models.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { buildOptionsResponse } from "../../util/modelsHandler" -import { proxyInference } from "~/lib/inference-proxy" - -export async function OPTIONS(_input: APIEvent) { - return buildOptionsResponse() -} - -export function GET(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/go/v1/responses.ts b/packages/console/app/src/routes/zen/go/v1/responses.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/go/v1/responses.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/go/v1/systemone.ts b/packages/console/app/src/routes/zen/go/v1/systemone.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/go/v1/systemone.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/go/v1/usage.ts b/packages/console/app/src/routes/zen/go/v1/usage.ts deleted file mode 100644 index c6192920e7d9..000000000000 --- a/packages/console/app/src/routes/zen/go/v1/usage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function GET(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/util/modelsHandler.ts b/packages/console/app/src/routes/zen/util/modelsHandler.ts deleted file mode 100644 index 21a81c98f3ea..000000000000 --- a/packages/console/app/src/routes/zen/util/modelsHandler.ts +++ /dev/null @@ -1,10 +0,0 @@ -export async function buildOptionsResponse() { - return new Response(null, { - status: 200, - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization", - }, - }) -} diff --git a/packages/console/app/src/routes/zen/v1/chat/completions.ts b/packages/console/app/src/routes/zen/v1/chat/completions.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/v1/chat/completions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/v1/messages.ts b/packages/console/app/src/routes/zen/v1/messages.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/v1/messages.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts deleted file mode 100644 index 7acb6c4e26d8..000000000000 --- a/packages/console/app/src/routes/zen/v1/models.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { buildOptionsResponse } from "~/routes/zen/util/modelsHandler" -import { proxyInference } from "~/lib/inference-proxy" - -export async function OPTIONS(_input: APIEvent) { - return buildOptionsResponse() -} - -export function GET(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/v1/models/[model].ts b/packages/console/app/src/routes/zen/v1/models/[model].ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/v1/models/[model].ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/v1/responses.ts b/packages/console/app/src/routes/zen/v1/responses.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/v1/responses.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/packages/console/app/src/routes/zen/v1/systemone.ts b/packages/console/app/src/routes/zen/v1/systemone.ts deleted file mode 100644 index 71e39e27bd3f..000000000000 --- a/packages/console/app/src/routes/zen/v1/systemone.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { proxyInference } from "~/lib/inference-proxy" - -export function POST(input: APIEvent) { - return proxyInference(input.request) -} diff --git a/salesforce/README.md b/salesforce/README.md index d98a7dd98850..2afe8a7d9741 100644 --- a/salesforce/README.md +++ b/salesforce/README.md @@ -13,7 +13,7 @@ Lead fields to Contact on conversion. Existing records are not backfilled. New signup Leads use the lowercased email domain as Company (for example, `acme.com`). Recognized personal-email domains use `Individual`. This is an exact-domain list in -`packages/console/app/src/lib/salesforce.ts`, not a legal-company-name lookup. +the Console repo (`packages/core/src/crm.ts`), not a legal-company-name lookup. Signup sync preserves existing Lead companies and Contact account associations; Enterprise inquiries use the company supplied by the visitor. @@ -28,11 +28,10 @@ sf project deploy start --source-dir force-app --target-org opencode --wait 10 sf org assign permset --name Console_Account_Sync --target-org opencode ``` -The application integration uses the existing SST Salesforce credentials. Set the -same `CONSOLE_CRM_TOKEN` on the SST website and Console API. The SST website -links `CONSOLE_CRM_LOOKUP_URL` to the stage's private Console account lookup. +The Console writes these fields when someone signs up. Enterprise inquiry Leads from +the website no longer receive them from the website; Salesforce automation sets them. For local development, run `bun run dev:enterprise` in `packages/console/app` with -`CONSOLE_CRM_TOKEN` set and `sf` on PATH. It serves the same Enterprise request and -signup-sync handlers on `127.0.0.1:3101`, using the CLI's authenticated transport. -It never sends local sales notifications or EmailOctopus subscriptions. +`sf` on PATH. It serves the same Enterprise request handler on `127.0.0.1:3101`, using +the CLI's authenticated transport. It never sends local sales notifications or +EmailOctopus subscriptions.