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
+58
View File
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The coding-agent contract this template ships: CLAUDE.md imports AGENTS.md instead of repeating it, the
// committed Claude Code settings keep secrets unreadable and a push behind a person, the support-report skill is
// readable by its tool, and the format-on-edit hook formats only this repository's own source files.
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { fileToFormat } from "../scripts/agent-format.mjs"
const APP_ROOT = new URL("..", import.meta.url).pathname
const read = (path: string) => readFileSync(join(APP_ROOT, path), "utf8")
const made: string[] = []
afterEach(() => {
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
})
describe("the coding-agent contract", () => {
it("has CLAUDE.md import AGENTS.md, the one source of the rules", () => {
expect(read("CLAUDE.md")).toMatch(/^@AGENTS\.md$/m)
expect(read("AGENTS.md")).toContain("## Work that starts from a support report")
})
it("commits Claude Code settings that refuse .env reads and ask before a push or a migration run", () => {
const settings = JSON.parse(read(".claude/settings.json"))
expect(settings.permissions.deny).toEqual(expect.arrayContaining(["Read(/.env)", "Read(/.env.local)"]))
expect(settings.permissions.ask).toEqual(expect.arrayContaining(["Bash(git push *)", "Bash(pnpm migrate *)"]))
expect(settings.permissions.allow).toContain("Bash(pnpm gate)")
// Nothing that publishes or deploys is pre-approved.
for (const rule of settings.permissions.allow) expect(rule).not.toMatch(/push|migrate|deploy/)
expect(read(".gitignore")).toContain(".claude/settings.local.json")
})
it("ships the support-report skill with the frontmatter its tool reads first", () => {
const skill = read(".claude/skills/fix-support-report/SKILL.md")
expect(skill.startsWith("---\nname: fix-support-report\ndescription: ")).toBe(true)
expect(skill).toMatch(/Reproduce it before changing code/)
})
})
describe("the format-on-edit hook", () => {
it("names a repository source file Biome formats, and nothing else", () => {
const root = mkdtempSync(join(tmpdir(), "agent-format-"))
made.push(root)
writeFileSync(join(root, "a.ts"), "export const a = 1\n")
writeFileSync(join(root, "notes.md"), "# notes\n")
expect(fileToFormat({ tool_input: { file_path: join(root, "a.ts") } }, root)).toBe("a.ts")
expect(fileToFormat({ tool_input: { file_path: "a.ts" } }, root)).toBe("a.ts")
expect(fileToFormat({ tool_input: { file_path: "notes.md" } }, root)).toBeNull()
expect(fileToFormat({ tool_input: { file_path: "missing.ts" } }, root)).toBeNull()
expect(fileToFormat({ tool_input: { file_path: "../outside.ts" } }, root)).toBeNull()
expect(fileToFormat({ tool_input: {} }, root)).toBeNull()
expect(fileToFormat(null, root)).toBeNull()
})
})
+124
View File
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The deployment contract per process (decision AT-D20): which Postgres role each process may
// hold, and what happens when it is handed one it may not.
import { describe, expect, it } from "vitest"
import { loadAppEnv, roleOf } from "../config/src/env.js"
import { assertBoundByRowLevelSecurity, assertOwnsSchema } from "../config/src/postgres.js"
const base = {
CONUSAI_API_URL: "https://api.example.test/api",
CONUSAI_API_TOKEN: "token",
CONUSAI_PROJECT_ID: "42",
CONUSAI_ORG_ID: "9",
CONUSAI_ORG_SLUG: "acme",
POSTGRES_URL: "postgresql://app_app:secret@db:5432/app",
POSTGRES_READER_URL: "postgresql://app_reader:secret@db:5432/app",
}
describe("the deployment contract", () => {
it("gives web and worker the runtime role and the global reader", () => {
for (const service of ["web", "worker"] as const) {
const env = loadAppEnv(service, { ...base })
expect(env.postgres?.runtime).toContain("app_app")
expect(env.postgres?.reader).toContain("app_reader")
expect(env.postgres?.owner).toBeUndefined()
}
})
it("refuses to start web or worker with the owner's URL", () => {
for (const service of ["web", "worker"] as const) {
expect(() =>
loadAppEnv(service, { ...base, POSTGRES_MIGRATE_URL: "postgresql://app_owner:s@db:5432/app" }),
).toThrow(/POSTGRES_MIGRATE_URL is set for the \w+ process/)
}
})
it("requires the platform organization slug for production web and worker usage billing, but not migrate", () => {
const { CONUSAI_ORG_SLUG: _slug, ...withoutSlug } = base
for (const service of ["web", "worker"] as const) {
expect(() => loadAppEnv(service, { ...withoutSlug })).toThrow(/CONUSAI_ORG_SLUG/)
}
expect(() =>
loadAppEnv("migrate", {
...withoutSlug,
POSTGRES_MIGRATE_URL: "postgresql://app_owner:secret@db:5432/app",
}),
).not.toThrow()
})
it("refuses the migrate job without the owner's URL, naming postgres.migrate", () => {
expect(() => loadAppEnv("migrate", { ...base })).toThrow(/declares postgres\.migrate/)
})
it("refuses the migrate job when two of the three URLs name one role", () => {
expect(() => loadAppEnv("migrate", { ...base, POSTGRES_MIGRATE_URL: base.POSTGRES_URL })).toThrow(
/three separate logins/,
)
})
it("names the Postgres service to link when a URL is missing", () => {
const { POSTGRES_URL: _dropped, ...without } = base
expect(() => loadAppEnv("web", without)).toThrow(/link a Postgres service/)
})
it("refuses a runtime URL and a reader URL that name one login", () => {
expect(() => loadAppEnv("web", { ...base, POSTGRES_READER_URL: base.POSTGRES_URL })).toThrow(/name the same role/)
})
it("refuses a role name that is not a plain identifier", () => {
expect(roleOf("postgresql://app_reader:s@db:5432/app")).toBe("app_reader")
expect(() => roleOf("postgresql://app%2Dreader:s@db:5432/app")).toThrow(/not a plain identifier/)
})
it("loads Convex server credentials and refuses missing or misplaced credentials", () => {
const convex = {
...base,
CONVEX_SELF_HOSTED_URL: "http://convex:3210",
CONVEX_SELF_HOSTED_ADMIN_KEY: "server-only-test-key",
}
const loaded = loadAppEnv("web", convex, "convex")
expect(loaded.convex).toEqual({ url: "http://convex:3210", adminKey: "server-only-test-key" })
expect(loaded).not.toHaveProperty("postgres")
const { CONVEX_SELF_HOSTED_ADMIN_KEY: _key, ...missingKey } = convex
expect(() => loadAppEnv("web", missingKey, "convex")).toThrow(/CONVEX_SELF_HOSTED_ADMIN_KEY/)
expect(() => loadAppEnv("migrate", convex, "convex")).toThrow(/not supported by the Convex app runtime/)
})
})
describe("the role the database answers with", () => {
const facts = {
role: "app_app",
ownedTables: 0,
bypassesRowLevelSecurity: false,
isSuperuser: false,
ownsSchema: false,
}
it("accepts a role that owns nothing and bypasses nothing", () => {
expect(assertBoundByRowLevelSecurity(facts, "POSTGRES_URL")).toBe(facts)
})
it("refuses a connection that owns a table, bypasses row-level security or is a superuser", () => {
expect(() => assertBoundByRowLevelSecurity({ ...facts, ownedTables: 3 }, "POSTGRES_URL")).toThrow(/owns 3 table/)
expect(() => assertBoundByRowLevelSecurity({ ...facts, bypassesRowLevelSecurity: true }, "POSTGRES_URL")).toThrow(
/BYPASSRLS/,
)
expect(() => assertBoundByRowLevelSecurity({ ...facts, isSuperuser: true }, "POSTGRES_READER_URL")).toThrow(
/superuser/,
)
})
it("refuses a migrate job that does not own schema public", () => {
expect(() => assertOwnsSchema(facts, "POSTGRES_MIGRATE_URL")).toThrow(/does not own schema public/)
expect(assertOwnsSchema({ ...facts, role: "app_owner", ownsSchema: true }, "POSTGRES_MIGRATE_URL").ownsSchema).toBe(
true,
)
})
it("names the variable whose connection is wrong", () => {
expect(() => assertBoundByRowLevelSecurity({ ...facts, ownedTables: 1 }, "POSTGRES_READER_URL")).toThrow(
/POSTGRES_READER_URL/,
)
})
})
+16
View File
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { afterEach, expect, it, vi } from "vitest"
afterEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
})
it("keeps compiled chunk URLs stable when the platform injects a deployment asset prefix", async () => {
// Compose serves the image's assets itself. Changing only the server's prefix at startup
// leaves Turbopack waiting for chunk URLs that differ from those registered by the scripts.
vi.stubEnv("CONUSAI_ASSET_PREFIX", "/_conusai/assets/app-42")
vi.resetModules()
const { default: config } = await import("../apps/web/next.config")
expect(config.assetPrefix ?? "").toBe("")
})
+77
View File
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What a failed sign-in becomes (spec §7): a redirect to the sign-in page with a code from a fixed
// set, never the provider's text, and never to an address outside this app.
import { forbidden, unauthenticated } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import {
AUTH_ERROR_MESSAGES,
AUTH_ERRORS,
authErrorOf,
parseAuthError,
safeReturnTo,
signInErrorHref,
} from "../apps/web/src/server/auth-errors.js"
import { appCatalogues } from "../apps/web/src/ui/messages.js"
const url = (query = ""): URL => new URL(`https://app.example.test/auth/callback${query}`)
describe("which code a failure gets", () => {
it("is `denied` when the provider said no, whatever the provider's text", () => {
expect(authErrorOf(new Error("anything"), url("?error=access_denied&error_description=secret+words"))).toBe(
"denied",
)
})
it("is `noTransaction` for a callback no sign-in started in this browser", () => {
const refusal = unauthenticated("no sign-in is in progress for this browser; start again at /auth/sign-in")
expect(authErrorOf(refusal, url())).toBe("noTransaction")
})
it("is `sessionExpired` for any other 401, `failed` for any other refusal of the app's own", () => {
expect(authErrorOf(unauthenticated("the session ended"), url())).toBe("sessionExpired")
expect(authErrorOf(forbidden("no"), url())).toBe("failed")
})
it("is `failed` for a deployment that has no identity provider to complete a sign-in with", () => {
expect(authErrorOf(unauthenticated("this deployment has no identity provider configured"), url())).toBe("failed")
})
it("is `provider` for what is not the app's own error: the exchange with the provider failed", () => {
expect(authErrorOf(new TypeError("fetch failed"), url())).toBe("provider")
})
})
describe("the sign-in page's address", () => {
it("carries the code and, when it is safe, where the person was going", () => {
expect(signInErrorHref("denied")).toBe("/auth/sign-in?error=denied")
expect(signInErrorHref("failed", "/invitations/acme/abc")).toBe(
"/auth/sign-in?error=failed&return_to=%2Finvitations%2Facme%2Fabc",
)
})
it.each(["https://evil.example", "//evil.example", "/\\evil.example", "javascript:alert(1)", "", null, undefined])(
"never returns to %j",
(target) => {
expect(safeReturnTo(target)).toBe("/")
expect(signInErrorHref("failed", target)).toBe("/auth/sign-in?error=failed")
},
)
it("shows only the five words, not whatever the query holds", () => {
expect(parseAuthError("denied")).toBe("denied")
expect(parseAuthError("<script>alert(1)</script>")).toBeUndefined()
expect(parseAuthError(undefined)).toBeUndefined()
})
})
describe("the words for each code", () => {
it("exist in English and in Lithuanian for every code", () => {
for (const code of AUTH_ERRORS) {
const [module, ...rest] = (AUTH_ERROR_MESSAGES[code] as string).split(".")
const key = rest.join(".")
expect(appCatalogues.en?.[module as string]?.[key], `en ${code}`).toBeTruthy()
expect(appCatalogues.lt?.[module as string]?.[key], `lt ${code}`).toBeTruthy()
}
})
})
+95
View File
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The page's unload path (apps/web/src/ui/client.ts `beacon`): a call made as the page goes away is
// a same-origin keep-alive POST to the procedure's mount with the page's CSRF token, and the
// server's real CSRF wall admits exactly that request for this session and refuses it for another.
// The browser half (keep-alive after a navigation and a closed tab) is proven in a real browser by
// @conusai-kit/guides' Playwright suite (test-browser/beacon.spec.ts).
import type { Principal } from "@conusai-kit/auth"
import { afterEach, beforeAll, describe, expect, it } from "vitest"
const ORIGIN = "https://app.example.test"
const session = (sessionId: string): Principal => ({
actor: { userId: "11111111-1111-1111-1111-111111111111" },
method: "session",
sessionId,
})
let csrfToken: (sessionId: string) => string
let requireCsrf: (request: Request, principal: Principal | null) => void
let beacon: (procedure: string, input: unknown) => boolean
beforeAll(async () => {
process.env.AUTH_SESSION_SECRET = "0123456789abcdef0123456789abcdef"
process.env.CONUSAI_PUBLIC_URL = ORIGIN
process.env.OIDC_ISSUER = "https://id.example.test"
process.env.OIDC_WEB_CLIENT_ID = "web"
process.env.OIDC_AUDIENCE = "api"
const module = await import("../apps/web/src/server/csrf")
csrfToken = module.csrfToken
requireCsrf = module.requireCsrf
// Imported before any page global is stubbed: the beacon reads the page at call time, and the UI
// kit's dependencies set up browser listeners on import when they see a window.
beacon = (await import("../apps/web/src/ui/client")).beacon
})
const globals = globalThis as unknown as { window?: unknown; document?: unknown; fetch: typeof fetch }
const realFetch = globals.fetch
afterEach(() => {
globals.fetch = realFetch
delete globals.window
delete globals.document
})
/** A page of this app holding `token` in its CSRF meta tag; returns what the page's fetch was handed. */
function page(token: string): Array<[URL, RequestInit]> {
const sent: Array<[URL, RequestInit]> = []
globals.window = { location: { href: `${ORIGIN}/acme/main/notes/notes`, origin: ORIGIN } }
globals.document = {
querySelector: (selector: string) =>
selector === 'meta[name="csrf-token"]'
? { getAttribute: (name: string) => (name === "content" ? token : null) }
: null,
}
globals.fetch = ((url: URL, init: RequestInit) => {
sent.push([url, init])
return Promise.resolve(new Response(null, { status: 200 }))
}) as typeof fetch
return sent
}
describe("the page's beacon", () => {
it("is a same-origin keep-alive POST to the procedure that the CSRF wall admits for this session only", () => {
const sent = page(csrfToken("session-1"))
const input = {
org: "acme",
idempotencyKey: "guides:1",
events: [{ schema_version: 1, event_id: "e1", type: "track" }],
}
expect(beacon("guides.runtime.record", input)).toBe(true)
expect(sent).toHaveLength(1)
const [url, init] = sent[0] as [URL, RequestInit]
expect(url.href).toBe(`${ORIGIN}/api/v1/guides.runtime.record`)
expect(init).toMatchObject({
method: "POST",
keepalive: true,
credentials: "same-origin",
body: JSON.stringify(input),
})
// What the server receives: the page's headers, plus the fetch metadata the browser adds to a
// request of this page's own origin.
const request = new Request(url, {
method: "POST",
headers: { ...(init.headers as Record<string, string>), "sec-fetch-site": "same-origin" },
body: init.body ?? null,
})
expect(() => requireCsrf(request, session("session-1"))).not.toThrow()
expect(() => requireCsrf(request, session("session-2"))).toThrow(/x-csrf-token/)
})
it("is declined when the body would not fit the keep-alive budget, so the engine keeps the batch", () => {
const sent = page(csrfToken("session-1"))
expect(beacon("guides.runtime.record", { padding: "x".repeat(70_000) })).toBe(false)
expect(sent).toHaveLength(0)
})
})
+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)
})
})
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The boundary rules are generated from the folder tree (spec §2), so a module added tomorrow is
// covered without anybody editing a list. This suite is that promise.
import { createRequire } from "node:module"
import { describe, expect, it } from "vitest"
const require = createRequire(import.meta.url)
const { buildForbidden, listModules } = require("../scripts/dep-rules.cjs") as {
buildForbidden(
moduleIds: string[],
): { name: string; from: { path?: string; pathNot?: string }; to: { path?: string | string[]; circular?: boolean } }[]
listModules(root: string): string[]
}
const named = (rules: ReturnType<typeof buildForbidden>, name: string) => rules.find((rule) => rule.name === name)
describe("the generated boundary rules", () => {
it("find this app's modules on disk", () => {
const found = listModules(new URL("..", import.meta.url).pathname)
expect(Array.isArray(found)).toBe(true)
})
it("keep the worker entry out of every app but the worker", () => {
const rule = named(buildForbidden([]), "worker-entry-is-the-workers-own")
expect(rule?.from.pathNot).toBe("^apps/worker/")
// The resolved path, which is what dependency-cruiser matches on.
expect(rule?.to.path).toBe("@conusai-kit/core/dist/worker\\.")
})
it("keep a driver, an SDK and a framework out of a module's core", () => {
const rule = named(buildForbidden(["notes"]), "core-imports-no-engine-sdk-or-framework")
const patterns = (rule?.to.path ?? []) as string[]
expect(patterns.join("|")).toMatch(/kysely/)
expect(patterns.join("|")).toMatch(/next/)
expect(patterns.join("|")).toMatch(/@conusai-sdk/)
})
it("add a rule per module pair, so a module reaches another only through its contract", () => {
const rules = buildForbidden(["documents", "notes"])
expect(named(rules, "module-notes-crosses-only-contracts")?.to.path).toBe("^modules/(documents)/(?!contract/)")
expect(named(rules, "module-documents-crosses-only-contracts")?.to.path).toBe("^modules/(notes)/(?!contract/)")
})
it("add no module rule when there is only one module to cross into", () => {
expect(buildForbidden(["notes"]).some((rule) => rule.name.startsWith("module-"))).toBe(false)
})
it("refuse a cycle anywhere", () => {
expect(named(buildForbidden([]), "no-circular")?.to.circular).toBe(true)
})
})
+101
View File
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The brand is the only source of colour (decision AT-D11), and every string a page shows is a
// message in both locales (spec §10.4).
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, it } from "vitest"
import { appCatalogues } from "../apps/web/src/ui/messages"
import { appBrand, BRAND_TOKEN_NAMES, brandTokenStyle } from "../config/src/brand.js"
const walk = (dir: string, suffix: string): string[] =>
readdirSync(dir).flatMap((name) => {
// Installed packages are not this app's pages, and a linked workspace package would pull its
// own sources (stories, fixtures) into the scan.
if (name === "node_modules" || name === ".next") return []
const path = join(dir, name)
if (statSync(path).isDirectory()) return walk(path, suffix)
return path.endsWith(suffix) ? [path] : []
})
const appDir = new URL("..", import.meta.url).pathname
describe("the app brand", () => {
it("reads its tokens from @conusai-kit/brand, so the app defines none", () => {
expect(BRAND_TOKEN_NAMES.length).toBeGreaterThan(0)
expect(appBrand.tokens).toEqual({})
expect(brandTokenStyle({ ...appBrand, tokens: { primary: "#123456" } })).toEqual({ "--brand-primary": "#123456" })
})
it("ships no colour literal of its own in the stylesheet", () => {
const css = readFileSync(join(appDir, "apps/web/app/globals.css"), "utf8")
const literals = css.match(/#[0-9a-fA-F]{3,8}\b|\brgb\(|\bhsl\(|\boklch\(/g) ?? []
expect(literals).toEqual([])
})
it("loads no external stylesheet, font or script", () => {
for (const file of [...walk(join(appDir, "apps/web/app"), ".tsx"), join(appDir, "apps/web/app/globals.css")]) {
const text = readFileSync(file, "utf8")
expect(text, `${file} reaches outside this app`).not.toMatch(/https?:\/\/(?!127\.0\.0\.1|localhost)/)
}
})
it("has no attribution turned on by default: a customer sells their own product", () => {
expect(appBrand.attribution).toBe(false)
})
})
describe("the message catalogues", () => {
const en = appCatalogues.en ?? {}
const lt = appCatalogues.lt ?? {}
it("hold the same keys in English and in Lithuanian", () => {
for (const [module, messages] of Object.entries(en)) {
expect(Object.keys(lt[module] ?? {}).sort(), `lt is missing keys of ${module}`).toEqual(
Object.keys(messages).sort(),
)
}
})
it("select the Lithuanian plural categories, which English does not have", () => {
expect(lt.app?.memberCount).toContain("few {")
expect(en.app?.memberCount).not.toContain("few {")
})
it("hold the messages a page picks by a value rather than by name: roles, sign-in errors, invitation states", () => {
const keys = [
...["viewer", "member", "admin", "owner"].map((role) => `roles.${role}`),
...["sessionExpired", "noTransaction", "denied", "provider", "failed"].map((code) => `signin.error.${code}`),
...["invalid", "used", "expired", "member", "failed"].map((state) => `invite.error.${state}`),
]
for (const key of keys) {
const [module, ...rest] = key.split(".")
const message = rest.join(".")
expect(en[module as string]?.[message], `en is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `lt is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `${key} is not translated`).not.toBe(en[module as string]?.[message])
}
})
it("select the Lithuanian plural categories of the counts the workspace pages show", () => {
expect(lt.workspace?.projectCount).toContain("few {")
expect(en.workspace?.projectCount).not.toContain("few {")
})
it("hold every message the pages ask for, in both locales", () => {
// Every label a page shows is `i18n.message("<key>")`, so the keys the pages use and the keys
// the catalogues hold are comparable -- and a renamed message fails here rather than
// rendering its own key to a customer.
const used = new Set<string>()
for (const file of walk(join(appDir, "apps/web"), ".tsx")) {
for (const [, key] of readFileSync(file, "utf8").matchAll(/i18n\.message\("([^"]+)"/g)) used.add(key as string)
}
expect(used.size).toBeGreaterThan(10)
for (const key of [...used].sort()) {
const [module, ...rest] = key.split(".")
const message = rest.join(".")
expect(en[module as string]?.[message], `en is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `lt is missing ${key}`).toBeTruthy()
}
})
})
+83
View File
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What a page says about itself, rendered with a customer's brand: their name, their colours on the
// first byte of the document, and no trace of ConusAI's (plan item AT2.6 done_when, the rendered-page
// half; there are no emails or share images in the template to render).
import { defineBrand } from "@app/config"
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { describe, expect, it } from "vitest"
import { brandMetadata, Document } from "../apps/web/src/ui/document"
const customer = defineBrand({
name: "Acme Review",
shortName: "Acme",
primary: "#1a1a8c",
logo: { light: "/acme-light.svg", dark: "/acme-dark.svg", favicon: "/acme.svg" },
})
const render = (extra: { theme?: "dark" | "light"; observe?: { release: string; environment: string } } = {}): string =>
renderToStaticMarkup(
createElement(Document, {
brand: customer,
locale: "lt",
theme: extra.theme ?? "dark",
signedIn: false,
signIn: "Prisijungti",
signOut: null,
controls: null,
...(extra.observe ? { observe: extra.observe } : {}),
children: createElement("main", null, "turinys"),
}),
)
const html = render()
describe("a page rendered with a customer's brand", () => {
it("carries the customer's name and logos and no ConusAI name", () => {
expect(html).toContain("Acme Review")
expect(html).toContain('src="/acme-dark.svg"')
expect(html.toLowerCase()).not.toContain("conusai")
})
it("writes the brand layer on the document element itself, so the first paint is already branded", () => {
const root = /<html[^>]*>/.exec(html)?.[0] ?? ""
expect(root).toContain('lang="lt"')
expect(root).toContain("--brand-accent:#1a1a8c")
expect(root).toMatch(/--brand-accent-ink:light-dark\(#[0-9a-f]{6}, ?#[0-9a-f]{6}\)/)
})
it("names the customer, and only the customer, in the tab and in a share card", () => {
const metadata = JSON.stringify(brandMetadata(customer))
expect(metadata).toContain("Acme Review")
expect(metadata.toLowerCase()).not.toContain("conusai")
})
it("points the share card at the app's own image, or at the organization's on its own pages", () => {
const app = brandMetadata(customer)
expect(app.openGraph?.images).toEqual([{ url: "/api/brand/og", width: 1200, height: 630, alt: "Acme Review" }])
expect(app.twitter).toMatchObject({ card: "summary_large_image", images: ["/api/brand/og"] })
const organization = brandMetadata(customer, "/acme/brand/og")
expect(JSON.stringify(organization)).toContain("/acme/brand/og")
expect(JSON.stringify(organization)).not.toContain("/api/brand/og")
})
it("has exactly one element that carries the theme class: the page, not a component", () => {
expect((html.match(/class="[^"]*\blight\b/g) ?? []).length).toBeLessThanOrEqual(1)
})
it("puts the light theme on the root element from the first byte, and the dark one on nothing", () => {
expect(/<html[^>]*>/.exec(html)?.[0]).not.toMatch(/class="[^"]*\blight\b/)
const light = render({ theme: "light" })
expect(/<html[^>]*>/.exec(light)?.[0]).toMatch(/class="light"/)
expect((light.match(/class="(?:[^"]* )?light(?: [^"]*)?"/g) ?? []).length).toBe(1)
})
it("tells the browser to start observability only when the server turned it on, without naming ConusAI", () => {
expect(html).not.toContain("app-observe")
const observed = render({ observe: { release: "2026.10.1", environment: "production" } })
expect(observed).toContain('name="app-observe"')
expect(observed).toContain('data-release="2026.10.1"')
expect(observed.toLowerCase()).not.toContain("conusai")
})
})
+70
View File
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The app brand (plan item AT2.6). A customer replaces the ConusAI defaults in config/src/brand.ts with
// `defineBrand`, and what they give it has to be safe to write into a stylesheet and a page. The
// legibility of their colours is @conusai-kit/brand's own test: this file checks what the app does with them.
import { appBrand, brandContrast, brandTokenStyle, defineBrand } from "@app/config"
import { describe, expect, it } from "vitest"
const base = { name: "Acme Review", shortName: "Acme" }
describe("defineBrand", () => {
it("returns the brand, frozen, with its colours as a style", () => {
const brand = defineBrand({ ...base, primary: "#1a1a8c" })
expect(brand.name).toBe("Acme Review")
expect(Object.isFrozen(brand)).toBe(true)
expect(brand.attribution).toBe(false)
expect(brandTokenStyle(brand)["--brand-accent"]).toBe("#1a1a8c")
})
it("reports the contrast its colours have on each theme", () => {
const report = brandContrast(defineBrand({ ...base, primary: "#1a1a8c" }))
expect(report.dark?.inkContrast).toBeGreaterThanOrEqual(4.5)
expect(report.light?.inkContrast).toBeGreaterThanOrEqual(4.5)
expect(brandContrast(defineBrand(base))).toEqual({})
})
it("refuses a colour that is not hex, naming the field and not the value", () => {
expect(() => defineBrand({ ...base, primary: "red" })).toThrow(/primary/)
expect(() => defineBrand({ ...base, primary: "secret-colour" })).not.toThrow(/secret-colour/)
expect(() => defineBrand({ ...base, accent: "#12" })).toThrow(/accent/)
})
it("refuses a name that is empty, too long or carries a control character", () => {
for (const name of ["", " ", "x".repeat(61), "bad\u0007name"]) {
expect(() => defineBrand({ name, shortName: "x" })).toThrow(/name/)
}
})
it("refuses a logo that is not a path on this app's own origin", () => {
for (const logo of [
"https://cdn.example.test/logo.svg",
"//cdn.example.test/logo.svg",
"javascript:alert(1)",
"data:image/svg+xml,<svg/>",
"logo.svg",
]) {
expect(() => defineBrand({ ...base, logo: { light: logo, dark: "/a.svg", favicon: "/a.svg" } }), logo).toThrow(
/logo/,
)
}
expect(defineBrand({ ...base, logo: { light: "/l.svg", dark: "/d.svg", favicon: "/f.svg" } }).logo.dark).toBe(
"/d.svg",
)
})
it("refuses a token that could break out of a style attribute", () => {
for (const value of ["red; background: url(//evil)", "#fff}", "url(//evil)", "expression(1)", "a\\b", "<b>"]) {
expect(() => defineBrand({ ...base, tokens: { "--brand-accent": value } }), value).toThrow(/token/)
}
expect(() => defineBrand({ ...base, tokens: { "not a name": "#fff" } })).toThrow(/token/)
expect(brandTokenStyle(defineBrand({ ...base, tokens: { "radius-panel": "0.5rem" } }))).toEqual({
"--brand-radius-panel": "0.5rem",
})
})
it("is what the app ships: a neutral name, no ConusAI wording, no attribution", () => {
expect(JSON.stringify(appBrand).toLowerCase()).not.toContain("conusai")
expect(appBrand.attribution).toBe(false)
})
})
+324
View File
@@ -0,0 +1,324 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The tenant brand end to end (plan item AT2.6): who may change it, what is refused, what is stored,
// and what the organization layout will read back. Every call goes through the kernel's caller, so
// the permission check, the validation and the data wall are the real ones.
import { type BrandView, tenantStyle } from "@app/modules/branding"
import { checkLogo } from "@conusai-kit/brand/logo/check"
import { AppError } from "@conusai-kit/core"
import { organizations } from "@conusai-kit/tenancy"
import { afterEach, describe, expect, it } from "vitest"
import { closeApps, type DocumentsApp, makeDocumentsApp, type SeededProject } from "./support/documents-app.js"
import { chunk, dimensions, MARK_WEBP, markPng, noisePng } from "./support/images.js"
afterEach(closeApps)
const document = { engine: { name: "unused", version: "0" }, page_count: 0, pages: [] }
async function fresh(): Promise<{ app: DocumentsApp; project: SeededProject }> {
const app = await makeDocumentsApp({ document })
return { app, project: await app.seedProject("acme", "owner@acme.test") }
}
// Real images: the kit's checks read a PNG's chunks and decode its pixels.
const PNG = markPng(1000, 250)
const png = (width: number) => markPng(width, 60)
const svgOf = (inner: string) => Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4">${inner}</svg>`)
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
const stored = (app: DocumentsApp, project: SeededProject) =>
app.data
.global(organizations)
.get(project.orgId)
.then((row) => row?.branding ?? null)
describe("reading the brand", () => {
it("answers nothing set for an organization that has no brand", async () => {
const { project } = await fresh()
expect(await project.call.callByName("branding.brand.get", { org: project.orgSlug })).toEqual({
displayName: null,
primary: null,
accent: null,
logoLight: null,
logoDark: null,
})
})
it("lets any member read it", async () => {
const { app, project } = await fresh()
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
expect(await viewer.call.callByName("branding.brand.get", { org: project.orgSlug })).toMatchObject({
primary: null,
})
})
})
describe("changing the brand", () => {
it("stores the name and the colours on the organization and answers with them", async () => {
const { app, project } = await fresh()
const view = await project.call.callByName("branding.brand.set", {
org: project.orgSlug,
displayName: " Acme Review ",
primary: "#1a1a8c",
accent: "#f4c430",
})
expect(view).toMatchObject({ displayName: "Acme Review", primary: "#1a1a8c", accent: "#f4c430" })
expect(await stored(app, project)).toEqual({ displayName: "Acme Review", primary: "#1a1a8c", accent: "#f4c430" })
})
it("leaves what is not sent as it was, and clears what is sent as null", async () => {
const { app, project } = await fresh()
await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#1a1a8c", accent: "#f4c430" })
await project.call.callByName("branding.brand.set", { org: project.orgSlug, accent: null })
expect(await stored(app, project)).toEqual({ primary: "#1a1a8c" })
await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: null })
// an empty brand is no brand: one representation of "none"
expect(await stored(app, project)).toBeNull()
})
it("refuses a colour that is not hex, with the field named and the value not repeated", async () => {
const { app, project } = await fresh()
for (const bad of ["red", "#12", "#gggggg", "rgb(1,2,3)", "#fff;}body{display:none", "url(//evil)"]) {
const error = await refusal(project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: bad }))
expect(error.status, bad).toBe(422)
// the refusal names the field and does not repeat what was typed in it
expect(JSON.stringify(error.problem), bad).toContain("primary")
expect(JSON.stringify(error.problem), bad).not.toContain(bad)
}
expect(await stored(app, project)).toBeNull()
})
it("refuses an empty display name, which would be a name nobody sees", async () => {
const { project } = await fresh()
const error = await refusal(
project.call.callByName("branding.brand.set", { org: project.orgSlug, displayName: " " }),
)
expect(error.status).toBe(422)
})
it("is an administrator's act: a member and a viewer are answered as strangers, and nothing changes", async () => {
const { app, project } = await fresh()
const admin = await app.addMember(project, "admin@acme.test", "admin")
const member = await app.addMember(project, "member@acme.test", "member")
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
for (const who of [member, viewer]) {
// 404, not 403: the kernel answers a member who lacks the permission as it answers a stranger (spec §3.3)
expect(
(await refusal(who.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#000000" }))).status,
).toBe(404)
expect(
(
await refusal(
who.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant: "light",
contentType: "image/png",
body: PNG.toString("base64"),
}),
)
).status,
).toBe(404)
}
expect(await stored(app, project)).toBeNull()
await admin.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#000000" })
expect(await stored(app, project)).toEqual({ primary: "#000000" })
})
it("is declared to write one audit event per change", async () => {
const app = await makeDocumentsApp({ document })
expect(app.registry.procedures.get("branding.brand.set")?.def.audit).toBe("branding.brand_changed")
expect(app.registry.procedures.get("branding.logo.set")?.def.audit).toBe("branding.brand_changed")
expect(app.registry.audit["branding.brand_changed"]).toBeDefined()
})
})
const logoKeys = (app: DocumentsApp, project: SeededProject) =>
app.store
.keysOf({ level: "org", actor: project.owner, orgId: project.orgId })
.filter((key) => key.startsWith("branding/"))
.sort()
const setLogo = (project: SeededProject, variant: "light" | "dark", contentType: string, bytes: Uint8Array) =>
project.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant,
contentType,
body: Buffer.from(bytes).toString("base64"),
}) as Promise<BrandView>
const getLogo = async (project: SeededProject, variant: "light" | "dark", size?: "1x" | "2x") => {
const back = (await project.call.callByName("branding.logo.get", {
org: project.orgSlug,
variant,
...(size === undefined ? {} : { size }),
})) as { contentType: string; body: string }
return { contentType: back.contentType, bytes: Buffer.from(back.body, "base64") }
}
describe("the logos", () => {
it("stores a raster logo as its 1x and 2x renditions, and serves the 2x unless the 1x is asked for", async () => {
const { app, project } = await fresh()
const view = await setLogo(project, "light", "image/png", PNG)
expect(view.logoLight).toMatch(/^\/acme\/brand\/logo\/light\?v=[0-9a-f]{16}$/)
expect(view.logoDark).toBeNull()
const two = await getLogo(project, "light")
expect(two.contentType).toBe("image/png")
expect(dimensions(two.bytes)).toEqual({ width: 840, height: 210 })
expect(dimensions((await getLogo(project, "light", "1x")).bytes)).toEqual({ width: 420, height: 105 })
expect(two.bytes.equals(PNG)).toBe(false)
// the object lives under the organization's own namespace of the store: two files, never the upload
expect(logoKeys(app, project)).toHaveLength(2)
})
it("resizes an image larger than a stored logo may be instead of refusing it, and every stored file passes the kit check", async () => {
const { project } = await fresh()
const big = noisePng(700, 300)
expect(big.length).toBeGreaterThan(256 * 1024)
await setLogo(project, "dark", "image/png", big)
for (const size of ["1x", "2x"] as const) {
const { bytes, contentType } = await getLogo(project, "dark", size)
expect(bytes.length).toBeLessThanOrEqual(256 * 1024)
expect(checkLogo(contentType, bytes)).toMatchObject({ ok: true })
}
})
it("takes a WebP and stores it as PNG", async () => {
const { project } = await fresh()
await setLogo(project, "dark", "image/webp", MARK_WEBP)
const { bytes, contentType } = await getLogo(project, "dark")
expect(contentType).toBe("image/png")
expect(dimensions(bytes)).toEqual({ width: 400, height: 100 })
})
it("replaces a logo and removes the files it replaced", async () => {
const { app, project } = await fresh()
await setLogo(project, "dark", "image/png", png(100))
const second = await setLogo(project, "dark", "image/png", png(200))
const keys = logoKeys(app, project)
expect(keys).toHaveLength(2)
expect(keys.every((key) => second.logoDark?.includes(key.split("-")[2] ?? "?"))).toBe(true)
// the same image again is the same logo: nothing is lost
const third = await setLogo(project, "dark", "image/png", png(200))
expect(third.logoDark).toBe(second.logoDark)
expect(logoKeys(app, project)).toEqual(keys)
})
it("keeps an SVG that is shapes alone exactly as uploaded, one file for both sizes", async () => {
const { app, project } = await fresh()
const logo = svgOf('<rect width="4" height="4" fill="#123456"/>')
const view = await setLogo(project, "light", "image/svg+xml", logo)
expect(view.logoLight).not.toBeNull()
expect(logoKeys(app, project)).toHaveLength(1)
for (const size of [undefined, "1x", "2x"] as const) {
const back = await getLogo(project, "light", size)
expect(back.contentType).toBe("image/svg+xml")
expect(back.bytes.equals(logo)).toBe(true)
}
})
it("refuses an SVG that carries a script, stores nothing and leaves the brand as it was", async () => {
const { app, project } = await fresh()
await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#1a1a8c" })
const before = await stored(app, project)
for (const inner of [
"<script>alert(1)</script>",
'<rect onload="alert(1)" width="1" height="1"/>',
'<image href="https://evil.example.test/x.png"/>',
]) {
const error = await refusal(setLogo(project, "light", "image/svg+xml", svgOf(inner)))
expect(error.status, inner).toBe(422)
}
expect(await stored(app, project)).toEqual(before)
expect(logoKeys(app, project)).toEqual([])
})
it("refuses a file the kit's checks refuse, naming the rule and not the file", async () => {
const { app, project } = await fresh()
const damaged = Buffer.from(PNG)
damaged[40] = (damaged[40] ?? 0) ^ 0xff
const lie = Buffer.concat([
PNG.subarray(0, 8),
PNG.subarray(8, 33),
chunk("IDAT", Buffer.from("not zlib data at all")),
chunk("IEND", new Uint8Array()),
])
for (const [name, type, bytes, rule] of [
["a PNG that is really markup", "image/png", Buffer.from("<svg/>"), /not a PNG/],
["a WebP that is a PNG", "image/webp", PNG, /not a WebP/],
["a PNG with a damaged chunk", "image/png", damaged, /damaged/],
["a PNG cut short", "image/png", PNG.subarray(0, 200), /damaged|PNG/],
["a PNG whose data does not decode", "image/png", lie, /decoded|visible/],
["a type that is not an image", "text/html", Buffer.from("<html>"), /PNG, WebP or SVG/],
[
"more than the upload limit",
"image/png",
Buffer.concat([PNG, Buffer.alloc(2 * 1024 * 1024 + 1 - PNG.length)]),
/larger than/,
],
] as const) {
const error = await refusal(setLogo(project, "light", type, bytes))
expect(error.status, name).toBe(422)
expect(JSON.stringify(error.problem), name).toMatch(rule)
}
expect(await stored(app, project)).toBeNull()
expect(logoKeys(app, project)).toEqual([])
})
it("refuses a body that is not base64", async () => {
const { project } = await fresh()
const text = await refusal(
project.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant: "light",
contentType: "image/png",
body: "not base64!",
}),
)
expect(text.status).toBe(422)
})
it("is removed from the brand and from the store when it is cleared, and then there is no logo to read", async () => {
const { app, project } = await fresh()
await setLogo(project, "light", "image/png", PNG)
await setLogo(project, "dark", "image/png", png(100))
expect(logoKeys(app, project)).toHaveLength(4)
await project.call.callByName("branding.logo.clear", { org: project.orgSlug, variant: "light" })
expect(logoKeys(app, project)).toHaveLength(2)
expect(
(await refusal(project.call.callByName("branding.logo.get", { org: project.orgSlug, variant: "light" }))).status,
).toBe(404)
expect(dimensions((await getLogo(project, "dark")).bytes)).toEqual({ width: 100, height: 60 })
})
it("is not readable by another organization's member", async () => {
const { app, project } = await fresh()
await setLogo(project, "light", "image/png", PNG)
const other = await app.seedProject("other", "owner@other.test")
expect(
(await refusal(other.call.callByName("branding.logo.get", { org: project.orgSlug, variant: "light" }))).status,
).toBe(404)
})
})
describe("what the organization layout writes from a stored brand", () => {
it("is the brand's own colours, legible in both themes", () => {
const { style, report } = tenantStyle({ primary: "#1a1a8c", accent: "#f4c430" })
expect(style["--brand-accent"]).toBe("#1a1a8c")
expect(style["--brand-accent-foreground"]).toBe("#ffffff")
expect(report.dark?.inkContrast).toBeGreaterThanOrEqual(4.5)
expect(report.light?.inkContrast).toBeGreaterThanOrEqual(4.5)
})
it("is nothing for an organization with no colour of its own, so the app's brand shows", () => {
expect(tenantStyle({ primary: null, accent: null }).style).toEqual({})
})
})
+59
View File
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The sign-out control ends every enabled module's browser context (joint contract §12, J1). Sign-out
// is a full navigation, so no provider unmounts: without the lifecycle run, the guides progress and
// unsent events of the person who signed out stay in the tab's `sessionStorage`.
import { afterEach, describe, expect, it, vi } from "vitest"
import { composedClientModules, endClientModules } from "../apps/web/src/ui/client-module-list"
/** A `Storage` over a Map, as much of it as the modules' handlers use. */
function memoryStorage(seed: Record<string, string>): Storage {
const values = new Map(Object.entries(seed))
const storage = {
get length() {
return values.size
},
key: (index: number) => [...values.keys()][index] ?? null,
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => values.clear(),
}
// `Object.keys(storage)` lists the stored keys, as it does for the browser's own.
return new Proxy(storage, {
ownKeys: () => [...values.keys()],
getOwnPropertyDescriptor: (_target, key) =>
typeof key === "string" && values.has(key)
? { enumerable: true, configurable: true, value: values.get(key) }
: undefined,
}) as unknown as Storage
}
afterEach(() => {
vi.unstubAllGlobals()
})
describe("signing out ends the client modules' context", () => {
it("clears what each enabled module keeps in the tab and leaves the app's own keys", async () => {
const own = "app.preference"
const keys = [own]
keys.push("conusai.guides.acme.u-7.queue", "conusai.guides.beta.u-7.active") // kit:guides
keys.push("conusai.support.session.v1:spk_example") // kit:support
const storage = memoryStorage(Object.fromEntries(keys.map((key) => [key, "{}"])))
vi.stubGlobal("window", { sessionStorage: storage })
const report = await endClientModules("sign-out")
expect(report.ok).toBe(true)
expect(report.outcomes.map((outcome) => outcome.module).sort()).toEqual([...composedClientModules.modules].sort())
expect(Object.keys(storage)).toEqual([own])
})
it("issues a new context instance, so a late answer of the old one is discarded", async () => {
vi.stubGlobal("window", { sessionStorage: memoryStorage({}) })
const before = composedClientModules.current()
await endClientModules("sign-out")
expect(before.signal.aborted).toBe(true)
expect(composedClientModules.isCurrent(before.instance)).toBe(false)
})
})
+252
View File
@@ -0,0 +1,252 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: a ${NAME} in this file is what the
// compose document holds verbatim -- compose interpolates it from the deployment's env file -- so
// the assertions compare against that text and not against a value.
// compose.yaml: the ordering the platform does not give (decision AT-D14), and the keys the
// platform's own compose model accepts.
//
// The model is crates/conusai-deployer/src/compose_model.rs: a service key it has no field for
// fails the deployment's parse, and so does a host binding in `ports`. This suite is that list,
// read from the document, so a key added here in a hurry fails in review and not on stage.
import { readFileSync } from "node:fs"
import { describe, expect, it } from "vitest"
import { parse } from "yaml"
interface Service {
build?: { context: string; dockerfile: string; target?: string }
image?: string
command?: string[]
environment?: Record<string, string>
expose?: (string | number)[]
ports?: unknown[]
depends_on?: Record<string, { condition: string }>
restart?: string
healthcheck?: { test: string[]; interval?: string }
deploy?: { resources?: { limits?: { cpus?: string; memory?: string } } }
init?: boolean
working_dir?: string
stop_grace_period?: string
}
const document = parse(readFileSync(new URL("../compose.yaml", import.meta.url), "utf8")) as {
services: Record<string, Service>
}
const services = document.services
const optional = (name: string): string => `\${${name}:-}`
/**
* The passthroughs every profile owes each process (BUG-APP-02): a variable the code reads but
* compose never names never reaches the container, so the deployment's value silently loses to
* the default. `${VAR}` where boot refuses to run without it, `${VAR:-}` where a default stands.
*/
function expectPassthroughs(named: Record<string, Service>): void {
for (const name of [
"OIDC_WEB_CLIENT_SECRET",
"OIDC_RESOURCE",
"OIDC_INTROSPECTION_CLIENT_ID",
"OIDC_INTROSPECTION_CLIENT_SECRET",
"OIDC_RESOURCE_STATUS_URL",
"OIDC_SCOPES",
"AUTH_PROVIDER_GENERATION",
"AUTH_COOKIE_NAME",
"AUTH_SESSION_IDLE_SECONDS",
"AUTH_SESSION_ABSOLUTE_SECONDS",
]) {
expect(named.web?.environment?.[name], `web ${name}`).toBe(optional(name))
}
for (const name of ["web", "worker"] as const) {
if (named[name]) expect(named[name]?.environment?.CONUSAI_ORG_SLUG).toBe(optional("CONUSAI_ORG_SLUG"))
}
if (named.worker) {
expect(named.worker.environment?.CONUSAI_PUBLIC_URL).toBe("${CONUSAI_PUBLIC_URL}")
expect(named.worker.environment?.DOCUMENTS_EXCLUDED_PAGE_CLASSES).toBe(optional("DOCUMENTS_EXCLUDED_PAGE_CLASSES"))
}
if (named.migrate) expect(named.migrate.environment?.CONUSAI_RELEASE).toBe("${CONUSAI_RELEASE}")
for (const name of [
"CONUSAI_ENVIRONMENT",
"CONUSAI_DSN",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
]) {
if (named.migrate) expect(named.migrate.environment?.[name], `migrate ${name}`).toBe(optional(name))
}
}
/** Every key TenantService models (compose_model.rs). Anything else fails the platform's parse. */
const MODELLED = new Set([
"image",
"build",
"command",
"environment",
"ports",
"expose",
"volumes",
"depends_on",
"restart",
"healthcheck",
"deploy",
"init",
"stop_grace_period",
"stop_signal",
"working_dir",
"user",
])
describe("compose.yaml", () => {
it("has the three services of conusai.json and nothing else", () => {
expect(Object.keys(services).sort()).toEqual(["migrate", "web", "worker"])
})
it("uses only the keys the platform's compose model has a field for", () => {
for (const [name, service] of Object.entries(services)) {
for (const key of Object.keys(service)) {
expect(MODELLED.has(key), `${name}.${key} is not in the platform's compose model`).toBe(true)
}
}
})
it("builds every service from one Dockerfile, so one image serves all three", () => {
const builds = Object.values(services).map((service) => JSON.stringify(service.build))
expect(new Set(builds).size).toBe(1)
expect(Object.values(services).every((service) => service.image === undefined)).toBe(true)
})
it("starts each process with its own argv, never a shell string", () => {
for (const [name, service] of Object.entries(services)) {
expect(Array.isArray(service.command), `${name} has no argv command`).toBe(true)
}
expect(services.migrate?.command?.at(-1)).toContain("migrate.js")
})
it("runs the migrate job to completion before web and worker start", () => {
// `kind: job` in conusai.json is inert at deploy time: one `docker compose up -d --wait`
// starts everything at once, and `pre_deploy_command` has no executor at all. This is the
// ordering (decision AT-D14).
expect(services.migrate?.restart).toBe("no")
for (const name of ["web", "worker"] as const) {
expect(services[name]?.depends_on?.migrate).toEqual({ condition: "service_completed_successfully" })
}
})
it("publishes no host port", () => {
for (const service of Object.values(services)) expect(service.ports).toBeUndefined()
expect(services.web?.expose).toEqual(["3000"])
})
it("gives every service a limit, and the two long-running ones a healthcheck", () => {
for (const [name, service] of Object.entries(services)) {
expect(service.deploy?.resources?.limits?.memory, `${name} has no memory limit`).toBeTruthy()
expect(service.deploy?.resources?.limits?.cpus, `${name} has no cpu limit`).toBeTruthy()
expect(service.init, `${name} does not reap its children`).toBe(true)
}
expect(services.web?.healthcheck?.test).toBeTruthy()
expect(services.worker?.healthcheck?.test).toBeTruthy()
// A job is not healthy or unhealthy; it succeeds or it fails.
expect(services.migrate?.healthcheck).toBeUndefined()
})
it("hands the owner's URL to the migrate job alone", () => {
expect(services.migrate?.environment?.POSTGRES_MIGRATE_URL).toBe("${POSTGRES_MIGRATE_URL}")
expect(services.web?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined()
expect(services.worker?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined()
for (const name of ["web", "worker", "migrate"] as const) {
expect(services[name]?.environment?.POSTGRES_URL).toBe("${POSTGRES_URL}")
expect(services[name]?.environment?.POSTGRES_READER_URL).toBe("${POSTGRES_READER_URL}")
}
})
it("passes every variable each process reads through to its container", () => {
expectPassthroughs(services)
})
})
describe("profiles/mariadb/compose.yaml", () => {
const profile = parse(readFileSync(new URL("../profiles/mariadb/compose.yaml", import.meta.url), "utf8")) as {
services: Record<string, Service>
}
it("hands the one MariaDB login to every process and the Valkey queue to web and worker", () => {
for (const name of ["web", "worker", "migrate"] as const) {
expect(profile.services[name]?.environment?.MARIADB_URL).toBe("${MARIADB_URL}")
}
for (const name of ["web", "worker"] as const) {
expect(profile.services[name]?.environment?.REDIS_URL).toBe("${REDIS_URL}")
}
// MariaDB has no row-level security: there is no owner/runtime/reader split to hand out.
expect(profile.services.migrate?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined()
})
it("passes every variable each process reads through to its container", () => {
expectPassthroughs(profile.services)
})
})
for (const name of ["mongodb", "convex"] as const) {
describe(`profiles/${name}/compose.yaml`, () => {
const profile = parse(readFileSync(new URL(`../profiles/${name}/compose.yaml`, import.meta.url), "utf8")) as {
services: Record<string, Service>
}
it("passes the platform billing slug and web auth settings through to the containers", () => {
for (const service of ["web", "worker"] as const) {
if (profile.services[service]) {
expect(profile.services[service]?.environment?.CONUSAI_ORG_SLUG).toBe(
name === "convex" ? `\${CONUSAI_ORG_SLUG}` : optional("CONUSAI_ORG_SLUG"),
)
}
}
for (const variable of [
"OIDC_WEB_CLIENT_SECRET",
"OIDC_RESOURCE",
"OIDC_INTROSPECTION_CLIENT_ID",
"OIDC_INTROSPECTION_CLIENT_SECRET",
"OIDC_RESOURCE_STATUS_URL",
"OIDC_SCOPES",
"AUTH_PROVIDER_GENERATION",
"AUTH_COOKIE_NAME",
"AUTH_SESSION_IDLE_SECONDS",
"AUTH_SESSION_ABSOLUTE_SECONDS",
]) {
expect(profile.services.web?.environment?.[variable], `web ${variable}`).toBe(optional(variable))
}
})
})
}
describe("the local stack", () => {
const dev = readFileSync(new URL("../compose.dev.yaml", import.meta.url), "utf8")
const roles = readFileSync(new URL("../dev/postgres-roles.sql", import.meta.url), "utf8")
const example = readFileSync(new URL("../.env.example", import.meta.url), "utf8")
it("creates the three roles the platform creates, with the same powers", () => {
for (const role of ["appdev_owner", "appdev_app", "appdev_reader"]) {
expect(roles, `${role} is not created`).toMatch(new RegExp(`CREATE ROLE ${role}\\s+LOGIN`))
}
// The three are LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT NOREPLICATION,
// as data-services DS21 creates them.
expect(roles.match(/NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT NOREPLICATION/g)).toHaveLength(3)
// The owner owns schema public; the runtime role gets rows, never ownership.
expect(roles).toContain('ALTER SCHEMA public OWNER TO "appdev_owner"')
expect(roles).toContain('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "appdev_app"')
// The reader gets no table privilege here: the app's own migrations grant it what it reads.
expect(roles).not.toMatch(/GRANT SELECT ON (ALL )?TABLES? [^;]*"appdev_reader"/)
})
it("hands each local process the same URL the platform would", () => {
expect(example).toContain("POSTGRES_URL=postgresql://appdev_app:")
expect(example).toContain("POSTGRES_READER_URL=postgresql://appdev_reader:")
expect(example).toContain("POSTGRES_MIGRATE_URL=postgresql://appdev_owner:")
})
it("keeps the development stack out of what is deployed", () => {
// conusai.json names compose.yaml alone, so nothing in compose.dev.yaml is ever deployed.
expect(dev).toContain("image: pgvector/pgvector:pg18")
expect(readFileSync(new URL("../conusai.json", import.meta.url), "utf8")).not.toContain("compose.dev.yaml")
})
})
+132
View File
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// conusai.json against the real parser's own schema (AT2.1 done_when).
//
// `schemas/conusai.schema.json` is generated from crates/conusai-presets, and
// scripts/vendor/test-template.sh runs that generator's `--check` before this suite, so what is
// validated here is the closed schema the platform parses with: an unknown key fails, and so
// does a missing one.
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { Ajv2020 } from "ajv/dist/2020.js"
import addFormats from "ajv-formats"
import { describe, expect, it } from "vitest"
const repoRoot = process.env.CONUSAI_REPO_ROOT
const config = JSON.parse(readFileSync(new URL("../conusai.json", import.meta.url), "utf8")) as {
schema_version: number
compose: { file: string }
services: Record<
string,
{
kind?: string
pre_deploy_command?: string
public?: { port: number; primary?: boolean }
health_check?: { path: string }
size?: string
postgres?: { migrate?: boolean }
}
>
links: { type: string }[]
previews: { enabled: boolean; size?: string }
token?: { permissions: string[] }
guides?: { bundle: string }
}
describe("conusai.json", () => {
it.runIf(repoRoot)("validates against the generated conusai.json schema", () => {
const schema = JSON.parse(readFileSync(join(repoRoot as string, "schemas/conusai.schema.json"), "utf8")) as object
const ajv = new Ajv2020({ strict: false, allErrors: true })
addFormats(ajv)
const validate = ajv.compile(schema)
const valid = validate(config)
expect(validate.errors ?? [], JSON.stringify(validate.errors, null, 2)).toEqual([])
expect(valid).toBe(true)
})
it("is schema version 2 and names the compose file", () => {
expect(config.schema_version).toBe(2)
expect(config.compose.file).toBe("compose.yaml")
})
it("declares web, worker and the migrate service, all of one image and none of them a job", () => {
expect(Object.keys(config.services).sort()).toEqual(["migrate", "web", "worker"])
// kind "job" and pre_deploy_command are gone (KI-197): the migrate service is an ordinary
// service whose ordering comes from compose's depends_on (AT-D14).
for (const [name, service] of Object.entries(config.services)) {
expect(service.kind, `${name}.kind`).toBeUndefined()
expect(service.pre_deploy_command, `${name}.pre_deploy_command`).toBeUndefined()
}
})
// kit:guides:begin
it("declares the code-guides bundle the image build writes, at an absolute path (ADR-045)", () => {
expect(config.guides).toEqual({ bundle: "/app/.conusai/guides-bundle.json" })
const dockerfile = readFileSync(new URL("../Dockerfile", import.meta.url), "utf8")
// The build stage's WORKDIR is /app and the runtime stage copies /app whole, so the file the
// build writes is where conusai.json says the platform will find it.
expect(dockerfile).toContain(`node apps/worker/dist/guides-bundle.js ${config.guides?.bundle} guides-anchors.json`)
})
// kit:guides:end
it("exposes web alone, on its own health path", () => {
expect(config.services.web?.public).toEqual({ port: 3000, primary: true })
// The path the platform probes is the path compose probes: one definition of "this web container is
// fit to serve", /api/ready (store, reader, migration level), and not a second, weaker one.
expect(config.services.web?.health_check?.path).toBe("/api/ready")
const compose = readFileSync(new URL("../compose.yaml", import.meta.url), "utf8")
expect(compose).toContain("http://127.0.0.1:3000/api/ready")
expect(config.services.worker?.public).toBeUndefined()
expect(config.services.migrate?.public).toBeUndefined()
})
it("declares exactly one migrator, which is the migrate job (data-services DS21)", () => {
const migrators = Object.entries(config.services).filter(([, service]) => service.postgres?.migrate === true)
expect(migrators.map(([name]) => name)).toEqual(["migrate"])
})
it("links a Postgres service, and one sign-in link", () => {
// The identity link is what injects OIDC_ISSUER/OIDC_WEB_CLIENT_ID/OIDC_AUDIENCE at
// deploy (MIB I3); without the declaration the app boots signed-out-only (KI-199).
const links = config.links as Array<{ type: string; role?: string }>
expect(links[0]?.type).toBe("postgres")
const signInLinks = ["identity"]
signInLinks.push("better-auth") // kit:convex-better-auth -- the named Convex link replaces Identity (BL)
expect(links.filter((link) => signInLinks.includes(link.role ?? link.type))).toHaveLength(1)
})
it("requests the platform's canonical runtime scopes for document processing and usage metering", () => {
const permissions = config.token?.permissions ?? []
expect(permissions).toEqual(expect.arrayContaining(["ocr:use", "ai:execute", "usage:write"]))
expect(permissions).not.toContain("billing:ingest")
const maria = JSON.parse(readFileSync(new URL("../profiles/mariadb/conusai.json", import.meta.url), "utf8")) as {
token?: { permissions: string[] }
}
expect(maria.token?.permissions).toEqual(expect.arrayContaining(["ocr:use", "ai:execute", "usage:write"]))
expect(maria.token?.permissions).not.toContain("billing:ingest")
})
// The account page's platform analytics erasure and export (ADR-049) call the deployment-token
// routes, which take `privacy:subjects` by name: `*` never carries it, so it is listed explicitly.
it("requests privacy:subjects for the account page's analytics erasure and export", () => {
expect(config.token?.permissions).toContain("privacy:subjects")
expect(config.token?.permissions).not.toContain("*")
})
// Every selectable profile's conusai.json replaces this one at the app root
// (`kit template:prune --profile`), so each must request what the enabled modules need: the
// Convex profile asked for none, so a Convex app's deployment token lacked guides:runtime and
// support:widget-sessions and its guides and support calls were refused. `template:prune --without`
// removes a module's permission from every copy, so the lists stay equal after a prune too.
it.each(["convex", "mariadb"])("the %s profile requests the same runtime scopes as the default", (profile) => {
const copy = JSON.parse(readFileSync(new URL(`../profiles/${profile}/conusai.json`, import.meta.url), "utf8")) as {
token?: { permissions: string[] }
}
const moduleScopes: string[] = []
moduleScopes.push("guides:runtime") // kit:guides
moduleScopes.push("support:widget-sessions") // kit:support
expect(config.token?.permissions).toEqual(expect.arrayContaining(moduleScopes))
expect([...(copy.token?.permissions ?? [])].sort()).toEqual([...(config.token?.permissions ?? [])].sort())
})
})
+466
View File
@@ -0,0 +1,466 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { readFileSync } from "node:fs"
import { AppError, buildRegistry } from "@conusai-kit/core"
import { generateConvex } from "@conusai-kit/data-convex"
import { getFunctionName } from "convex/server"
import { ConvexError } from "convex/values"
import { describe, expect, it } from "vitest"
import { parse } from "yaml"
import { projectEntriesFor } from "../apps/web/src/ui/projects.js"
import {
CONVEX_ACTION_EFFECT_MODULES,
CONVEX_EXCLUDED_MODULES,
CONVEX_EXCLUDED_PROCEDURES,
convexBffModules,
convexModules,
} from "../convex/lib/convexModules.mjs"
import { appModules } from "../modules/index.js"
const root = new URL("..", import.meta.url)
describe("Convex deployable module closure", () => {
it("commits generated wrappers that forward explicit idempotency keys to the kernel", () => {
// The same profile filter as below: the Postgres-only modules are not Convex-deployed yet.
const convexBound = appModules.filter(
(module) => module.profiles === undefined || module.profiles.includes("convex"),
)
const registry = buildRegistry([...convexModules(convexBound)])
const generated = generateConvex(registry, {
header: "// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n",
})
for (const file of generated) {
expect(readFileSync(new URL(`convex/${file.path}`, root), "utf8"), file.path).toBe(file.content)
}
const proposal = generated.find(({ path }) => path === "generated/proposals.ts")
expect(proposal?.content).toContain(
'ctx.run("proposals.proposal.create", input, idempotencyKey === undefined ? undefined : { idempotencyKey })',
)
})
it("keeps the documents review route visible in the Convex project navigation", () => {
expect(projectEntriesFor("convex")).toContainEqual({
href: "documents",
label: "documents.title",
module: "documents",
})
})
it("includes server-side auth handlers and only excludes modules with an explicit runtime gap", () => {
// Modules that declare `profiles: ["postgres"]` (knowledge, agent) ship fail-closed Convex
// binding files, but the integration review (2026-10-23) kept them Postgres-only: their
// search/run effects and jobs leave the process and have no composed Convex Node actions yet
// (the reasons are in their module READMEs). The generated closure is therefore the modules
// that support the profile.
const convexBound = appModules.filter(
(module) => module.profiles === undefined || module.profiles.includes("convex"),
)
const modules = convexModules(convexBound)
const closure = ["tenancy", "auth", "files", "proposals"]
closure.push("guides") // kit:guides -- its effect procedures are generated as Convex actions
closure.push("support") // kit:support -- its effect procedures are generated as Convex actions
expect(modules.map(({ id }) => id)).toEqual([...closure, "branding", "documents", "notes", "account"])
// The only effects generated as actions are those of the platform-call modules (convex/lib/effects.ts).
const effects = modules.flatMap(({ id, procedures = [] }) =>
procedures.filter(({ kind }) => kind === "effect").map(({ name }) => [id, name] as const),
)
expect(effects.every(([id]) => CONVEX_ACTION_EFFECT_MODULES.has(id))).toBe(true)
const effectNames = effects.map(([, name]) => name)
// The account module's export and deletion: actions over its bound store (convex/account.ts).
const expectedEffects: string[] = ["account.data.export", "account.deletion.request"]
// kit:guides:begin
expectedEffects.push(
"guides.builder.open",
"guides.runtime.bootstrap",
"guides.runtime.deleteFile",
"guides.runtime.record",
)
// A survey file's upload is a streamed body: not a Convex action on this profile.
expect(CONVEX_EXCLUDED_PROCEDURES.has("guides.runtime.uploadFile")).toBe(true)
// kit:guides:end
// kit:support:begin
expectedEffects.push("support.widgetSession.mint", "support.widget.config")
// kit:support:end
expect([...effectNames].sort()).toEqual(expectedEffects.sort())
const fileProcedures = modules.find(({ id }) => id === "files")?.procedures?.map(({ name }) => name)
expect(fileProcedures ?? []).toContain("files.file.get")
expect(fileProcedures ?? []).toContain("files.file.list")
expect(fileProcedures ?? []).not.toContain("files.file.delete")
expect(CONVEX_EXCLUDED_PROCEDURES.has("files.file.delete")).toBe(true)
expect([...CONVEX_EXCLUDED_MODULES.keys()]).toEqual([])
const branding = modules.find(({ id }) => id === "branding")?.procedures?.map(({ name }) => name)
expect(branding).toEqual(["branding.brand.get", "branding.brand.set"])
for (const name of ["branding.logo.get", "branding.logo.set", "branding.logo.clear"]) {
expect(CONVEX_EXCLUDED_PROCEDURES.has(name)).toBe(true)
expect(
convexBffModules(appModules)
.flatMap(({ procedures = [] }) => procedures)
.map(({ name }) => name),
).not.toContain(name)
}
expect(CONVEX_EXCLUDED_MODULES.get("auth")).toBeUndefined()
})
it("keeps composed file effects in the web BFF registry without generating Convex effect wrappers", () => {
const filtered = appModules.filter((module) => module.profiles === undefined || module.profiles.includes("convex"))
const backend = convexModules(filtered)
const bff = convexBffModules(filtered)
expect(
backend.some(
({ id, procedures = [] }) =>
!CONVEX_ACTION_EFFECT_MODULES.has(id) && procedures.some(({ kind }) => kind === "effect"),
),
).toBe(false)
const fileProcedures = bff.find(({ id }) => id === "files")?.procedures?.map(({ name }) => name)
expect(fileProcedures ?? []).toContain("files.file.beginUpload")
expect(fileProcedures ?? []).toContain("files.file.putPart")
expect(fileProcedures ?? []).toContain("files.file.completeUpload")
expect(fileProcedures ?? []).toContain("files.file.delete")
expect(bff.find(({ id }) => id === "proposals")?.procedures?.map(({ name }) => name)).toContain(
"proposals.proposal.approve",
)
expect(bff.find(({ id }) => id === "documents")?.procedures?.map(({ name }) => name)).toContain(
"documents.document.get",
)
})
it("registers scheduled mutations through generated internal function references", () => {
const crons = readFileSync(new URL("convex/crons.ts", root), "utf8")
expect(crons).toContain("internal.relay.relayPass")
expect(crons).toContain("internal.files.recoverRows")
// The account module's deletion sweep, the Postgres worker's DELETION_SWEEP_JOB.
expect(crons).toContain("internal.account.sweep")
expect(crons).not.toContain('from "./relay"')
})
it("keeps audit credentials behind a root internal action", () => {
const config = readFileSync(new URL("convex/convex.config.ts", root), "utf8")
const provider = readFileSync(new URL("convex/auditCredentials.ts", root), "utf8")
const relay = readFileSync(new URL("convex/relay.ts", root), "utf8")
expect(config).not.toContain("CONUSAI_API_TOKEN")
expect(provider).toContain("export const get = internalAction(")
expect(provider).toContain("apiUrl: process.env.CONUSAI_API_URL")
expect(provider).toContain("apiToken: process.env.CONUSAI_API_TOKEN")
expect(relay).toContain("credentialsProvider: internal.auditCredentials.get")
})
it("deploys functions before web and backup with the provider's server-only credential names", () => {
const compose = parse(readFileSync(new URL("profiles/convex/compose.yaml", root), "utf8")) as {
services: Record<
string,
{
command?: string[]
environment?: Record<string, string>
restart?: string
depends_on?: Record<string, { condition: string }>
}
>
}
const profile = JSON.parse(readFileSync(new URL("profiles/convex/conusai.json", root), "utf8")) as {
services: Record<string, unknown>
}
expect(Object.keys(compose.services).sort()).toEqual(["backup", "convex-deploy", "web"])
expect(Object.keys(profile.services).sort()).toEqual(Object.keys(compose.services).sort())
expect(compose.services["convex-deploy"]?.command).toEqual(["node", "profiles/convex/deploy.mjs"])
expect(compose.services["convex-deploy"]?.restart).toBe("no")
for (const name of ["web", "backup"] as const) {
expect(compose.services[name]?.depends_on?.["convex-deploy"]).toEqual({
condition: "service_completed_successfully",
})
}
for (const name of ["web", "backup", "convex-deploy"] as const) {
const environment = compose.services[name]?.environment ?? {}
expect(environment.CONVEX_SELF_HOSTED_URL).toBe("$" + "{CONVEX_SELF_HOSTED_URL}")
expect(environment.CONVEX_SELF_HOSTED_ADMIN_KEY).toBe("$" + "{CONVEX_SELF_HOSTED_ADMIN_KEY}")
expect(Object.keys(environment).some((key) => key.startsWith("NEXT_PUBLIC_") && key.includes("ADMIN"))).toBe(
false,
)
expect(environment.CONVEX_URL).toBeUndefined()
expect(environment.CONVEX_ADMIN_KEY).toBeUndefined()
}
expect(compose.services["convex-deploy"]?.environment).toMatchObject({
CONUSAI_API_TOKEN: expect.stringMatching(/^\$\{CONUSAI_API_TOKEN\}$/),
CONUSAI_PROJECT_ID: expect.stringMatching(/^\$\{CONUSAI_PROJECT_ID\}$/),
CONUSAI_ORG_SLUG: expect.stringMatching(/^\$\{CONUSAI_ORG_SLUG\}$/),
})
expect(compose.services.web?.environment?.CONUSAI_ORG_SLUG).toBe("$" + "{CONUSAI_ORG_SLUG}")
const deploy = readFileSync(new URL("profiles/convex/deploy.mjs", root), "utf8")
expect(deploy).toContain('"env", "set", "--from-file", envFile')
expect(deploy).toContain("mode: 0o600")
expect(deploy).toContain('"deploy", "--typecheck", "disable"')
const runtime = readFileSync(new URL("convex/lib/runtime.ts", root), "utf8")
expect(runtime).toContain('"filesActions:recoverUpload"')
})
})
describe("the account module on the Convex profile", () => {
it("binds a store whose every method is one internal query (a read) or mutation (a change)", async () => {
const { ACCOUNT_STORE_READS, ACCOUNT_STORE_WRITES, convexAccountStore } = await import("../convex/lib/effects")
const { createAccountStore } = await import("../modules/account/index.js")
const methods = Object.keys(createAccountStore({ data: {} as never, system: {} as never })).sort()
expect([...ACCOUNT_STORE_READS, ...ACCOUNT_STORE_WRITES].sort()).toEqual(methods)
const calls: Array<[string, string, unknown]> = []
const record = (kind: string) => async (reference: unknown, args: unknown) => {
calls.push([kind, getFunctionName(reference as never), args])
return null
}
const store = convexAccountStore({
auth: { getUserIdentity: async () => null },
runQuery: record("query") as never,
runMutation: record("mutation") as never,
})
await store.deletion("user-1")
await store.identities("user-1")
await store.changeDeletion("user-1", { kind: "step", step: "credentials" })
expect(calls).toEqual([
["query", "account:storeRead", { method: "deletion", args: ["user-1"] }],
["query", "account:storeRead", { method: "identities", args: ["user-1"] }],
[
"mutation",
"account:storeWrite",
{ method: "changeDeletion", args: ["user-1", { kind: "step", step: "credentials" }] },
],
])
})
it("brings a backend refusal back as the kernel's own problem", async () => {
const { convexAccountStore } = await import("../convex/lib/effects")
const store = convexAccountStore({
auth: { getUserIdentity: async () => null },
runQuery: (async () => {
throw new ConvexError({ type: "/problems/not_found", title: "Not found", status: 404, code: "not_found" })
}) as never,
runMutation: (async () => null) as never,
})
const error = await store.deletion("user-1").then(
() => null,
(thrown: unknown) => thrown,
)
expect(error).toBeInstanceOf(AppError)
expect((error as AppError).status).toBe(404)
})
it("reads the account credential from the BFF's identity claims, and nothing from an absent one", async () => {
const { accountCredentialOf } = await import("../convex/lib/runtime")
const identity = (claims: Record<string, unknown>) =>
({ issuer: "https://conusai.internal/bff", subject: "u", tokenIdentifier: "t", ...claims }) as never
expect(accountCredentialOf(null)).toEqual({ interactive: false })
expect(accountCredentialOf(identity({}))).toEqual({ interactive: false })
expect(accountCredentialOf(identity({ conusaiCredential: "interactive", conusaiSessionId: "s-1" }))).toEqual({
interactive: true,
sessionId: "s-1",
})
expect(
accountCredentialOf(
identity({ conusaiCredential: "interactive", conusaiSignedInAt: "2026-10-25T00:00:00.000Z" }),
),
).toEqual({ interactive: true, signedInAt: "2026-10-25T00:00:00.000Z" })
expect(accountCredentialOf(identity({ conusaiCredential: true, conusaiSessionId: "s-1" }))).toEqual({
interactive: false,
})
})
// kit:convex-better-auth:begin
it("installs the Better Auth deployment's variables in the backend only for a Better Auth app", () => {
const deploy = readFileSync(new URL("profiles/convex/deploy.mjs", root), "utf8")
expect(deploy).toContain(
'variables.push("AUTH_PROVIDER", "AUTH_IDENTITY_NAMESPACE", "BETTER_AUTH_CONVEX_URL", "BETTER_AUTH_CONVEX_ADMIN_KEY")',
)
})
// kit:convex-better-auth:end
})
/**
* The sign-out revocation outbox on the Convex profile (joint contract §14): the Postgres profile's
* own outbox (modules/account/core/revocation.ts) on Convex storage. The web records a revoked app
* session through the internal action `account:revokeSession`, which writes the row and delivers it
* at once from the backend; what a platform did not confirm the one-minute cron `account:revocationSweep`
* retries with the row's backoff, once per module, until 65 minutes have passed. Here the backend
* functions are the very store the Convex functions run, over a memory data source in place of ctx.db.
*/
describe("the revocation outbox on the Convex profile", () => {
it("keeps the outbox in the backend: every store method is one internal query or mutation", async () => {
const { REVOCATION_STORE_READS, REVOCATION_STORE_WRITES, convexRevocationStore } = await import(
"../convex/lib/effects"
)
const { createRevocationStore } = await import("../modules/account/index.js")
expect([...REVOCATION_STORE_READS, ...REVOCATION_STORE_WRITES].sort()).toEqual(
Object.keys(createRevocationStore({} as never)).sort(),
)
const calls: Array<[string, string, unknown]> = []
const record = (kind: string) => async (reference: unknown, args: unknown) => {
calls.push([kind, getFunctionName(reference as never), args])
return kind === "query" ? [] : null
}
const store = convexRevocationStore({
auth: { getUserIdentity: async () => null },
runQuery: record("query") as never,
runMutation: record("mutation") as never,
})
await store.due("2026-10-26T00:00:00.000Z", 10)
await store.purge("2026-10-25T00:00:00.000Z")
expect(calls).toEqual([
["query", "account:revocationRead", { method: "due", args: ["2026-10-26T00:00:00.000Z", 10] }],
["mutation", "account:revocationWrite", { method: "purge", args: ["2026-10-25T00:00:00.000Z"] }],
])
})
it("binds the device session a derived service session is minted for, from the BFF's identity", async () => {
const { requestSessionOf } = await import("../convex/lib/runtime")
const { effectContainer } = await import("../convex/lib/effects")
const { REQUEST_SESSION } = await import("@conusai-kit/tenancy")
const identity = (claims: Record<string, unknown>) =>
({ issuer: "https://conusai.internal/bff", subject: "u", tokenIdentifier: "t", ...claims }) as never
const fake = process.env.CONUSAI_FAKE_PLATFORM
process.env.CONUSAI_FAKE_PLATFORM = "1"
expect(requestSessionOf(null)).toBeNull()
expect(requestSessionOf(identity({}))).toBeNull()
expect(requestSessionOf(identity({ conusaiIdentitySession: 7 }))).toBeNull()
expect(requestSessionOf(identity({ conusaiIdentitySession: "4:oidc:36:s-1" }))).toBe("4:oidc:36:s-1")
const ctx = {
auth: { getUserIdentity: async () => null },
runQuery: (async () => null) as never,
runMutation: (async () => null) as never,
}
expect(effectContainer(ctx, { interactive: true }, "4:oidc:36:s-1").resolve(REQUEST_SESSION)).toBe("4:oidc:36:s-1")
expect(effectContainer(ctx).resolve(REQUEST_SESSION)).toBeNull()
if (fake === undefined) delete process.env.CONUSAI_FAKE_PLATFORM
else process.env.CONUSAI_FAKE_PLATFORM = fake
// The BFF names the device session of every call it makes for a signed-in person.
const caller = readFileSync(new URL("apps/web/src/server/convex-caller.ts", root), "utf8")
expect(caller).toContain("conusaiIdentitySession: options.identitySession")
const request = readFileSync(new URL("apps/web/src/server/request.ts", root), "utf8")
expect(request).toContain("identitySession: sessionRefKey(session.sessionRef)")
})
it("records a sign-out through an internal action and retries it on a one-minute cron", () => {
const account = readFileSync(new URL("convex/account.ts", root), "utf8")
for (const name of ["revocationRead = internalQuery", "revocationWrite = internalMutation"]) {
expect(account).toContain(`export const ${name}`)
}
expect(account).toContain("export const revokeSession = internalAction")
expect(account).toContain("export const revocationSweep = internalAction")
const crons = readFileSync(new URL("convex/crons.ts", root), "utf8")
expect(crons).toContain(
'crons.interval("account session revocations", { minutes: 1 }, internal.account.revocationSweep, {})',
)
// The web asks the backend's outbox, and falls back to the modules only when nothing was recorded.
const app = readFileSync(new URL("apps/web/src/server/app.ts", root), "utf8")
expect(app).toContain("durable: (session: string) => revokeConvexSession(convex, session)")
})
// kit:support:begin
describe("delivered from an action's container, against the fake platform", () => {
const START = Date.parse("2026-10-25T10:00:00.000Z")
type Platform = Awaited<ReturnType<typeof import("@conusai-kit/testing/fake-platform").startFakePlatform>>
type Clock = { at(ms: number): void }
/** One test against a fresh fake platform, the backend's deployment variables and a fixed clock. */
async function withPlatform(run: (platform: Platform, clock: Clock) => Promise<void>): Promise<void> {
const { vi } = await import("vitest")
const { startFakePlatform } = await import("@conusai-kit/testing/fake-platform")
const env = { ...process.env }
const platform = await startFakePlatform()
process.env.CONUSAI_API_URL = platform.url
process.env.CONUSAI_API_TOKEN = "cnsi_test"
process.env.CONUSAI_SUPPORT_CHANNEL = "web"
delete process.env.CONUSAI_FAKE_PLATFORM
vi.useFakeTimers({ toFake: ["Date"] })
vi.setSystemTime(START)
try {
await run(platform, { at: (ms) => vi.setSystemTime(START + ms) })
} finally {
vi.useRealTimers()
process.env = env
await platform.close()
}
}
/** The backend: `account:revocationRead`/`revocationWrite` over one store, as convex/account.ts runs them. */
async function backend() {
const { createMemoryDataSource } = await import("@conusai-kit/data")
const { accountSessionRevocations, createRevocationStore } = await import("../modules/account/index.js")
const data = createMemoryDataSource({ entities: [accountSessionRevocations] })
const store = createRevocationStore(data)
const run = (expected: string) => async (reference: unknown, input: unknown) => {
const name = getFunctionName(reference as never)
if (name !== expected) throw new Error(`unexpected backend call ${name}`)
const { method, args } = input as { method: keyof typeof store; args: unknown[] }
const call = store[method] as (...values: unknown[]) => Promise<unknown>
return JSON.parse(JSON.stringify((await call.apply(store, args)) ?? null))
}
const ctx = {
auth: { getUserIdentity: async () => null },
runQuery: run("account:revocationRead") as never,
runMutation: run("account:revocationWrite") as never,
}
const rows = async () => (await data.global(accountSessionRevocations).list({ limit: 50 })).items
return { ctx, rows }
}
const revocationsOf = async (platform: Platform, session: string) => {
const response = await fetch(new URL("/__e2e/state", platform.url), {
headers: { "x-conusai-organization": "e2e" },
})
const state = (await response.json()) as {
support: Array<{ route: string; body?: { identity_session?: string } }>
}
return state.support.filter((call) => call.route === "revoke" && call.body?.identity_session === session)
}
it("a platform down at sign-out gets the revocation once it is back, and nothing twice", () =>
withPlatform(async (platform, clock) => {
const { revokeSessionInBackend, sweepRevocationsInBackend } = await import("../convex/lib/effects")
const { ctx, rows } = await backend()
const session = "18:oidc:0199abcd-1234-7000-8000-00000000c001"
platform.outage("/v1/support/", true)
// The sign-out is not blocked: the action answers once the row is written.
await expect(revokeSessionInBackend(ctx, session)).resolves.toBeUndefined()
expect((await rows())[0]).toMatchObject({ session, state: "pending", pending: ["support"], attempts: 1 })
expect(await revocationsOf(platform, session)).toEqual([])
// Due, still down: the cron's run fails (the line an operator is alerted on), the row waits longer.
clock.at(31_000)
await expect(sweepRevocationsInBackend(ctx)).rejects.toMatchObject({ code: "account_revocations_waiting" })
expect((await rows())[0]).toMatchObject({ state: "pending", attempts: 2 })
platform.outage("/v1/support/", false)
clock.at(31_000 + 61_000)
await sweepRevocationsInBackend(ctx)
expect((await rows())[0]).toMatchObject({ state: "delivered", pending: [], delivered: ["support"] })
expect(await revocationsOf(platform, session)).toHaveLength(1)
clock.at(20 * 60_000)
await sweepRevocationsInBackend(ctx)
expect(await revocationsOf(platform, session)).toHaveLength(1)
// A second notification of the same revocation writes nothing and asks nobody.
await revokeSessionInBackend(ctx, session)
expect(await rows()).toHaveLength(1)
expect(await revocationsOf(platform, session)).toHaveLength(1)
}))
it("delivers at once when the platform is up", () =>
withPlatform(async (platform) => {
const { revokeSessionInBackend } = await import("../convex/lib/effects")
const { ctx, rows } = await backend()
const session = "18:oidc:0199abcd-1234-7000-8000-00000000c002"
await revokeSessionInBackend(ctx, session)
expect((await rows())[0]).toMatchObject({ state: "delivered", pending: [] })
expect(await revocationsOf(platform, session)).toHaveLength(1)
}))
it("lapses after 65 minutes, when no widget session it minted can still be alive", () =>
withPlatform(async (platform, clock) => {
const { revokeSessionInBackend, sweepRevocationsInBackend } = await import("../convex/lib/effects")
const { ctx, rows } = await backend()
const session = "18:oidc:0199abcd-1234-7000-8000-00000000c003"
platform.outage("/v1/support/", true)
await revokeSessionInBackend(ctx, session)
platform.outage("/v1/support/", false)
clock.at(66 * 60_000)
await sweepRevocationsInBackend(ctx)
expect((await rows())[0]).toMatchObject({ state: "lapsed" })
expect(await revocationsOf(platform, session)).toEqual([])
}))
})
// kit:support:end
})
+92
View File
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The CSRF wall (spec §6.4): a state-changing call made with the session cookie carries the
// page's token, and a bearer or API-key caller is not asked for one, because no browser attaches
// those by itself.
import type { Principal } from "@conusai-kit/auth"
import { AppError } from "@conusai-kit/core"
import { beforeAll, describe, expect, it } from "vitest"
const session = (sessionId: string): Principal => ({
actor: { userId: "11111111-1111-1111-1111-111111111111" },
method: "session",
sessionId,
})
let csrfToken: (sessionId: string) => string
let requireCsrf: (request: Request, principal: Principal | null) => void
beforeAll(async () => {
// loadAuthConfig reads the environment at call time, so the secret is set before the import
// that uses it.
process.env.AUTH_SESSION_SECRET = "0123456789abcdef0123456789abcdef"
process.env.CONUSAI_PUBLIC_URL = "https://app.example.test"
process.env.OIDC_ISSUER = "https://id.example.test"
process.env.OIDC_WEB_CLIENT_ID = "web"
process.env.OIDC_AUDIENCE = "api"
const module = await import("../apps/web/src/server/csrf")
csrfToken = module.csrfToken
requireCsrf = module.requireCsrf
})
const post = (headers: Record<string, string>): Request =>
new Request("https://app.example.test/api/v1/tenancy.org.create", { method: "POST", headers })
describe("requireCsrf", () => {
it("accepts a POST carrying this session's token", () => {
const principal = session("session-1")
expect(() =>
requireCsrf(post({ "x-csrf-token": csrfToken("session-1"), "sec-fetch-site": "same-origin" }), principal),
).not.toThrow()
})
it("refuses a POST with no token", () => {
expect(() => requireCsrf(post({}), session("session-1"))).toThrow(AppError)
})
it("refuses another session's token", () => {
expect(() => requireCsrf(post({ "x-csrf-token": csrfToken("session-2") }), session("session-1"))).toThrow(
/x-csrf-token/,
)
})
it("refuses a cross-site POST even with a token", () => {
const headers = { "x-csrf-token": csrfToken("session-1"), "sec-fetch-site": "cross-site" }
expect(() => requireCsrf(post(headers), session("session-1"))).toThrow(/cross-site/)
})
it("asks nothing of a bearer or API-key caller", () => {
const bearer: Principal = { actor: { userId: "u" }, method: "bearer" }
const key: Principal = { actor: { userId: "u" }, method: "api-key", orgId: "o" }
expect(() => requireCsrf(post({}), bearer)).not.toThrow()
expect(() => requireCsrf(post({}), key)).not.toThrow()
})
it("asks every browser session method for its token, including dev-login and an external provider session", () => {
const dev: Principal = { actor: { userId: "u" }, method: "dev-login", sessionId: "session-1" }
const external: Principal = {
actor: { userId: "u" },
method: "external-session",
sessionRef: { provider: "convex-better-auth", opaqueId: "session-1" },
}
expect(() => requireCsrf(post({}), dev)).toThrow(/x-csrf-token/)
expect(() => requireCsrf(post({}), external)).toThrow(/x-csrf-token/)
// An external session's token is bound to its provider reference, never to a kit row of the same ID.
expect(() => requireCsrf(post({ "x-csrf-token": csrfToken("session-1") }), external)).toThrow(/x-csrf-token/)
})
it("fails closed on an external session disguised with a kit session row", () => {
const disguised: Principal = {
actor: { userId: "u" },
method: "external-session",
sessionId: "session-1",
sessionRef: { provider: "convex-better-auth", opaqueId: "session-1" },
}
expect(() => requireCsrf(post({ "x-csrf-token": csrfToken("session-1") }), disguised)).toThrow(AppError)
})
it("asks nothing of a GET", () => {
const request = new Request("https://app.example.test/api/health", { headers: {} })
expect(() => requireCsrf(request, session("session-1"))).not.toThrow()
})
})
+352
View File
@@ -0,0 +1,352 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A revoked app session ends the service sessions the modules minted for it (joint contract §14): the
// hook the web registers with kit-auth (`onSessionRevoked`) asks every installed module's account
// share to revoke them server-side by the session's reference, bounded, and never fails a sign-out.
// On the Postgres profile it is durable: the account module's revocation outbox records it, and the
// worker's revocation job delivers what a platform that was down did not confirm, exactly once.
import { createSessionStore, deriveKeys, kitSessionRef, sessionRefKey, sessions } from "@conusai-kit/auth"
import { buildRegistry, Container, contribute, defineModule } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA, toInstant } from "@conusai-kit/data"
import { SUPPORT_PLATFORM } from "@conusai-kit/support" // kit:support
import { recordingSupportPlatform } from "@conusai-kit/support/testing" // kit:support
import { accountData, accountDataOf } from "@conusai-kit/tenancy"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { revokeDerivedSessions } from "../apps/web/src/server/derived-sessions"
import { ACCOUNT_REGISTRY, accountSessionRevocations, recordSessionRevocation } from "../modules/account/index.js"
import { appModules } from "../modules/index.js"
const registry = buildRegistry([...appModules])
const ref = { provider: "oidc", opaqueId: "0199abcd-1234-7000-8000-00000000d001" }
function ports(bind: (root: Container) => void, timeoutMs?: number, using = registry) {
const root = new Container()
bind(root)
const warnings: string[] = []
return {
ports: {
registry: using,
root,
log: { warn: (message: string) => warnings.push(message) },
...(timeoutMs ? { timeoutMs } : {}),
},
warnings,
}
}
describe("revokeDerivedSessions", () => {
it("answers at once when no installed module mints derived sessions", async () => {
const without = buildRegistry(appModules.filter((module) => module.id !== "support"))
const none = ports(() => undefined, undefined, without)
await expect(revokeDerivedSessions(none.ports, ref)).resolves.toBeUndefined()
expect(none.warnings).toEqual([])
expect(sessionRefKey(ref)).toContain(ref.opaqueId)
})
// kit:support:begin
it("revokes the support widget sessions minted for exactly that app session", async () => {
const support = recordingSupportPlatform()
const { ports: bound, warnings } = ports((root) => root.value(SUPPORT_PLATFORM, support))
await revokeDerivedSessions(bound, ref)
expect(support.calls).toEqual([
{ method: "revokeWidgetSessions", subject: { subject: "", identitySession: sessionRefKey(ref) } },
])
expect(warnings).toEqual([])
})
it("never fails the sign-out: an unreachable or slow module is logged by name and the hook answers", async () => {
const support = recordingSupportPlatform()
const client = support.client as { revokeWidgetSessions: unknown }
client.revokeWidgetSessions = async () => {
throw new TypeError("fetch failed")
}
const down = ports((root) => root.value(SUPPORT_PLATFORM, support))
await expect(revokeDerivedSessions(down.ports, ref)).resolves.toBeUndefined()
expect(down.warnings).toEqual(["auth: support did not revoke the sessions of a revoked app session"])
client.revokeWidgetSessions = () => new Promise(() => undefined)
const slow = ports((root) => root.value(SUPPORT_PLATFORM, support), 50)
await expect(revokeDerivedSessions(slow.ports, ref)).resolves.toBeUndefined()
expect(slow.warnings).toHaveLength(1)
})
it("asks the modules directly when the revocation could not be recorded", async () => {
const support = recordingSupportPlatform()
const { ports: bound, warnings } = ports((root) => root.value(SUPPORT_PLATFORM, support))
await revokeDerivedSessions(
{
...bound,
durable: async () => {
throw new Error("the database refused the write")
},
},
ref,
)
expect(support.calls).toEqual([
{ method: "revokeWidgetSessions", subject: { subject: "", identitySession: sessionRefKey(ref) } },
])
expect(warnings).toEqual([
"auth: the revocation of an app session could not be recorded; asking the modules directly",
])
})
it("does nothing on the fake platform, where no widget session was minted", async () => {
const offline = ports((root) => root.value(SUPPORT_PLATFORM, { mode: "offline" }))
await expect(revokeDerivedSessions(offline.ports, ref)).resolves.toBeUndefined()
expect(offline.warnings).toEqual([])
})
// kit:support:end
it("asks only the modules that mint derived sessions", () => {
const revoking = accountDataOf(registry)
.filter((entry) => entry.revokeSessions)
.map((entry) => entry.key)
const expected: string[] = []
expected.push("support") // kit:support
expect(revoking).toEqual(expected)
})
})
/**
* The durable path, end to end on the memory data source: kit-auth's own session store signs a
* device out and calls the web's hook, which records the revocation in the account module's outbox;
* the worker's revocation job (the registry's own handler) delivers it. `minting` stands for any
* module that mints derived sessions, and its platform can be taken down and brought back.
*/
describe("the revocation outbox", () => {
const platform = { down: false, delivered: [] as string[], refused: 0 }
const minting = defineModule({
id: "minting",
version: "0.1.0",
kit: "^0.1.0",
requires: ["tenancy"],
extends: [
contribute(accountData, [
{
key: "minting",
async revokeSessions({ session }) {
if (platform.down) {
platform.refused += 1
throw new TypeError("fetch failed")
}
if (session) platform.delivered.push(session)
},
},
]),
],
})
const registry = buildRegistry([...appModules, minting])
const job = registry.jobs["account.sessions.revoke"]
const START = Date.parse("2026-10-25T10:00:00.000Z")
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date"] })
vi.setSystemTime(START)
platform.down = false
platform.delivered.length = 0
platform.refused = 0
})
afterEach(() => {
vi.useRealTimers()
})
async function app() {
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
root.value(ACCOUNT_REGISTRY, registry)
root.value(SUPPORT_PLATFORM, { mode: "offline" }) // kit:support
const warnings: string[] = []
const log = { warn: (message: string) => warnings.push(message) }
const store = createSessionStore({
config: {
idleSeconds: 3600,
absoluteSeconds: 86400,
cookieName: "s",
secret: "x".repeat(32),
secureCookies: false,
},
data,
keys: await deriveKeys("x".repeat(32)),
// What apps/web/src/server/app.ts registers on the Postgres profile.
onRevoked: (revoked) =>
revokeDerivedSessions(
{ registry, root, log, durable: (session) => recordSessionRevocation(root, session, { log }) },
revoked,
),
})
const signIn = async (subject: string) =>
(
await store.open({
userId: "0199abcd-1234-7000-8000-00000000e001",
issuer: "https://id.example.test",
subject,
tokens: { accessToken: "a", expiresAt: toInstant(Date.now() + 3600_000) },
})
).row
const runJob = () => {
if (!job) throw new Error("the account module declares no revocation job")
return job.def.handler(null, { container: root.child(), correlationId: "test-revoke", attempt: 1 })
}
const rows = async () => (await data.global(accountSessionRevocations).list({ limit: 50 })).items
const later = (ms: number) => vi.setSystemTime(Date.now() + ms)
return { data, store, signIn, runJob, rows, later, warnings }
}
it("is scheduled by the worker every minute", () => {
expect(job?.module).toBe("account")
})
it("delivers at once when the platform is up, and the job has nothing left to do", async () => {
const { store, signIn, runJob, rows, later } = await app()
const device = await signIn("sub-up")
await store.revoke(device, "sign-out")
const key = sessionRefKey(kitSessionRef(device))
expect(platform.delivered).toEqual([key])
expect((await rows())[0]).toMatchObject({
state: "delivered",
pending: [],
delivered: expect.arrayContaining(["minting"]),
})
later(10 * 60_000)
await runJob()
expect(platform.delivered).toEqual([key])
})
it("a platform down at sign-out gets the revocation once it is back, and nothing twice", async () => {
const { data, store, signIn, runJob, rows, later, warnings } = await app()
const device = await signIn("sub-down")
platform.down = true
// The sign-out is not blocked: it answers, and the app session is revoked.
await expect(store.revoke(device, "sign-out")).resolves.toBeUndefined()
expect((await data.global(sessions).get(device.id))?.revokedAt).not.toBeNull()
expect(platform.delivered).toEqual([])
const [pending] = await rows()
expect(pending).toMatchObject({ state: "pending", pending: ["minting"], attempts: 1, claimedUntil: null })
expect(warnings).toEqual(["auth: minting did not revoke the sessions of a revoked app session; retrying"])
// Not due yet (backoff): the job asks nobody.
await runJob()
expect(platform.refused).toBe(1)
// Due, platform still down: one more refusal, scheduled again with a longer backoff; the run fails.
later(31_000)
await expect(runJob()).rejects.toMatchObject({ code: "account_revocations_waiting" })
expect(platform.refused).toBe(2)
expect((await rows())[0]).toMatchObject({ state: "pending", attempts: 2 })
// Back: the next due run delivers it, once.
platform.down = false
later(61_000)
await runJob()
const key = sessionRefKey(kitSessionRef(device))
expect(platform.delivered).toEqual([key])
expect((await rows())[0]).toMatchObject({
state: "delivered",
pending: [],
delivered: expect.arrayContaining(["minting"]),
})
later(10 * 60_000)
await runJob()
expect(platform.delivered).toEqual([key])
})
it("two runs at once deliver one revocation once", async () => {
const { store, signIn, runJob, later } = await app()
const device = await signIn("sub-race")
platform.down = true
await store.revoke(device, "sign-out")
platform.down = false
later(31_000)
await Promise.all([runJob(), runJob(), runJob()])
expect(platform.delivered).toEqual([sessionRefKey(kitSessionRef(device))])
})
it("gives up once no derived session can still be alive, and removes finished rows a day later", async () => {
const { store, signIn, runJob, rows, later } = await app()
const down = await signIn("sub-lapse")
platform.down = true
await store.revoke(down, "sign-out")
later(66 * 60_000)
await runJob()
const refused = platform.refused
expect((await rows())[0]).toMatchObject({ state: "lapsed" })
later(10 * 60_000)
await runJob()
expect(platform.refused).toBe(refused)
later(25 * 60 * 60_000)
await runJob()
expect(await rows()).toEqual([])
})
})
/**
* The Convex profile's sign-out (apps/web/src/server/convex-revocation.ts): the web records the revoked
* app session in the backend's outbox through the internal action `account:revokeSession`, with the
* admin key and no identity, bounded. A refusal or a backend that does not answer is thrown, so the
* hook asks the modules directly instead.
*/
describe("the Convex profile's durable revocation", () => {
async function backend(answer: (body: { path: string; args: unknown[] }) => unknown | undefined) {
const { createServer } = await import("node:http")
const seen: Array<{ path: string; args: unknown[]; authorization: string | undefined }> = []
const server = createServer((request, response) => {
let raw = ""
request.on("data", (chunk) => {
raw += chunk
})
request.on("end", () => {
const body = JSON.parse(raw) as { path: string; args: unknown[] }
seen.push({ ...body, authorization: request.headers.authorization })
const value = answer(body)
if (value === undefined) return // never answers
response.writeHead(200, { "content-type": "application/json" })
response.end(JSON.stringify(value))
})
})
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
const address = server.address() as { port: number }
return {
convex: { url: `http://127.0.0.1:${address.port}`, adminKey: "app|admin-key" },
seen,
close: () =>
new Promise<void>((resolve) => {
server.closeAllConnections()
server.close(() => resolve())
}),
}
}
it("records the revocation in the backend's outbox with the admin key", async () => {
const { revokeConvexSession } = await import("../apps/web/src/server/convex-revocation")
const fake = await backend(() => ({ status: "success", value: null, logLines: [] }))
try {
await revokeConvexSession(fake.convex, sessionRefKey(ref))
expect(fake.seen).toMatchObject([
{
path: "account:revokeSession",
args: [{ session: sessionRefKey(ref) }],
authorization: "Convex app|admin-key",
},
])
expect(fake.seen).toHaveLength(1)
} finally {
await fake.close()
}
})
it("throws when nothing was recorded, so the hook asks the modules directly, and never waits long", async () => {
const { revokeConvexSession } = await import("../apps/web/src/server/convex-revocation")
const refused = await backend(() => ({ status: "error", errorMessage: "Server Error", logLines: [] }))
const silent = await backend(() => undefined)
try {
await expect(revokeConvexSession(refused.convex, "s-1")).rejects.toBeDefined()
const started = Date.now()
await expect(revokeConvexSession(silent.convex, "s-2", { timeoutMs: 100 })).rejects.toBeDefined()
expect(Date.now() - started).toBeLessThan(2000)
} finally {
await refused.close()
await silent.close()
}
})
})
+63
View File
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The image the three processes run from (spec §13). These are the properties a deployment
// depends on, and each one has cost a deployment somewhere: a manifest the install stage does not
// copy, a root process, a base image nobody pinned to the Node the app is written for.
import { readdirSync, readFileSync } from "node:fs"
import { describe, expect, it } from "vitest"
import { parse } from "yaml"
const dockerfile = readFileSync(new URL("../Dockerfile", import.meta.url), "utf8")
const workspace = parse(readFileSync(new URL("../pnpm-workspace.yaml", import.meta.url), "utf8")) as {
minimumReleaseAgeExclude: string[]
}
describe("the Dockerfile", () => {
it("is multi-stage, with the runtime stage compose.yaml names", () => {
expect(dockerfile).toMatch(/^FROM node:24-slim AS deps$/m)
expect(dockerfile).toMatch(/^FROM deps AS build$/m)
expect(dockerfile).toMatch(/^FROM node:24-slim AS runtime$/m)
})
it("copies the manifest of every workspace package before installing", () => {
// The install stage resolves the whole workspace, so a package whose manifest is missing
// fails the install with "no package named @app/<id> is present in the workspace".
const packages = readdirSync(new URL("..", import.meta.url), { withFileTypes: true })
expect(packages.some((entry) => entry.name === "pnpm-workspace.yaml")).toBe(true)
for (const dir of ["config", "modules", "apps/web", "apps/worker"]) {
expect(dockerfile, `${dir}/package.json is not copied`).toContain(`COPY ${dir}/package.json ./${dir}/`)
}
})
it("runs as a user that owns none of the files it runs", () => {
expect(dockerfile).toMatch(/^USER node$/m)
expect(dockerfile).toContain("--chown=root:root")
})
it("takes the registry credential as a build secret, never as a layer or a build argument", () => {
// Two build paths deliver the same credential under two ids: a preset single-image build
// mounts it as `npmrc`, a compose build as `conusai_npmrc` (the platform prefixes its compose
// secrets so a tenant's own can never collide). The install must accept either — a
// deployment that built through the preset path got no credential at all and pnpm fell back
// to the public registry, where the vendor scopes 404 (stage, deployment 4, 2026-10-20).
expect(dockerfile).toContain("--mount=type=secret,id=conusai_npmrc,target=/run/secrets/conusai_npmrc")
expect(dockerfile).toContain("--mount=type=secret,id=npmrc,target=/run/secrets/npmrc")
expect(dockerfile).not.toMatch(/ARG .*(TOKEN|PASSWORD|SECRET)/)
expect(dockerfile).not.toMatch(/_authToken/)
})
it("excludes the vendor scopes from the release-age cooldown", () => {
// minimumReleaseAge blocks versions published in the last seven days — but every dev
// bundle publishes the vendor packages minutes before a deployment installs them, from
// the ConusAI vendor registry, so the cooldown must never apply to either scope
// (stage, deployment 5, 2026-10-20: ERR_PNPM_NO_MATURE_MATCHING_VERSION).
expect(workspace.minimumReleaseAgeExclude).toContain("@conusai-sdk/*")
expect(workspace.minimumReleaseAgeExclude).toContain("@conusai-kit/*")
})
it("starts no process by default: compose.yaml names the one each service runs", () => {
const cmd = dockerfile.match(/^CMD (.*)$/m)?.[1] ?? ""
expect(cmd).toContain("compose.yaml")
expect(cmd).not.toContain("next")
})
})
+431
View File
@@ -0,0 +1,431 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The extraction of the documents module through the ModelGateway (plan item AT2.3): what the model is
// sent, what is done with its answer, and what happens when it does not answer. The model is the fake
// platform's gateway route, scripted per test; the gateway client, the extractor, the verification, the
// cache, the proposals and the review all run for real.
import { type LineView, LOW_TRUST_NOTE } from "@app/modules/documents"
import { uploadViaProcedures } from "@conusai-kit/testing"
import type { FakeChatResponder } from "@conusai-kit/testing/fake-platform"
import { afterEach, describe, expect, it } from "vitest"
import {
bytesOf,
closeApps,
type DocumentsApp,
makeDocumentsApp,
type SeededProject,
uploadAndProcess,
} from "./support/documents-app.js"
import { deterministicModel } from "./support/fake-model.js"
afterEach(closeApps)
/** Every page image the platform drew, as the fake platform recorded it. */
async function pagesDrawn(app: DocumentsApp): Promise<Array<{ job: number; page: number; dpi: number }>> {
const state = (await (await fetch(`${app.platform.url}/__e2e/state`)).json()) as {
ocrPages: Array<{ job: number; page: number; dpi: number }>
}
return state.ocrPages
}
const PAGE = { width: 1000, height: 1400, unit: "px", dpi: 150, rotation: 0 }
/**
* One page of an OCR document whose lines are `rows`, 40 px apart, one block per line (`p{n}-b{m}`), as
* the platform returns it once the page is classified: a scan when it was recognised, text otherwise.
* `repeated` lists the rows the reader flagged as repeating on most pages.
*/
function pageOf(
index: number,
rows: string[],
source: "text_layer" | "ocr" = "text_layer",
confidence = 0.97,
options: { pageClass?: string; repeated?: readonly string[] } = {},
) {
return {
...PAGE,
index,
class: options.pageClass ?? (source === "ocr" ? "scan" : "text"),
source,
text: rows.join("\n"),
lines: rows.map((text, order) => ({
text,
bbox: [100, 100 + order * 40, 600, 130 + order * 40],
polygon: null,
confidence,
order,
words: [],
repeated: options.repeated?.includes(text) ?? false,
})),
blocks: rows.map((_, order) => ({
id: `p${index + 1}-b${order + 1}`,
bbox: [90, 90 + order * 40, 700, 140 + order * 40],
order,
lines: [order],
})),
}
}
/** A row long enough that a page of it is not weak for want of text: its image is not asked for. */
const SHEET_ROW = "Reference: QR-2026-0417 (rev 2)"
const documentOf = (...pages: Array<ReturnType<typeof pageOf>>) => ({
schema_version: "conusai.ocr.document/v1",
engine: { name: "fixture-engine", version: "1", detection_model: null, recognition_model: null },
page_count: pages.length,
pages,
})
/** A one-page OCR document whose lines are `rows`. */
const onePage = (rows: string[], source: "text_layer" | "ocr" = "text_layer", confidence = 0.97) =>
documentOf(pageOf(0, rows, source, confidence))
interface Detail {
document: {
status: string
failureCode: string | null
pending: number
skippedPages: Array<{ page: number; pageClass: string }>
}
lines: LineView[]
}
async function run(
responder: FakeChatResponder | undefined,
document: Record<string, unknown>,
upload: { filename: string; contentType: string } = { filename: "report.pdf", contentType: "application/pdf" },
): Promise<{ app: DocumentsApp; project: SeededProject; detail: Detail }> {
const app = await makeDocumentsApp({ document, ...(responder ? { responder } : {}) })
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project, upload.filename, upload.contentType)
const detail = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as Detail
return { app, project, detail }
}
const reply = (lines: Array<{ label: string; value: string; quote: string }>) => ({
content: JSON.stringify({ lines }),
})
type Message = { role: string; content: string | Array<{ type: string; text?: string; image_url?: { url: string } }> }
const messagesOf = (request: Record<string, unknown>) => request.messages as Message[]
const userParts = (request: Record<string, unknown>) => {
const content = messagesOf(request).find((message) => message.role === "user")?.content
return typeof content === "string" ? [] : (content ?? [])
}
describe("what the model is sent", () => {
it("asks once per page, with the page's text and the schema of the module's line", async () => {
const { app } = await run(undefined, onePage(["Reference: QR-1", "Issued on: 2026-04-01"]))
expect(app.model.requests).toHaveLength(1)
const request = app.model.requests[0] as Record<string, unknown>
expect(request.model).toBe("fake-model")
expect(request.temperature).toBe(0)
const [system] = messagesOf(request)
expect(system?.role).toBe("system")
const [text, ...rest] = userParts(request)
expect(text?.text).toContain("Page 1 of 1.")
expect(text?.text).toContain("Reference: QR-1\nIssued on: 2026-04-01")
// a text-layer PDF page needs no image
expect(rest).toEqual([])
const format = request.response_format as {
type: string
json_schema: { schema: { properties: { lines: { items: { required: string[]; properties: object } } } } }
}
expect(format.type).toBe("json_schema")
const item = format.json_schema.schema.properties.lines.items
expect(item.required.sort()).toEqual(["label", "quote", "value"])
// the page, the position and the confidence are the code's to set, so the model is not asked for them
expect(Object.keys(item.properties).sort()).toEqual(["label", "quote", "value"])
})
it("adds the page image, as a content part, for a scanned page of an image upload", async () => {
const { app } = await run(undefined, onePage(["Reference: QR-1"], "ocr", 0.7), {
filename: "scan.png",
contentType: "image/png",
})
const parts = userParts(app.model.requests[0] as Record<string, unknown>)
expect(parts.map((part) => part.type)).toEqual(["text", "image_url"])
expect(parts[1]?.image_url?.url).toMatch(/^data:image\/png;base64,/)
})
it("fetches the image of a scanned page of a PDF from the platform's page route and sends it with the text", async () => {
const { app, detail } = await run(undefined, onePage(["Reference: QR-1"], "ocr", 0.7))
const parts = userParts(app.model.requests[0] as Record<string, unknown>)
expect(parts.map((part) => part.type)).toEqual(["text", "image_url"])
expect(parts[0]?.text).toContain(LOW_TRUST_NOTE)
expect(parts[1]?.image_url?.url).toMatch(/^data:image\/png;base64,iVBORw0KGgo/)
// the page was drawn once, for the job that read the file, at the default dpi
expect(await pagesDrawn(app)).toEqual([{ job: 1, page: 1, dpi: 150 }])
expect(detail.lines.map((line) => line.value)).toEqual(["QR-1"])
})
it("sends a table page as image and text, and a text page as text alone without asking for its image", async () => {
const { app } = await run(
undefined,
documentOf(pageOf(0, ["Item | Qty", "Bolts | 12"], "ocr", 0.97, { pageClass: "table" }), pageOf(1, [SHEET_ROW])),
)
const kinds = app.model.requests.map((request) =>
userParts(request as Record<string, unknown>).map((part) => part.type),
)
expect(kinds).toContainEqual(["text", "image_url"])
expect(kinds).toContainEqual(["text"])
expect(await pagesDrawn(app)).toEqual([{ job: 1, page: 1, dpi: 150 }])
})
it("leaves the header that repeats on the pages out of the text the model is shown", async () => {
const header = "ACME: Specification sheet"
const { app, detail } = await run(
undefined,
documentOf(
pageOf(0, [header, "Reference: QR-1"], "text_layer", 0.97, { repeated: [header] }),
pageOf(1, [header, "Issued on: 2026-04-01"], "text_layer", 0.97, { repeated: [header] }),
),
)
for (const request of app.model.requests) {
expect(userParts(request as Record<string, unknown>)[0]?.text).not.toContain("ACME")
}
expect(detail.lines.map((line) => line.label)).toEqual(["Reference", "Issued on"])
})
it("puts the reader's block id in the evidence of a line, and its block's box", async () => {
const { project, detail } = await run(undefined, onePage(["Reference: QR-1", "Issued on: 2026-04-01"]))
expect(detail.lines.map((line) => line.blockId)).toEqual(["p1-b1", "p1-b2"])
expect(detail.lines[1]?.box).toEqual({ x: 0.09, y: 130 / 1400, width: 0.61, height: 50 / 1400 })
const proposals = (await project.call.callByName("proposals.proposal.list", {
...project.slugs,
status: "pending",
})) as { proposals: Array<{ evidence: Array<{ blockId?: string }> }> }
expect(proposals.proposals.flatMap((proposal) => proposal.evidence.map((item) => item.blockId)).sort()).toEqual([
"p1-b1",
"p1-b2",
])
})
})
describe("the pages left out", () => {
const labels = Array.from({ length: 300 }, (_, at) => `A-${at} 2400`)
it("never asks the model about a drawing or an empty page, and reports them on the document", async () => {
const { app, detail } = await run(
undefined,
documentOf(
pageOf(0, [SHEET_ROW]),
pageOf(1, labels, "text_layer", 0.97, { pageClass: "drawing" }),
pageOf(2, [], "ocr", 0.97, { pageClass: "empty" }),
),
)
expect(app.model.requests).toHaveLength(1)
expect(detail.document).toMatchObject({
status: "extracted",
skippedPages: [
{ page: 2, pageClass: "drawing" },
{ page: 3, pageClass: "empty" },
],
})
expect(detail.lines.map((line) => line.value)).toEqual(["QR-2026-0417 (rev 2)"])
// no image was drawn for a page nobody reads
expect(await pagesDrawn(app)).toEqual([])
})
it("reads a scan beside a drawing: the scan produces lines, the drawing none", async () => {
const { detail } = await run(
undefined,
documentOf(
pageOf(0, labels, "text_layer", 0.97, { pageClass: "drawing" }),
pageOf(1, ["Delivery no: DN-77", "Received by: A. Sample"], "ocr", 0.8),
),
)
expect(detail.lines.map((line) => [line.page, line.value])).toEqual([
[2, "DN-77"],
[2, "A. Sample"],
])
expect(detail.document.skippedPages).toEqual([{ page: 1, pageClass: "drawing" }])
})
it("reads a drawing when the deployment's list does not exclude it", async () => {
const app = await makeDocumentsApp({
document: documentOf(pageOf(0, ["Scale: 1:50"], "ocr", 0.9, { pageClass: "drawing" })),
excludedPageClasses: [],
})
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project, "plan.pdf", "application/pdf")
const detail = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as Detail
expect(detail.document.skippedPages).toEqual([])
expect(detail.lines.map((line) => line.value)).toEqual(["1:50"])
})
})
describe("a quote taken from the image of a scanned page", () => {
it("stays needs_review with the scanned-quote note when the page text does not hold it", async () => {
const { detail } = await run(
() => reply([{ label: "Qty", value: "12", quote: "Qty 12 pcs" }]),
onePage(["Delivery note"], "ocr", 0.7),
)
expect(detail.lines).toHaveLength(1)
expect(detail.lines[0]).toMatchObject({ verification: "needs_review", note: "scanned_quote", confidence: 0 })
})
it("carries no note for a quote the text holds", async () => {
const { detail } = await run(undefined, onePage(["Reference: QR-1"], "ocr", 0.7))
expect(detail.lines[0]).toMatchObject({ verification: "verified", note: null, blockId: "p1-b1" })
})
})
describe("what is done with the answer", () => {
const rows = ["Sąskaita: INV-2026-07", "Suma: 1 250,00 EUR", "Pastaba: apmokėti per 14 dienų"]
it("verifies each quote on the page it was asked about, folding only whitespace, case and diacritics", async () => {
const { detail } = await run(
() =>
reply([
{ label: "Sąskaita", value: "INV-2026-07", quote: "Sąskaita: INV-2026-07" },
// case, diacritics and whitespace differ from the page: still the same words
{ label: "Suma", value: "1 250,00 EUR", quote: "suma: 1 250,00\n eur" },
{ label: "Pastaba", value: "apmokėti per 14 dienų", quote: "PASTABA: APMOKETI PER 14 DIENU" },
]),
onePage(rows),
)
expect(detail.lines.map((line) => [line.label, line.verification])).toEqual([
["Sąskaita", "verified"],
["Suma", "verified"],
["Pastaba", "verified"],
])
// a verified line is located from the reader's own lines, and carries the reader's confidence
expect(detail.lines[0]?.box).not.toBeNull()
expect(detail.lines[0]?.confidence).toBe(0.97)
expect(detail.lines.every((line) => line.page === 1 && line.status === "pending")).toBe(true)
})
it("keeps a line whose quote is not on the page as needs_review, with no box and no confidence", async () => {
const { detail } = await run(
() =>
reply([
{ label: "Sąskaita", value: "INV-2026-07", quote: "Sąskaita: INV-2026-07" },
// a changed digit is a different word: folding does not forgive it
{ label: "Suma", value: "1 520,00 EUR", quote: "Suma: 1 520,00 EUR" },
{ label: "Made up", value: "x", quote: "Nothing like this is written here" },
]),
onePage(rows),
)
expect(detail.lines.map((line) => [line.label, line.verification])).toEqual([
["Sąskaita", "verified"],
["Suma", "needs_review"],
["Made up", "needs_review"],
])
const flagged = detail.lines.filter((line) => line.verification === "needs_review")
expect(flagged.every((line) => line.box === null && line.confidence === 0 && line.status === "pending")).toBe(true)
// kept, not dropped and not approved: the document is waiting for a person
expect(detail.document).toMatchObject({ status: "extracted", pending: 3 })
})
it("verifies against the page the model was asked about, not against another page", async () => {
const two = {
...onePage(["Reference: QR-1"]),
page_count: 2,
pages: [...onePage(["Reference: QR-1"]).pages, { ...onePage(["Total: 5 EUR"]).pages[0], index: 1 }],
}
// the model answers every page with page 1's line: on page 2 it is not there
const { detail } = await run(() => reply([{ label: "Reference", value: "QR-1", quote: "Reference: QR-1" }]), two)
expect(detail.lines.map((line) => [line.page, line.verification])).toEqual([
[1, "verified"],
[2, "needs_review"],
])
})
it("drops a line the model repeats", async () => {
const line = { label: "Reference", value: "QR-1", quote: "Reference: QR-1" }
const { detail } = await run(() => reply([line, line]), onePage(["Reference: QR-1"]))
expect(detail.lines).toHaveLength(1)
})
})
describe("when the model does not answer", () => {
const document = onePage(["Reference: QR-1"])
it("a refusal is asked again once, then ends the document with a typed code and no proposals", async () => {
const { app, detail } = await run(() => ({ content: null, refusal: "I cannot help with that." }), document)
expect(app.model.requests).toHaveLength(2)
expect(detail.document).toMatchObject({ status: "failed", failureCode: "extraction_refused" })
expect(detail.lines).toEqual([])
})
it("a reply cut off at the token ceiling is asked again with more room, and the second answer is used", async () => {
let calls = 0
const { app, detail } = await run((request) => {
calls += 1
return calls === 1 ? { content: '{"lines": [{"label"', finishReason: "length" } : deterministicModel(request)
}, document)
expect(app.model.requests.map((request) => request.max_tokens)).toEqual([4096, 8192])
expect(detail.document.status).toBe("extracted")
expect(detail.lines).toHaveLength(1)
})
it("a reply that is always cut off ends the document as truncated", async () => {
const { app, detail } = await run(() => ({ content: '{"lines": [', finishReason: "length" }), document)
expect(app.model.requests).toHaveLength(2)
expect(detail.document).toMatchObject({ status: "failed", failureCode: "extraction_truncated" })
})
it("a reply outside the schema is asked again once, by the gateway, and then ends the document as invalid", async () => {
const { app, detail } = await run(() => ({ content: JSON.stringify({ lines: "none" }) }), document)
// two asks in all: the gateway's retry with the validation errors is the one retry
expect(app.model.requests).toHaveLength(2)
expect(detail.document).toMatchObject({ status: "failed", failureCode: "extraction_invalid" })
const retry = messagesOf(app.model.requests[1] as Record<string, unknown>).at(-1)
expect(retry?.content).toContain("did not match the required schema")
})
it("records no document text in the failure: the code is all the document keeps", async () => {
const { detail } = await run(() => ({ content: null, refusal: "Reference: QR-1" }), document)
expect(JSON.stringify(detail.document)).not.toContain("QR-1")
})
})
describe("the cache", () => {
it("a file with the same bytes is neither read nor asked about again", async () => {
const { app, project } = await run(undefined, onePage(["Reference: QR-1", "Issued on: 2026-04-01"]))
expect(app.ocr.submitted).toHaveLength(1)
expect(app.model.requests).toHaveLength(1)
const stored = app.store.keysOf({ level: "project", orgId: project.orgId } as never)
// the OCR result, the job it came from (so its pages can still be drawn), and the page's model reply
expect(stored.filter((key) => key.startsWith("documents/cache/"))).toHaveLength(3)
// the same bytes under another name: a second file, the same content hash
const second = await uploadViaProcedures(project.call, project.slugs, {
filename: "copy.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 fixture report.pdf"),
})
await app.deliver()
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
expect(app.model.requests).toHaveLength(1)
const view = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: second.fileId,
})) as Detail
expect(view.lines.map((line) => line.value)).toEqual(["QR-1", "2026-04-01"])
})
it("a change of the page's text is a different request", async () => {
const { app, project } = await run(undefined, onePage(["Reference: QR-1"]))
app.platform.ocr.setDocument(onePage(["Reference: QR-2"]))
await uploadViaProcedures(project.call, project.slugs, {
filename: "other.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 something else"),
})
await app.deliver()
await app.runJobs()
expect(app.model.requests).toHaveLength(2)
})
})
+415
View File
@@ -0,0 +1,415 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The document-review flow end to end on the fake platform (plan item AT2.3): an uploaded file is
// read by an OCR job, becomes proposals, a person approves, edits or rejects each line, and only
// approved lines are exported. Every step runs through the kernel's caller, the files module's
// processor point, the proposals module's writer and the outbox relay's behaviour, so what is proven
// is the wiring and not a unit.
import { readFileSync } from "node:fs"
import { type LineView, lines as linesTable, REVIEW_PROCEDURES, type ReadDocument } from "@app/modules/documents"
import { AppError } from "@conusai-kit/core"
import { uploadViaProcedures } from "@conusai-kit/testing"
import { afterEach, describe, expect, it } from "vitest"
import { sheetRows } from "../modules/documents/__tests__/support/unzip.js"
import {
bytesOf,
closeApps,
type DocumentsApp,
makeDocumentsApp,
type SeededProject,
uploadAndProcess,
} from "./support/documents-app.js"
const document = JSON.parse(
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
) as ReadDocument & Record<string, unknown>
afterEach(closeApps)
interface Detail {
document: {
status: string
failureCode: string | null
pending: number
approved: number
pageCount: number | null
skippedPages: unknown[]
}
lines: LineView[]
}
async function fresh(): Promise<{ app: DocumentsApp; project: SeededProject }> {
const app = await makeDocumentsApp({ document })
return { app, project: await app.seedProject("acme", "owner@acme.test") }
}
const detail = async (project: SeededProject, fileId: string): Promise<Detail> =>
(await project.call.callByName("documents.document.get", { ...project.slugs, fileId })) as Detail
const decide = (project: SeededProject, procedure: string, input: Record<string, unknown>) =>
project.call.callByName(procedure, { ...project.slugs, ...input })
const exportRows = async (project: SeededProject, fileId: string) => {
const made = (await decide(project, "documents.export.create", { fileId })) as {
body: string
lineCount: number
filename: string
}
return { ...made, rows: sheetRows(Buffer.from(made.body, "base64")) }
}
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
describe("uploading a document", () => {
it("answers before OCR runs: the request enqueues, and the worker reads", async () => {
const { app, project } = await fresh()
await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 fixture"),
})
// The upload has returned and the reader has not been asked anything.
expect(app.ocr.submitted).toHaveLength(0)
expect(app.queue.of("documents.ocr")).toHaveLength(0)
// The stored file's event is what enqueues the job, once.
await app.deliver()
expect(app.queue.of("documents.ocr")).toHaveLength(1)
expect(app.ocr.submitted).toHaveLength(0)
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
})
it("is not read by this module when it is not a document type it reads", async () => {
const { app, project } = await fresh()
await uploadViaProcedures(project.call, project.slugs, {
filename: "notes.csv",
contentType: "text/csv",
bytes: bytesOf("a,b"),
})
await app.deliver()
expect(app.queue.of("documents.ocr")).toHaveLength(0)
})
it("lists an upload nobody has read yet as queued", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4"),
})
const { documents } = (await decide(project, "documents.document.list", {})) as {
documents: Array<{ fileId: string; status: string }>
}
expect(documents).toEqual([expect.objectContaining({ fileId: file.fileId, status: "queued" })])
expect(app.ocr.submitted).toHaveLength(0)
})
})
describe("the extraction", () => {
it("turns the fixture document into one pending line per labelled line, each with its page and verbatim quote", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const { document: view, lines } = await detail(project, file.fileId)
// the fixture's third page is a drawing: left out, and said so on the document
expect(view).toMatchObject({
status: "extracted",
pending: 4,
approved: 0,
pageCount: 3,
skippedPages: [{ page: 3, pageClass: "drawing" }],
})
expect(lines.map((line) => [line.page, line.label, line.value, line.status])).toEqual([
[1, "Reference", "QR-2026-04", "pending"],
[1, "Issued on", "2026-04-01", "pending"],
[2, "Total amount", "1 250,00 EUR", "pending"],
[2, "Signed by", "A. Example", "pending"],
])
for (const line of lines) {
expect(document.pages[line.page - 1]?.text).toContain(line.quote)
expect(line.box).not.toBeNull()
// The line says which engine and extractor produced it: the Art. 50 disclosure's subject.
expect(line.model).toContain("fixture-engine")
expect(line.promptVersion).toBe("extract-lines.v2")
}
})
it("submits the document once, with a key that names the file and its content", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
expect(app.ocr.submitted).toHaveLength(1)
expect(app.ocr.submitted[0]?.idempotencyKey).toMatch(
new RegExp(`^documents\\.ocr\\.${file.fileId}\\.[0-9a-f]{64}$`),
)
})
it("writes nothing the reviewer has not approved", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
expect(
await app.data
.forScope({ level: "project", actor: project.owner, ...scopeIds(project) })
.of(linesTable)
.count(),
).toBe(0)
// and so an export has nothing to say yet
const error = await refusal(decide(project, "documents.export.create", { fileId: file.fileId }))
expect(error.status).toBe(409)
})
})
describe("retrying the job", () => {
it("a job run twice adds no proposal and submits no second document", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const job = app.queue.jobs.find((entry) => entry.job === "documents.ocr")
expect(job).toBeDefined()
// The queue delivers the same job again (a lapsed claim): run its handler once more.
const again = app.queue.jobs.length
await app.queue.enqueue(job?.scope as never, "documents.ocr", job?.payload as never)
expect(app.queue.jobs).toHaveLength(again + 1)
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
expect((await detail(project, file.fileId)).lines).toHaveLength(4)
})
it("a transient failure while waiting resumes the stored OCR job instead of submitting another", async () => {
const { app, project } = await fresh()
app.ocr.failWaitOnce(new Error("the platform is unreachable"))
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4"),
})
await app.deliver()
await expect(app.runJobs()).rejects.toThrow("unreachable")
expect((await detail(project, file.fileId)).document.status).toBe("processing")
// pg-boss retries a job that throws: the same payload, the same scope.
const job = app.queue.jobs.find((entry) => entry.job === "documents.ocr")
await app.queue.enqueue(job?.scope as never, "documents.ocr", job?.payload as never, { key: "retry" })
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
expect(app.ocr.waited).toHaveLength(2)
expect((await detail(project, file.fileId)).document).toMatchObject({ status: "extracted", pending: 4 })
})
it("marks the document failed, with a code and no proposals, when the platform says the job failed", async () => {
const { app, project } = await fresh()
app.platform.ocr.failJobs(true)
const file = await uploadAndProcess(app, project)
const { document: view, lines } = await detail(project, file.fileId)
expect(view).toMatchObject({ status: "failed", failureCode: "ocr_failed" })
expect(lines).toEqual([])
})
it("marks the document failed when the stored bytes are not the ones that were uploaded", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 original"),
})
for (const [key, object] of app.store.objects)
app.store.objects.set(key, { ...object, bytes: new TextEncoder().encode("tampered") })
await app.deliver()
await app.runJobs()
expect((await detail(project, file.fileId)).document).toMatchObject({
status: "failed",
failureCode: "file_changed",
})
expect(app.ocr.submitted).toHaveLength(0)
})
})
describe("the review", () => {
it("approving a line writes the record and one audit event; the other lines stay pending", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
await app.deliver()
const after = await detail(project, file.fileId)
expect(after.document).toMatchObject({ pending: 3, approved: 1 })
expect(after.lines.find((line) => line.lineId === first?.lineId)?.status).toBe("approved")
const approvals = app.audit.events.filter((event) => event.action === "proposals.proposal_approved")
expect(approvals).toHaveLength(1)
// ids and hashes only: no label, value or quote in an audit event
expect(JSON.stringify(approvals[0])).not.toContain("QR-2026-04")
})
it("rejecting a line changes no record, and the line stays rejected with its audit event", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: first?.lineId, reason: "not a field" })
await app.deliver()
const after = await detail(project, file.fileId)
expect(after.lines.find((line) => line.lineId === first?.lineId)?.status).toBe("rejected")
expect(after.document).toMatchObject({ pending: 3, approved: 0 })
expect(app.audit.events.filter((event) => event.action === "proposals.proposal_rejected")).toHaveLength(1)
// nothing was written to the approved table: the export has nothing to say
expect((await refusal(decide(project, "documents.export.create", { fileId: file.fileId }))).status).toBe(409)
})
it("an edit replaces the line with the reviewer's words, which are then approved like any other", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [, second] = (await detail(project, file.fileId)).lines
const edited = (await decide(project, "documents.line.edit", {
lineId: second?.lineId,
label: "Issue date",
value: "2026-04-02",
})) as LineView
// the same line, with the reviewer's words in it
expect(edited).toMatchObject({
lineId: second?.lineId,
label: "Issue date",
value: "2026-04-02",
edited: true,
status: "pending",
})
// the quote still names the words the machine read, not the reviewer's
expect(edited.quote).toBe(second?.quote)
const listed = (await detail(project, file.fileId)).lines
expect(listed).toHaveLength(4)
expect(listed.find((line) => line.lineId === second?.lineId)).toMatchObject({ label: "Issue date", edited: true })
await app.deliver()
// one audit event for the edit, with no word of it
const audited = app.audit.events.filter((event) => event.action === "proposals.proposal_edited")
expect(audited).toHaveLength(1)
expect(JSON.stringify(audited[0])).not.toContain("Issue date")
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: edited.lineId,
expectedVersion: edited.expectedVersion,
})
const { rows } = await exportRows(project, file.fileId)
expect(rows).toEqual([
["Label", "Value", "Page", "Source quote", "Edited by a reviewer"],
["Issue date", "2026-04-02", 1, "Issued on: 2026-04-01", "yes"],
])
})
it("a line that has been decided cannot be edited", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: first?.lineId })
const error = await refusal(
decide(project, "documents.line.edit", { lineId: first?.lineId, label: "x", value: "y" }),
)
expect(error.status).toBe(409)
})
it("approves every pending line in one decision", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const lines = (await detail(project, file.fileId)).lines
const ids = lines.map((line) => line.lineId)
const expectations = Object.fromEntries(
ids.map((id, index) => [id, { expectedVersion: lines[index]?.expectedVersion }]),
)
await decide(project, REVIEW_PROCEDURES.approveMany, { proposalIds: ids, expectations })
expect((await detail(project, file.fileId)).document).toMatchObject({ pending: 0, approved: 4 })
})
})
describe("the export", () => {
it("contains the approved lines and only those, as an Excel workbook", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project, "quarterly.report.pdf")
const lines = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: lines[2]?.lineId,
expectedVersion: lines[2]?.expectedVersion,
})
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: lines[0]?.lineId,
expectedVersion: lines[0]?.expectedVersion,
})
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: lines[1]?.lineId })
const made = await exportRows(project, file.fileId)
expect(made.filename).toBe("quarterly.report-approved.xlsx")
expect(made.lineCount).toBe(2)
expect(made.rows.slice(1)).toEqual([
["Reference", "QR-2026-04", 1, "Reference: QR-2026-04", "no"],
["Total amount", "1 250,00 EUR", 2, "Total amount: 1 250,00 EUR", "no"],
])
})
it("writes one audit event naming no content", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
await decide(project, "documents.export.create", { fileId: file.fileId })
// The transport records a procedure's declared audit event (apps/web router); here the
// contract's own declaration is the proof, since no transport runs.
expect(app.registry.procedures.get("documents.export.create")?.def.audit).toBe("documents.export_created")
expect(app.registry.procedures.get("documents.line.edit")?.def.audit).toBe("documents.line_edited")
})
})
describe("who may do what", () => {
it("lets a viewer read a document and refuses them (404, as a stranger) an edit, an approval and an export", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
const read = (await viewer.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as Detail
expect(read.lines).toHaveLength(4)
for (const [name, input] of [
["documents.line.edit", { lineId: first?.lineId, label: "x", value: "y" }],
[REVIEW_PROCEDURES.approve, { proposalId: first?.lineId, expectedVersion: first?.expectedVersion }],
["documents.export.create", { fileId: file.fileId }],
] as const) {
const error = await refusal(viewer.call.callByName(name, { ...project.slugs, ...input }))
// The kernel answers a member who lacks the permission exactly as it answers a stranger
// (spec §3.3): 404, so a refusal does not say which procedures exist for whom.
expect(error.status, name).toBe(404)
}
expect((await detail(project, file.fileId)).document.approved).toBe(0)
})
it("lets a member review: the permission of the line's writer is the member role's", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
const member = await app.addMember(project, "member@acme.test", "member")
await member.call.callByName(REVIEW_PROCEDURES.approve, {
...project.slugs,
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
expect((await detail(project, file.fileId)).document.approved).toBe(1)
})
})
const scopeIds = (project: SeededProject) => ({
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
})
+145
View File
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The OCR adapter against the fake platform of @conusai-kit/testing, through the real
// `@conusai-sdk/ocr` client (kit-platform's `DocumentReader`): the job is submitted, polled and read
// over HTTP exactly as the platform's async job API does it, so what is proven here is the adapter's
// calls and not a stub of them.
import { readFileSync } from "node:fs"
import { OcrJobFailed, sdkDocumentOcr } from "@app/modules/documents"
import { OcrError } from "@conusai-sdk/ocr"
import { afterEach, describe, expect, it } from "vitest"
import { closeApps, makeDocumentsApp, uploadAndProcess } from "./support/documents-app.js"
import { startOcr } from "./support/ocr.js"
const fixture = JSON.parse(
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
) as Record<string, unknown>
const open: Array<{ close(): Promise<void> }> = []
afterEach(async () => {
await closeApps()
await Promise.all(open.splice(0).map((platform) => platform.close()))
})
async function ocr() {
const started = await startOcr(fixture)
open.push(started.platform)
return started
}
const stateOf = async (platform: { url: string }) =>
(await (await fetch(`${platform.url}/__e2e/state`)).json()) as { ocr: unknown[] }
describe("the OCR adapter on the fake platform", () => {
it("submits a document, polls its job and returns the document the platform read", async () => {
const { ocr: port } = await ocr()
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-1",
})
const read = await port.wait(jobId)
expect(read.engine.name).toBe("fixture-engine")
expect(read.pages).toHaveLength(3)
})
it("answers a repeated idempotency key with the same job, so a retried job pays once", async () => {
const { ocr: port, platform } = await ocr()
const bytes = new TextEncoder().encode("%PDF-1.4 same")
const first = await port.submit(bytes, { filename: "a.pdf", idempotencyKey: "k-same" })
const second = await port.submit(bytes, { filename: "a.pdf", idempotencyKey: "k-same" })
expect(second.jobId).toBe(first.jobId)
expect((await stateOf(platform)).ocr).toHaveLength(1)
})
it("reads a job the platform failed as permanent", async () => {
const { ocr: port, platform } = await ocr()
platform.ocr.failJobs(true)
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-f",
})
await expect(port.wait(jobId)).rejects.toBeInstanceOf(OcrJobFailed)
})
it("draws a page of a finished job through the platform's page route: bytes, a media type, the asked dpi", async () => {
const { ocr: port, platform } = await ocr()
platform.ocr.setPageImage(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 7]))
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-p",
})
await port.wait(jobId)
const image = await port.page(jobId, 2)
expect(image?.mediaType).toBe("image/png")
expect([...(image?.bytes ?? [])]).toEqual([0x89, 0x50, 0x4e, 0x47, 7])
const state = (await (await fetch(`${platform.url}/__e2e/state`)).json()) as {
ocrPages: Array<{ job: number; page: number; dpi: number }>
}
expect(state.ocrPages).toEqual([{ job: jobId, page: 2, dpi: 150 }])
})
it("reads a page the platform has no image of (a job it no longer holds, a page past the end) as no image", async () => {
const { ocr: port } = await ocr()
expect(await port.page(999, 1)).toBeUndefined()
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-q",
})
await port.wait(jobId)
// the fixture has three pages
expect(await port.page(jobId, 4)).toBeUndefined()
})
it("reads a failure of the page route that is not the page's as retryable", async () => {
const down = new OcrError("OCR request failed: network", { type: "network" })
const adapter = sdkDocumentOcr({
submit: async () => ({ id: 1 }) as never,
wait: async () => ({}) as never,
page: async () => Promise.reject(down),
})
await expect(adapter.page(1, 1)).rejects.toBe(down)
})
it("reads anything else as retryable: the error is thrown on, unchanged", async () => {
const transient = new OcrError("OCR request failed: network", { type: "network" })
const adapter = sdkDocumentOcr({
submit: async () => ({ id: 1 }) as never,
wait: async () => Promise.reject(transient),
page: async () => ({ bytes: new Uint8Array(), contentType: "image/png" }),
})
await expect(adapter.wait(1)).rejects.toBe(transient)
})
it("carries a whole upload through the worker over HTTP: the fixture becomes proposals", async () => {
const app = await makeDocumentsApp({ document: fixture })
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project)
const view = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as {
document: { status: string; pending: number }
}
expect(view.document).toMatchObject({ status: "extracted", pending: 4 })
expect((await stateOf(app.platform)).ocr).toHaveLength(1)
})
it("marks a document extracted with nothing to review when the platform reads a page with no lines", async () => {
const app = await makeDocumentsApp({
document: {
...fixture,
page_count: 1,
pages: [{ index: 0, width: 10, height: 10, class: "empty", text: "", lines: [], blocks: [] }],
},
})
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project)
const view = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as {
document: { status: string; pending: number }
}
expect(view.document).toMatchObject({ status: "extracted", pending: 0 })
})
})
+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// `.env.example` is the complete, documented list of the variables this app reads (plan item AT4.5):
// every variable the code reads is in it, every variable in it is read by something, and each has a
// comment above it saying what it is for.
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, it } from "vitest"
const root = new URL("..", import.meta.url).pathname
const example = readFileSync(join(root, ".env.example"), "utf8")
const declared = [...example.matchAll(/^([A-Z][A-Z0-9_]+)=/gm)].map((match) => match[1] as string)
const walk = (dir: string): string[] =>
readdirSync(dir).flatMap((name) => {
if (["node_modules", ".next", "dist", ".logs", "__tests__"].includes(name)) return []
const path = join(dir, name)
if (statSync(path).isDirectory()) return walk(path)
return /\.(ts|tsx|mjs)$/.test(name) ? [path] : []
})
/** The variables the code of this repository reads: `process.env.X`, `env.X` and the loaders' quoted names. */
function read(): Set<string> {
const found = new Set<string>()
const files = [
...walk(join(root, "apps")),
...walk(join(root, "config", "src")),
...walk(join(root, "scripts")),
...walk(join(root, "e2e", "support")),
join(root, "better-auth", "deploy.mjs"), // kit:convex-better-auth
]
for (const file of files) {
const text = readFileSync(file, "utf8")
for (const match of text.matchAll(/\b(?:process\.env|env)\.([A-Z][A-Z0-9_]+)/g)) found.add(match[1] as string)
for (const match of text.matchAll(/\b(?:process\.env|env)\[\s*"([A-Z][A-Z0-9_]+)"\s*\]/g))
found.add(match[1] as string)
}
// The names config/src/env.ts holds in constants.
for (const match of readFileSync(join(root, "config/src/env.ts"), "utf8").matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) {
found.add(match[1] as string)
}
for (const file of ["ai.ts", "documents.ts"]) {
for (const match of readFileSync(join(root, "config/src", file), "utf8").matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) {
found.add(match[1] as string)
}
}
// The platform contract and the sign-in variables are read by the kit's loaders from the same environment.
const kit = (path: string) => readFileSync(join(root, "node_modules", "@conusai-kit", path), "utf8")
for (const match of kit("auth/dist/config.js").matchAll(/\b((?:AUTH|OIDC)_[A-Z_]+)\b/g)) found.add(match[1] as string)
for (const name of [
"CONUSAI_API_URL",
"CONUSAI_API_TOKEN",
"CONUSAI_PROJECT_ID",
"CONUSAI_ORG_ID",
"CONUSAI_ORG_SLUG",
"CONUSAI_PUBLIC_URL",
"CONUSAI_RELEASE",
"CONUSAI_ENVIRONMENT",
"CONUSAI_DSN",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
"DEV_WEB_PORT",
"DEV_PLATFORM_PORT",
"DEV_POSTGRES_PORT",
"CONVEX_SELF_HOSTED_URL",
"CONVEX_SELF_HOSTED_ADMIN_KEY",
"CONVEX_BACKUP_INTERVAL_HOURS",
"CONVEX_BACKUP_RETENTION",
"CONVEX_CLI",
"CONVEX_APP_DIR",
]) {
found.add(name)
}
return found
}
describe(".env.example", () => {
it("lists every variable the code reads", () => {
const missing = [...read()].filter((name) => !declared.includes(name)).sort()
// Set by the runtime and not by a person: NODE_ENV is in the file for what it means here, and the
// framework's own variable names are not this app's to document.
const framework = new Set([
"NEXT_RUNTIME",
"npm_execpath",
"PORT",
"HOST",
"HOSTNAME",
"E2E_BASE_URL",
"E2E_PLATFORM_URL",
])
expect(
missing.filter((name) => !framework.has(name)),
"add these to .env.example, with a comment",
).toEqual([])
})
it("lists nothing that nothing reads", () => {
const used = read()
const fixed = new Set([
"POSTGRES_URL",
"POSTGRES_READER_URL",
"POSTGRES_MIGRATE_URL",
"AI_MODEL",
"LOG_LEVEL",
"WORKER_HEARTBEAT_FILE",
"NODE_ENV",
])
const unused = declared.filter((name) => !used.has(name) && !fixed.has(name))
expect(unused, "remove these from .env.example, or the code that reads them is not found by this test").toEqual([])
})
it("has no variable twice", () => {
expect(declared.filter((name, index) => declared.indexOf(name) !== index)).toEqual([])
})
it("explains every variable: a comment sits above it, or above the block of variables it belongs to", () => {
const lines = example.split("\n")
const undocumented: string[] = []
for (const [index, line] of lines.entries()) {
const name = /^([A-Z][A-Z0-9_]+)=/.exec(line)?.[1]
if (!name) continue
let above = index - 1
while (above >= 0 && /^[A-Z][A-Z0-9_]+=/.test(lines[above] as string)) above -= 1
if (!(lines[above] ?? "").startsWith("#")) undocumented.push(name)
}
expect(undocumented).toEqual([])
})
it("holds no real secret: the values are the development ones or empty", () => {
for (const name of ["CONUSAI_API_TOKEN", "AUTH_SESSION_SECRET", "OIDC_WEB_CLIENT_SECRET", "BETTER_AUTH_SECRET"]) {
const value = new RegExp(`^${name}=(.*)$`, "m").exec(example)?.[1] ?? ""
expect(value === "" || /dev|replace-me/.test(value), `${name} looks like a real secret`).toBe(true)
}
})
})
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The security headers of spec §6.4, and the two things a page may not do: load a script inline
// or reach an origin that is not this app's.
import { describe, expect, it } from "vitest"
import { contentSecurityPolicy, securityHeaders } from "../apps/web/src/server/headers"
const policy = contentSecurityPolicy({ nonce: "test-nonce" })
describe("the content security policy", () => {
it("allows a script only with this request's nonce", () => {
expect(policy).toContain("script-src 'self' 'nonce-test-nonce' 'strict-dynamic'")
expect(policy).not.toContain("script-src 'self' 'unsafe-inline'")
expect(policy).not.toContain("unsafe-eval")
})
it("refuses framing, a base tag and a plugin outright", () => {
expect(policy).toContain("frame-ancestors 'none'")
expect(policy).toContain("base-uri 'none'")
expect(policy).toContain("object-src 'none'")
})
it("names no origin but this app's", () => {
const sources = policy
.split("; ")
.flatMap((directive) => directive.split(" ").slice(1))
.filter((source) => source.includes("."))
expect(sources).toEqual([])
})
it("keeps fonts and connections on this origin, so no CDN or font host is reachable", () => {
expect(policy).toContain("font-src 'self'")
expect(policy).toContain("connect-src 'self'")
})
})
describe("the content security policy of a development run", () => {
it("allows eval for React's development build, and only there", () => {
expect(contentSecurityPolicy({ nonce: "n", development: true })).toContain("'strict-dynamic' 'unsafe-eval'")
expect(contentSecurityPolicy({ nonce: "n" })).not.toContain("unsafe-eval")
expect(contentSecurityPolicy({ nonce: "n", development: false })).not.toContain("unsafe-eval")
expect(securityHeaders({ nonce: "n", secure: false, development: true })["content-security-policy"]).toContain(
"unsafe-eval",
)
expect(securityHeaders({ nonce: "n", secure: true })["content-security-policy"]).not.toContain("unsafe-eval")
})
})
describe("the response headers", () => {
it("pin the transport only where there is one to pin", () => {
expect(securityHeaders({ nonce: "n", secure: true })["strict-transport-security"]).toContain("max-age=63072000")
expect(securityHeaders({ nonce: "n", secure: false })["strict-transport-security"]).toBeUndefined()
})
it("carry the rest of the baseline", () => {
const headers = securityHeaders({ nonce: "n", secure: true })
expect(headers["x-content-type-options"]).toBe("nosniff")
expect(headers["referrer-policy"]).toBe("strict-origin-when-cross-origin")
expect(headers["cross-origin-opener-policy"]).toBe("same-origin")
expect(headers["permissions-policy"]).toContain("camera=()")
})
})
describe("the browser's half of the API path", () => {
it("types a call by its contract's own schemas", async () => {
// A compile-time property, checked here by construction: `callContract` takes the contract's
// input type and answers its output type. If either drifted, this file would not typecheck.
const { callContract } = await import("../apps/web/src/ui/client")
const contract = { name: "notes.note.create", input: { _input: { title: "" } }, output: { _output: { id: "" } } }
expect(typeof callContract).toBe("function")
expect(contract.name).toBe("notes.note.create")
})
})
+66
View File
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { readFileSync } from "node:fs"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { createTable } from "@conusai-kit/data-kysely"
import { createMysqlTable } from "@conusai-kit/data-kysely/mysql"
import { createMongoCollection } from "@conusai-kit/data-mongo"
import { appUserIdentities, tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8")
describe("app_user_identity profile contracts", () => {
it.each(["postgres", "mariadb", "mongodb", "convex"] as const)("is registered for %s", (profile) => {
const entities = entitiesOf(buildRegistry([tenancyModule]), profile)
expect(entities.filter((entity) => entity.table === appUserIdentities.table)).toHaveLength(1)
})
it("creates and eagerly backfills the Postgres alias table in a forward migration", () => {
const sql = read("migrations/postgres/0006_app-user-identity.sql")
for (const statement of createTable(appUserIdentities)) expect(sql).toContain(`${statement};`)
expect(sql).toContain('insert into "app_user_identity"')
expect(sql).toContain('from "users"')
expect(sql).toContain('"user_id",\n "migration_id"\n)\nselect\n "id",')
expect(read("migrations/postgres/0001_kernel.sql")).not.toContain('create table "app_user_identity"')
})
it("uses bounded MariaDB unique-key columns and preserves user IDs during backfill", () => {
const sql = read("profiles/mariadb/migrations/0006_app-user-identity.sql")
expect(sql).toContain("`issuer` varchar(255) not null")
expect(sql).toContain("`subject` varchar(255) not null")
expect(sql).toContain("unique (`issuer`, `subject`)")
expect(sql).toContain("insert into `app_user_identity`")
expect(sql).toContain("from `users`")
// The generated table contract has the same fields, constraints and indexes; the migration
// deliberately narrows the two string columns so InnoDB can index their full values.
const generated = createMysqlTable(appUserIdentities).join(";\n")
for (const fragment of [
"create table `app_user_identity`",
"`user_id` char(36) not null",
"`app_user_identity_user_id_idx`",
]) {
expect(generated).toContain(fragment)
expect(sql).toContain(fragment)
}
})
it("commits the generated MongoDB collection and unique/user indexes", () => {
const commands = JSON.parse(read("profiles/mongodb/migrations/0006_app-user-identity.json"))
expect(commands).toEqual(JSON.parse(JSON.stringify(createMongoCollection(appUserIdentities))))
expect(commands[1]).toMatchObject({ createIndex: { unique: true, keys: { issuer: 1, subject: 1 } } })
})
it("generates the Convex table and routes resolution through one internal mutation", () => {
const schema = read("convex/schema.ts")
expect(schema).toContain('"app_user_identity": defineTable')
expect(schema).toContain('.index("by_unique_issuer_subject", ["issuer", "subject"])')
expect(schema).toContain('.index("by_userId", ["userId"])')
const system = read("convex/system.ts")
expect(system).toContain("export const appUserResolve = internalMutation")
expect(system).toContain("resolveAppUser(data(ctx)")
expect(system).not.toContain("export const userInsert")
expect(system).not.toContain("export const userByIdentity")
})
})
+426
View File
@@ -0,0 +1,426 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Wall three (spec §3.3): the generated suite calls every tenant-scoped procedure of this app's
// registry as a member of organization A with organization B's slugs and ids, and fails any
// procedure that answers with B's data or writes anything at all.
//
// It runs on the in-memory adapter, which is the repository predicate alone: that is wall one.
// Wall two, what Postgres refuses on its own, is proven against a real Postgres by
// @conusai-kit/data-kysely's own gate, and wall three is proven here for every procedure this
// app registers -- including the ones a module adds tomorrow, because the list comes from the
// registry and a procedure without a fixture fails the suite by name.
import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path"
import { type Actor, buildRegistry, Container, createCaller, type TransportCaller } from "@conusai-kit/core"
import {
createMemoryDataSource,
DATA_SOURCE,
entitiesOf,
JOB_QUEUE,
OBJECT_STORE,
SYSTEM_DATA,
} from "@conusai-kit/data"
import { GUIDES_PLATFORM, GUIDES_REGISTRY } from "@conusai-kit/guides" // kit:guides
import { guidesIsolationExempt, guidesIsolationFixtures, recordingGuidesPlatform } from "@conusai-kit/guides/testing" // kit:guides
import { PROPOSALS_REGISTRY } from "@conusai-kit/proposals"
import { SUPPORT_PLATFORM } from "@conusai-kit/support" // kit:support
// kit:support:begin
import {
recordingSupportPlatform,
supportIsolationExempt,
supportIsolationFixtures,
} from "@conusai-kit/support/testing"
// kit:support:end
import { createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy"
import {
describeIsolation,
fakeJobQueue,
fakeObjectStore,
type IsolationFixture,
type IsolationTenant,
runIsolation,
snapshotOf,
uploadViaProcedures,
} from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { accountIsolationExempt } from "../modules/account/__tests__/exempt.js"
import { appModules } from "../modules/index.js"
import { KNOWLEDGE_ENABLEMENT, KNOWLEDGE_SEARCH } from "../modules/knowledge/index.js"
import type { ModuleIsolation } from "../modules/notes/__tests__/isolation.js"
const registry = buildRegistry([...appModules])
/**
* The fixtures and the seed rows a module brings in `modules/<id>/__tests__/isolation.ts`, found by
* their place: a module added with `pnpm new:module` is covered without a line here, and a
* procedure it registers with no fixture fails the suite by name. (The older modules' fixtures are
* listed below; they move into their own folders as each is next touched.)
*/
const modulesDir = join(import.meta.dirname, "..", "modules")
const moduleIsolations: ModuleIsolation[] = await Promise.all(
readdirSync(modulesDir)
.filter((id) => existsSync(join(modulesDir, id, "__tests__", "isolation.ts")))
.map(async (id) => {
const loaded = (await import(/* @vite-ignore */ join(modulesDir, id, "__tests__", "isolation.ts"))) as {
isolation: ModuleIsolation
}
return loaded.isolation
}),
)
const entities = entitiesOf(registry, "postgres")
/** The guides module's platform, recording: a refused cross-tenant call must never reach it. */ // kit:guides
const guidesPlatform = recordingGuidesPlatform() // kit:guides
/** The support module's platform, recording: a refused cross-tenant mint must never reach it. */ // kit:support
const supportPlatform = recordingSupportPlatform() // kit:support
/** The app wired the way apps/web wires it, with the in-memory adapter in place of Postgres. */
function makeApp() {
const data = createMemoryDataSource({ entities })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
// The files module's store: a seeded upload needs one, and a refused cross-tenant call never reaches it.
root.value(OBJECT_STORE, fakeObjectStore())
// A module whose writes enqueue work (a job started by a procedure) needs a queue to be driven at all.
root.value(JOB_QUEUE, fakeJobQueue())
root.value(PROPOSALS_REGISTRY, registry)
root.value(GUIDES_PLATFORM, guidesPlatform) // kit:guides
root.value(GUIDES_REGISTRY, registry) // kit:guides
root.value(SUPPORT_PLATFORM, supportPlatform) // kit:support
// The knowledge module's ports, faked: the search answers nothing (the suite asserts a
// cross-tenant call is refused or answered empty), and the enablement checks pass.
root.value(KNOWLEDGE_SEARCH, {
readiness: async () => ({ state: "ready" as const, reasons: [] }),
index: async () => undefined,
search: async () => [],
remove: async () => undefined,
})
root.value(KNOWLEDGE_ENABLEMENT, {
checkEligibility: async () => ({ ok: true as const }),
indexingQuota: async () => Number.MAX_SAFE_INTEGER,
linkManagedQdrant: async () => ({ serviceId: "fake", url: null }),
probeQdrant: async () => ({ ok: true as const }),
operatorQdrant: () => null,
})
const caller = (actor: Actor): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy }, { actor })
}
return { data, caller }
}
const app = makeApp()
async function signUp(email: string): Promise<Actor> {
const user = await app.data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
async function tenant(slug: string): Promise<IsolationTenant> {
const owner = await signUp(`owner@${slug}.test`)
const call = app.caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as {
orgId: string
slug: string
workspaceSlug: string
}
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { workspaceId: string; slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "tenancy",
})) as { projectId: string; slug: string }
const invite = (await call.callByName("tenancy.member.invite", {
org: slug,
email: `invited-${slug}@example.test`,
role: "member",
})) as { invitationId: string; token: string }
const key = (await call.callByName("auth.apiKey.create", { org: slug, name: "ci" })) as { key: { id: string } }
// One upload and one proposal in the project, so a fixture names rows that exist in the other
// tenant: the call has to be refused for being another tenant's, not for naming nothing.
const inProject = { org: slug, workspace: workspace.slug, project: project.slug }
const file = await uploadViaProcedures(call, inProject, {
filename: "seed.pdf",
contentType: "application/pdf",
bytes: new TextEncoder().encode("%PDF-1.4"),
})
const proposal = (await call.callByName(
"proposals.proposal.create",
{
...inProject,
targetKey: "documents.line",
change: {
lineKey: "a".repeat(64),
label: "Reference",
value: "QR-1",
page: 1,
position: 0,
quote: "Reference: QR-1",
confidence: 0.9,
verification: "verified",
edited: false,
},
evidence: [{ fileId: file.fileId, page: 1, quote: "Reference: QR-1" }],
model: "seed",
promptVersion: "seed.v1",
},
{ idempotencyKey: `seed.${slug}` },
)) as { proposalId: string }
// What each module that brings fixtures seeds in this tenant: its ids are what the other tenant's fixtures name.
const seeded: Record<string, string> = {}
for (const isolation of moduleIsolations) Object.assign(seeded, await isolation.seed(call, inProject))
return {
orgSlug: org.slug,
workspaceSlug: workspace.slug,
projectSlug: project.slug,
orgId: org.orgId,
workspaceId: workspace.workspaceId,
projectId: project.projectId,
userId: owner.userId,
email: `owner@${slug}.test`,
extra: {
invitationId: invite.invitationId,
token: invite.token,
apiKeyId: key.key.id,
fileId: file.fileId,
proposalId: proposal.proposalId,
...seeded,
},
}
}
let seeded: Promise<{ a: IsolationTenant; b: IsolationTenant }> | undefined
const tenants = () => {
seeded ??= (async () => ({ a: await tenant("alpha"), b: await tenant("bravo") }))()
return seeded
}
/** The three slugs a project-scoped procedure's input carries, from the tenant it must not reach. */
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
/** The other tenant's seeded rows; the ghost tenant has none, so its ids are well-formed and absent. */
const NOWHERE = "00000000-0000-0000-0000-000000000000"
const fileOf = (t: IsolationTenant) => t.extra.fileId ?? NOWHERE
const proposalOf = (t: IsolationTenant) => t.extra.proposalId ?? NOWHERE
/** One fixture per tenant-scoped procedure, built from the *other* tenant's ids. */
const fixtures: Record<string, IsolationFixture> = {
...Object.assign({}, ...moduleIsolations.map((isolation) => isolation.fixtures)),
...guidesIsolationFixtures, // kit:guides
...supportIsolationFixtures, // kit:support
"tenancy.org.get": { input: (t) => ({ org: t.orgSlug }) },
"tenancy.org.update": {
input: (t) => ({ org: t.orgSlug, name: "Updated Name", expectedVersion: 1 }),
},
"tenancy.invitation.list": { input: (t) => ({ org: t.orgSlug }) },
"tenancy.member.list": { input: (t) => ({ org: t.orgSlug }) },
"tenancy.member.invite": { input: (t) => ({ org: t.orgSlug, email: "intruder@example.test", role: "member" }) },
"tenancy.member.change-role": { input: (t) => ({ org: t.orgSlug, userId: t.userId, role: "viewer" }) },
"tenancy.member.remove": { input: (t) => ({ org: t.orgSlug, userId: t.userId }) },
"tenancy.member.leave": { input: (t) => ({ org: t.orgSlug }) },
"tenancy.workspace.list": { input: (t) => ({ org: t.orgSlug }) },
"tenancy.workspace.create": {
input: (t) => ({ org: t.orgSlug, slug: "intruder", name: "Intruder", visibility: "org" }),
},
"tenancy.workspace.set-member": {
input: (t) => ({ org: t.orgSlug, workspace: t.workspaceSlug, userId: t.userId, role: "admin" }),
},
"tenancy.project.list": { input: (t) => ({ org: t.orgSlug, workspace: t.workspaceSlug }) },
"tenancy.project.get": { input: (t) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug }) },
"tenancy.project.create": {
input: (t) => ({
org: t.orgSlug,
workspace: t.workspaceSlug,
slug: "intruder",
name: "Intruder",
moduleKey: "tenancy",
}),
},
"auth.apiKey.create": { input: (t) => ({ org: t.orgSlug, name: "intruder" }) },
"auth.apiKey.list": { input: (t) => ({ org: t.orgSlug }) },
"auth.apiKey.revoke": {
input: (t) => ({ org: t.orgSlug, id: t.extra.apiKeyId ?? "00000000-0000-0000-0000-000000000000" }),
},
"files.file.beginUpload": {
input: (t) => ({ ...inProjectOf(t), filename: "intruder.pdf", contentType: "application/pdf", size: 8 }),
},
"files.file.putPart": {
input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t), part: 1, body: new TextEncoder().encode("%PDF-1.4") }),
},
"files.file.completeUpload": {
input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t), parts: [{ number: 1, etag: "etag-intruder" }] }),
},
"files.file.get": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"files.file.download": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"files.file.list": { input: (t) => inProjectOf(t) },
"files.file.delete": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"proposals.proposal.create": {
input: (t) => ({
...inProjectOf(t),
targetKey: "documents.line",
change: {
lineKey: "b".repeat(64),
label: "Intruder",
value: "x",
page: 1,
position: 0,
quote: "Intruder: x",
confidence: 0.5,
edited: false,
},
// Nothing of the other tenant's: `create` reads no id it is given, so the only cross-tenant
// case it has is the slugs, which the resolver refuses. An id of B's here would be echoed
// back in the answer, which is the caller's own input and not B's data.
evidence: [{ fileId: NOWHERE, page: 1 }],
model: "intruder",
promptVersion: "intruder.v1",
}),
},
"proposals.proposal.edit": {
input: (t) => ({
...inProjectOf(t),
proposalId: proposalOf(t),
change: {
lineKey: "c".repeat(64),
label: "Intruder",
value: "x",
page: 1,
position: 0,
quote: "Intruder: x",
confidence: 0.5,
edited: true,
},
}),
},
"proposals.proposal.get": { input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t) }) },
"proposals.proposal.list": { input: (t) => inProjectOf(t) },
"proposals.proposal.approve": {
input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t), expectedVersion: 1 }),
},
"proposals.proposal.approveMany": {
input: (t) => {
const proposalId = proposalOf(t)
return { ...inProjectOf(t), proposalIds: [proposalId], expectations: { [proposalId]: { expectedVersion: 1 } } }
},
},
"proposals.proposal.reject": {
input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t), reason: "intruder" }),
},
"documents.document.list": { input: (t) => inProjectOf(t) },
"documents.document.get": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"documents.line.edit": {
input: (t) => ({ ...inProjectOf(t), lineId: proposalOf(t), label: "Intruder", value: "x" }),
},
"documents.export.create": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"branding.brand.get": { input: (t) => ({ org: t.orgSlug }) },
"branding.brand.set": { input: (t) => ({ org: t.orgSlug, primary: "#000000", displayName: "Intruder" }) },
"branding.logo.set": {
input: (t) => ({ org: t.orgSlug, variant: "light", contentType: "image/png", body: "iVBORw0KGgoAAAAA" }),
},
"branding.logo.clear": { input: (t) => ({ org: t.orgSlug, variant: "light" }) },
"branding.logo.get": { input: (t) => ({ org: t.orgSlug, variant: "light" }) },
// User-scoped, so the kernel has no scope to resolve -- and it still names an organization,
// which is exactly where a hole hides: `alsoCheck` puts it through the same test with the
// other tenant's own open token.
"tenancy.invitation.accept": { input: (t) => ({ org: t.orgSlug, token: t.extra.token ?? "z".repeat(43) }) },
}
const alsoCheck = ["tenancy.invitation.accept"]
const exempt = {
"tenancy.me.get": "it takes no organization and answers only about the signed-in user",
"tenancy.me.update": "it takes no organization and no id: the only row it can write is the signed-in user's own",
"tenancy.org.create": "a unique slug necessarily answers 409 for a taken one; it writes in no tenant",
"auth.session.current": "it answers only about the signed-in user's own row",
...accountIsolationExempt,
...guidesIsolationExempt, // kit:guides
...supportIsolationExempt, // kit:support
}
const options = async () => {
const { a, b } = await tenants()
return {
registry,
seed: async () => ({ a, b }),
caller: (actor: Actor) => app.caller(actor),
snapshot: () => snapshotOf(app.data, entities, { outbox: app.data.outbox }),
fixtures,
alsoCheck,
exempt,
}
}
describeIsolation(
{
registry,
seed: async () => tenants(),
caller: (actor) => ({
call: async () => undefined as never,
callByName: async (name, input) => app.caller(actor).callByName(name, input),
}),
snapshot: async () => snapshotOf(app.data, entities, { outbox: app.data.outbox }),
fixtures,
alsoCheck,
exempt,
},
{ describe, it },
)
describe("the generated isolation suite", () => {
it("covers every procedure this app registers", async () => {
const result = await runIsolation(await options())
expect(result.failures).toEqual([])
expect(result.passed.length).toBe(registry.isolationFixtures.length + alsoCheck.length)
})
it("covers every procedure of the notes and files modules, by name", async () => {
const result = await runIsolation(await options())
const covered = new Set(result.passed)
const names = [...registry.procedures.keys()].filter((name) => /^(notes|files)\./.test(name))
expect(names.length).toBe(14)
for (const name of names) expect(covered.has(name), name).toBe(true)
})
it("fails, naming it, when a module's own fixture is missing", async () => {
const base = await options()
const { "notes.attachment.remove": _removed, ...rest } = fixtures
const result = await runIsolation({ ...base, fixtures: rest })
expect(result.failures.map((failure) => failure.procedure)).toEqual(["notes.attachment.remove"])
})
// kit:guides:begin
it("reaches the guides platform with no cross-tenant call", async () => {
await runIsolation(await options())
expect(guidesPlatform.calls).toEqual([])
})
// kit:guides:end
// kit:support:begin
it("reaches the support platform with no cross-tenant call", async () => {
await runIsolation(await options())
expect(supportPlatform.calls).toEqual([])
})
// kit:support:end
it("fails, naming it, when a procedure has no fixture", async () => {
const base = await options()
const { "tenancy.member.list": _removed, ...rest } = fixtures
const result = await runIsolation({ ...base, fixtures: rest })
expect(result.failures.map((failure) => failure.procedure)).toEqual(["tenancy.member.list"])
})
})
+51
View File
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The committed kernel schema, and the rule that keeps it committable.
//
// `migrations/postgres/0001_kernel.sql` is `schemaStatements()` over the kernel's own entities,
// written once. It is a snapshot of the *kernel's* tables alone, not of the whole registry, so
// adding a module does not change this file -- a module ships its own numbered migration, which
// is what a forward-only runner needs (it refuses a migration whose text changed after it ran).
// A kit upgrade that changes a kernel table does change it, and that is exactly the moment a
// customer has to see a new migration in review.
import { authModule } from "@conusai-kit/auth"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { schemaStatements } from "@conusai-kit/data-kysely"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import { appUserIdentities, tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { kernelSchema } from "../apps/worker/src/migrations.js"
import { SPDX_LINE } from "../scripts/licence.mjs"
// Tenancy and sign-in alone: later kit tables have their own forward migrations, because 0001 is
// never edited once it has run. app_user_identity belongs to 0006, not this historical snapshot.
const kernelEntities = [
...entitiesOf(buildRegistry([tenancyModule, authModule]), "postgres"),
...KIT_JOBS_ENTITIES,
].filter((entity) => entity.table !== appUserIdentities.table)
describe("the kernel migration", () => {
it("matches the committed 0001_kernel.sql", async () => {
// The file leads with the template's SPDX line (AT5.1); the runner reads it as the comment it is.
await expect(`-- ${SPDX_LINE}\n${kernelSchema(kernelEntities)}`).toMatchFileSnapshot(
"../migrations/postgres/0001_kernel.sql",
)
})
it("puts forced row-level security on every tenant table", () => {
const sql = schemaStatements(kernelEntities).join(";\n")
for (const entity of kernelEntities) {
if (entity.tenancy === "global") continue
expect(sql, `${entity.table} is not forced`).toContain(`alter table "${entity.table}" force row level security`)
expect(sql).toContain(`create policy "${entity.table}_tenant" on "${entity.table}"`)
}
})
it("compares a tenant row against the transaction's organization and nothing else", () => {
const sql = schemaStatements(kernelEntities).join(";\n")
// The policy reads NULLIF(current_setting(...), '')::uuid: a pooled connection whose previous
// transaction set app.org_id sees no rows rather than an error (kit-data-kysely's decision 2).
expect(sql).toContain("nullif(current_setting('app.org_id', true), '')::uuid")
})
})
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Where the knowledge module's vectors backend keeps its vectors (decision 2026-10-25): a linked
// Qdrant, or pgvector when chosen explicitly, and otherwise no store at all. pgvector is never the
// silent default: the platform's default managed Postgres image has no pgvector, and the migrate
// job of a fresh app must not stop on `extension "vector" is not available` before anyone asked for
// document AI. The migrate job and the readiness probe read the same decision.
import {
KNOWLEDGE_VECTOR_STORE_VAR,
NO_VECTOR_STORE_REASON,
vectorStoreMigrationName,
vectorStoreOf,
} from "@app/config"
import { describe, expect, it } from "vitest"
import { vectorStoreMigration } from "../apps/worker/src/migrations.js"
const QDRANT = { QDRANT_URL: "http://qdrant.internal:6333" }
describe("the knowledge vector store", () => {
it("is none by default, so a fresh app migrates on a Postgres without pgvector", () => {
expect(vectorStoreOf({})).toBeNull()
expect(vectorStoreMigrationName({})).toBeNull()
expect(vectorStoreMigration({})).toBeNull()
expect(NO_VECTOR_STORE_REASON).toContain(`${KNOWLEDGE_VECTOR_STORE_VAR}=pgvector`)
})
it("is the linked Qdrant when one is linked, with no vector table", () => {
expect(vectorStoreOf(QDRANT)).toBe("qdrant")
expect(vectorStoreMigrationName(QDRANT)).toBeNull()
expect(vectorStoreOf({ ...QDRANT, KNOWLEDGE_VECTOR_STORE: "qdrant" })).toBe("qdrant")
// Qdrant required but not linked: no store, never a fallback to pgvector.
expect(vectorStoreOf({ KNOWLEDGE_VECTOR_STORE: "qdrant" })).toBeNull()
expect(vectorStoreMigrationName({ KNOWLEDGE_VECTOR_STORE: "qdrant" })).toBeNull()
})
it("is pgvector only when chosen, and then the migrate job builds the table", () => {
const chosen = { KNOWLEDGE_VECTOR_STORE: " PgVector " }
expect(vectorStoreOf(chosen)).toBe("pgvector")
expect(vectorStoreMigrationName(chosen)).toBe("0006_vectors")
const migration = vectorStoreMigration({ ...chosen, VECTOR_DIMENSIONS: "8" })
expect(migration?.name).toBe("0006_vectors")
expect(migration?.statements[0]).toBe("create extension if not exists vector")
// An explicit choice wins over a linked Qdrant.
expect(vectorStoreOf({ ...QDRANT, KNOWLEDGE_VECTOR_STORE: "pgvector" })).toBe("pgvector")
})
it("has no store on the platform backend and refuses an unknown choice", () => {
expect(vectorStoreOf({ KNOWLEDGE_BACKEND: "platform", KNOWLEDGE_VECTOR_STORE: "pgvector" })).toBeNull()
expect(vectorStoreMigrationName({ KNOWLEDGE_BACKEND: "platform", ...QDRANT })).toBeNull()
expect(() => vectorStoreOf({ KNOWLEDGE_VECTOR_STORE: "sqlite" })).toThrow(KNOWLEDGE_VECTOR_STORE_VAR)
})
})
+176
View File
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The licence layout (plan item AT5.1, spec section 14.3): LICENSE-TEMPLATE says what it is, the
// customer's LICENSE is a stub, NOTICE and TEMPLATE-FILES exist and agree with the tree, and every
// template file that can hold a comment carries the SPDX header. The negative controls make the check
// fail where it should: a scaffold file that loses its header, a file nobody listed, a listed file that
// is gone.
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import {
hasHeader,
headerFor,
LIST_FILE,
listText,
parseList,
problems,
SPDX_ID,
SPDX_LINE,
templateFiles,
withHeader,
write,
} from "../scripts/licence.mjs"
const APP_ROOT = new URL("..", import.meta.url).pathname
const read = (path: string) => readFileSync(join(APP_ROOT, path), "utf8")
const made: string[] = []
afterEach(() => {
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
})
/** A small tree shaped like the template: a scaffold, a module and the layout's own files. */
function fixture(files: Record<string, string>): string {
const root = mkdtempSync(join(tmpdir(), "licence-"))
made.push(root)
const all: Record<string, string> = {
LICENSE: "yours\n",
"LICENSE-TEMPLATE": "placeholder\n",
NOTICE: "notice\n",
"THIRD-PARTY-NOTICES": "none\n",
"package.json": "{}\n",
"src/a.ts": "export const a = 1\n",
"modules/notes/index.ts": "export const notes = 1\n",
...files,
}
for (const [path, text] of Object.entries(all)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), text)
}
write(root)
return root
}
describe("this template's licence layout", () => {
it("is whole: every file is listed, every file that can hold a header has it, and the four documents exist", () => {
expect(problems(APP_ROOT)).toEqual([])
})
it("says in LICENSE-TEMPLATE that it is a placeholder, until counsel decides (AT-O2)", () => {
const text = read("LICENSE-TEMPLATE")
expect(text).toMatch(/PLACEHOLDER/)
expect(text).toContain("AT-O2")
expect(text).toContain(SPDX_ID)
expect(text).toMatch(/counsel/i)
})
it("keeps LICENSE the customer's own, a stub that names what it does not cover", () => {
const text = read("LICENSE")
expect(text).toMatch(/This file is yours/)
expect(text).toContain("LICENSE-TEMPLATE")
expect(text).toContain("TEMPLATE-FILES")
})
it("lists the template's files and not the customer's", () => {
const listed = parseList(read(LIST_FILE))
expect(listed).toEqual(templateFiles(APP_ROOT))
expect(listed).toContain("apps/web/app/layout.tsx")
expect(listed).toContain("LICENSE-TEMPLATE")
expect(listed).toContain(LIST_FILE)
expect(listed).not.toContain("LICENSE")
expect(listed).not.toContain("pnpm-lock.yaml")
expect(listed.some((path) => path.startsWith("modules/"))).toBe(false)
expect(listed).not.toContain(".conusai/template.json")
})
it("puts the header first in a TypeScript file, a kernel migration, a workflow, the Dockerfile and the guide", () => {
expect(read("Dockerfile").split("\n")[0]).toBe(`# ${SPDX_LINE}`)
expect(read("test/registry.test.ts").split("\n")[0]).toBe(`// ${SPDX_LINE}`)
expect(read("migrations/postgres/0001_kernel.sql").split("\n")[0]).toBe(`-- ${SPDX_LINE}`)
expect(read(".gitea/workflows/ci.yml").split("\n")[0]).toBe(`# ${SPDX_LINE}`)
expect(read("AGENTS.md").split("\n")[0]).toBe(`<!-- ${SPDX_LINE} -->`)
})
})
describe("the header", () => {
it("is a comment of the file's own kind, and none for a file that cannot hold one", () => {
expect(headerFor("a/b.ts")).toBe(`// ${SPDX_LINE}`)
expect(headerFor("a/b.mjs")).toBe(`// ${SPDX_LINE}`)
expect(headerFor("a/b.css")).toBe(`/* ${SPDX_LINE} */`)
expect(headerFor("a/b.sql")).toBe(`-- ${SPDX_LINE}`)
expect(headerFor("a/b.md")).toBe(`<!-- ${SPDX_LINE} -->`)
expect(headerFor("compose.yaml")).toBe(`# ${SPDX_LINE}`)
expect(headerFor("Dockerfile")).toBe(`# ${SPDX_LINE}`)
expect(headerFor(".env.example")).toBe(`# ${SPDX_LINE}`)
expect(headerFor("package.json")).toBeNull()
expect(headerFor("llms.txt")).toBeNull()
expect(headerFor("LICENSE-TEMPLATE")).toBeNull()
})
it("goes after a shebang, and is added once", () => {
const script = "#!/usr/bin/env bash\nset -e\n"
const once = withHeader(script, "run.sh")
expect(once).toBe(`#!/usr/bin/env bash\n# ${SPDX_LINE}\nset -e\n`)
expect(withHeader(once, "run.sh")).toBe(once)
expect(hasHeader(once, "run.sh")).toBe(true)
expect(hasHeader(script, "run.sh")).toBe(false)
})
it("goes after the frontmatter of a Markdown file that opens with one (an agent skill), and is added once", () => {
const skill = "---\nname: fix\ndescription: Fix it.\n---\n# Fix\n"
const once = withHeader(skill, ".claude/skills/fix/SKILL.md")
expect(once).toBe(`---\nname: fix\ndescription: Fix it.\n---\n<!-- ${SPDX_LINE} -->\n# Fix\n`)
expect(withHeader(once, ".claude/skills/fix/SKILL.md")).toBe(once)
expect(hasHeader(once, ".claude/skills/fix/SKILL.md")).toBe(true)
// A header above the frontmatter would hide the skill from its tool, so it does not count.
expect(hasHeader(`<!-- ${SPDX_LINE} -->\n${skill}`, ".claude/skills/fix/SKILL.md")).toBe(true)
expect(hasHeader(skill, ".claude/skills/fix/SKILL.md")).toBe(false)
})
})
describe("the check fails where it should", () => {
it("passes a tree that `write` has laid out, and ignores modules/** and LICENSE", () => {
const root = fixture({})
expect(problems(root)).toEqual([])
expect(parseList(readFileSync(join(root, LIST_FILE), "utf8"))).toEqual([
"LICENSE-TEMPLATE",
"NOTICE",
"TEMPLATE-FILES",
"THIRD-PARTY-NOTICES",
"package.json",
"src/a.ts",
])
expect(readFileSync(join(root, "modules/notes/index.ts"), "utf8")).not.toContain("SPDX")
expect(readFileSync(join(root, "LICENSE"), "utf8")).toBe("yours\n")
})
it("fails, naming the file, when a scaffold file loses its header", () => {
const root = fixture({})
writeFileSync(join(root, "src/a.ts"), "export const a = 1\n")
expect(problems(root)).toEqual([`src/a.ts has no "${SPDX_LINE}" header`])
})
it("fails when a file is not in TEMPLATE-FILES, and when a listed file is gone", () => {
const root = fixture({})
writeFileSync(join(root, "src/new.ts"), `// ${SPDX_LINE}\nexport {}\n`)
expect(problems(root)).toEqual(["TEMPLATE-FILES does not list src/new.ts"])
rmSync(join(root, "src/new.ts"))
rmSync(join(root, "src/a.ts"))
expect(problems(root)).toEqual(["TEMPLATE-FILES lists src/a.ts, which is not a template file"])
})
it("fails when one of the four documents is missing", () => {
const root = fixture({})
rmSync(join(root, "NOTICE"))
expect(problems(root)).toContain("NOTICE is missing")
})
it("is what listText writes: comments, then one path a line", () => {
const text = listText(["a", "b"])
expect(parseList(text)).toEqual(["a", "b"])
expect(text.startsWith("# ")).toBe(true)
})
})
+149
View File
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Drive the same SDK handler with the official SDK client; authorization remains in the app caller.
import { buildRegistry, forbidden } from "@conusai-kit/core"
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
import { describe, expect, it, vi } from "vitest"
import { createMcpHandler } from "../apps/web/src/server/mcp.js"
import { appModules } from "../modules/index.js"
const registry = buildRegistry([...appModules])
const principal = { method: "bearer", actor: { userId: "user-a" } } as never
function harness(
options: {
authenticated?: boolean
call?: (name: string, args: unknown) => Promise<unknown>
csrf?: () => void
} = {},
) {
const callByName = vi.fn(options.call ?? (async () => ({ notes: [], hasMore: false })))
const checkCsrf = vi.fn(options.csrf ?? (() => undefined))
const handle = createMcpHandler({
registry,
contextFor: async () => ({
principal: options.authenticated === false ? null : principal,
caller: { callByName } as never,
}),
checkCsrf,
})
return { handle, callByName, checkCsrf }
}
function rawRequest(body: unknown, extra: Record<string, string> = {}) {
return new Request("https://app.example.test/api/mcp", {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...extra },
body: JSON.stringify(body),
})
}
function connect(handle: ReturnType<typeof createMcpHandler>, requestId = "sdk-request") {
const transport = new StreamableHTTPClientTransport(new URL("https://app.example.test/api/mcp"), {
fetch: (url, init) => handle(new Request(url, init), requestId),
})
const client = new Client({ name: "app-template-test", version: "1.0.0" }, { versionNegotiation: { mode: "auto" } })
return { client, transport }
}
describe("the stateless MCP endpoint", () => {
it("negotiates with the official client, lists opted-in tools, and calls through the scoped caller", async () => {
const output = { notes: [], hasMore: false }
const { handle, callByName, checkCsrf } = harness({ call: async () => output })
const { client, transport } = connect(handle)
try {
await client.connect(transport)
expect(client.getProtocolEra()).toBe("modern")
const listed = await client.listTools()
expect(listed.tools.map((tool) => tool.name)).toEqual(["agent.run.status", "knowledge.search", "notes.note.list"])
expect(listed.tools.find((tool) => tool.name === "notes.note.list")?.inputSchema.required).toEqual([
"org",
"workspace",
"project",
])
const result = await client.callTool({
name: "notes.note.list",
arguments: { org: "org-a", workspace: "ws-a", project: "project-a" },
})
expect(callByName).toHaveBeenCalledWith("notes.note.list", {
org: "org-a",
workspace: "ws-a",
project: "project-a",
})
expect(checkCsrf.mock.calls.length).toBeGreaterThanOrEqual(3)
expect(result.isError).not.toBe(true)
expect(result.structuredContent).toEqual(output)
} finally {
await client.close()
}
})
it("refuses unauthenticated requests before SDK protocol handling", async () => {
const { handle, callByName } = harness({ authenticated: false })
const response = await handle(rawRequest({ jsonrpc: "2.0", id: 4, method: "initialize" }), "request-4")
expect(response.status).toBe(401)
expect(response.headers.get("www-authenticate")).toBe("Bearer")
expect(callByName).not.toHaveBeenCalled()
expect(JSON.stringify(await response.json())).not.toContain("user-a")
})
it("applies the same session CSRF refusal and does not disclose its detail", async () => {
const { handle } = harness({
csrf: () => {
throw forbidden("sensitive csrf detail")
},
})
const response = await handle(rawRequest({ jsonrpc: "2.0", id: 5, method: "initialize" }), "request-5")
expect(response.status).toBe(403)
expect(JSON.stringify(await response.json())).not.toContain("sensitive csrf detail")
})
it("returns a safe tool error when kernel authorization refuses the caller", async () => {
const call = vi.fn(async (_name: string, args: unknown) => {
if (typeof args === "object" && args !== null && "org" in args && args.org === "other-org") {
throw forbidden("tenant-private note data")
}
return { notes: [], hasMore: false }
})
const { handle } = harness({
call,
})
const { client, transport } = connect(handle, "request-tool-refusal")
try {
await client.connect(transport)
const result = await client.callTool({
name: "notes.note.list",
arguments: { org: "other-org", workspace: "other-ws", project: "other-project" },
})
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ type: "text", text: "The tool request was refused." })
expect(JSON.stringify(result)).not.toContain("tenant-private note data")
expect(call).toHaveBeenCalledWith("notes.note.list", {
org: "other-org",
workspace: "other-ws",
project: "other-project",
})
} finally {
await client.close()
}
})
it("refuses non-tool names and non-POST methods", async () => {
const { handle, callByName } = harness()
const { client, transport } = connect(handle, "request-7")
try {
await client.connect(transport)
await expect(client.callTool({ name: "notes.note.create", arguments: {} })).rejects.toMatchObject({
code: -32602,
})
expect(callByName).not.toHaveBeenCalled()
} finally {
await client.close()
}
const get = await handle(new Request("https://app.example.test/api/mcp"), "request-8")
expect(get.status).toBe(405)
})
})
+97
View File
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What the migrate job applies, and the one grant list the global reader gets (decision AT-D20).
import { readdirSync, readFileSync } from "node:fs"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { OUTBOX_READER_GRANT } from "@conusai-kit/data-kysely"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import { describe, expect, it } from "vitest"
import { CROSS_TENANT_READS, jobSchemaGrants, readerGrants, splitStatements } from "../apps/worker/src/migrations.js"
import { appModules } from "../modules/index.js"
const dir = new URL("../migrations/postgres/", import.meta.url)
const files = readdirSync(dir)
.filter((name) => name.endsWith(".sql"))
.sort()
const committed = files.map((name) => readFileSync(new URL(name, dir), "utf8")).join("\n")
const registry = buildRegistry([...appModules])
describe("the committed migrations", () => {
it("are numbered, forward only, and start at 0001", () => {
expect(files.length).toBeGreaterThan(0)
expect(files[0]).toMatch(/^0001_/)
for (const name of files) expect(name).toMatch(/^\d{4}_[a-z0-9-]+\.sql$/)
})
it("create a table for every entity this app's registry declares", () => {
const declared = [...entitiesOf(registry, "postgres"), ...KIT_JOBS_ENTITIES]
const inModules = new Set(registry.migrations.postgres.map(({ module }) => module))
for (const entity of declared) {
// A module that ships its own migration folder is allowed to own its tables there.
if (inModules.size > 0 && !committed.includes(`create table "${entity.table}"`)) continue
expect(committed, `no migration creates ${entity.table}`).toContain(`create table "${entity.table}"`)
}
})
it("split into statements without losing a dollar-quoted body", () => {
const statements = splitStatements('create table "a" ("x" int);\n\ndo $$ begin perform 1; end $$;\n')
expect(statements).toHaveLength(2)
expect(statements[1]).toContain("do $$ begin perform 1; end $$")
})
})
describe("the global reader's grants", () => {
const migration = readerGrants("postgresql://app_reader:secret@db:5432/app")
const sql = migration.statements.join(";\n")
const tables = ["outbox", "org_memberships", "projects", "auth_api_keys"]
it("grant the cross-tenant reads the kit modules declare, the outbox, and no other table", () => {
// The list is the modules' own declarations, so adding a read to a module adds the grant here
// and a table nobody declared is never granted.
expect(CROSS_TENANT_READS.map((read) => read.entity.table).sort()).toEqual([
"auth_api_keys",
"org_memberships",
"projects",
])
const granted = migration.statements.filter((statement) => statement.startsWith("grant select"))
expect(granted).toHaveLength(tables.length)
for (const table of tables) expect(sql).toContain(`on "${table}" to "app_reader"`)
})
it("let the reader write nothing but the columns the relay keeps its books in", () => {
// A grant on the whole row would let the reader rewrite an event's organization, name or
// payload, which the relay then hands to a subscriber running as a system actor.
const update = migration.statements.filter((statement) => statement.startsWith("grant update"))
expect(update).toHaveLength(1)
expect(update[0]).toContain('on "outbox" to "app_reader"')
for (const column of OUTBOX_READER_GRANT.update ?? []) expect(update[0]).toContain(`"${column}"`)
expect(update[0]).not.toContain('"payload"')
expect(update[0]).not.toContain('"org_id"')
expect(update[0]).not.toContain('"name"')
expect(sql).not.toMatch(/grant [^;]*insert/)
expect(sql).not.toMatch(/grant [^;]*delete/)
})
it("give the reader a policy of its own, because row-level security binds it too", () => {
// The reader owns nothing and has NOBYPASSRLS (data-services DS21), so without a policy
// naming it the forced policy on app.org_id would answer zero rows for every read.
for (const table of tables) {
expect(sql).toContain(
`create policy "${table}_global_reader" on "${table}" for select to "app_reader" using (true)`,
)
}
expect(sql).toContain('create policy "outbox_global_reader_update"')
})
it("name the role from the reader's own URL, quoted", () => {
expect(readerGrants("postgresql://other_reader:s@db:5432/app").statements[0]).toContain('"other_reader"')
})
it("grant the runtime role the job schema a transactional enqueue needs", () => {
const jobs = jobSchemaGrants("postgresql://app_app:secret@db:5432/app").statements.join(";\n")
expect(jobs).toContain('grant usage on schema "pgboss" to "app_app"')
expect(jobs).toContain('alter default privileges in schema "pgboss"')
expect(jobs).not.toContain("app_reader")
})
})
+144
View File
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Every table of a module the kit ships has a committed, forward-only migration, and each is exactly
// `schemaStatements()` over that module's own entities (decision AT-D20: the entity definitions are
// the one source of a column, so a hand-written column cannot drift from the schema a module validates
// its rows with). A kit upgrade that changes a table changes a snapshot here, which is the moment a
// customer must see a new migration in review.
//
// `0002` and `0003` are the grants the migrate job builds at run time (apps/worker/src/migrations.ts),
// so the files of the kit's modules take the next numbers.
import { readFileSync } from "node:fs"
import { DOCUMENTS_ENTITIES, documentsModule } from "@app/modules/documents"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { schemaStatements } from "@conusai-kit/data-kysely"
import { mysqlSchemaStatements } from "@conusai-kit/data-kysely/mysql"
import { MONGO_KERNEL_COMMANDS, mongoSchemaCommands } from "@conusai-kit/data-mongo"
import { filesModule } from "@conusai-kit/files"
import { proposalsModule } from "@conusai-kit/proposals"
import { tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { SPDX_LINE } from "../scripts/licence.mjs"
/** The statements for these tables alone: `schemaStatements` leads with the kernel's own two. */
function tablesOf(entities: Parameters<typeof schemaStatements>[0]): string {
const kernel = schemaStatements([]).length
return `${schemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
/** The same for a module's MariaDB folder (spec §5.3: one numbered SQL folder per dialect). */
function mysqlTablesOf(entities: Parameters<typeof mysqlSchemaStatements>[0]): string {
const kernel = mysqlSchemaStatements([]).length
return `${mysqlSchemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
type MongoCommands = ReturnType<typeof mongoSchemaCommands>
/** The same for a module's MongoDB folder: `mongoSchemaCommands` leads with the kernel's own collections. */
function mongoCommandsOf(entities: Parameters<typeof mongoSchemaCommands>[0]): MongoCommands {
return mongoSchemaCommands(entities).slice(MONGO_KERNEL_COMMANDS.length)
}
const entitiesOfModule = (module: Parameters<typeof buildRegistry>[0][number]) =>
entitiesOf(buildRegistry([tenancyModule, module]), "postgres").filter(
(entity) => !entitiesOf(buildRegistry([tenancyModule]), "postgres").some((own) => own.table === entity.table),
)
describe("the migrations of the modules the kit ships", () => {
it("0004_files.sql creates the files table", async () => {
await expect(`-- ${SPDX_LINE}\n${tablesOf(entitiesOfModule(filesModule))}`).toMatchFileSnapshot(
"../migrations/postgres/0004_files.sql",
)
})
it("0005_proposals.sql creates the proposals table", async () => {
await expect(`-- ${SPDX_LINE}\n${tablesOf(entitiesOfModule(proposalsModule))}`).toMatchFileSnapshot(
"../migrations/postgres/0005_proposals.sql",
)
})
})
describe("the migration of the documents module", () => {
const migration = (name: string) =>
readFileSync(new URL(`../modules/documents/migrations/${name}`, import.meta.url), "utf8")
// A migration that has run is never edited, so a column added later is a file of its own: the first
// file stays what it was, the second adds the column, and together they are exactly the entities.
it("0001 is its two tables as first shipped, and 0002 adds the one column since", () => {
expect(migration("0002_skipped_pages.sql")).toBe(
'alter table "documents_document" add column "skipped_pages" jsonb;\n',
)
const generated = tablesOf(DOCUMENTS_ENTITIES)
expect(generated).toContain(' "skipped_pages" jsonb,\n')
expect(generated.replace(' "skipped_pages" jsonb,\n', "")).toBe(migration("0001_documents.sql"))
})
it("is declared by the manifest, so the migrate job finds it", () => {
expect(documentsModule.migrations).toEqual({
postgres: "migrations",
mariadb: "adapters/kysely/mysql/migrations",
mongodb: "adapters/mongo/migrations",
})
const sql = migration("0001_documents.sql")
for (const table of ["documents_document", "documents_line"]) {
expect(sql).toContain(`create table "${table}"`)
expect(sql).toContain(`alter table "${table}" force row level security`)
}
})
it("ships the same migrations in the MariaDB dialect (AT3.1)", () => {
const mysqlMigration = (name: string) =>
readFileSync(new URL(`../modules/documents/adapters/kysely/mysql/migrations/${name}`, import.meta.url), "utf8")
expect(mysqlMigration("0002_skipped_pages.sql")).toBe(
"alter table `documents_document` add column `skipped_pages` json;\n",
)
const generated = mysqlTablesOf(DOCUMENTS_ENTITIES)
expect(generated).toContain(" `skipped_pages` json,\n")
expect(generated.replace(" `skipped_pages` json,\n", "")).toBe(mysqlMigration("0001_documents.sql"))
})
it("ships the same migrations as MongoDB commands: 0001 the collections as first shipped, 0002 the added field (AT3.2)", () => {
const mongoMigration = (name: string): MongoCommands =>
JSON.parse(
readFileSync(new URL(`../modules/documents/adapters/mongo/migrations/${name}`, import.meta.url), "utf8"),
)
const current = mongoCommandsOf(DOCUMENTS_ENTITIES)
const createDocument = current.find(
(command) => "createCollection" in command && command.createCollection === "documents_document",
)
if (!createDocument || !("createCollection" in createDocument))
throw new Error("no createCollection for documents_document")
// A migration that has run is never edited, so a field added later is a collMod of its own:
// 0001's validator is the current one without skipped_pages, and 0002 carries the current one.
const first = mongoMigration("0001_documents.json")
const created = first.find(
(command) => "createCollection" in command && command.createCollection === "documents_document",
)
if (!created || !("createCollection" in created)) throw new Error("0001 does not create documents_document")
expect(JSON.stringify(created.validator)).not.toContain("skipped_pages")
const withoutSkippedPages = structuredClone(createDocument)
const properties = withoutSkippedPages.validator.$jsonSchema.properties as Record<string, unknown>
expect(JSON.stringify(properties)).toContain("skipped_pages")
delete properties.skipped_pages
expect(JSON.stringify(created.validator)).toBe(JSON.stringify(withoutSkippedPages.validator))
// The other collection and every index are exactly the generated ones.
const rest = (commands: MongoCommands) =>
JSON.stringify(
commands.filter(
(command) => !("createCollection" in command && command.createCollection === "documents_document"),
),
)
expect(rest(first)).toBe(rest(current))
const second = mongoMigration("0002_skipped_pages.json")
expect(second).toEqual([{ collMod: "documents_document", validator: createDocument.validator }])
})
})
+341
View File
@@ -0,0 +1,341 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The notes module through the kernel's caller, the files module, the outbox relay's behaviour and
// the worker's job runner: what is proven is the wiring and not a unit. The three things its plan item
// asks for are here by name -- an upload records one usage event and one audit event, the flag
// switches attachments off, and a tenant's notes and files are its own.
import { MAX_ATTACHMENTS, NOTES_CREATED_METER, type NotesFlags, type NoteView } from "@app/modules/notes"
import { AppError, Container } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import { FILES_AUDIT, FILES_SCOPE, FILES_USAGE } from "@conusai-kit/files"
import { uploadViaProcedures } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { bytesOf, makeNotesApp, type NotesApp, type SeededProject } from "./support/notes-app.js"
interface Detail {
note: NoteView
attachments: Array<{ attachmentId: string; fileId: string; filename: string; contentType: string; size: number }>
attachmentsEnabled: boolean
}
async function fresh(flags?: NotesFlags): Promise<{ app: NotesApp; project: SeededProject }> {
const app = makeNotesApp(flags ? { flags } : {})
return { app, project: await app.seedProject("acme", "owner@acme.test") }
}
const run = (project: SeededProject, procedure: string, input: Record<string, unknown> = {}) =>
project.call.callByName(procedure, { ...project.slugs, ...input })
const create = async (project: SeededProject, title = "Kick-off", body = "Agenda") =>
(await run(project, "notes.note.create", { title, body })) as NoteView
const detail = async (project: SeededProject, noteId: string) =>
(await run(project, "notes.note.get", { noteId })) as Detail
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
describe("notes", () => {
it("creates, lists, reads, edits and deletes a note", async () => {
const { project } = await fresh()
const note = await create(project)
expect(note).toMatchObject({ title: "Kick-off", body: "Agenda", version: 1, attachmentCount: 0 })
const listed = (await run(project, "notes.note.list")) as { notes: NoteView[] }
expect(listed.notes.map((entry) => entry.noteId)).toEqual([note.noteId])
const edited = (await run(project, "notes.note.update", {
noteId: note.noteId,
title: "Kick-off call",
body: "Agenda and owners",
expectedVersion: note.version,
})) as NoteView
expect(edited).toMatchObject({ title: "Kick-off call", version: 2 })
expect((await detail(project, note.noteId)).note.body).toBe("Agenda and owners")
await run(project, "notes.note.delete", { noteId: note.noteId })
expect(((await run(project, "notes.note.list")) as { notes: NoteView[] }).notes).toEqual([])
expect((await refusal(run(project, "notes.note.get", { noteId: note.noteId }))).status).toBe(404)
})
it("refuses a second editor's stale save with 409, so nothing is overwritten silently", async () => {
const { project } = await fresh()
const note = await create(project)
await run(project, "notes.note.update", { noteId: note.noteId, title: "A", body: "", expectedVersion: 1 })
const stale = await refusal(
run(project, "notes.note.update", { noteId: note.noteId, title: "B", body: "", expectedVersion: 1 }),
)
expect(stale.status).toBe(409)
expect((await detail(project, note.noteId)).note.title).toBe("A")
})
it("lets a viewer read and nothing else, and answers a write as the kernel answers a stranger: 404", async () => {
const { app, project } = await fresh()
const note = await create(project)
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
const asViewer = (procedure: string, input: Record<string, unknown>) =>
viewer.call.callByName(procedure, { ...project.slugs, ...input })
expect(((await asViewer("notes.note.list", {})) as { notes: unknown[] }).notes).toHaveLength(1)
expect((await refusal(asViewer("notes.note.create", { title: "x", body: "" }))).status).toBe(404)
expect(
(await refusal(asViewer("notes.note.update", { noteId: note.noteId, title: "x", body: "", expectedVersion: 1 })))
.status,
).toBe(404)
expect((await refusal(asViewer("notes.note.delete", { noteId: note.noteId }))).status).toBe(404)
})
})
describe("what a note records, exactly once", () => {
it("writes one audit event and one usage fact per note created, and none of its text", async () => {
const { app, project } = await fresh()
const note = await create(project, "Secret plan", "Body text")
expect(app.audit.events).toEqual([])
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toEqual(["notes.note_created"])
expect(app.audit.events[0]).toMatchObject({
targetId: `${project.orgId}/${note.noteId}`,
actorId: project.owner.userId,
})
expect(JSON.stringify(app.audit.events)).not.toContain("Secret plan")
expect(JSON.stringify(app.audit.events)).not.toContain("Body text")
expect(app.usage.facts).toEqual([
{
meter: NOTES_CREATED_METER,
quantity: 1,
customer: project.orgId,
idempotencyKey: `${NOTES_CREATED_METER}.${note.noteId}`,
},
])
// The record is published: a second pass of the relay delivers nothing more.
expect(await app.deliver()).toBe(0)
})
it("gives a redelivered record the same audit event id and usage key, so the platform sees one", async () => {
const { app, project } = await fresh()
await create(project)
const record = (await app.data.outbox.pending(10))[0]
expect(record).toBeDefined()
const subscriber = app.registry.events["notes.NoteCreated"]?.subscribers[0]
const container = new Container()
container.value(FILES_SCOPE, { level: "org", actor: systemActor("app.relay"), orgId: record?.orgId ?? "" })
container.value(FILES_AUDIT, app.audit)
container.value(FILES_USAGE, app.usage)
const deliverOnce = () =>
subscriber?.handler(
{ name: "notes.NoteCreated", payload: record?.payload },
{ container, correlationId: record?.id ?? "" },
)
await deliverOnce()
await deliverOnce()
expect(app.audit.events).toHaveLength(2)
expect(app.audit.events[0]?.eventId).toBeTruthy()
expect(app.audit.events[0]?.eventId).toBe(app.audit.events[1]?.eventId)
expect(app.usage.facts[0]?.idempotencyKey).toBe(app.usage.facts[1]?.idempotencyKey)
})
it("audits an edit once per version, so two edits are two events", async () => {
const { app, project } = await fresh()
const note = await create(project)
await run(project, "notes.note.update", { noteId: note.noteId, title: "A", body: "", expectedVersion: 1 })
await run(project, "notes.note.update", { noteId: note.noteId, title: "B", body: "", expectedVersion: 2 })
await app.deliver()
const edits = app.audit.events.filter((event) => event.action === "notes.note_updated")
expect(edits).toHaveLength(2)
expect(new Set(edits.map((event) => event.eventId)).size).toBe(2)
})
})
describe("attachments", () => {
it("records one usage event and one audit event for an upload, and one more audit event for the link", async () => {
const { app, project } = await fresh()
const note = await create(project)
await app.deliver()
app.audit.events.length = 0
app.usage.facts.length = 0
const stored = await app.upload(project, "minutes.txt", "agreed on Friday")
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toEqual(["files.file_uploaded"])
expect(app.usage.facts).toEqual([
expect.objectContaining({ meter: "files_bytes_stored", quantity: stored.size, customer: project.orgId }),
])
const attached = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
filename: string
}
expect(attached.filename).toBe("minutes.txt")
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toEqual(["files.file_uploaded", "notes.attachment_added"])
expect(app.usage.facts).toHaveLength(1)
const seen = await detail(project, note.noteId)
expect(seen.attachments).toEqual([
expect.objectContaining({ fileId: stored.fileId, filename: "minutes.txt", contentType: "text/plain" }),
])
expect(seen.note.attachmentCount).toBe(1)
})
it("links a file once: attaching it again answers with the first link and records nothing more", async () => {
const { app, project } = await fresh()
const note = await create(project)
const stored = await app.upload(project)
const first = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
const second = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
expect(second.attachmentId).toBe(first.attachmentId)
await app.deliver()
expect(app.audit.events.filter((event) => event.action === "notes.attachment_added")).toHaveLength(1)
})
it("refuses a file that is not this project's, and one that does not exist, alike", async () => {
const { app, project } = await fresh()
const note = await create(project)
const other = await app.seedProject("globex", "owner@globex.test")
const foreign = await app.upload(other)
expect(
(await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: foreign.fileId }))).status,
).toBe(404)
expect(
(
await refusal(
run(project, "notes.attachment.add", { noteId: note.noteId, fileId: "00000000-0000-0000-0000-000000000000" }),
)
).status,
).toBe(404)
})
it(`allows at most ${MAX_ATTACHMENTS} attachments to one note`, async () => {
const { app, project } = await fresh()
const note = await create(project)
for (let at = 0; at < MAX_ATTACHMENTS; at += 1) {
const stored = await app.upload(project, `f${at}.txt`, `body ${at}`)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
}
const extra = await app.upload(project, "extra.txt", "one too many")
expect(
(await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: extra.fileId }))).status,
).toBe(409)
})
it("refuses a file type a note does not carry, though the files module stores it", async () => {
const { project } = await fresh()
const note = await create(project)
const sheet = await uploadViaProcedures(project.call, project.slugs, {
filename: "plan.csv",
contentType: "text/csv",
bytes: bytesOf("a,b"),
})
const refused = await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: sheet.fileId }))
expect(refused.status).toBe(422)
expect((await detail(project, note.noteId)).attachments).toEqual([])
})
it("removes one link, audits it, and leaves the file in the project", async () => {
const { app, project } = await fresh()
const note = await create(project)
const stored = await app.upload(project)
const link = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
await run(project, "notes.attachment.remove", { noteId: note.noteId, attachmentId: link.attachmentId })
expect((await detail(project, note.noteId)).attachments).toEqual([])
expect(((await run(project, "files.file.list")) as { files: unknown[] }).files).toHaveLength(1)
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toContain("notes.attachment_removed")
})
it("does not remove another note's attachment through this note", async () => {
const { app, project } = await fresh()
const first = await create(project, "First")
const second = await create(project, "Second")
const stored = await app.upload(project)
const link = (await run(project, "notes.attachment.add", { noteId: first.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
expect(
(
await refusal(
run(project, "notes.attachment.remove", { noteId: second.noteId, attachmentId: link.attachmentId }),
)
).status,
).toBe(404)
expect((await detail(project, first.noteId)).attachments).toHaveLength(1)
})
it("deleting a note removes its links, audits how many, and keeps the files", async () => {
const { app, project } = await fresh()
const note = await create(project)
for (const name of ["a.txt", "b.txt"]) {
const stored = await app.upload(project, name, name)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
}
await run(project, "notes.note.delete", { noteId: note.noteId })
await app.deliver()
const deleted = app.audit.events.find((event) => event.action === "notes.note_deleted")
expect(deleted?.data).toMatchObject({ attachmentCount: 2 })
expect(((await run(project, "files.file.list")) as { files: unknown[] }).files).toHaveLength(2)
})
it("drops a deleted file's links when the files module has deleted the object", async () => {
const { app, project } = await fresh()
const note = await create(project)
const stored = await app.upload(project)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
await run(project, "files.file.delete", { fileId: stored.fileId })
// The marking transaction, then the job that deletes the object and publishes `files.FileDeleted`.
await app.deliver()
await app.runJobs()
await app.deliver()
const seen = await detail(project, note.noteId)
expect(seen.attachments).toEqual([])
expect(seen.note.attachmentCount).toBe(0)
})
it("is switched off by the notes.attachments flag: the link is refused and the page is told", async () => {
const off: NotesFlags = { enabled: () => false }
const { app, project } = await fresh(off)
const note = await create(project)
const stored = await app.upload(project)
expect(
(await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId }))).status,
).toBe(403)
expect((await detail(project, note.noteId)).attachmentsEnabled).toBe(false)
})
it("is on by default, with no flag service bound", async () => {
const { project } = await fresh()
const note = await create(project)
expect((await detail(project, note.noteId)).attachmentsEnabled).toBe(true)
})
})
describe("tenants", () => {
it("never shows one organization's notes or attachments to another", async () => {
const { app, project } = await fresh()
const other = await app.seedProject("globex", "owner@globex.test")
const note = await create(project)
const stored = await app.upload(project)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
expect(((await run(other, "notes.note.list")) as { notes: unknown[] }).notes).toEqual([])
expect((await refusal(run(other, "notes.note.get", { noteId: note.noteId }))).status).toBe(404)
// The other tenant's own slugs with this tenant's ids: the same 404 as an id nobody has.
expect(
(await refusal(run(other, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId }))).status,
).toBe(404)
// This tenant's caller naming the other organization's slugs is refused outright.
const crossed = await refusal(project.call.callByName("notes.note.list", { ...other.slugs }))
expect([403, 404]).toContain(crossed.status)
})
})
+292
View File
@@ -0,0 +1,292 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Observability of the server processes (spec §9.5, plan item AT4.2): errors, traces and logs
// configured from the deployment contract, carrying the request's trace id, with secrets and tokens
// taken out before anything leaves the process, and a plain no-op with a clear log line when the
// deployment has no DSN. The SDK is the real @conusai-sdk/observe-node; only the network is a
// recording `fetch`.
import {
createRedactor,
FILTERED,
loadAppEnv,
observabilityConfig,
observabilityLine,
secretsOf,
startObservability,
} from "@app/config"
import { ids, withRequest } from "@conusai-sdk/observe-node"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
const TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736"
const TRACEPARENT = `00-${TRACE_ID}-00f067aa0ba902b7-01`
const SECRET = "s3cr3t-session-key-0123456789abcdef"
const TOKEN = "dt_0123456789abcdef0123456789abcdef"
const base: Record<string, string> = {
NODE_ENV: "test",
CONUSAI_FAKE_PLATFORM: "1",
CONUSAI_API_URL: "http://127.0.0.1:4010/api",
CONUSAI_API_TOKEN: TOKEN,
CONUSAI_PROJECT_ID: "42",
CONUSAI_ORG_ID: "9",
POSTGRES_URL: "postgresql://app_app:hunter2hunter2@db:5432/app",
POSTGRES_READER_URL: "postgresql://app_reader:another-long-pass@db:5432/app",
AUTH_SESSION_SECRET: SECRET,
}
const appEnv = (extra: Record<string, string> = {}, service: "web" | "worker" = "web") =>
loadAppEnv(service, { ...base, ...extra })
describe("the observability settings", () => {
it("reads release, environment and service name from the deployment contract", () => {
const env = appEnv({
CONUSAI_DSN: "https://pub@errors.example.test/42",
CONUSAI_RELEASE: "2026.10.1",
CONUSAI_ENVIRONMENT: "preview",
OTEL_SERVICE_NAME: "acme-review",
OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.test",
})
expect(observabilityConfig("web", env.deployment, env.production)).toMatchObject({
errors: true,
otlp: true,
release: "2026.10.1",
environment: "preview",
serviceName: "acme-review",
})
})
it("falls back to OTEL_SERVICE_VERSION, then to `dev`, and names the run mode when the platform names no environment", () => {
const platformRelease = appEnv({ OTEL_SERVICE_VERSION: "abc1234" })
expect(observabilityConfig("worker", platformRelease.deployment, false)).toMatchObject({
release: "abc1234",
environment: "development",
serviceName: "app-worker",
})
expect(observabilityConfig("web", appEnv().deployment, true)).toMatchObject({
release: "dev",
environment: "production",
})
})
it("says in one line what a process without a DSN will not send", () => {
const env = appEnv()
const line = observabilityLine(observabilityConfig("web", env.deployment, false))
expect(line).toContain("errors off (CONUSAI_DSN is not set)")
expect(line).toContain("logs and traces off (OTEL_EXPORTER_OTLP_ENDPOINT is not set)")
})
})
describe("redaction", () => {
const redact = createRedactor(secretsOf({ ...base }))
it.each([
["MONGODB_URL", "mongodb://app:encoded%2Fpassword-long@db:27017/app", "encoded/password-long"],
["CONVEX_ADMIN_KEY", "opaque-convex-administrator-value", "opaque-convex-administrator-value"],
["CONVEX_SELF_HOSTED_ADMIN_KEY", "opaque-self-hosted-convex-admin-value", "opaque-self-hosted-convex-admin-value"],
])("removes %s credentials from profile driver errors", (name, value, credential) => {
const profileRedactor = createRedactor(secretsOf({ [name]: value }))
expect(profileRedactor.text(`driver rejected ${credential}`)).toBe(`driver rejected ${FILTERED}`)
expect(profileRedactor.deep({ exception: { value: `driver rejected ${credential}` } })).toEqual({
exception: { value: `driver rejected ${FILTERED}` },
})
})
it("takes the process's own secret values out of any string, wherever they appear", () => {
expect(redact.text(`session ${SECRET} and token ${TOKEN}`)).toBe(`session ${FILTERED} and token ${FILTERED}`)
// A connection string's password goes on its own, too, as the driver quotes it in an error.
expect(redact.text("password authentication failed: hunter2hunter2")).toBe(
`password authentication failed: ${FILTERED}`,
)
})
it.each([
["a bearer credential", "sent Authorization: Bearer abcdef0123456789xyz", "abcdef0123456789xyz"],
["a credential in a URL", "connect postgresql://user:p4ssw0rd-long@db:5432/app", "p4ssw0rd-long"],
["a JSON web token", "id eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJl", "eyJzdWIi"],
["an organization API key", "key cak_ab12cd34_0123456789abcdefghij", "0123456789abcdefghij"],
["an invitation link", "GET /invitations/acme/Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA", "Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA"],
[
"an invitation link inside a return_to query",
"GET /auth/sign-in?return_to=%2Finvitations%2Facme%2FZm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA",
"Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA",
],
["an authorization code in a query", "GET /auth/callback?code=abc123&state=xyz789", "abc123"],
["an e-mail address", "invited ada@example.test", "ada@example.test"],
])("removes %s", (_name, text, secret) => {
const cleaned = redact.text(text)
expect(cleaned).not.toContain(secret)
expect(cleaned.length).toBeGreaterThan(0)
})
it("keeps what is not a secret: the organization in an invitation link, the path, the words", () => {
expect(redact.text("GET /invitations/acme/Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA failed")).toContain("/invitations/acme/")
expect(redact.text("procedure notes.note.create failed")).toBe("procedure notes.note.create failed")
})
it("filters a value under a key that names a secret, at any depth, and strings inside arrays", () => {
const cleaned = redact.deep({
headers: { authorization: "Bearer x", "x-csrf-token": "t", accept: "text/html" },
nested: { api_key: "k", session: { id: "s" }, list: [{ password: "p" }, "mail bob@example.test"] },
count: 3,
})
expect(cleaned).toEqual({
headers: { authorization: FILTERED, "x-csrf-token": FILTERED, accept: "text/html" },
nested: { api_key: FILTERED, session: FILTERED, list: [{ password: FILTERED }, "mail [email]"] },
count: 3,
})
})
it("survives a value that points at itself", () => {
const loop: Record<string, unknown> = { name: "loop" }
loop.self = loop
expect(redact.deep(loop)).toEqual({ name: "loop", self: FILTERED })
})
it("scrubs an error event but leaves the ids that join it to its request", () => {
const event = redact.event({
message: `failed for ${SECRET}`,
request: { url: "https://app.example.test/auth/callback?code=abc&state=def", headers: { cookie: "s=1" } },
contexts: {
trace: { trace_id: TRACE_ID, span_id: "00f067aa0ba902b7" },
conusai: { session_id: "8f6a1c0e-2b8d-4a43-9d6e-5d1f6c2b7a90", request_id: "req-1" },
runtime: { token: "x" },
},
exception: { values: [{ type: "Error", value: "bad Bearer abcdef0123456789xyz" }] },
})
expect(JSON.stringify(event)).not.toContain(SECRET)
expect(event.request?.url).toBe(`https://app.example.test/auth/callback?code=${FILTERED}&state=${FILTERED}`)
expect(event.request?.headers?.cookie).toBe(FILTERED)
expect(event.exception?.values?.[0]?.value).toBe(`bad Bearer ${FILTERED}`)
expect(event.contexts?.trace).toEqual({ trace_id: TRACE_ID, span_id: "00f067aa0ba902b7" })
expect(event.contexts?.conusai).toMatchObject({ request_id: "req-1" })
expect(event.contexts?.runtime).toEqual({ token: FILTERED })
})
})
describe("startObservability", () => {
const handlers = new Map<string, unknown[]>()
beforeEach(() => {
// Record the process handlers the SDK installs instead of installing them in the test worker.
vi.spyOn(process, "on").mockImplementation(((event: string, fn: unknown) => {
handlers.set(event, [...(handlers.get(event) ?? []), fn])
return process
}) as never)
vi.spyOn(process, "off").mockImplementation((() => process) as never)
})
afterEach(() => {
handlers.clear()
vi.restoreAllMocks()
})
it("is a no-op with one clear log line when the deployment has no DSN", async () => {
const lines: string[] = []
const observe = startObservability(appEnv(), { env: base, out: (line) => lines.push(line) })
expect(observe.errors).toBeUndefined()
expect(observe.otlp).toBeUndefined()
observe.captureException(new Error("nobody listens"))
await observe.shutdown()
expect(lines).toHaveLength(1)
expect(JSON.parse(lines[0] as string)).toMatchObject({
level: "info",
service: "app-web",
message: expect.stringContaining("errors off (CONUSAI_DSN is not set)"),
})
expect(handlers.size).toBe(0)
})
it("sends an error with the request's trace and request ids, and no secret", async () => {
const sent: Array<{ url: string; body: string }> = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
const body = init?.body
const text = typeof body === "string" ? body : Buffer.from(body as Uint8Array).toString("utf8")
sent.push({ url: String(url), body: text })
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(
appEnv({ CONUSAI_DSN: "https://pub123@errors.example.test/42", CONUSAI_RELEASE: "r-1" }),
{ env: base, out: () => undefined, fetch: recording },
)
expect(observe.errors).toBeDefined()
withRequest(new Headers({ traceparent: TRACEPARENT, "x-request-id": "req-77" }), () => {
observe.captureException(new Error(`db down at postgresql://app_app:hunter2hunter2@db:5432/app with ${TOKEN}`), {
procedure: "notes.note.create",
})
})
await observe.errors?.flush(2_000)
const [request] = sent
expect(request?.url).toContain("https://errors.example.test/api/42/envelope/")
expect(request?.body).toContain(TRACE_ID)
expect(request?.body).toContain("req-77")
expect(request?.body).toContain('"release":"r-1"')
expect(request?.body).not.toContain("hunter2hunter2")
expect(request?.body).not.toContain(TOKEN)
expect(request?.body).toContain(FILTERED)
await observe.errors?.close()
})
it("sends a log line over OTLP with the request's trace id, redacted, and prints it as a JSON line", async () => {
const posts: Array<{ url: string; body: string }> = []
const lines: string[] = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
posts.push({ url: String(url), body: String(init?.body) })
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(
appEnv({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4010/otel", OTEL_SERVICE_NAME: "acme-review" }),
{ env: base, out: (line) => lines.push(line), fetch: recording },
)
expect(observe.otlp?.enabled).toBe(true)
withRequest(new Headers({ traceparent: TRACEPARENT, "x-request-id": "req-78" }), () => {
expect(ids().trace_id).toBe(TRACE_ID)
observe.log.warn(`token ${TOKEN} refused`, { "conusai.org_slug": "acme", password: "p" })
})
await observe.otlp?.flush(2_000)
const logs = posts.filter((post) => post.url.endsWith("/otel/v1/logs"))
expect(logs.length).toBeGreaterThan(0)
const body = logs.map((post) => post.body).join("\n")
expect(body).toContain(TRACE_ID)
expect(body).toContain("acme-review")
expect(body).not.toContain(TOKEN)
expect(body).not.toContain('"p"')
const printed = lines
.map((line) => JSON.parse(line) as Record<string, string>)
.find((line) => line.level === "warn")
expect(printed).toMatchObject({ trace_id: TRACE_ID, request_id: "req-78", "conusai.org_slug": "acme" })
expect(JSON.stringify(printed)).not.toContain(TOKEN)
expect(printed?.password).toBe(FILTERED)
await observe.shutdown()
})
it("runs a function as a span under the request's trace and returns what it returns", async () => {
const posts: string[] = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
if (String(url).endsWith("/otel/v1/traces")) posts.push(String(init?.body))
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(appEnv({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4010/otel" }), {
env: base,
out: () => undefined,
fetch: recording,
})
const answer = withRequest(new Headers({ traceparent: TRACEPARENT }), () =>
observe.span("procedure notes.note.create", { "conusai.org_slug": "acme" }, () => 42),
)
expect(answer).toBe(42)
await observe.otlp?.flush(2_000)
const body = posts.join("\n")
expect(body).toContain("procedure notes.note.create")
expect(body).toContain(TRACE_ID)
expect(body).toContain("conusai.org_slug")
await observe.shutdown()
})
it("runs a function as itself when traces are off", () => {
const observe = startObservability(appEnv(), { env: base, out: () => undefined })
expect(observe.span("anything", {}, () => "plain")).toBe("plain")
})
})
+69
View File
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The browser half of observability (plan item AT4.2): `instrumentation-client.ts` starts
// @conusai-sdk/observe only when the server wrote the settings into the document, with the options this
// app chose, and an error boundary reports through the same instance. The SDK is the real one with
// the recording transport of @conusai-sdk/observe-testing, so what is asserted is what would be sent.
import { createTestObserve } from "@conusai-sdk/observe-testing"
import { describe, expect, it, vi } from "vitest"
import {
browserObserveOptions,
observeMeta,
readBrowserSettings,
reportClientError,
startBrowserObserve,
} from "../apps/web/src/observe/browser.js"
/** A document with the meta element the server writes, as far as the module reads one. */
const documentWith = (attributes: Record<string, string> | undefined) => ({
querySelector: (selector: string) =>
attributes && selector === 'meta[name="app-observe"]'
? { getAttribute: (name: string) => attributes[name] ?? null }
: null,
})
describe("the settings the server writes into the document", () => {
it("round-trips through the meta element", () => {
const settings = { release: "2026.10.1", environment: "preview" }
expect(readBrowserSettings(documentWith(observeMeta(settings)) as never)).toEqual(settings)
})
it("reads nothing from a document without the element, or with it off", () => {
expect(readBrowserSettings(documentWith(undefined) as never)).toBeUndefined()
expect(readBrowserSettings(documentWith({ content: "off" }) as never)).toBeUndefined()
})
it("names neither ConusAI nor a build-time variable: the element is the app's own", () => {
expect(JSON.stringify(observeMeta({ release: "r", environment: "e" })).toLowerCase()).not.toContain("conusai")
})
})
describe("starting the browser SDK", () => {
it("starts nothing when the deployment has no observability", () => {
const start = vi.fn()
expect(startBrowserObserve(documentWith(undefined) as never, start)).toBeUndefined()
expect(start).not.toHaveBeenCalled()
})
it("starts the SDK with the release and environment the server wrote, anonymously, errors and traces only", () => {
const start = vi.fn((options) => createTestObserve({ ...options, modules: { errors: false } }))
startBrowserObserve(documentWith(observeMeta({ release: "r-9", environment: "production" })) as never, start)
expect(start).toHaveBeenCalledWith(
expect.objectContaining({ release: "r-9", environment: "production", consent: "anonymous" }),
)
const options = start.mock.calls[0]?.[0]
expect(options?.modules).toEqual({ traces: true })
expect(options?.modules).not.toHaveProperty("analytics")
expect(options?.modules).not.toHaveProperty("replay")
expect(browserObserveOptions({ release: "r-9", environment: "production" })).toEqual(options)
})
it("sends an error a boundary caught, through the instance the page started", () => {
const instance = createTestObserve({ modules: { errors: false } })
startBrowserObserve(documentWith(observeMeta({ release: "r", environment: "e" })) as never, () => instance)
reportClientError(new Error("the page threw"))
expect(instance.mock.kinds()).toContain("error")
const [item] = instance.mock.items()
expect(JSON.stringify(item)).toContain("the page threw")
})
})
+104
View File
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The OpenAPI 3.1 document and its drift check (AT2.1 done_when).
//
// `openapi.json` in the repository root is this function's output. Regenerate it with
// `pnpm openapi` after changing a procedure's schemas; the diff is then part of the review, which
// is the point -- an input schema that changed is an API change somebody's client will feel.
import { appBrand } from "@app/config"
import { buildRegistry } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import { openapiDocument, openapiJson } from "../apps/web/src/server/openapi"
import { appModules } from "../modules/index.js"
const registry = buildRegistry([...appModules])
// The title is the app brand's name, as the route serves it (router.ts): renaming the app in
// config/brand.ts is a customer's first edit and must not turn the drift check red (AT2.7).
const info = { title: `${appBrand.name} API`, version: registry.kit }
describe("the OpenAPI document", () => {
it("matches the committed openapi.json", async () => {
await expect(openapiJson(registry, info)).toMatchFileSnapshot("../openapi.json")
})
it("is OpenAPI 3.1 with one path per procedure", () => {
const document = openapiDocument(registry, info) as { openapi: string; paths: Record<string, unknown> }
expect(document.openapi).toBe("3.1.0")
expect(Object.keys(document.paths).length).toBe(registry.procedures.size)
expect(document.paths["/api/v1/tenancy.org.create"]).toBeTruthy()
})
it("describes every refusal as problem+json", () => {
const document = openapiDocument(registry, info) as {
paths: Record<string, { post: { responses: Record<string, { content?: Record<string, unknown> }> } }>
}
const operation = document.paths["/api/v1/tenancy.member.invite"]?.post
expect(Object.keys(operation?.responses ?? {})).toEqual(expect.arrayContaining(["200", "401", "404", "422", "429"]))
expect(operation?.responses["404"]?.content).toHaveProperty(["application/problem+json"])
})
it("requires an Idempotency-Key exactly where the contract declares one", () => {
type Operation = { parameters: { name: string; required?: boolean }[] }
const document = openapiDocument(registry, info) as {
paths: Record<string, { post?: Operation; put?: Operation }>
}
for (const [name, { def }] of registry.procedures) {
const path = document.paths[`/api/v1/${name}`]
const parameters = (path?.post ?? path?.put)?.parameters ?? []
const header = parameters.find((parameter) => parameter.name === "Idempotency-Key")
expect(Boolean(header), `${name} is idempotent=${def.idempotent}`).toBe(def.idempotent)
}
})
it("documents a procedure with a streamed body as a PUT: the bytes as the body, the rest of its input as the query", () => {
const document = openapiDocument(registry, info) as {
paths: Record<
string,
{
post?: unknown
put?: {
parameters: { name: string; in: string; required: boolean }[]
requestBody: { content: Record<string, { schema: unknown }> }
}
}
>
}
const streamed = [...registry.procedures].filter(([, { def }]) => def.stream !== undefined).map(([name]) => name)
const expected = ["files.file.putPart"]
expected.push("guides.runtime.uploadFile") // kit:guides -- a survey file question's file
expect(streamed.sort()).toEqual(expected)
const path = document.paths["/api/v1/files.file.putPart"]
expect(path?.post).toBeUndefined()
const query = path?.put?.parameters.filter((parameter) => parameter.in === "query")
expect(query?.map((parameter) => parameter.name).sort()).toEqual(["fileId", "org", "part", "project", "workspace"])
expect(query?.every((parameter) => parameter.required)).toBe(true)
// The body is the one field that is not a parameter, and it is binary.
expect(query?.map((parameter) => parameter.name)).not.toContain("body")
expect(path?.put?.requestBody.content["application/octet-stream"]?.schema).toEqual({
type: "string",
format: "binary",
})
})
it("documents file downloads as authenticated scoped GET streams rather than JSON or signed URLs", () => {
type DownloadOperation = {
parameters: { name: string; in: string; required: boolean }[]
responses: Record<string, { content?: Record<string, { schema?: unknown }> }>
}
const document = openapiDocument(registry, info) as {
paths: Record<string, { get?: DownloadOperation; post?: unknown }>
}
const path = document.paths["/api/v1/files.file.download"]
expect(path?.post).toBeUndefined()
expect(path?.get?.parameters.map(({ name, in: location, required }) => [name, location, required])).toEqual([
["org", "query", true],
["workspace", "query", true],
["project", "query", true],
["fileId", "query", true],
])
expect(path?.get?.responses["200"]?.content).toHaveProperty(
["application/octet-stream", "schema", "format"],
"binary",
)
})
})
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The pages a person sees when there is no page (spec §10.4): the error boundary, the loading state, and
// the shared shape of not found and forbidden. Rendered to markup, so what is asserted is what a screen
// reader is given: one heading, a sentence, an alert for the failure, and the way out. The browser half
// (a 404 and a 403 as statuses, axe on both) is e2e/shell.spec.ts.
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { describe, expect, it, vi } from "vitest"
import { BoundaryMessages, ErrorView } from "../apps/web/src/ui/boundary.js"
import { LoadingView, StateView } from "../apps/web/src/ui/state-view.js"
const english = {
title: "Something went wrong",
body: "The page could not be shown. Try again.",
code: "Error code: {digest}",
retry: "Try again",
home: "Back to the start",
}
const render = (error: Error & { digest?: string }, retry = vi.fn()): string =>
renderToStaticMarkup(
createElement(BoundaryMessages, { strings: english, children: createElement(ErrorView, { error, retry }) }),
)
describe("the error boundary", () => {
it("has one heading, the failure as an alert, a way to try again and a way out", () => {
const html = render(new Error("hidden detail"))
expect(html.match(/<h1/g)).toHaveLength(1)
expect(html).toContain("<h1>Something went wrong</h1>")
expect(html).toContain('role="alert">The page could not be shown. Try again.')
expect(html).toContain("<button")
expect(html).toContain("Try again</button>")
expect(html).toContain('<a href="/">Back to the start</a>')
})
it("never prints the error's own message, which can carry a tenant's data, and prints the digest an operator searches the log for", () => {
const html = render(Object.assign(new Error("password=hunter2 for ada@example.test"), { digest: "3275165072" }))
expect(html).not.toContain("hunter2")
expect(html).not.toContain("ada@example.test")
expect(html).toContain("Error code: 3275165072")
})
it("prints no code when there is no digest", () => {
expect(render(new Error("x"))).not.toContain("Error code")
})
it("hands the error up to the boundary above when nothing gave it words", () => {
expect(() => renderToStaticMarkup(createElement(ErrorView, { error: new Error("up"), retry: vi.fn() }))).toThrow(
"up",
)
})
})
describe("the loading and the empty states", () => {
it("announce once, politely, and hide the spinner from a screen reader", () => {
const html = renderToStaticMarkup(createElement(LoadingView, { label: "Loading" }))
expect(html).toContain('role="status"')
expect(html).toContain('<span class="app-spinner" aria-hidden="true"></span>')
expect(html).toContain("<span>Loading</span>")
expect(html).not.toContain("<h1")
})
it("give a state page a landmark, a heading, its words and its actions, in that order", () => {
const html = renderToStaticMarkup(
createElement(StateView, {
title: "Page not found",
actions: createElement("a", { href: "/" }, "Back"),
children: createElement("p", null, "Nothing here."),
}),
)
expect(html).toBe(
'<main class="app-state"><h1>Page not found</h1><p>Nothing here.</p><p><a href="/">Back</a></p></main>',
)
})
})
+339
View File
@@ -0,0 +1,339 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The platform's services are really bound (plan item AT4.1, spec §9): the AI gateway, OCR, Blob, the
// audit log and usage metering, built from the deployment contract by `platformClients`, bound to the
// tokens modules resolve them by in BOTH composition roots (apps/web/src/server/ports.ts and
// apps/worker/src/ports.ts), and each one reaching the platform over HTTP. The platform here is the
// fake of @conusai-kit/testing; nothing between a token and its HTTP request is stubbed.
import { readFileSync } from "node:fs"
import { loadAppEnv, PlatformEnvError, platformClients, probePlatform, publicUrlOf } from "@app/config"
import { appModules } from "@app/modules"
import { DOCUMENT_MODEL, DOCUMENT_OCR, MODEL_GATEWAY } from "@app/modules/documents"
import { buildRegistry, Container } from "@conusai-kit/core"
import { OBJECT_STORE } from "@conusai-kit/data"
import { FILES_AUDIT, FILES_REGISTRY, FILES_USAGE } from "@conusai-kit/files"
import { PROPOSALS_AUDIT, PROPOSALS_REGISTRY } from "@conusai-kit/proposals"
import { startFakePlatform } from "@conusai-kit/testing/fake-platform"
import { afterEach, beforeEach, describe, expect, it } from "vitest"
import { z } from "zod"
import { bindWebPorts } from "../apps/web/src/server/ports.js"
import { bindModulePorts } from "../apps/worker/src/ports.js"
const fixture = JSON.parse(
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
) as Record<string, unknown>
type Platform = Awaited<ReturnType<typeof startFakePlatform>>
let platform: Platform
beforeEach(async () => {
platform = await startFakePlatform()
})
afterEach(async () => {
await platform.close()
})
const environment = (extra: Record<string, string> = {}): Record<string, string | undefined> => ({
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",
...extra,
})
const setup = (service: "web" | "worker") => {
const env = loadAppEnv(service, environment())
const registry = buildRegistry([...appModules])
const clients = platformClients(env, registry, environment())
return { env, registry, clients }
}
/**
* What the fake platform recorded for one organization of the platform. It keeps every tenant apart,
* as the real one does: usage arrives under the organization in its URL, everything else under the
* default one.
*/
const stateOf = async (organization?: string) =>
(await (
await fetch(
`${platform.url}/__e2e/state`,
organization ? { headers: { "x-conusai-organization": organization } } : {},
)
).json()) as {
audit: Array<{ action: string; resource_id: string | null }>
usage: Array<Record<string, unknown>>
blobs: Array<{ pathname: string; size: number }>
ocr: unknown[]
ai: Array<{ request?: { model?: string } }>
}
describe("platformClients", () => {
it("writes an audit event to the platform's audit log", async () => {
const { clients } = setup("web")
await clients.audit.log("tenancy.organization_created", {
actor: { id: "user-1", kind: "human" },
target: { id: "acme", type: "organization" },
outcome: "success",
})
await clients.audit.flush()
expect((await stateOf()).audit.map((event) => event.action)).toContain("tenancy.organization_created")
})
it("meters usage against the ConusAI organization that owns the deployment, not against a customer", async () => {
const { clients } = setup("web")
clients.usage.track({
customer: { externalId: "acme" },
meter: "notes_created",
quantity: 1,
idempotencyKey: "n-1",
})
await clients.usage.flush()
// The fake separates numeric ID from slug, matching the production billing route contract.
const [fact] = (await stateOf("owner-org")).usage
expect((await stateOf()).usage).toEqual([])
expect(fact).toMatchObject({ meter: "notes_created", quantity: "1", idempotency_key: "n-1" })
})
it("stores an object in Blob and reads its bytes back", async () => {
const { clients } = setup("worker")
const stored = await clients.objectStore.put("files/a.txt", "hello", { contentType: "text/plain" })
expect((await stateOf()).blobs.map((blob) => blob.pathname)).toContain("files/a.txt")
expect(await (await clients.objectStore.download(stored.url)).text()).toBe("hello")
})
it("asks the AI gateway for a validated object, naming the model the app is configured with", async () => {
platform.ai.respond(() => ({ content: JSON.stringify({ answer: "four" }) }))
const { clients } = setup("worker")
const reply = await clients.models.generateObject(z.object({ answer: z.string() }), [
{ role: "user", content: "2 + 2?" },
])
expect(reply).toEqual({ answer: "four" })
const [call] = (await stateOf()).ai
expect(call?.request?.model).toBe(clients.model)
})
it("reads a document through the OCR service", async () => {
platform.ocr.setDocument(fixture)
const { clients } = setup("worker")
const job = await clients.documents.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-1",
})
const read = await clients.documents.wait(job, { intervalMs: 10 })
expect(read.pages).toHaveLength((fixture as { pages: unknown[] }).pages.length)
expect((await stateOf()).ocr).toHaveLength(1)
})
})
describe("what each process binds", () => {
const resolvable = (container: Container, tokens: Array<Parameters<Container["resolve"]>[0]>): boolean[] =>
tokens.map((token) => {
try {
container.resolve(token)
return true
} catch {
return false
}
})
it("binds Blob, the AI gateway, OCR and the registries in the web process", () => {
const { registry, clients } = setup("web")
const container = new Container()
bindWebPorts(container, clients, registry)
expect(
resolvable(container, [
OBJECT_STORE,
MODEL_GATEWAY,
DOCUMENT_MODEL,
DOCUMENT_OCR,
FILES_REGISTRY,
PROPOSALS_REGISTRY,
]),
).toEqual([true, true, true, true, true, true])
// The sinks that belong to the outbox are the worker's alone.
expect(resolvable(container, [FILES_AUDIT, FILES_USAGE, PROPOSALS_AUDIT])).toEqual([false, false, false])
})
it("binds the same five and the audit and usage sinks in the worker process", () => {
const { registry, clients } = setup("worker")
const container = new Container()
bindModulePorts(container, clients, registry)
expect(
resolvable(container, [
OBJECT_STORE,
MODEL_GATEWAY,
DOCUMENT_MODEL,
DOCUMENT_OCR,
FILES_REGISTRY,
PROPOSALS_REGISTRY,
FILES_AUDIT,
FILES_USAGE,
PROPOSALS_AUDIT,
]),
).toEqual([true, true, true, true, true, true, true, true, true])
})
it("sends the worker's usage and audit sinks to the platform, through the bound tokens", async () => {
const { registry, clients } = setup("worker")
const container = new Container()
bindModulePorts(container, clients, registry)
const usage = container.resolve(FILES_USAGE)
await usage.track({
customer: { externalId: "acme" },
meter: "files_bytes_stored",
quantity: 12,
idempotencyKey: "f-1",
})
await usage.flush()
await container.resolve(FILES_AUDIT).log("files.file_uploaded", {
actor: { id: "user-1", kind: "human" },
target: { id: "acme/file-1", type: "file" },
})
await clients.audit.flush()
const state = await stateOf()
expect((await stateOf("owner-org")).usage).toEqual([
expect.objectContaining({ meter: "files_bytes_stored", quantity: "12" }),
])
expect(state.audit.map((event) => event.action)).toContain("files.file_uploaded")
})
})
describe("the platform contract at start-up", () => {
const fake = (): Record<string, string | undefined> => environment()
it("refuses to start in production without the platform URL, naming the variable", () => {
const { CONUSAI_API_URL: _url, CONUSAI_FAKE_PLATFORM: _fake, ...rest } = fake()
const error = (() => {
try {
loadAppEnv("web", { ...rest, NODE_ENV: "production" })
} catch (thrown) {
return thrown
}
})()
expect(error).toBeInstanceOf(PlatformEnvError)
expect(error).toMatchObject({ variable: "CONUSAI_API_URL" })
expect((error as Error).message).toMatch(/CONUSAI_API_URL is not set: link this project to the platform/)
})
it("refuses to start without the platform token, whichever mode, naming the variable", () => {
for (const NODE_ENV of ["production", "development", undefined]) {
const { CONUSAI_API_TOKEN: _token, ...rest } = fake()
const run = () =>
loadAppEnv("worker", { ...rest, ...(NODE_ENV ? { NODE_ENV } : {}), CONUSAI_FAKE_PLATFORM: undefined })
expect(run, String(NODE_ENV)).toThrow(PlatformEnvError)
expect(run, String(NODE_ENV)).toThrow(/CONUSAI_API_TOKEN is not set/)
}
})
it("treats a run with no NODE_ENV as production, so the fake platform must be asked for in a development run", () => {
const { NODE_ENV: _mode, ...rest } = fake()
expect(() => loadAppEnv("web", rest)).toThrow(/CONUSAI_FAKE_PLATFORM is set in a production run/)
})
it("points a development run that has no platform at `pnpm dev` and the explicit switch", () => {
const { CONUSAI_API_URL: _url, CONUSAI_FAKE_PLATFORM: _fake, ...rest } = fake()
expect(() => loadAppEnv("web", { ...rest, NODE_ENV: "development" })).toThrow(/pnpm dev.*CONUSAI_FAKE_PLATFORM=1/)
})
it("accepts the fake platform in development and in a test, and says which it is using", () => {
for (const NODE_ENV of ["development", "test"]) {
expect(loadAppEnv("web", { ...fake(), NODE_ENV })).toMatchObject({ platform: "fake", production: false })
}
const { CONUSAI_FAKE_PLATFORM: _fake, ...real } = fake()
expect(loadAppEnv("web", { ...real, NODE_ENV: "development" }).platform).toBe("platform")
})
it("refuses the fake platform in a production run even when everything else is set", () => {
expect(() => loadAppEnv("web", { ...fake(), NODE_ENV: "production" })).toThrow(PlatformEnvError)
expect(() => loadAppEnv("web", { ...fake(), NODE_ENV: "production" })).toThrow(/CONUSAI_FAKE_PLATFORM/)
})
it("refuses the fake switch when the platform URL is somebody's public address", () => {
expect(() => loadAppEnv("web", { ...fake(), CONUSAI_API_URL: "https://api.conusai.example/api" })).toThrow(
/CONUSAI_API_URL names api\.conusai\.example/,
)
// ... while a compose service name and loopback are what the fake platform runs at.
expect(() => loadAppEnv("web", { ...fake(), CONUSAI_API_URL: "http://platform:4010/api" })).not.toThrow()
expect(() => loadAppEnv("web", { ...fake(), CONUSAI_API_URL: "http://127.0.0.1:4010/api" })).not.toThrow()
})
it("requires the owning organization in production, where usage would otherwise be metered against nothing", () => {
const { CONUSAI_FAKE_PLATFORM: _fake, CONUSAI_ORG_ID: _org, ...rest } = fake()
const production = { ...rest, NODE_ENV: "production" }
expect(() => loadAppEnv("web", production)).toThrow(/CONUSAI_ORG_ID is not set/)
expect(() => loadAppEnv("worker", production)).toThrow(/CONUSAI_ORG_ID is not set/)
// The migrate job meters nothing.
expect(() =>
loadAppEnv("migrate", { ...production, POSTGRES_MIGRATE_URL: "postgresql://app_owner:secret@db:5432/app" }),
).not.toThrow()
})
})
describe("probePlatform", () => {
it("uses public readiness when the platform descriptor is protected", async () => {
const authorization: Array<string | null> = []
const answered = await probePlatform(
{ CONUSAI_API_URL: "https://platform.example.test/api" },
{
fetch: async (input, init) => {
authorization.push(new Headers(init?.headers).get("authorization"))
const path = new URL(String(input)).pathname
// The real platform mounts its descriptor under authenticated /api;
// /readyz is its public readiness contract.
if (path === "/readyz") return new Response("ready")
return new Response("not public", { status: path.startsWith("/api/") ? 401 : 404 })
},
},
)
expect(answered).toBe(true)
expect(authorization).toEqual([null])
})
it("is true when the platform answers its public readiness probe, and sends no credential", async () => {
const seen: Array<string | null> = []
const answered = await probePlatform(
{ CONUSAI_API_URL: `${platform.url}/api` },
{
fetch: async (input, init) => {
seen.push(new Headers(init?.headers).get("authorization"))
return fetch(input, init)
},
},
)
expect(answered).toBe(true)
expect(seen).toEqual([null])
})
it("is false when nothing answers, without throwing", async () => {
expect(await probePlatform({ CONUSAI_API_URL: "http://127.0.0.1:9/api" }, { timeoutMs: 500 })).toBe(false)
})
})
describe("the public address", () => {
it("is the one the deployment states", () => {
expect(publicUrlOf({ NODE_ENV: "production", CONUSAI_PUBLIC_URL: "https://app.example.test" })).toBe(
"https://app.example.test",
)
})
it("is refused by name in a production run that has none, and never guessed", () => {
for (const env of [{ NODE_ENV: "production" }, {}]) {
expect(() => publicUrlOf(env)).toThrow(PlatformEnvError)
try {
publicUrlOf(env)
} catch (error) {
expect((error as PlatformEnvError).variable).toBe("CONUSAI_PUBLIC_URL")
}
}
})
it("falls back to the development address only in development and tests", () => {
expect(publicUrlOf({ NODE_ENV: "development" })).toBe("http://localhost:3000")
expect(publicUrlOf({ NODE_ENV: "test" })).toBe("http://localhost:3000")
})
})
+69
View File
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The language and the theme a person chooses (spec §10.4): precedence, the cookies that carry a
// visitor's choice, and the roles and invitation states the shell words from the catalogues.
import { describe, expect, it } from "vitest"
import { cookieValue, localeOf, themeOf } from "../apps/web/src/server/preferences.js"
import { inviteErrorKey } from "../apps/web/src/ui/invite-accept.js"
import {
LOCALE_COOKIE,
PREFERENCE_SECONDS,
preferenceCookie,
THEME_COOKIE,
} from "../apps/web/src/ui/preference-cookies.js"
describe("the language", () => {
it("is the signed-in person's own setting before anything else", () => {
expect(
localeOf({ appLocale: "en", userLocale: "lt", cookieHeader: `${LOCALE_COOKIE}=en`, acceptLanguage: "en" }),
).toBe("lt")
})
it("is a visitor's cookie when nobody is signed in", () => {
expect(localeOf({ appLocale: "en", cookieHeader: `a=1; ${LOCALE_COOKIE}=lt; b=2` })).toBe("lt")
})
it("falls to the app's default, then to the browser's, and ignores a language it does not serve", () => {
expect(localeOf({ appLocale: "lt" })).toBe("lt")
expect(localeOf({ appLocale: "xx", acceptLanguage: "lt-LT,en;q=0.8" })).toBe("lt")
expect(localeOf({ appLocale: "en", userLocale: "fr", cookieHeader: `${LOCALE_COOKIE}=de` })).toBe("en")
})
})
describe("the theme", () => {
it("is dark unless the cookie says light: the brand is dark-first", () => {
expect(themeOf(null)).toBe("dark")
expect(themeOf(`${THEME_COOKIE}=light`)).toBe("light")
expect(themeOf(`${THEME_COOKIE}=dark`)).toBe("dark")
expect(themeOf(`${THEME_COOKIE}=neon`)).toBe("dark")
})
})
describe("the cookies", () => {
it("are written for a year, to every path, and never to another site", () => {
expect(preferenceCookie(LOCALE_COOKIE, "lt")).toBe(
`app_locale=lt; Path=/; Max-Age=${PREFERENCE_SECONDS}; SameSite=Lax`,
)
expect(PREFERENCE_SECONDS).toBe(31_536_000)
})
it("read back what was written, and survive a value that is not valid percent-encoding", () => {
expect(cookieValue(`x=1; ${preferenceCookie(THEME_COOKIE, "light").split(";")[0]}`, THEME_COOKIE)).toBe("light")
expect(cookieValue("app_locale=%E0%A4%A", LOCALE_COOKIE)).toBeUndefined()
expect(cookieValue(undefined, LOCALE_COOKIE)).toBeUndefined()
})
})
describe("an invitation's states", () => {
it.each([
["not_found", "invalid"],
["invitation_used", "used"],
["invitation_expired", "expired"],
["already_member", "member"],
["conflict", "failed"],
["rate_limited", "failed"],
["", "failed"],
] as const)("words the code %j as %s", (code, key) => {
expect(inviteErrorKey(code)).toBe(key)
})
})
+115
View File
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// `/api/ready` (spec §13): the verdict, the migration level it compares against, and what the answer
// does not say. The probes are functions, so the verdict is tested without a database; the migration
// list is the real one, read from this repository's folders.
import { readdirSync } from "node:fs"
import { appRoot, expectedMigrations, GRANT_MIGRATIONS, missingMigrations } from "@app/config"
import { appModules } from "@app/modules"
import { buildRegistry } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import { checkReadiness, type ReadinessProbes } from "../apps/web/src/server/readiness.js"
const ALL = [
"0001_kernel",
"0004_files",
"0005_proposals",
"0006_app-user-identity",
"notes/0001_notes",
...GRANT_MIGRATIONS,
]
const probes = (overrides: Partial<ReadinessProbes> = {}, warnings: string[] = []): ReadinessProbes => ({
store: async () => 1,
reader: async () => 1,
expected: async () => ALL,
applied: async () => new Set(ALL),
platform: async () => true,
warn: (message) => warnings.push(message),
...overrides,
})
describe("the readiness verdict", () => {
it("is ready when the store, the reader and the migration level are there", async () => {
expect(await checkReadiness(probes())).toEqual({
ready: true,
checks: { store: true, reader: true, migrations: true, platform: true },
})
})
it("is not ready when the runtime role cannot query, or the reader cannot", async () => {
const down = async () => {
throw new Error("connect ECONNREFUSED postgresql://app_app:secret@db:5432/app")
}
expect((await checkReadiness(probes({ store: down }))).checks).toMatchObject({ store: false, reader: true })
expect((await checkReadiness(probes({ reader: down }))).checks).toMatchObject({ store: true, reader: false })
expect((await checkReadiness(probes({ store: down }))).ready).toBe(false)
expect((await checkReadiness(probes({ reader: down }))).ready).toBe(false)
})
it("is not ready while a migration this build expects has not run", async () => {
const behind = new Set(ALL.slice(0, -1))
const report = await checkReadiness(probes({ applied: async () => behind }))
expect(report.ready).toBe(false)
expect(report.checks.migrations).toBe(false)
})
it("is ready when the database is ahead of this build: a newer release has already migrated it", async () => {
const ahead = new Set([...ALL, "9999_from_the_future"])
expect((await checkReadiness(probes({ applied: async () => ahead }))).ready).toBe(true)
})
it("reports the platform without requiring it", async () => {
const report = await checkReadiness(probes({ platform: async () => false }))
expect(report.ready).toBe(true)
expect(report.checks.platform).toBe(false)
const thrown = await checkReadiness(
probes({
platform: async () => {
throw new Error("boom")
},
}),
)
expect(thrown.checks.platform).toBe(false)
})
it("answers with booleans and nothing else, and logs the reason where an operator reads it", async () => {
const warnings: string[] = []
const report = await checkReadiness(
probes(
{
store: async () => {
throw new Error("password authentication failed for user app_app\nHINT: pg_hba.conf")
},
applied: async () => new Set(["0001_kernel"]),
},
warnings,
),
)
const answer = JSON.stringify(report)
expect(answer).not.toContain("password")
expect(answer).not.toContain("0004_files")
expect(Object.values(report.checks).every((value) => typeof value === "boolean")).toBe(true)
expect(warnings).toEqual([
"ready: store failed: password authentication failed for user app_app",
expect.stringMatching(/^ready: migrations failed: the migrate job has not applied 0004_files/),
])
})
})
describe("the migration level", () => {
it("lists the app's files, then each module's, then the grants the migrate job adds", async () => {
const expected = await expectedMigrations(buildRegistry([...appModules]))
const files = readdirSync(`${appRoot()}/migrations/postgres`)
.filter((name) => name.endsWith(".sql"))
.map((name) => name.replace(/\.sql$/, ""))
.sort()
expect(expected.slice(0, files.length)).toEqual(files)
expect(expected).toContain("notes/0001_notes")
expect(expected.slice(-2)).toEqual([...GRANT_MIGRATIONS])
})
it("names what a database lacks", () => {
expect(missingMigrations(["a", "b", "c"], new Set(["a", "c", "z"]))).toEqual(["b"])
})
})
+56
View File
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The registry this app boots with (spec §4.1): built from the manifests in modules/index.ts and
// from nothing else, so what the router, the navigation and the permissions hold is what the
// modules declared.
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import { describe, expect, it } from "vitest"
import { appModules, domainModules, kernelModules } from "../modules/index.js"
const registry = buildRegistry([...appModules])
describe("the app registry", () => {
it("boots with the kernel modules a tenant app needs", () => {
const kernel = ["tenancy", "auth", "files", "proposals"]
kernel.push("guides") // kit:guides
kernel.push("support") // kit:support
expect(kernelModules.map((module) => module.id)).toEqual(kernel)
expect(appModules.length).toBe(kernelModules.length + domainModules.length)
})
it("registers every tenancy and auth procedure", () => {
const names = [...registry.procedures.keys()]
expect(names).toContain("tenancy.org.create")
expect(names).toContain("tenancy.member.invite")
expect(names).toContain("auth.apiKey.create")
expect(registry.procedures.size).toBeGreaterThanOrEqual(15)
})
it("declares a permission for every tenant-scoped procedure", () => {
for (const [name, { def }] of registry.procedures) {
if (def.scope === "public" || def.scope === "user") continue
expect(registry.permissions[def.permission], `${name} declares ${def.permission}`).toBeTruthy()
}
})
it("lists every tenant-scoped procedure as needing an isolation fixture", () => {
const tenantScoped = [...registry.procedures.values()].filter(
({ def }) => def.scope !== "public" && def.scope !== "user",
)
expect(registry.isolationFixtures.length).toBe(tenantScoped.length)
})
it("has a table for every entity the app's schema must hold", () => {
const tables = [...entitiesOf(registry, "postgres"), ...KIT_JOBS_ENTITIES].map((entity) => entity.table)
expect(tables).toContain("organizations")
expect(tables).toContain("auth_sessions")
expect(tables).toContain("kit_processed_events")
expect(new Set(tables).size).toBe(tables.length)
})
it("gives the web shell a navigation entry per kernel area", () => {
expect(registry.nav.map((item) => item.href)).toEqual(expect.arrayContaining(["members", "settings", "api-keys"]))
})
})
+199
View File
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The software bill of materials of the built image and the THIRD-PARTY-NOTICES generated from it
// (plan item AT5.1): the SBOM reads what is installed in a pnpm store (a runtime dependency and not a
// link to one, once per version), and the notices list every component of it, with each licence text
// printed once however many packages share it. The committed THIRD-PARTY-NOTICES is checked against
// the dependencies this template declares.
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { dpkgComponents, npmComponents, sbom } from "../scripts/sbom.mjs"
import { notices, unlisted } from "../scripts/third-party-notices.mjs"
const APP_ROOT = new URL("..", import.meta.url).pathname
const made: string[] = []
afterEach(() => {
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
})
const MIT = "MIT License\n\nCopyright (c) 2020 Someone\n\nPermission is hereby granted, free of charge, ..."
/** A pnpm virtual store: each entry holds its own package and links to the packages it depends on. */
function image(): string {
const root = mkdtempSync(join(tmpdir(), "sbom-"))
made.push(root)
const put = (path: string, text: string) => {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), text)
}
const store = "node_modules/.pnpm"
put("package.json", JSON.stringify({ name: "demo-app", version: "1.2.3" }))
put(
`${store}/alpha@1.0.0/node_modules/alpha/package.json`,
JSON.stringify({ name: "alpha", version: "1.0.0", license: "MIT" }),
)
put(`${store}/alpha@1.0.0/node_modules/alpha/LICENSE`, MIT)
put(
`${store}/beta@2.0.0/node_modules/beta/package.json`,
JSON.stringify({ name: "beta", version: "2.0.0", license: "MIT" }),
)
put(`${store}/beta@2.0.0/node_modules/beta/LICENSE.md`, MIT)
// the same version under two peer sets is one component
put(
`${store}/beta@2.0.0_react@19/node_modules/beta/package.json`,
JSON.stringify({ name: "beta", version: "2.0.0", license: "MIT" }),
)
put(
`${store}/@scope+gamma@3.0.0/node_modules/@scope/gamma/package.json`,
JSON.stringify({
name: "@scope/gamma",
version: "3.0.0",
license: { type: "ISC" },
repository: { url: "git+https://example.test/gamma.git" },
}),
)
put(
`${store}/@conusai-kit+core@0.1.0/node_modules/@conusai-kit/core/package.json`,
JSON.stringify({ name: "@conusai-kit/core", version: "0.1.0", license: "LicenseRef-ConusAI-Template-1.0" }),
)
put(
`${store}/delta@4.0.0/node_modules/delta/package.json`,
JSON.stringify({ name: "delta", version: "4.0.0", license: "(MIT OR Apache-2.0)" }),
)
// alpha depends on beta: a link inside alpha's entry, which is not a second copy of beta
symlinkSync(join(root, `${store}/beta@2.0.0/node_modules/beta`), join(root, `${store}/alpha@1.0.0/node_modules/beta`))
return root
}
describe("the SBOM of an image", () => {
it("holds each installed package once, with its licence and the text of its licence file", () => {
const components = npmComponents(image())
expect(components.map((component) => component["bom-ref"])).toEqual([
"pkg:npm/%40conusai-kit/core@0.1.0",
"pkg:npm/%40scope/gamma@3.0.0",
"pkg:npm/alpha@1.0.0",
"pkg:npm/beta@2.0.0",
"pkg:npm/delta@4.0.0",
])
const alpha = components.find((component) => component.name === "alpha")
expect(alpha?.licenses[0]?.license).toMatchObject({ id: "MIT", text: { content: MIT } })
expect(components.find((component) => component.name === "gamma")).toMatchObject({
group: "@scope",
licenses: [{ license: { id: "ISC" } }],
externalReferences: [{ type: "vcs", url: "https://example.test/gamma" }],
})
expect(components.find((component) => component.name === "delta")?.licenses).toEqual([
{ expression: "(MIT OR Apache-2.0)" },
])
})
it("marks the ConusAI packages, whose licence is LICENSE-TEMPLATE, and names their licence as a reference", () => {
const core = npmComponents(image()).find((component) => component.name === "core")
expect(core?.properties).toContainEqual({ name: "conusai:component", value: "true" })
expect(core?.licenses[0]?.license).toEqual({ name: "LicenseRef-ConusAI-Template-1.0" })
})
it("is a CycloneDX 1.5 document with the application, the Node.js runtime and the packages", () => {
const document = sbom({ root: image(), dpkg: "/nonexistent", nodeVersion: "v24.1.0" })
expect(document).toMatchObject({
bomFormat: "CycloneDX",
specVersion: "1.5",
metadata: { component: { type: "application", name: "app", version: "0.0.0" } },
})
expect(document.components[0]).toMatchObject({ type: "platform", name: "node", version: "24.1.0" })
expect(document.components).toHaveLength(6)
})
it("reads the operating system packages that are installed, and no others", () => {
const dir = mkdtempSync(join(tmpdir(), "dpkg-"))
made.push(dir)
const status = join(dir, "status")
writeFileSync(
status,
[
"Package: libc6\nStatus: install ok installed\nVersion: 2.41-1\nArchitecture: amd64",
"Package: gone\nStatus: deinstall ok config-files\nVersion: 1.0",
"Package: zlib1g\nStatus: install ok installed\nVersion: 1:1.3.dfsg-1",
"",
].join("\n\n"),
)
const components = dpkgComponents(status, dir)
expect(components.map((component) => `${component.name} ${component.version}`)).toEqual([
"libc6 2.41-1",
"zlib1g 1:1.3.dfsg-1",
])
})
})
describe("THIRD-PARTY-NOTICES generated from an SBOM", () => {
const document = () =>
sbom({ root: image(), dpkg: "/nonexistent", name: "demo-app", version: "1.2.3", nodeVersion: "v24.1.0" })
it("lists every component, and prints a licence text shared by several packages once", () => {
const text = notices(document())
expect(unlisted(document(), text)).toEqual([])
expect(text).toContain("@conusai-kit/core@0.1.0")
expect(text).toContain("node@24.1.0")
expect(text.split("Permission is hereby granted").length - 1).toBe(1)
expect(text).toContain("alpha@1.0.0")
expect(text).toContain("beta@2.0.0")
expect(text).toContain("these packages ship no licence file; they declare ISC")
})
it("is the same text every time it is generated from the same SBOM", () => {
expect(notices(document())).toBe(notices(document()))
})
it("is checked against the SBOM: a component it does not list is named", () => {
const text = notices(document())
const more = document()
more.components.push({
type: "library",
"bom-ref": "pkg:npm/epsilon@5.0.0",
name: "epsilon",
version: "5.0.0",
purl: "pkg:npm/epsilon@5.0.0",
licenses: [],
})
expect(unlisted(more, text)).toEqual(["epsilon@5.0.0"])
// a version that moved is drift by name, not a missing component
const moved = document()
moved.components = moved.components.map((component) =>
component.name === "alpha" ? { ...component, version: "1.0.1" } : component,
)
expect(unlisted(moved, text)).toEqual(["alpha@1.0.1"])
expect(unlisted(moved, text, { byName: true })).toEqual([])
})
})
describe("the committed THIRD-PARTY-NOTICES", () => {
const text = readFileSync(join(APP_ROOT, "THIRD-PARTY-NOTICES"), "utf8")
it("says what it is generated from, and that it is not edited by hand", () => {
expect(text).toContain("software bill of materials of the built image")
expect(text).toContain("do not edit it by hand")
})
it("lists every runtime dependency this template declares, by name", () => {
const declared = new Set<string>()
for (const path of [
"package.json",
"apps/web/package.json",
"apps/worker/package.json",
"config/package.json",
"modules/package.json",
]) {
const manifest = JSON.parse(readFileSync(join(APP_ROOT, path), "utf8")) as {
dependencies?: Record<string, string>
}
for (const name of Object.keys(manifest.dependencies ?? {})) if (!name.startsWith("@app/")) declared.add(name)
}
expect(declared.size).toBeGreaterThan(15)
const missing = [...declared].filter((name) => !text.includes(` ${name}@`))
expect(missing).toEqual([])
})
})
+101
View File
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The demo data of a development run (apps/worker/src/seed.ts): two organizations, one with a tenant
// brand, written with the kit's own use cases as the people who may write them, and written once --
// a second run changes nothing. The data source is the in-memory one, which the kit proves to behave
// as Postgres does (the adapter contract suite); the procedures, the permission checks and the
// invitation flow are the real ones.
import { DEV_LOGIN_ISSUER } from "@conusai-kit/auth"
import { users } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { DEMO_ORGANIZATIONS, DEMO_PEOPLE, seedDemo } from "../apps/worker/src/seed.js"
import { makeNotesApp } from "./support/notes-app.js"
const seeded = async () => {
const app = makeNotesApp()
const deps = { data: app.data, registry: app.registry, container: app.root }
return { app, deps, summary: await seedDemo(deps) }
}
describe("the demo data", () => {
it("makes two organizations, and the people in them with the roles the demo describes", async () => {
const { app, summary } = await seeded()
expect(summary.organizations.map((org) => org.slug)).toEqual(["acme-review", "northwind"])
const rolesOf = async (email: string) => {
const rows = await app.data.global(users).list({ where: [{ field: "email", op: "eq", value: email }], limit: 1 })
const me = (await app.caller({ userId: rows.items[0]?.id ?? "" }).callByName("tenancy.me.get", undefined)) as {
organizations: Array<{ slug: string; role: string }>
}
return Object.fromEntries(me.organizations.map((org) => [org.slug, org.role]))
}
expect(await rolesOf("ada@example.test")).toEqual({ "acme-review": "owner", northwind: "admin" })
expect(await rolesOf("grace@example.test")).toEqual({ "acme-review": "member", northwind: "owner" })
expect(await rolesOf("linus@example.test")).toEqual({ "acme-review": "viewer" })
})
it("gives exactly one of the organizations a tenant brand", async () => {
const { app } = await seeded()
const grace = (
await app.data
.global(users)
.list({ where: [{ field: "email", op: "eq", value: "grace@example.test" }], limit: 1 })
).items[0]
const call = app.caller({ userId: grace?.id ?? "" })
expect(await call.callByName("branding.brand.get", { org: "northwind" })).toMatchObject({
displayName: "Northwind",
primary: "#1a1a8c",
accent: "#f4c430",
})
expect(await call.callByName("branding.brand.get", { org: "acme-review" })).toMatchObject({
displayName: null,
primary: null,
})
})
it("puts a project and two notes where a module's pages can show them", async () => {
const { app } = await seeded()
const ada = (
await app.data.global(users).list({ where: [{ field: "email", op: "eq", value: "ada@example.test" }], limit: 1 })
).items[0]
const call = app.caller({ userId: ada?.id ?? "" })
const listed = (await call.callByName("notes.note.list", {
org: "acme-review",
workspace: "general",
project: "notes",
})) as { notes: Array<{ title: string }> }
expect(listed.notes.map((note) => note.title).sort()).toEqual(["Roles", "Welcome"])
})
it("creates the people as a sign-in does, under the dev-login issuer, so the dev sign-in lets them in", async () => {
const { app } = await seeded()
const rows = await app.data.global(users).list({ limit: 10 })
expect(rows.items.map((user) => [user.issuer, user.subject, user.email]).sort()).toEqual(
DEMO_PEOPLE.map((person) => [DEV_LOGIN_ISSUER, person.email, person.email]).sort(),
)
})
it("changes nothing when it runs again", async () => {
const { app, deps, summary } = await seeded()
expect(summary.created.length).toBeGreaterThan(5)
const before = JSON.stringify({
users: (await app.data.global(users).list({ limit: 50 })).items.map((user) => [user.email, user.version]),
outbox: (await app.data.outbox.pending(500)).length,
})
const again = await seedDemo(deps)
expect(again.created).toEqual([])
expect(
JSON.stringify({
users: (await app.data.global(users).list({ limit: 50 })).items.map((user) => [user.email, user.version]),
outbox: (await app.data.outbox.pending(500)).length,
}),
).toBe(before)
})
it("names the organizations the way the README and the sign-in help do", () => {
expect(DEMO_ORGANIZATIONS.acme.slug).toBe("acme-review")
expect(DEMO_ORGANIZATIONS.northwind.brand.displayName).toBe("Northwind")
})
})
+222
View File
@@ -0,0 +1,222 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The brand as a share image and an email show it (spec §10.2, §10.3): the app brand's name and logo, or an
// organization's, drawn through the kit's logo checks. Every logo here goes the way it goes in production: an
// organization's through the caller (so a stored, normalised file), the app's from a file of a public folder.
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import {
type BrandView,
type ComposedEmail,
composeEmail,
type MailTransport,
renderShareImage,
type ShareBrand,
sendEmail,
shareBrand,
} from "@app/modules/branding"
import { afterEach, describe, expect, it } from "vitest"
import { appLogoSource, readersFor } from "../apps/web/src/server/brand-assets"
import { closeApps, makeDocumentsApp, type SeededProject } from "./support/documents-app.js"
import { dimensions, markPng, pixelAt } from "./support/images.js"
const directories: string[] = []
afterEach(async () => {
await closeApps()
for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true })
})
const TEAL = [0x80, 0xcd, 0xc6, 255]
const svg = (fill: string) =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 80"><rect width="300" height="80" fill="${fill}"/></svg>`
/** A public folder holding the app brand's two logos: a blue rectangle for light, a red one for dark. */
function publicFolder(): string {
const dir = mkdtempSync(join(tmpdir(), "share-public-"))
directories.push(dir)
writeFileSync(join(dir, "app-light.svg"), svg("#3366aa"))
writeFileSync(join(dir, "app-dark.svg"), svg("#aa3333"))
return dir
}
const appIdentity = { name: "Example App", logo: { light: "/app-light.svg", dark: "/app-dark.svg" } }
const empty: BrandView = { displayName: null, primary: null, accent: null, logoLight: null, logoDark: null }
async function acme(): Promise<{ project: SeededProject }> {
const app = await makeDocumentsApp({
document: { engine: { name: "unused", version: "0" }, page_count: 0, pages: [] },
})
return { project: await app.seedProject("acme", "owner@acme.test") }
}
const upload = (project: SeededProject, variant: "light" | "dark", bytes: Uint8Array) =>
project.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant,
contentType: "image/png",
body: Buffer.from(bytes).toString("base64"),
})
// The logo box of the share image: 300 x 80 at (80, 80); a wide logo's middle is at (230, 120).
const logoCentre = (png: Uint8Array) => pixelAt(png, 230, 120)
describe("the app brand's logo from the public folder", () => {
it("is read from a file of the folder and from nowhere else", async () => {
const dir = publicFolder()
expect(await appLogoSource("/app-dark.svg", dir)).toMatchObject({ contentType: "image/svg+xml" })
await expect(appLogoSource("/../secret.svg", dir)).rejects.toThrow(/public folder/)
await expect(appLogoSource("/app.exe", dir)).rejects.toThrow(/PNG, WebP or SVG/)
await expect(appLogoSource("/missing.svg", dir)).rejects.toThrow()
})
})
describe("the share image", () => {
it("draws the app brand's logo for the dark page when there is no organization", async () => {
const dir = publicFolder()
const share = await shareBrand({
app: appIdentity,
tenant: null,
readers: { appLogo: (path) => appLogoSource(path, dir), tenantLogo: () => Promise.reject(new Error("none")) },
})
expect(share.name).toBe("Example App")
const png = await renderShareImage(share, { title: share.name })
expect(dimensions(png)).toEqual({ width: 1200, height: 630 })
expect(logoCentre(png)).toEqual([0xaa, 0x33, 0x33, 255])
})
it("draws an organization's own logo and display name, and the logo is the stored one", async () => {
const { project } = await acme()
await project.call.callByName("branding.brand.set", { org: project.orgSlug, displayName: "Acme Review" })
await upload(project, "dark", markPng(2000, 500))
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand },
readers: readersFor(project.call, project.orgSlug, publicFolder()),
})
expect(share.name).toBe("Acme Review")
expect(share.logo.contentType).toBe("image/png")
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL)
})
it("draws the only logo the organization has when it is the light one, because the page is dark", async () => {
const { project } = await acme()
await upload(project, "light", markPng(2000, 500))
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
expect(brand.logoDark).toBeNull()
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand },
readers: readersFor(project.call, project.orgSlug, publicFolder()),
})
expect(share.name).toBe("Acme Ltd")
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL)
})
it("is not stopped by a missing app logo file when the organization has a good logo of its own", async () => {
const { project } = await acme()
await upload(project, "dark", markPng(2000, 500))
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand },
readers: readersFor(project.call, project.orgSlug, mkdtempSync(join(tmpdir(), "share-nofile-"))),
})
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL)
})
it("falls back to the app brand's logo for an organization with none", async () => {
const { project } = await acme()
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand: empty },
readers: readersFor(project.call, project.orgSlug, publicFolder()),
})
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual([0xaa, 0x33, 0x33, 255])
})
it("draws the app brand's logo instead when the chosen one cannot be drawn, and does not fail", async () => {
const dir = publicFolder()
const broken: ShareBrand = {
name: "Acme Ltd",
logo: { contentType: "image/png", bytes: new TextEncoder().encode("<svg/>") },
fallback: await appLogoSource("/app-dark.svg", dir),
}
expect(logoCentre(await renderShareImage(broken, { title: broken.name }))).toEqual([0xaa, 0x33, 0x33, 255])
const refused: ShareBrand = { ...broken, fallback: broken.logo }
await expect(renderShareImage(refused, { title: "x" })).rejects.toThrow(/logo cannot be used/)
})
})
describe("the email", () => {
const recorded = () => {
const sent: Array<ComposedEmail & { to: string }> = []
const transport: MailTransport = {
async send(message) {
sent.push(message)
},
}
return { sent, transport }
}
async function shareOf(logo: Uint8Array | null, dir = publicFolder()): Promise<ShareBrand> {
const { project } = await acme()
if (logo) await upload(project, "dark", logo)
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
return shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand: { ...brand, displayName: "Acme Review" } },
readers: readersFor(project.call, project.orgSlug, dir),
})
}
it("carries the organization's name and its logo as an inline attachment, and names no host", async () => {
const mail = await composeEmail(await shareOf(markPng(2000, 500)), {
subject: "You are invited",
paragraphs: ["Hello,", "Open the app to accept."],
})
expect(mail.subject).toBe("You are invited - Acme Review")
expect(mail.html).toContain('alt="Acme Review"')
expect(mail.html).toContain('src="cid:brand-logo"')
expect(mail.html).not.toMatch(/https?:/)
expect(mail.html.toLowerCase()).not.toContain("conusai")
expect(mail.text).toBe("Hello,\n\nOpen the app to accept.\n\nAcme Review\n")
const [logo] = mail.attachments
expect(logo).toMatchObject({ cid: "brand-logo", contentType: "image/png" })
expect(dimensions(logo?.content ?? new Uint8Array())).toEqual({ width: 420, height: 105 })
expect(pixelAt(logo?.content ?? new Uint8Array(), 210, 52)).toEqual(TEAL)
})
it("uses the app brand's logo for an organization with none", async () => {
const mail = await composeEmail(await shareOf(null), { subject: "Hi", paragraphs: ["x"] })
const content = mail.attachments[0]?.content ?? new Uint8Array()
expect(dimensions(content).width).toBe(420)
expect(pixelAt(content, 210, 52)).toEqual([0xaa, 0x33, 0x33, 255])
})
it("escapes what it is given as text, and refuses a subject that is more than one line", async () => {
const share = await shareOf(null)
const mail = await composeEmail(share, { subject: "x", paragraphs: ['<script>alert("1")</script> & more'] })
expect(mail.html).toContain("&#60;script&#62;alert(&#34;1&#34;)&#60;/script&#62; &#38; more")
expect(mail.html).not.toContain("<script>")
await expect(composeEmail(share, { subject: "x\r\nBcc: someone@example.test", paragraphs: ["x"] })).rejects.toThrow(
/one line/,
)
await expect(composeEmail(share, { subject: "x", paragraphs: [] })).rejects.toThrow(/paragraph/)
})
it("is handed to the transport with one address, and refuses an address that carries a header", async () => {
const share = await shareOf(null)
const { sent, transport } = recorded()
await sendEmail(transport, share, "member@acme.test", { subject: "Welcome", paragraphs: ["Hello"] })
expect(sent).toHaveLength(1)
expect(sent[0]).toMatchObject({ to: "member@acme.test", subject: "Welcome - Acme Review" })
for (const to of ["a@b.test\r\nBcc: c@d.test", "a@b.test, c@d.test", "no-address", "a b@c.test"]) {
await expect(sendEmail(transport, share, to, { subject: "x", paragraphs: ["x"] })).rejects.toThrow(
/one email address/,
)
}
expect(sent).toHaveLength(1)
})
})
+271
View File
@@ -0,0 +1,271 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The whole app of this template, wired the way apps/web and apps/worker wire it, on the in-memory
// data source and the fakes of fakes.ts: the registry of every module, one caller per actor, the
// outbox relay's behaviour and the worker's job runner.
//
// `deliver()` stands in for the outbox relay (kit-jobs-pgboss): it drains the outbox once, runs each
// record's subscribers in a child container with the record's organization and a system actor, and
// marks it published -- exactly once per subscriber, never more forgiving than the real relay.
// `runJobs()` stands in for the worker: it runs every queued job in a child container carrying the
// scope the job was enqueued in, with a system actor. What neither does is retry, so a test of a
// retry runs the handler again itself.
import {
DOCUMENT_EXCLUDED_PAGE_CLASSES,
DOCUMENT_MODEL,
DOCUMENT_OCR,
DOCUMENTS_SCOPE,
MODEL_GATEWAY,
type PageClass,
} from "@app/modules/documents"
import { type Actor, buildRegistry, Container, createCaller, type TransportCaller } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import {
createMemoryDataSource,
DATA_SOURCE,
entitiesOf,
JOB_QUEUE,
OBJECT_STORE,
SYSTEM_DATA,
} from "@conusai-kit/data"
import { FILES_AUDIT, FILES_REGISTRY, FILES_SCOPE, FILES_USAGE } from "@conusai-kit/files"
import { createModelGateway } from "@conusai-kit/platform"
import { PROPOSALS_AUDIT, PROPOSALS_REGISTRY, PROPOSALS_SCOPE } from "@conusai-kit/proposals"
import { createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy"
import {
fakeJobQueue,
fakeObjectStore,
recordingAudit,
recordingUsage,
uploadViaProcedures,
} from "@conusai-kit/testing"
import type { FakeChatResponder } from "@conusai-kit/testing/fake-platform"
import { appModules } from "../../modules/index.js"
import { deterministicModel } from "./fake-model.js"
import { type FakePlatform, startOcr } from "./ocr.js"
const started: FakePlatform[] = []
/** Stops every fake platform the test file started: call it from `afterEach`. */
export async function closeApps(): Promise<void> {
await Promise.all(started.splice(0).map((platform) => platform.close()))
}
export interface SeededProject {
readonly owner: Actor
readonly call: TransportCaller
readonly orgSlug: string
readonly workspaceSlug: string
readonly projectSlug: string
readonly orgId: string
readonly workspaceId: string
readonly projectId: string
/** The three slugs a project-scoped procedure's input carries. */
readonly slugs: { org: string; workspace: string; project: string }
}
/** `document` is what the fake platform's OCR reads every file as: a `conusai.ocr.document/v1`. */
export async function makeDocumentsApp(options: {
document: Record<string, unknown>
/** What the model answers; by default the deterministic reader of fake-model.ts. */
responder?: FakeChatResponder
/** The page classes the deployment leaves out; by default the module's own (drawings and empty pages). */
excludedPageClasses?: readonly PageClass[]
}) {
const registry = buildRegistry([...appModules])
const entities = entitiesOf(registry, "postgres")
const data = createMemoryDataSource({ entities })
const store = fakeObjectStore()
const queue = fakeJobQueue()
const audit = recordingAudit()
const usage = recordingUsage()
const { platform, ocr } = await startOcr(options.document)
started.push(platform)
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
root.value(OBJECT_STORE, store)
root.value(JOB_QUEUE, queue)
root.value(FILES_REGISTRY, registry)
root.value(FILES_AUDIT, audit)
root.value(FILES_USAGE, usage)
root.value(PROPOSALS_REGISTRY, registry)
root.value(PROPOSALS_AUDIT, audit)
root.value(DOCUMENT_OCR, ocr)
// The model is the real gateway client over HTTP to the fake platform, whose reply is scripted.
const modelRequests: Array<Record<string, unknown>> = []
const script = (responder: FakeChatResponder | undefined): void =>
platform.ai.respond(
responder &&
((request) => {
modelRequests.push(request)
return responder(request)
}),
)
script(options.responder ?? deterministicModel)
root.value(
MODEL_GATEWAY,
createModelGateway({ CONUSAI_API_URL: `${platform.url}/api`, CONUSAI_API_TOKEN: "deployment-token" }),
)
root.value(DOCUMENT_MODEL, "fake-model")
if (options.excludedPageClasses) root.value(DOCUMENT_EXCLUDED_PAGE_CLASSES, options.excludedPageClasses)
const caller = (actor: Actor): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy }, { actor })
}
const signUp = async (email: string): Promise<Actor> => {
const user = await data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
const delivered: Array<{ name: string }> = []
const deliver = async (): Promise<number> => {
const pending = await data.outbox.pending(500)
let ran = 0
for (const record of pending) {
const entry = registry.events[record.name]
if (entry) {
for (const subscriber of entry.subscribers) {
const container = root.child()
const scope = { level: "org" as const, actor: systemActor("app.relay"), orgId: record.orgId }
container.value(FILES_SCOPE, scope)
container.value(PROPOSALS_SCOPE, scope)
await subscriber.handler(
{ name: record.name, payload: record.payload },
{ container, correlationId: record.id },
)
delivered.push({ name: record.name })
ran += 1
}
}
await data.outbox.markPublished([record.id])
}
return ran
}
/** The jobs not yet run, in the order they were enqueued. */
let consumed = 0
const runJobs = async (): Promise<number> => {
let ran = 0
while (consumed < queue.jobs.length) {
const job = queue.jobs[consumed] as (typeof queue.jobs)[number]
consumed += 1
const registered = registry.jobs[job.job]
if (!registered) throw new Error(`no module declares the job ${job.job}`)
const container = root.child()
const tenancy = createTenancyHooks({ data, registry })
container.value(TENANCY, tenancy)
container.value(DOCUMENTS_SCOPE, { ...job.scope, actor: systemActor(`${job.job}`) })
await registered.def.handler(registered.def.payload.parse(job.payload), {
container,
correlationId: job.id,
attempt: 1,
})
ran += 1
}
return ran
}
async function seedProject(slug: string, email: string): Promise<SeededProject> {
const owner = await signUp(email)
const call = caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string; slug: string }
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { workspaceId: string; slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "documents",
})) as { projectId: string; slug: string }
return {
owner,
call,
orgSlug: org.slug,
workspaceSlug: workspace.slug,
projectSlug: project.slug,
orgId: org.orgId,
workspaceId: workspace.workspaceId,
projectId: project.projectId,
slugs: { org: org.slug, workspace: workspace.slug, project: project.slug },
}
}
/** A signed-up user who joined the project's organization with a role. */
async function addMember(project: SeededProject, email: string, role: "viewer" | "member" | "admin") {
const actor = await signUp(email)
const invite = (await project.call.callByName("tenancy.member.invite", {
org: project.orgSlug,
email,
role,
})) as { token: string }
const call = caller(actor)
await call.callByName("tenancy.invitation.accept", { org: project.orgSlug, token: invite.token })
return { actor, call }
}
return {
registry,
data,
entities,
store,
queue,
audit,
usage,
/** The OCR port the worker uses, observed, and the fake platform behind it. */
ocr,
platform,
/** Every request the model was sent, as the gateway received it, and a way to change its answers. */
model: { requests: modelRequests, respond: script },
caller,
signUp,
deliver,
runJobs,
delivered,
seedProject,
addMember,
}
}
export type DocumentsApp = Awaited<ReturnType<typeof makeDocumentsApp>>
/** Some text as the bytes of a file, for an upload. */
export const bytesOf = (text: string): Uint8Array => new TextEncoder().encode(text)
/** Uploads a file (a PDF unless told otherwise) as its project's owner and carries it through the relay and the worker. */
export async function uploadAndProcess(
app: DocumentsApp,
project: SeededProject,
filename = "report.pdf",
contentType = "application/pdf",
) {
const file = await uploadViaProcedures(project.call, project.slugs, {
filename,
contentType,
// The files module checks the first bytes against the type the upload declares, so a picture
// starts with the PNG signature and a document with the PDF one.
bytes:
contentType === "image/png"
? new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, ...bytesOf(`fixture ${filename}`)])
: bytesOf(`%PDF-1.4 fixture ${filename}`),
})
await app.deliver()
await app.runJobs()
return file
}
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The test double of the AI gateway for the documents module: a deterministic reader, standing in for a
// model behind `platform.ai.respond` of the fake platform.
//
// It answers the extraction request exactly as a well-behaved model would: it finds the page's text
// between the tags the prompt names, takes every `label: value` line as a field, and quotes the line
// itself. It never sees an image, never invents a word, and returns the same lines for the same text,
// so a test (or the offline eval) that runs it measures the module's own code -- the request it builds,
// the schema it parses, the verification, the cache, the proposals -- and not a model.
import { PAGE_TEXT_CLOSE, PAGE_TEXT_OPEN } from "@app/modules/documents"
import type { FakeChatReply } from "@conusai-kit/testing/fake-platform"
const FIELD = /^\s*([^::]+?)\s*[::]\s*(\S.*?)\s*$/u
const MAX_LABEL_CHARS = 60
const MAX_LABEL_WORDS = 6
const MAX_LINE_CHARS = 2_000
export interface ReadLineOfModel {
label: string
value: string
quote: string
}
/**
* The fields a reader of two line shapes finds in a page's text: `label: value`, and a table row
* `first cell | other cells`, whose label is the first cell and whose value is the rest, as the prompt
* asks a model to read it. The first row of a block of rows is the table's header, which names the
* columns and is not a field.
*/
export function keyValueLines(text: string): ReadLineOfModel[] {
const found: ReadLineOfModel[] = []
let inTable = false
for (const line of text.split("\n")) {
if (line.includes(" | ")) {
const header = !inTable
inTable = true
const [label, ...rest] = line.split(" | ").map((cell) => cell.trim())
if (!header && label && rest.length > 0 && rest.every((cell) => cell !== "") && line.length <= MAX_LINE_CHARS) {
found.push({ label, value: rest.join(" | "), quote: line.trim() })
}
continue
}
inTable = false
const match = FIELD.exec(line)
if (!match) continue
const label = match[1] as string
const value = match[2] as string
if (label.length > MAX_LABEL_CHARS || label.split(/\s+/u).length > MAX_LABEL_WORDS) continue
if (line.length > MAX_LINE_CHARS) continue
found.push({ label, value, quote: line.trim() })
}
return found
}
type Part = { type?: string; text?: string }
/** The page text of an extraction request: what lies between the prompt's tags in the user's message. */
export function pageTextOf(request: Record<string, unknown>): string | undefined {
const messages = (request.messages ?? []) as Array<{ role?: string; content?: string | Part[] }>
const user = [...messages].reverse().find((message) => message.role === "user")
const content = user?.content
const text = typeof content === "string" ? content : (content ?? []).map((part) => part.text ?? "").join("\n")
const from = text.indexOf(PAGE_TEXT_OPEN)
const to = text.lastIndexOf(PAGE_TEXT_CLOSE)
return from < 0 || to < from ? undefined : text.slice(from + PAGE_TEXT_OPEN.length + 1, to - 1)
}
/** What an extraction request is answered with by the deterministic reader. */
export const deterministicModel = (request: Record<string, unknown>): FakeChatReply => {
const text = pageTextOf(request)
return { content: JSON.stringify({ lines: text === undefined ? [] : keyValueLines(text) }) }
}
+114
View File
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Real images for the logo tests: a PNG written by hand, so a test can break one chunk, and a WebP
// that libwebp wrote. The kit's logo checks read structure and decode pixels, so a few magic bytes
// no longer stand in for an image.
import { deflateSync, inflateSync } from "node:zlib"
const TABLE = (() => {
const table = new Uint32Array(256)
for (let n = 0; n < 256; n++) {
let c = n
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
table[n] = c >>> 0
}
return table
})()
const crc32 = (bytes: Uint8Array): number => {
let c = 0xffffffff
for (const byte of bytes) c = (TABLE[(c ^ byte) & 0xff] as number) ^ (c >>> 8)
return (c ^ 0xffffffff) >>> 0
}
const concat = (...parts: Uint8Array[]): Buffer => Buffer.concat(parts)
export const chunk = (type: string, data: Uint8Array): Buffer => {
const out = Buffer.alloc(12 + data.length)
out.writeUInt32BE(data.length, 0)
out.write(type, 4, "latin1")
out.set(data, 8)
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length)
return out
}
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
/** A PNG over `pixel(x, y)` -> [r, g, b, a]. */
export function pngOf(width: number, height: number, pixel: (x: number, y: number) => readonly number[]): Buffer {
const header = Buffer.alloc(13)
header.writeUInt32BE(width, 0)
header.writeUInt32BE(height, 4)
header.set([8, 6, 0, 0, 0], 8)
const rows = Buffer.alloc(height * (1 + width * 4))
for (let y = 0; y < height; y++)
for (let x = 0; x < width; x++) rows.set(pixel(x, y), y * (1 + width * 4) + 1 + x * 4)
return concat(SIGNATURE, chunk("IHDR", header), chunk("IDAT", deflateSync(rows)), chunk("IEND", new Uint8Array()))
}
/** A teal disc on transparent that fills the height. */
export const markPng = (width: number, height: number, colour: readonly number[] = [0x80, 0xcd, 0xc6, 255]): Buffer =>
pngOf(width, height, (x, y) => {
const dx = x - width / 2
const dy = y - height / 2
return dx * dx + dy * dy < (Math.min(width, height) / 2) ** 2 ? colour : [0, 0, 0, 0]
})
/** Pseudo-random pixels, which PNG cannot compress: a file about as large as its pixels. */
export function noisePng(width: number, height: number): Buffer {
let seed = 12345
const next = () => {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
return seed >>> 24
}
return pngOf(width, height, () => [next(), next(), next(), 255])
}
/** A lossless WebP of a teal disc, 400 x 100, written by libwebp. */
export const MARK_WEBP = Buffer.from(
"UklGRvwAAABXRUJQVlA4TO8AAAAvj8EYEA8wzQM4xvMf8HBU27bT0DNEwpeCNJCGlC/hD9NvKuXe91InEf135EaSInlgeU8DlfmEzM4yACXbFuj4j/SHeNfSX9Nfnu/A/oT+QQToH6RHgfxN+hfPj8CEJzn1l/Qfjs821P+i/1V6Fqj/Rf+r47OefxgAlmFCTjxw6IfOOGVjV5zS2pWmBLu2Uzq7jlN6u85T8NphUv7KDdPK/6D3cdV7G3chH3dsH5OHjwnKxyToY6JVzNmKWd5FwlDkHkW2UuQ3RUZU5FBF1lXkaUFmV/QCiu5B0W8oOhRBT6PoghR9k6LTEvRmdrq5DAA=",
"base64",
)
export const dimensions = (png: Uint8Array): { width: number; height: number } => ({
width: Buffer.from(png).readUInt32BE(16),
height: Buffer.from(png).readUInt32BE(20),
})
/** The [r, g, b, a] of one pixel of an 8-bit RGBA, non-interlaced PNG (what the renderers write), by unfiltering its rows. */
export function pixelAt(png: Uint8Array, x: number, y: number): number[] {
const bytes = Buffer.from(png)
const { width, height } = dimensions(bytes)
if (bytes[24] !== 8 || bytes[25] !== 6 || bytes[28] !== 0)
throw new Error("pixelAt reads 8-bit RGBA PNGs without interlace")
const data: Buffer[] = []
for (let at = 8; at < bytes.length; ) {
const length = bytes.readUInt32BE(at)
if (bytes.toString("latin1", at + 4, at + 8) === "IDAT") data.push(bytes.subarray(at + 8, at + 8 + length))
at += 12 + length
}
const raw = inflateSync(Buffer.concat(data))
const stride = width * 4
const rows: Buffer[] = []
for (let row = 0; row < height; row++) {
const filter = raw[row * (stride + 1)] as number
const line = Buffer.from(raw.subarray(row * (stride + 1) + 1, (row + 1) * (stride + 1)))
const above = rows[row - 1]
for (let i = 0; i < stride; i++) {
const left = i >= 4 ? (line[i - 4] as number) : 0
const up = above ? (above[i] as number) : 0
const corner = above && i >= 4 ? (above[i - 4] as number) : 0
const predictor = [0, left, up, (left + up) >> 1, paeth(left, up, corner)][filter] as number
line[i] = ((line[i] as number) + predictor) & 0xff
}
rows.push(line)
}
const line = rows[y] as Buffer
return [line[x * 4], line[x * 4 + 1], line[x * 4 + 2], line[x * 4 + 3]] as number[]
}
function paeth(a: number, b: number, c: number): number {
const p = a + b - c
const [pa, pb, pc] = [Math.abs(p - a), Math.abs(p - b), Math.abs(p - c)]
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c
}
+193
View File
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The whole app of this template on the in-memory data source, wired the way apps/web and apps/worker
// wire it, for the notes module's tests: the registry of every module, one caller per actor, the
// outbox relay's behaviour and the worker's job runner.
//
// `deliver()` stands in for the outbox relay: it drains the outbox once, runs each record's
// subscribers in a child container with the record's organization and a system actor, and marks it
// published -- exactly once per subscriber, never more forgiving than the real relay. `runJobs()`
// stands in for the worker's job runner. What neither does is retry, so a test of a redelivery
// runs a handler again itself.
import { type Actor, buildRegistry, Container, createCaller, type TransportCaller } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import {
createMemoryDataSource,
DATA_SOURCE,
entitiesOf,
JOB_QUEUE,
OBJECT_STORE,
SYSTEM_DATA,
} from "@conusai-kit/data"
import { FILES_AUDIT, FILES_REGISTRY, FILES_SCOPE, FILES_USAGE } from "@conusai-kit/files"
import { PROPOSALS_REGISTRY } from "@conusai-kit/proposals"
import { createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy"
import {
fakeJobQueue,
fakeObjectStore,
recordingAudit,
recordingUsage,
uploadViaProcedures,
} from "@conusai-kit/testing"
import { appModules } from "../../modules/index.js"
import { NOTES_FLAGS, type NotesFlags } from "../../modules/notes/index.js"
export interface SeededProject {
readonly owner: Actor
readonly call: TransportCaller
readonly orgSlug: string
readonly orgId: string
readonly slugs: { org: string; workspace: string; project: string }
}
/** Standard base64 of some text, for the upload procedure's body. */
export const bytesOf = (text: string): Uint8Array => new TextEncoder().encode(text)
export function makeNotesApp(options: { flags?: NotesFlags } = {}) {
const registry = buildRegistry([...appModules])
const entities = entitiesOf(registry, "postgres")
const data = createMemoryDataSource({ entities })
const store = fakeObjectStore()
const queue = fakeJobQueue()
const audit = recordingAudit()
const usage = recordingUsage()
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
root.value(OBJECT_STORE, store)
root.value(JOB_QUEUE, queue)
root.value(FILES_REGISTRY, registry)
root.value(FILES_AUDIT, audit)
root.value(FILES_USAGE, usage)
root.value(PROPOSALS_REGISTRY, registry)
if (options.flags) root.value(NOTES_FLAGS, options.flags)
const caller = (actor: Actor): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy }, { actor })
}
const signUp = async (email: string): Promise<Actor> => {
const user = await data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
/** Runs every subscriber of every record in the outbox, once each; answers how many ran. */
const deliver = async (): Promise<number> => {
let ran = 0
for (const record of await data.outbox.pending(500)) {
const entry = registry.events[record.name]
for (const subscriber of entry?.subscribers ?? []) {
const container = root.child()
container.value(FILES_SCOPE, { level: "org", actor: systemActor("app.relay"), orgId: record.orgId })
await subscriber.handler(
{ name: record.name, payload: record.payload },
{ container, correlationId: record.id },
)
ran += 1
}
await data.outbox.markPublished([record.id])
}
return ran
}
/** The jobs not yet run, in the order they were enqueued. */
let consumed = 0
const runJobs = async (): Promise<number> => {
let ran = 0
while (consumed < queue.jobs.length) {
const job = queue.jobs[consumed] as (typeof queue.jobs)[number]
consumed += 1
const registered = registry.jobs[job.job]
if (!registered) throw new Error(`no module declares the job ${job.job}`)
const container = root.child()
container.value(TENANCY, createTenancyHooks({ data, registry }))
container.value(FILES_SCOPE, { ...job.scope, actor: systemActor(job.job) })
await registered.def.handler(registered.def.payload.parse(job.payload), {
container,
correlationId: job.id,
attempt: 1,
})
ran += 1
}
return ran
}
async function seedProject(slug: string, email: string): Promise<SeededProject> {
const owner = await signUp(email)
const call = caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string; slug: string }
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "notes",
})) as { slug: string }
return {
owner,
call,
orgSlug: org.slug,
orgId: org.orgId,
slugs: { org: org.slug, workspace: workspace.slug, project: project.slug },
}
}
/** A signed-up user who joined the project's organization with a role. */
async function addMember(project: SeededProject, email: string, role: "viewer" | "member" | "admin") {
const actor = await signUp(email)
const invite = (await project.call.callByName("tenancy.member.invite", {
org: project.orgSlug,
email,
role,
})) as { token: string }
const call = caller(actor)
await call.callByName("tenancy.invitation.accept", { org: project.orgSlug, token: invite.token })
return { actor, call }
}
/** Uploads a small text file through the files module, the way the page's form does. */
async function upload(project: SeededProject, filename = "minutes.txt", text = "agreed") {
return await uploadViaProcedures(project.call, project.slugs, {
filename: filename,
contentType: "text/plain",
bytes: bytesOf(text),
})
}
return {
registry,
/** The process container the procedures resolve their ports from (a seed script needs it). */
root,
data,
entities,
store,
queue,
audit,
usage,
caller,
signUp,
deliver,
runJobs,
seedProject,
addMember,
upload,
}
}
export type NotesApp = ReturnType<typeof makeNotesApp>
+61
View File
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The OCR path of a test: the real `@conusai-sdk/ocr` client (through kit-platform's `DocumentReader`)
// over HTTP to the fake platform of @conusai-kit/testing, which accepts a job and answers it as the
// platform's job API does. Nothing here stands in for the OCR path; `observed` only watches it, and can
// make one call fail the way a network does, which the fake platform has no way to do.
import { type DocumentOcr, sdkDocumentOcr } from "@app/modules/documents"
import { createDocumentReader } from "@conusai-kit/platform"
import { startFakePlatform } from "@conusai-kit/testing/fake-platform"
export type FakePlatform = Awaited<ReturnType<typeof startFakePlatform>>
export interface ObservedOcr extends DocumentOcr {
/** Every document accepted, with the idempotency key it came with. */
readonly submitted: Array<{ jobId: number; filename: string; idempotencyKey: string }>
/** Every `wait`, by job id: a retried job must resume its job and not submit another. */
readonly waited: number[]
/** The next `wait` throws this, once, before asking the platform anything. */
failWaitOnce(error: Error): void
}
export function observed(inner: DocumentOcr): ObservedOcr {
const submitted: ObservedOcr["submitted"] = []
const waited: number[] = []
let nextWait: Error | undefined
return {
submitted,
waited,
failWaitOnce(error) {
nextWait = error
},
async submit(bytes, options) {
const accepted = await inner.submit(bytes, options)
// The same key is the same job on the platform; a repeat is not a second submission.
if (!submitted.some((entry) => entry.jobId === accepted.jobId)) {
submitted.push({ jobId: accepted.jobId, filename: options.filename, idempotencyKey: options.idempotencyKey })
}
return accepted
},
page: (jobId, page, options) => inner.page(jobId, page, options),
async wait(jobId, options) {
waited.push(jobId)
if (nextWait) {
const error = nextWait
nextWait = undefined
throw error
}
return inner.wait(jobId, options)
},
}
}
/** A fake platform that reads every document as `document`, and the app-side OCR port over it. */
export async function startOcr(
document: Record<string, unknown>,
): Promise<{ platform: FakePlatform; ocr: ObservedOcr }> {
const platform = await startFakePlatform()
platform.ocr.setDocument(document)
const reader = createDocumentReader({ CONUSAI_API_URL: `${platform.url}/api`, CONUSAI_API_TOKEN: "deployment-token" })
return { platform, ocr: observed(sdkDocumentOcr(reader, { intervalMs: 1 })) }
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.paths.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"types": ["node"],
"noEmit": true,
"lib": ["ES2023", "dom"]
},
"include": ["**/*.ts", "../vitest.config.ts", "../scripts/*.cjs"],
"//": "The tests read browser-facing modules as well as server ones, so the DOM library is in scope here even though nothing in test/ runs in a browser."
}