mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-31 21:51:11 +00:00
chore(console): remove legacy Console leftovers from the website (#57434)
This commit is contained in:
parent
5916014733
commit
772e2aed12
27 changed files with 19 additions and 569 deletions
|
|
@ -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}` },
|
||||
])
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>,
|
||||
method: "GET" | "POST" | "PATCH" = payload ? "POST" : "GET",
|
||||
) {
|
||||
async function authenticatedRequest(path: string, payload?: Record<string, unknown>) {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<typeof consoleAccountSchema>
|
||||
|
||||
export async function lookupConsoleAccount(email: string): Promise<ConsoleAccount | null> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ReturnType<typeof Bun.serve>>()
|
||||
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<string, unknown>[] = initial.map((person) => ({ ...person }))
|
||||
const writes: { method: string; path: string; payload: Record<string, unknown> }[] = []
|
||||
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)
|
||||
})
|
||||
|
|
@ -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 }),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string, unknown>) => Promise<boolean>
|
||||
notifications?: boolean
|
||||
accountLookup?: (email: string) => Promise<ConsoleAccount | null>
|
||||
captcha?: Parameters<typeof verifyHcaptcha>[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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ const submit = async (captcha: string | undefined, verify: () => Promise<Respons
|
|||
return true
|
||||
},
|
||||
notifications: false,
|
||||
accountLookup: async () => null,
|
||||
},
|
||||
)
|
||||
return { status: response.status, leads }
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import { Resource } from "@opencode/console-resource"
|
||||
|
||||
const paths: Record<string, string | undefined> = {
|
||||
"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 })
|
||||
}
|
||||
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>,
|
||||
) => Promise<unknown>
|
||||
|
||||
export async function salesforceRequest(): Promise<SalesforceRequest> {
|
||||
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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue