ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+399
View File
@@ -0,0 +1,399 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The Convex Better Auth adapter of this app (AUTH_PROVIDER=convex-better-auth, joint contract §14),
// over fake transports: the deployment contract, the sign-out (derived service sessions first, then
// Better Auth, then every cookie), the account's end and the deletion's Better Auth step bound in the
// web and the worker, the relay's account events, the form's own password rule, and the locally
// installed component schema with the admin plugin's fields, which is
// generated from the app's Better Auth options. A real Better Auth session on the platform's pinned
// self-hosted Convex runtime, in a browser, is e2e/better-auth (gate J-auth); this file proves no
// live integration. `kit template:prune --auth oidc` removes it.
import { readFileSync } from "node:fs"
import { loadAppEnv, platformClients } from "@app/config"
import { appModules } from "@app/modules"
import { ACCOUNT_IDENTITY_PROVIDER } from "@app/modules/account"
import { buildRegistry, Container } from "@conusai-kit/core"
import { createMemoryDataSource, entitiesOf } from "@conusai-kit/data"
import { requireAppUser, tenancyModule } from "@conusai-kit/tenancy"
import { startFakePlatform } from "@conusai-kit/testing/fake-platform"
import { convexAdapter } from "@convex-dev/better-auth"
import { describe, expect, it, vi } from "vitest"
import { parse } from "yaml"
import { bindModulePorts } from "../apps/worker/src/ports.js"
import { betterAuthOptions, MIN_PASSWORD_LENGTH as OPTIONS_MIN } from "../better-auth/functions/betterAuth/options"
// The composition root is not under test here; better-auth.ts reaches it only to build its singleton.
vi.mock("../apps/web/src/server/app", () => ({
webRuntime: () => {
throw new Error("not in this test")
},
sessionRevoked: async () => undefined,
}))
const {
betterAuthEnv,
betterAuthFormLabels,
bindBetterAuthAccountEnd,
createBetterAuthRuntime,
deliverAccountEvent,
MIN_PASSWORD_LENGTH,
} = await import("../apps/web/src/server/better-auth")
const ENV = {
BETTER_AUTH_CONVEX_URL: "http://cloud.test",
BETTER_AUTH_CONVEX_SITE_URL: "http://site.test/",
BETTER_AUTH_CONVEX_ADMIN_KEY: "admin-key-of-the-test",
BETTER_AUTH_RELAY_INTERVAL_MS: "60000",
CONUSAI_PUBLIC_URL: "http://app.test",
}
const config = { provider: "convex-better-auth" as const, identityNamespace: "better-auth:test" }
describe("the Better Auth deployment contract", () => {
it("reads the deployment's URLs, its admin key and the relay interval", () => {
expect(betterAuthEnv(ENV)).toEqual({
url: "http://cloud.test",
siteUrl: "http://site.test",
adminKey: "admin-key-of-the-test",
relayIntervalMs: 60000,
})
expect(betterAuthEnv({ ...ENV, BETTER_AUTH_RELAY_INTERVAL_MS: "" }).relayIntervalMs).toBe(2000)
})
it("refuses a missing or malformed variable by name, never by value", () => {
for (const [name, value, message] of [
["BETTER_AUTH_CONVEX_URL", "", /BETTER_AUTH_CONVEX_URL is not set/],
["BETTER_AUTH_CONVEX_SITE_URL", "site.test", /BETTER_AUTH_CONVEX_SITE_URL is not an absolute/],
["BETTER_AUTH_CONVEX_ADMIN_KEY", " ", /BETTER_AUTH_CONVEX_ADMIN_KEY is not set/],
["BETTER_AUTH_RELAY_INTERVAL_MS", "10", /BETTER_AUTH_RELAY_INTERVAL_MS/],
] as const) {
expect(() => betterAuthEnv({ ...ENV, [name]: value }), name).toThrow(message)
}
expect(() => betterAuthEnv({ ...ENV, BETTER_AUTH_CONVEX_ADMIN_KEY: "" })).not.toThrow(/admin-key-of-the-test/)
})
})
type Seen = { url: string; init: RequestInit | undefined }
/** Better Auth's token endpoint, the validation query, the sign-out and the admin mutation, faked. */
function fakeDeployment() {
const seen: Seen[] = []
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
seen.push({ url, init })
if (url === "http://site.test/api/auth/convex/token") return Response.json({ token: "convex-token" })
if (url === "http://site.test/api/auth/sign-out") return Response.json({ success: true })
if (url.startsWith("http://cloud.test/api/query")) {
const body = JSON.parse(String(init?.body)) as { path: string }
if (body.path === "conusaiAuth:pendingRevocations")
return Response.json({ status: "success", value: [], logLines: [] })
const value = {
userId: "ba-user",
sessionId: "ba-session",
expiresAt: Date.now() + 3_600_000,
createdAt: Date.now() - 1000,
email: "ada@example.test",
name: "Ada",
emailVerified: false,
}
return Response.json({ status: "success", value, logLines: [] })
}
if (url.startsWith("http://cloud.test/api/mutation"))
return Response.json({ status: "success", value: null, logLines: [] })
throw new Error(`unexpected ${url}`)
}) as typeof globalThis.fetch
return { fetch, seen }
}
function runtimeWith(onSessionRevoked = vi.fn(async () => undefined)) {
const registry = buildRegistry([tenancyModule])
const identities = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const deployment = fakeDeployment()
const warnings: string[] = []
const runtime = createBetterAuthRuntime({
config,
identities,
onSessionRevoked,
onAccountEvent: async () => undefined,
log: { warn: (message) => warnings.push(message) },
env: ENV,
fetch: deployment.fetch,
})
return { runtime, deployment, onSessionRevoked, warnings }
}
const signedIn = (cookie = "better-auth.session_token=tok.sig") =>
new Request("http://app.test/auth/sign-out", { method: "POST", headers: { cookie } })
describe("the Better Auth sign-out", () => {
it("ends the device's derived service sessions, then the session at Better Auth, then every cookie", async () => {
const { runtime, deployment, onSessionRevoked } = runtimeWith()
try {
const response = await runtime.signOut(signedIn())
expect(onSessionRevoked).toHaveBeenCalledWith(
{ provider: "convex-better-auth", opaqueId: "ba-session" },
"sign-out",
)
const signOut = deployment.seen.findIndex((call) => call.url === "http://site.test/api/auth/sign-out")
expect(signOut).toBeGreaterThan(0)
expect(new Headers(deployment.seen[signOut]?.init?.headers).get("origin")).toBe("http://app.test")
expect(response.status).toBe(303)
expect(response.headers.get("location")).toBe("http://app.test/auth/signed-out")
expect(response.headers.getSetCookie()).toContain(
"better-auth.session_token=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax",
)
} finally {
await runtime.stop()
}
})
it("clears the cookies of a browser whose session Better Auth cannot confirm, and says so", async () => {
const { runtime, onSessionRevoked, warnings } = runtimeWith()
try {
const response = await runtime.signOut(signedIn(""))
expect(onSessionRevoked).not.toHaveBeenCalled()
expect(response.status).toBe(303)
expect(response.headers.getSetCookie().length).toBeGreaterThan(0)
expect(warnings).toEqual([])
} finally {
await runtime.stop()
}
})
it("ends a Better Auth account with the deployment's admin key, by its Better Auth user ID", async () => {
const { runtime, deployment } = runtimeWith()
try {
await runtime.endAccount("ba-user")
const call = deployment.seen.find((candidate) => candidate.url.startsWith("http://cloud.test/api/mutation"))
expect(JSON.parse(String(call?.init?.body))).toMatchObject({
path: "conusaiAuth:endAccount",
args: [{ userId: "ba-user" }],
})
expect(new Headers(call?.init?.headers).get("authorization")).toBe("Convex admin-key-of-the-test")
} finally {
await runtime.stop()
}
})
})
describe("the account deletion's Better Auth step, bound in every process that may run it", () => {
const BOUND = {
AUTH_PROVIDER: "convex-better-auth",
AUTH_IDENTITY_NAMESPACE: "better-auth:test",
BETTER_AUTH_CONVEX_URL: "http://cloud.test",
BETTER_AUTH_CONVEX_ADMIN_KEY: "admin-key-of-the-test",
}
const providerOf = (container: Container) =>
container.has(ACCOUNT_IDENTITY_PROVIDER) ? container.resolve(ACCOUNT_IDENTITY_PROVIDER) : null
it("the web process binds the provider the ledger records, under the namespace; an OIDC app binds none", () => {
const web = new Container()
bindBetterAuthAccountEnd(web, BOUND)
expect([providerOf(web)?.id, providerOf(web)?.issuer]).toEqual(["convex-better-auth", "better-auth:test"])
const oidc = new Container()
bindBetterAuthAccountEnd(oidc, { ...BOUND, AUTH_PROVIDER: "oidc" })
expect(providerOf(oidc)).toBeNull()
})
it("the worker binds the same for its sweep, and refuses to start without the deployment's admin key", async () => {
const platform = await startFakePlatform()
try {
const env = {
NODE_ENV: "test",
CONUSAI_FAKE_PLATFORM: "1",
CONUSAI_API_URL: `${platform.url}/api`,
CONUSAI_API_TOKEN: "test-token",
CONUSAI_PROJECT_ID: "42",
CONUSAI_ORG_ID: "42",
CONUSAI_ORG_SLUG: "owner-org",
POSTGRES_URL: "postgresql://app_app:secret@db:5432/app",
POSTGRES_READER_URL: "postgresql://app_reader:secret@db:5432/app",
}
const registry = buildRegistry([...appModules])
const clients = platformClients(loadAppEnv("worker", env), registry, env)
// The worker reads its process environment, as it does in a container.
const bound = (extra: Record<string, string>) => {
for (const [name, value] of Object.entries(extra)) vi.stubEnv(name, value)
try {
const container = new Container()
bindModulePorts(container, clients, registry)
return container
} finally {
vi.unstubAllEnvs()
}
}
const worker = bound(BOUND)
expect([providerOf(worker)?.id, providerOf(worker)?.issuer]).toEqual(["convex-better-auth", "better-auth:test"])
expect(() => bound({ ...BOUND, BETTER_AUTH_CONVEX_ADMIN_KEY: "" })).toThrow("BETTER_AUTH_CONVEX_ADMIN_KEY")
expect(providerOf(bound({ AUTH_PROVIDER: "" }))).toBeNull()
} finally {
await platform.close()
}
})
})
describe("the Better Auth deployment's credentials stay server-side (no platform link injects them)", () => {
type Compose = { services: Record<string, { build?: { args?: unknown }; environment?: Record<string, string> }> }
const compose = (path: string) => parse(readFileSync(new URL(`../${path}`, import.meta.url), "utf8")) as Compose
const holders = (file: Compose, name: string) =>
Object.entries(file.services)
.filter(([, service]) => service.environment?.[name] !== undefined)
.map(([id]) => id)
.sort()
it("names the admin key only in the server processes that use it, each from the deployment's own variable", () => {
const postgres = compose("compose.yaml")
const convex = compose("profiles/convex/compose.yaml")
// The web server (session checks, the relay), the worker (the sweep ends a deleted account), the
// job that deploys the Better Auth functions; on Convex the deploy job also installs it in the
// app's backend, whose account actions end the account there.
expect(holders(postgres, "BETTER_AUTH_CONVEX_ADMIN_KEY")).toEqual(["migrate", "web", "worker"])
expect(holders(convex, "BETTER_AUTH_CONVEX_ADMIN_KEY")).toEqual(["convex-deploy", "web"])
// The secret: the deploy job alone.
expect(holders(postgres, "BETTER_AUTH_SECRET")).toEqual(["migrate"])
expect(holders(convex, "BETTER_AUTH_SECRET")).toEqual(["convex-deploy"])
for (const file of [postgres, convex]) {
for (const [id, service] of Object.entries(file.services)) {
for (const [name, value] of Object.entries(service.environment ?? {})) {
if (!name.startsWith("BETTER_AUTH_")) continue
// Passed through from the variable of the same name, defaulting to empty: never a literal.
expect(value, `${id}: ${name}`).toBe(`\${${name}:-}`)
}
expect(JSON.stringify(service.build?.args ?? {}), `${id} builds with no Better Auth value`).not.toContain(
"BETTER_AUTH",
)
expect(Object.keys(service.environment ?? {}).filter((name) => name.startsWith("NEXT_PUBLIC_"))).toEqual([])
}
}
const dockerfile = readFileSync(new URL("../Dockerfile", import.meta.url), "utf8")
expect(dockerfile).not.toContain("BETTER_AUTH")
const example = readFileSync(new URL("../.env.example", import.meta.url), "utf8")
expect(example).not.toMatch(/NEXT_PUBLIC_[A-Z_]*BETTER_AUTH|NEXT_PUBLIC_[A-Z_]*CONVEX/)
})
it("links the Better Auth deployment only as the named better-auth Convex link, never beside Identity", () => {
// A link role decides the injected names (BL): the default Convex link injects
// CONVEX_SELF_HOSTED_*, the better-auth one BETTER_AUTH_CONVEX_* as runtime-only variables.
// `kit template:prune --auth convex-better-auth` swaps the Identity link for it; an app that
// keeps OIDC declares none (docs/how-to/build-an-app-on-the-template.md).
for (const path of ["conusai.json", "profiles/convex/conusai.json"]) {
const config = JSON.parse(readFileSync(new URL(`../${path}`, import.meta.url), "utf8")) as {
links?: Array<{ type: string; role?: string }>
}
const links = config.links ?? []
const roles = links.filter((link) => link.type === "convex").map((link) => link.role ?? "default")
expect(new Set(roles).size, `${path}: one Convex link per role`).toBe(roles.length)
const betterAuth = links.filter((link) => link.role === "better-auth")
expect(
betterAuth.every((link) => link.type === "convex"),
path,
).toBe(true)
if (betterAuth.length > 0) {
expect(
links.some((link) => link.type === "identity"),
`${path}: no Identity link beside Better Auth`,
).toBe(false)
}
}
})
})
describe("Better Auth's account events", () => {
it("go to the account module's flow as the mapped app user; nothing for a subject never mapped", async () => {
const delivered: unknown[] = []
const ports = { accountFlow: async (event: unknown) => void delivered.push(event) }
const event = {
reason: "user-disabled",
identityNamespace: "better-auth:test",
providerSubject: "ba-user",
} as const
await deliverAccountEvent(ports, { ...event, kitUserId: null })
expect(delivered).toEqual([])
await deliverAccountEvent(ports, { ...event, kitUserId: "app-user" })
await deliverAccountEvent(ports, { ...event, reason: "account-deleted", kitUserId: "app-user" })
expect(delivered).toEqual([
{ reason: "user-disabled", userId: "app-user" },
{ reason: "account-deleted", userId: "app-user" },
])
})
it("throw the flow's failure, so the relay keeps the event and delivers it again", async () => {
const ports = {
accountFlow: async () => {
throw new Error("backend unavailable")
},
}
await expect(
deliverAccountEvent(ports, {
reason: "account-deleted",
identityNamespace: "better-auth:test",
providerSubject: "ba-user",
kitUserId: "app-user",
}),
).rejects.toThrow("backend unavailable")
})
})
describe("the sign-in form", () => {
it("checks the password length Better Auth enforces, and labels every error from a fixed set", () => {
expect(MIN_PASSWORD_LENGTH).toBe(OPTIONS_MIN)
const labels = betterAuthFormLabels(
{ message: (key, values) => `${key}${values ? JSON.stringify(values) : ""}` },
"sign-up",
)
expect(labels.passwordHelp).toBe(`signin.account.passwordHelp{"min":${MIN_PASSWORD_LENGTH}}`)
expect(Object.keys(labels.errors).sort()).toEqual([
"disabled",
"exists",
"failed",
"invalid",
"rateLimited",
"unavailable",
])
})
})
describe("the locally installed Better Auth component", () => {
it("is the schema the app's Better Auth options generate, with the admin plugin's fields", async () => {
const options = {
database: convexAdapter({} as never, {} as never),
...betterAuthOptions({ providers: [{ applicationID: "convex", domain: "" }] }),
}
const adapter = (
options.database as unknown as (all: unknown) => {
createSchema(all: unknown, file: string): Promise<{ code: string }>
}
)(options)
const { code } = await adapter.createSchema(options, "better-auth/functions/betterAuth/schema.ts")
const body = code.slice(code.indexOf("import "))
const header = `// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Generated from ./options.ts by the @convex-dev/better-auth schema generator: do not edit it by hand.
// After a change of the Better Auth options, regenerate it with
// pnpm exec vitest run test/better-auth.test.ts -u
// (the "local install" of @convex-dev/better-auth, which is how the admin plugin's fields get here).
`
for (const field of ["role:", "banned:", "banReason:", "banExpires:", "impersonatedBy:"])
expect(body).toContain(field)
await expect(`${header}${body}`).toMatchFileSnapshot("../better-auth/functions/betterAuth/schema.ts")
})
it("maps one Better Auth user to one app user, under the configured namespace and never by e-mail", async () => {
const registry = buildRegistry([tenancyModule])
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const first = await requireAppUser(data, {
issuer: config.identityNamespace,
subject: "ba-user",
email: "ada@example.test",
displayName: "Ada",
locale: "en",
})
const other = await requireAppUser(data, {
issuer: config.identityNamespace,
subject: "ba-other",
email: "ada@example.test",
displayName: "Ada",
locale: "en",
})
expect(other.id).not.toBe(first.id)
})
})