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
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The one route every module's pages are mounted through: `/<org>/<workspace>/<project>/<module>/...`.
//
// A module that brings pages lists them in `modules/index.ts` (`modulePages`), keyed by the segment
// it answers; this route looks the segment up, calls the module's server component with the
// request's own caller and messages, and wraps what it returns in the transport its client
// components call procedures through. A new module therefore adds no route file here. A segment no
// module answers, a path the module does not know and a record of another tenant are the same
// not-found page.
import { modulePages } from "@app/modules"
import { AppError } from "@conusai-kit/core"
import { notFound } from "next/navigation"
import type { ReactElement } from "react"
import { webRuntime } from "../../../../../../src/server/app"
import { pageContext } from "../../../../../../src/server/page"
import { AppTransport } from "../../../../../../src/ui/transport"
export const dynamic = "force-dynamic"
export default async function ModulePage({
params,
}: {
params: Promise<{ org: string; workspace: string; project: string; module: string; rest?: string[] }>
}): Promise<ReactElement> {
const { module, rest, ...slugs } = await params
if (!webRuntime().registry.modules.some((entry) => entry.id === module)) notFound()
// `Object.hasOwn`: a segment named `constructor` is not a module.
const load = Object.hasOwn(modulePages, module) ? modulePages[module] : undefined
if (!load) notFound()
const { caller, i18n, container } = await pageContext()
const page = await load()
const content = await page({ slugs, segments: rest ?? [], caller, i18n, container }).catch((error: unknown) => {
if (error instanceof AppError && error.status === 404) notFound()
throw error
})
return <AppTransport>{content}</AppTransport>
}
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// One document: its extracted lines, each with where it was read, and the reviewer's decisions.
// Approve, edit and reject are procedure calls from the client component; nothing is exported
// until a line is approved, and the export is the last control on the page.
import { PROFILE } from "@app/config"
import { documentGet } from "@app/modules/documents"
import { ExportButton, exportLabels, LineReview, reviewLabels } from "@app/modules/documents/ui"
import { AppError } from "@conusai-kit/core"
import { notFound } from "next/navigation"
import type { ReactElement } from "react"
import { pageContext } from "../../../../../../src/server/page"
import { AppTransport } from "../../../../../../src/ui/transport"
export const dynamic = "force-dynamic"
export default async function DocumentPage({
params,
}: {
params: Promise<{ org: string; workspace: string; project: string; fileId: string }>
}): Promise<ReactElement> {
const { fileId, ...slugs } = await params
if (PROFILE === "convex") notFound()
const { caller, i18n } = await pageContext()
const detail = await caller.call(documentGet, { ...slugs, fileId }).catch((error: unknown) => {
// A document of another organization and one that does not exist are the same page.
if (error instanceof AppError && error.status === 404) notFound()
throw error
})
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`documents.${key}`, values)
const failed = i18n.message("app.failed")
const { document, lines } = detail
return (
<>
<p>
<a href={`/${slugs.org}/${slugs.workspace}/${slugs.project}/documents`}>
{i18n.message("documents.review.back")}
</a>
</p>
<h1>{i18n.message("documents.review.title", { filename: document.filename })}</h1>
<p>
{i18n.message("documents.list.status")}: {i18n.message(`documents.status.${document.status}`)}
{document.failureCode ? ` ${i18n.message(`documents.failure.${document.failureCode}`)}` : ""}
</p>
<p>
{i18n.message("documents.review.pendingCount", { count: document.pending })}.{" "}
{i18n.message("documents.review.approvedCount", { count: document.approved })}.
</p>
{document.status === "queued" || document.status === "processing" ? (
<p role="status">{i18n.message("documents.review.waiting")}</p>
) : null}
{document.skippedPages.length > 0 ? (
<section aria-labelledby="skipped-pages">
<h2 id="skipped-pages">{i18n.message("documents.review.skippedTitle")}</h2>
<ul>
{document.skippedPages.map((skipped) => (
<li key={skipped.page}>
{i18n.message("documents.review.skippedPage", {
page: skipped.page,
kind: i18n.message(`documents.review.pageClass.${skipped.pageClass}`),
})}
</li>
))}
</ul>
</section>
) : null}
<AppTransport>
<LineReview
slugs={slugs}
fileId={fileId}
lines={lines}
labels={reviewLabels(t, {
failed,
aiDisclosure: i18n.message("ui.aiDisclosure"),
pageWord: i18n.message("ui.page"),
})}
locale={i18n.locale}
/>
<ExportButton slugs={slugs} fileId={fileId} approved={document.approved} labels={exportLabels(t, failed)} />
</AppTransport>
</>
)
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The project's documents: upload one, and see where each is in being read and reviewed. Every
// read is a procedure through the same caller the API uses; the upload is a client form that
// calls the files module's own procedure (spec §6.2: one API path).
import { PROFILE } from "@app/config"
import { documentList, READABLE_CONTENT_TYPES } from "@app/modules/documents"
import { DocumentList, listLabels, UploadForm, uploadLabels } from "@app/modules/documents/ui"
import { downloadHref } from "@conusai-kit/ui/download"
import { notFound } from "next/navigation"
import type { ReactElement } from "react"
import { pageContext } from "../../../../../src/server/page"
import { AppTransport } from "../../../../../src/ui/transport"
export const dynamic = "force-dynamic"
export default async function DocumentsPage({
params,
}: {
params: Promise<{ org: string; workspace: string; project: string }>
}): Promise<ReactElement> {
const slugs = await params
if (PROFILE === "convex") notFound()
const { caller, i18n } = await pageContext()
const { documents } = await caller.call(documentList, slugs)
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`documents.${key}`, values)
return (
<>
<h1>{i18n.message("documents.title")}</h1>
<p>{i18n.message("documents.intro")}</p>
<AppTransport>
<UploadForm
slugs={slugs}
accept={READABLE_CONTENT_TYPES}
labels={uploadLabels(t, i18n.message("app.failed"))}
/>
</AppTransport>
<DocumentList
documents={documents}
hrefFor={(fileId) => `/${slugs.org}/${slugs.workspace}/${slugs.project}/documents/${fileId}`}
downloadHrefFor={(fileId) => downloadHref("files.file.download", { ...slugs, fileId })}
labels={listLabels(t)}
formatDate={(iso) => i18n.date(new Date(iso))}
/>
</>
)
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The project layout: the project has to exist in this workspace of this organization, and a project
// of another tenant, an unknown slug and a project the person has no place in are one 404, answered
// before the response streams (spec §3.3). `tenancy.project.get` resolves the whole path from the
// organization down, each step a scoped read, so nothing here re-implements the wall.
import { projectGet } from "@conusai-kit/tenancy"
import type { ReactNode } from "react"
import { orFallback } from "../../../../src/server/guard"
import { pageContext } from "../../../../src/server/page"
export const dynamic = "force-dynamic"
export default async function ProjectLayout({
children,
params,
}: {
children: ReactNode
params: Promise<{ org: string; workspace: string; project: string }>
}): Promise<ReactNode> {
const { org, workspace, project } = await params
const { caller, path } = await pageContext()
await orFallback(path, () => caller.call(projectGet, { org, workspace, project }))
return children
}
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A project's home: what it is, and the pages of the modules that work in it (spec §3.1, §4.1).
// The entries come from the modules' manifests (src/ui/projects.ts), so a module added tomorrow
// appears here with no edit to this file.
import { projectGet, workspaceList } from "@conusai-kit/tenancy"
import type { ReactElement } from "react"
import { orFallback } from "../../../../src/server/guard"
import { pageContext } from "../../../../src/server/page"
import { Breadcrumb } from "../../../../src/ui/breadcrumb"
import { PROJECT_ENTRIES, projectHref } from "../../../../src/ui/projects"
export const dynamic = "force-dynamic"
export default async function ProjectHome({
params,
}: {
params: Promise<{ org: string; workspace: string; project: string }>
}): Promise<ReactElement> {
const { org, workspace, project } = await params
const { caller, i18n, path } = await pageContext()
const { found, workspaceName } = await orFallback(path, async () => {
const [found, { workspaces }] = await Promise.all([
caller.call(projectGet, { org, workspace, project }),
caller.call(workspaceList, { org }),
])
return { found, workspaceName: workspaces.find((entry) => entry.slug === workspace)?.name ?? workspace }
})
return (
<>
<Breadcrumb
label={i18n.message("shell.breadcrumb")}
crumbs={[
{ text: i18n.message("app.workspaces"), href: `/${org}/workspaces` },
{ text: workspaceName, href: `/${org}/${workspace}` },
{ text: found.name, href: projectHref(org, workspace, project) },
]}
/>
<h1>{i18n.message("project.title", { name: found.name })}</h1>
<p>{i18n.message("project.module", { module: found.moduleKey })}</p>
{found.archived ? <p role="status">{i18n.message("project.archived")}</p> : null}
<h2>{i18n.message("project.work")}</h2>
<ul>
{PROJECT_ENTRIES.map((entry) => (
<li key={entry.href}>
<a href={projectHref(org, workspace, project, entry)}>{i18n.message(entry.label)}</a>
</li>
))}
</ul>
</>
)
}
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The workspace layout: the workspace has to be one this person can see, and the page says 404 before
// it starts streaming if it is not (spec §3.3). A restricted workspace the person is not in is not in
// `tenancy.workspace.list`, so it is the same not-found as one that does not exist.
import { workspaceList } from "@conusai-kit/tenancy"
import { notFound } from "next/navigation"
import type { ReactNode } from "react"
import { orFallback } from "../../../src/server/guard"
import { pageContext } from "../../../src/server/page"
export const dynamic = "force-dynamic"
export default async function WorkspaceLayout({
children,
params,
}: {
children: ReactNode
params: Promise<{ org: string; workspace: string }>
}): Promise<ReactNode> {
const { org, workspace } = await params
const { caller, path } = await pageContext()
const { workspaces } = await orFallback(path, () => caller.call(workspaceList, { org }))
if (!workspaces.some((entry) => entry.slug === workspace)) notFound()
return children
}
+92
View File
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A workspace: its projects, and a form to create another (spec §3.1). The projects are listed
// through `tenancy.project.list`, which reads them inside the workspace's own scope -- a workspace of
// another organization, or one the person has no place in, is the not-found page.
import { projectList, workspaceList } from "@conusai-kit/tenancy"
import type { ReactElement } from "react"
import { orFallback } from "../../../src/server/guard"
import { holdsAtLeast, pageContext } from "../../../src/server/page"
import { Breadcrumb } from "../../../src/ui/breadcrumb"
import { ProcedureForm } from "../../../src/ui/procedure-form"
import { PROJECT_MODULE_KEYS, projectHref } from "../../../src/ui/projects"
export const dynamic = "force-dynamic"
export default async function WorkspaceHome({
params,
}: {
params: Promise<{ org: string; workspace: string }>
}): Promise<ReactElement> {
const { org, workspace } = await params
const page = await pageContext()
const { caller, i18n, path } = page
const { name, projects } = await orFallback(path, async () => {
const [{ projects }, { workspaces }] = await Promise.all([
caller.call(projectList, { org, workspace }),
caller.call(workspaceList, { org }),
])
// `projectList` answers for the workspace's scope; the name is the list's own, so the page does
// not need a procedure of its own to say it.
return { projects, name: workspaces.find((entry) => entry.slug === workspace)?.name ?? workspace }
})
const mayCreate = await holdsAtLeast(page, org, "editor")
return (
<>
<Breadcrumb
label={i18n.message("shell.breadcrumb")}
crumbs={[
{ text: i18n.message("app.workspaces"), href: `/${org}/workspaces` },
{ text: name, href: `/${org}/${workspace}` },
]}
/>
<h1>{i18n.message("workspace.title", { name })}</h1>
<h2>{i18n.message("workspace.projects")}</h2>
{projects.length === 0 ? (
<p>{i18n.message("workspace.noProjects")}</p>
) : (
<table>
<thead>
<tr>
<th scope="col">{i18n.message("app.name")}</th>
<th scope="col">{i18n.message("workspace.module")}</th>
</tr>
</thead>
<tbody>
{projects.map((project) => (
<tr key={project.slug}>
<td>
<a href={projectHref(org, workspace, project.slug)}>{project.name}</a>
</td>
<td>{project.moduleKey}</td>
</tr>
))}
</tbody>
</table>
)}
{mayCreate ? (
<>
<h2>{i18n.message("workspace.createProject")}</h2>
<ProcedureForm
procedure="tenancy.project.create"
hidden={{ org, workspace }}
submitLabel={i18n.message("app.create")}
failedTemplate={i18n.message("app.failed")}
fields={[
{ name: "name", label: i18n.message("app.name"), required: true },
{ name: "slug", label: i18n.message("app.slug"), required: true },
{
name: "moduleKey",
label: i18n.message("workspace.module"),
required: true,
options: PROJECT_MODULE_KEYS.map((key) => ({ value: key, label: key })),
},
]}
/>
</>
) : null}
</>
)
}
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The audit log is for administrators (spec §9.4). The role is asked for in this layout and not in the
// page: the page has a loading state, a loading state starts the response, and the response's status
// is fixed once it starts -- a member below `admin` must get a real 403, and a stranger a real 404.
import type { ReactNode } from "react"
import { requireRole } from "../../../src/server/guard"
import { pageContext } from "../../../src/server/page"
export const dynamic = "force-dynamic"
export default async function AuditLayout({
children,
params,
}: {
children: ReactNode
params: Promise<{ org: string }>
}): Promise<ReactNode> {
const { org } = await params
await requireRole(await pageContext(), org, "admin")
return children
}
+4
View File
@@ -0,0 +1,4 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The loading state of this page (src/ui/loading-page.tsx says why it is here and not above the layouts).
export { LoadingPage as default } from "../../../src/ui/loading-page"
+105
View File
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The audit viewer (spec §9.4). It reads the platform's audit log for this deployment with the
// deployment token, which carries `audit:read`, and shows only the events of this organization:
// every write goes through one function that puts the organization's slug in the event's target,
// and `isOwnedBy` is that rule read backwards (src/server/audit.ts).
//
// Administrators only, which the layout beside this page asks of the tenancy resolver
// (src/server/guard.ts) before the response starts.
import { orgGet } from "@conusai-kit/tenancy"
import { ConusAIClient } from "@conusai-sdk/node-sdk"
import type { ReactElement } from "react"
import { webRuntime } from "../../../src/server/app"
import { isOwnedBy } from "../../../src/server/audit"
import { pageContext } from "../../../src/server/page"
export const dynamic = "force-dynamic"
interface AuditRow {
id: number
action: string
outcome: string
occurred_at: string
resource_type?: string | null
resource_id?: string | null
}
/** How many of this organization's events the viewer shows, and how far back it scans for them. */
const PAGE_LIMIT = 200
const MAX_PAGES = 25
/**
* The platform's log is deployment-wide and pages newest-first with no server-side filter, so the
* organization's rows are collected by walking pages until the viewer's own limit is filled or the
* log ends -- a filter over the first page alone would show nothing once 200 newer events of other
* organizations exist.
*/
async function organizationEvents(org: { slug: string; id: string }): Promise<AuditRow[]> {
const { env } = webRuntime()
const platform = new ConusAIClient({
baseUrl: env.deployment.CONUSAI_API_URL,
apiKey: env.deployment.CONUSAI_API_TOKEN,
})
const rows: AuditRow[] = []
let cursor: string | null | undefined
for (let page = 0; page < MAX_PAGES && rows.length < PAGE_LIMIT; page += 1) {
const { data } = await platform.readEvents({
query: cursor ? { limit: PAGE_LIMIT, cursor } : { limit: PAGE_LIMIT },
throwOnError: true,
})
for (const row of (data?.items ?? []) as AuditRow[]) {
if (isOwnedBy(org, row.resource_id)) rows.push(row)
}
if (!data?.has_more || !data.next_cursor) break
cursor = data.next_cursor
}
return rows
}
export default async function Audit({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
const { org } = await params
const { caller, i18n } = await pageContext()
// The layout already established membership; the id is what subscriber-written events carry.
const organization = await caller.call(orgGet, { org })
const { observe } = webRuntime()
const rows = await organizationEvents({ slug: org, id: organization.orgId }).catch((error: unknown) => {
// The platform being unreachable is not the viewer's failure to explain to a customer; it is
// ours to know about, so it is logged, and the page says there is nothing to show.
observe.log.warn(
`audit viewer: the platform's log could not be read: ${error instanceof Error ? error.name : "failed"}`,
)
return []
})
return (
<>
<h1>{i18n.message("app.audit")}</h1>
{rows.length === 0 ? (
<p>{i18n.message("app.auditEmpty")}</p>
) : (
<table>
<thead>
<tr>
<th scope="col">{i18n.message("app.when")}</th>
<th scope="col">{i18n.message("app.action")}</th>
<th scope="col">{i18n.message("app.outcome")}</th>
<th scope="col">{i18n.message("app.resource")}</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>{i18n.date(new Date(row.occurred_at))}</td>
<td>{row.action}</td>
<td>{row.outcome}</td>
<td>{row.resource_type ?? ""}</td>
</tr>
))}
</tbody>
</table>
)}
</>
)
}
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// An organization's logo, served to its members from this app's own origin.
//
// It goes through the same caller as every page, so only a member of the organization reads it
// (`branding.logo.get`, `branding.brand.read`), and what comes back is bytes a checker approved when
// they were uploaded. The response is still treated as hostile: `nosniff` so a browser does not
// reinterpret it, a content security policy that allows nothing -- no script, no request, no
// framing -- so even an SVG that slipped past the checker runs nothing when it is opened directly,
// and `sandbox` for the same reason.
import { logoGet } from "@app/modules/branding"
import { AppError } from "@conusai-kit/core"
import { pageContext } from "../../../../../src/server/page"
export const dynamic = "force-dynamic"
export async function GET(
request: Request,
{ params }: { params: Promise<{ org: string; variant: string }> },
): Promise<Response> {
const { org, variant } = await params
if (variant !== "light" && variant !== "dark") return new Response(null, { status: 404 })
// `?s=1x` or `?s=2x`: the 1x or the 2x rendition of a raster logo (2x when absent; an SVG has one size).
const size = new URL(request.url).searchParams.get("s")
if (size !== null && size !== "1x" && size !== "2x") return new Response(null, { status: 404 })
try {
const { caller } = await pageContext()
const logo = await caller.call(logoGet, { org, variant, ...(size === null ? {} : { size }) })
return new Response(Buffer.from(logo.body, "base64"), {
headers: {
"content-type": logo.contentType,
"x-content-type-options": "nosniff",
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
// The logo's path carries its own content tag (`?v=`), so a changed logo is a new URL.
"cache-control": "private, max-age=31536000, immutable",
},
})
} catch (error) {
// A non-member, a missing organization and a missing logo are all the same answer.
return new Response(null, { status: error instanceof AppError && error.status < 500 ? 404 : 500 })
}
}
+41
View File
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// An organization's share image (spec §10.3): its display name and its logo for the dark page (its own, or
// the one it has when it has only one, or the app brand's), in the brandbook's treatment, 1200x630.
//
// It goes through the same caller as every page, so only a member of the organization draws it, and a
// non-member and a missing organization get the same 404. The logo is checked by the kit's rules wherever
// it comes from; one that cannot be drawn is replaced by the app brand's, and the image is still served.
import { appBrand } from "@app/config"
import { brandGet, renderShareImage, shareBrand } from "@app/modules/branding"
import { AppError } from "@conusai-kit/core"
import { orgGet } from "@conusai-kit/tenancy"
import { appIdentity, readersFor } from "../../../../src/server/brand-assets"
import { pageContext } from "../../../../src/server/page"
export const dynamic = "force-dynamic"
export async function GET(_request: Request, { params }: { params: Promise<{ org: string }> }): Promise<Response> {
const { org } = await params
try {
const { caller } = await pageContext()
const organization = await caller.call(orgGet, { org })
const brand = await caller.call(brandGet, { org })
const share = await shareBrand({
app: appIdentity(),
tenant: { name: organization.name, brand },
readers: readersFor(caller, org),
})
const png = await renderShareImage(share, { title: share.name, footer: appBrand.name })
return new Response(Buffer.from(png), {
headers: {
"content-type": "image/png",
"x-content-type-options": "nosniff",
"cache-control": "private, max-age=300",
},
})
} catch (error) {
// A non-member, a missing organization and a logo nobody can draw are not told apart.
return new Response(null, { status: error instanceof AppError && error.status < 500 ? 404 : 500 })
}
}
+43
View File
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The organization's brand: name, colours and logos, with a preview of both themes and the contrast
// each colour has there. Every member sees it; an administrator changes it (`branding.brand.write`,
// which the kernel checks before a handler runs), and a member without that permission is shown the
// same page read-only.
import { PROFILE } from "@app/config"
import { brandGet } from "@app/modules/branding"
import { BrandForm, brandFormLabels } from "@app/modules/branding/ui"
import { tokens } from "@conusai-kit/brand"
import { notFound } from "next/navigation"
import type { ReactElement } from "react"
import { orFallback } from "../../../src/server/guard"
import { holdsAtLeast, pageContext } from "../../../src/server/page"
import { AppTransport } from "../../../src/ui/transport"
export const dynamic = "force-dynamic"
export default async function BrandingPage({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
const { org } = await params
if (PROFILE === "convex") notFound()
const page = await pageContext()
const { caller, i18n, path } = page
const brand = await orFallback(path, () => caller.call(brandGet, { org }))
const canEdit = await holdsAtLeast(page, org, "admin")
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`branding.${key}`, values)
return (
<>
<h1>{i18n.message("branding.title")}</h1>
<p>{i18n.message("branding.intro")}</p>
<AppTransport>
<BrandForm
org={org}
brand={brand}
canEdit={canEdit}
defaultPrimary={tokens.color.dark.accent}
labels={brandFormLabels(t, i18n.message("app.failed"))}
/>
</AppTransport>
</>
)
}
+140
View File
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The organization layout. The scope is resolved on the server, once, by calling an
// organization-scoped procedure: a member who is not a member gets the same NotFound a stranger
// does, and nothing below this layout has to check again (spec §3.3, §6.4).
//
// It is also where the organization's own brand is applied (spec §10.3): the colours as custom
// properties on an element wrapped around everything the organization sees, so the first paint of
// every page is already branded, and the display name and logo in the switcher -- beside the
// organization switcher itself, for a person who belongs to more than one.
import { PROFILE } from "@app/config"
import { appModules } from "@app/modules"
import { type BrandView, logoUrl, tenantLogo, tenantStyle } from "@app/modules/branding"
import { atLeast, brandingSchema, meGet, ORG_ROLE_AS_ROLE, orgGet } from "@conusai-kit/tenancy"
import { AppShell } from "@conusai-kit/ui"
import { redirect } from "next/navigation"
import type { CSSProperties, ReactElement, ReactNode } from "react"
import { webRuntime } from "../../src/server/app"
import { orFallback, signInHref } from "../../src/server/guard"
import { pageContext } from "../../src/server/page"
import { ClientModules } from "../../src/ui/client-modules"
import { OrganizationSwitch } from "../../src/ui/shell-controls"
export const dynamic = "force-dynamic"
// No generateMetadata here: the organization's share image (`/{org}/brand/og`) answers 404 to
// anyone who is not a signed-in member, which is every link-preview crawler, and org existence is
// deliberately indistinguishable from the outside. The root layout's metadata therefore stands:
// the app's own public share image.
/** One stored logo, at 1x and at 2x for a high-density screen. */
function TenantLogo({ className, path }: { className: string; path: string }): ReactElement {
return (
<img
className={className}
src={logoUrl(path, "1x")}
srcSet={`${logoUrl(path, "1x")} 1x, ${logoUrl(path, "2x")} 2x`}
alt=""
/>
)
}
/** The modules' navigation, with each link made absolute inside this organization. */
const navigationFor = (org: string) =>
appModules
.filter((module) => PROFILE !== "convex" || ["tenancy", "auth", "files", "notes"].includes(module.id))
.map((module) =>
module.ui?.nav
? {
...module,
ui: { ...module.ui, nav: module.ui.nav.map((item) => ({ ...item, href: `/${org}/${item.href}` })) },
}
: module,
)
/** What `organizations.branding` holds, or nothing: a value that does not parse is not applied, whatever wrote it. */
function brandOf(raw: unknown): BrandView {
const parsed = brandingSchema.safeParse(raw)
const branding = parsed.success ? parsed.data : {}
return {
displayName: branding.displayName ?? null,
primary: branding.primary ?? null,
accent: branding.accent ?? null,
logoLight: branding.logoLight ?? null,
logoDark: branding.logoDark ?? null,
}
}
export default async function OrgLayout({
children,
params,
}: {
children: ReactNode
params: Promise<{ org: string }>
}): Promise<ReactElement> {
const { org } = await params
const { caller, i18n, signedIn, path } = await pageContext()
if (!signedIn) redirect(signInHref(path))
// A stranger and a missing organization both end at the not-found page (guard.ts): the words and
// the status are the same, so the layout cannot be used to ask which organizations exist.
const [organization, me] = await orFallback(path, () =>
Promise.all([caller.call(orgGet, { org }), caller.call(meGet, undefined)]),
)
const role = me.organizations.find((entry) => entry.slug === org)?.role
// The permissions this membership holds, so the shell drops nav items that would only be
// refused (kit-ui filters when the prop is given; the registry is the permission source).
const heldPermissions =
role === undefined
? []
: Object.entries(webRuntime().registry.permissions)
.filter(([, declared]) => atLeast(ORG_ROLE_AS_ROLE[role], declared.role))
.map(([name]) => name)
// The audit log and the brand are for administrators; a link a member would only be refused at is noise.
const administers = role === "admin" || role === "owner"
const brand = brandOf(organization.branding)
// A theme shows its own logo, or the other one when only one is uploaded (`tenantLogo`, the branding contract).
const lightLogo = tenantLogo(brand, "light")
const darkLogo = tenantLogo(brand, "dark")
// The shell renders the <main> every page inside this layout fills, so an organization page
// returns its content and not a second landmark (kit-ui's AppShell is the accessible one).
return (
<div
data-tenant-brand={brand.primary ?? brand.accent ?? undefined}
style={tenantStyle(brand).style as CSSProperties}
>
<AppShell
modules={navigationFor(org)}
scope="org"
t={(key) => i18n.message(key)}
navigationLabel={i18n.message("ui.navigation")}
permissions={heldPermissions}
switcher={
<>
<a href={`/${org}`} className="app-brand-link">
{darkLogo ? <TenantLogo className="app-logo app-logo-dark" path={darkLogo.path} /> : null}
{lightLogo ? <TenantLogo className="app-logo app-logo-light" path={lightLogo.path} /> : null}
{brand.displayName ?? organization.name}
</a>
{me.organizations.length > 1 ? (
<OrganizationSwitch
current={org}
label={i18n.message("shell.switchOrganization")}
organizations={me.organizations.map((entry) => ({ slug: entry.slug, name: entry.name }))}
/>
) : null}
{administers ? <a href={`/${org}/audit`}>{i18n.message("app.audit")}</a> : null}
{administers && PROFILE !== "convex" ? (
<a href={`/${org}/branding`}>{i18n.message("branding.title")}</a>
) : null}
</>
}
>
{children}
</AppShell>
{/* The enabled modules' browser providers (an in-app guide's overlay), beside the page. */}
<ClientModules context={{ org, locale: i18n.locale, user: me.userId }} />
</div>
)
}
+4
View File
@@ -0,0 +1,4 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The loading state of this page (src/ui/loading-page.tsx says why it is here and not above the layouts).
export { LoadingPage as default } from "../../../src/ui/loading-page"
+113
View File
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Members, their roles and the organization's open invitations (spec §3.2). Every change is a
// procedure call: the role scale, the "nobody grants a role above their own" rule and the last
// owner's protection are kit-tenancy's, not this page's. What this page decides is only who is shown
// the controls: administrators, who hold `tenancy.member.invite` and `tenancy.member.manage`.
import { invitationList, memberList, ORG_ROLES } from "@conusai-kit/tenancy"
import type { ReactElement } from "react"
import { orFallback } from "../../../src/server/guard"
import { holdsAtLeast, pageContext } from "../../../src/server/page"
import { InviteMember } from "../../../src/ui/invite-member"
import { ProcedureForm } from "../../../src/ui/procedure-form"
export const dynamic = "force-dynamic"
export default async function Members({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
const { org } = await params
const page = await pageContext()
const { caller, i18n, path } = page
const { members } = await orFallback(path, () => caller.call(memberList, { org }))
const administers = await holdsAtLeast(page, org, "admin")
const { invitations } = administers
? await orFallback(path, () => caller.call(invitationList, { org }))
: { invitations: [] }
const roleLabel = (role: string): string => i18n.message(`roles.${role}`)
const roleOptions = ORG_ROLES.map((role) => ({ value: role, label: roleLabel(role) }))
return (
<>
<h1>{i18n.message("app.members")}</h1>
<p>{i18n.message("app.memberCount", { count: members.length })}</p>
<table>
<thead>
<tr>
<th scope="col">{i18n.message("app.name")}</th>
<th scope="col">{i18n.message("app.email")}</th>
<th scope="col">{i18n.message("app.role")}</th>
</tr>
</thead>
<tbody>
{members.map((member) => (
<tr key={member.userId}>
<td>{member.displayName}</td>
<td>{member.email}</td>
<td>{roleLabel(member.role)}</td>
</tr>
))}
</tbody>
</table>
{administers ? (
<>
<h2>{i18n.message("app.invite")}</h2>
<InviteMember
org={org}
roles={roleOptions}
labels={{
email: i18n.message("app.email"),
role: i18n.message("app.role"),
submit: i18n.message("app.invite"),
created: i18n.message("members.created"),
link: i18n.message("members.link"),
failed: i18n.message("app.failed"),
}}
/>
<h2>{i18n.message("members.openInvitations")}</h2>
{invitations.length === 0 ? (
<p>{i18n.message("members.noInvitations")}</p>
) : (
<table>
<thead>
<tr>
<th scope="col">{i18n.message("app.email")}</th>
<th scope="col">{i18n.message("app.role")}</th>
<th scope="col">{i18n.message("members.invitedBy")}</th>
<th scope="col">{i18n.message("members.expires")}</th>
</tr>
</thead>
<tbody>
{invitations.map((invitation) => (
<tr key={invitation.invitationId}>
<td>{invitation.email}</td>
<td>{roleLabel(invitation.role)}</td>
<td>{invitation.inviter.displayName}</td>
<td>{i18n.date(new Date(invitation.expiresAt))}</td>
</tr>
))}
</tbody>
</table>
)}
<h2>{i18n.message("members.changeRole")}</h2>
<ProcedureForm
procedure="tenancy.member.change-role"
hidden={{ org }}
submitLabel={i18n.message("app.save")}
failedTemplate={i18n.message("app.failed")}
fields={[
{
name: "userId",
label: i18n.message("app.members"),
required: true,
options: members.map((member) => ({ value: member.userId, label: member.email })),
},
{ name: "role", label: i18n.message("app.role"), required: true, options: roleOptions },
]}
/>
</>
) : null}
</>
)
}
+55
View File
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The organization's workspaces and the projects of each (spec §3.1).
import { projectList, workspaceList } from "@conusai-kit/tenancy"
import type { ReactElement } from "react"
import { orFallback } from "../../src/server/guard"
import { pageContext } from "../../src/server/page"
import { PROJECT_ENTRIES, projectHref } from "../../src/ui/projects"
export const dynamic = "force-dynamic"
export default async function OrgHome({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
const { org } = await params
const { caller, i18n, path } = await pageContext()
const projects = await orFallback(path, async () => {
const { workspaces } = await caller.call(workspaceList, { org })
return Promise.all(
workspaces.map(async (workspace) => ({
workspace,
projects: (await caller.call(projectList, { org, workspace: workspace.slug })).projects,
})),
)
})
return (
<>
<h1>{i18n.message("app.workspaces")}</h1>
<p>
<a href={`/${org}/workspaces`}>{i18n.message("workspace.create")}</a>
</p>
{projects.length === 0 ? <p>{i18n.message("workspace.none")}</p> : null}
{projects.map(({ workspace, projects: inWorkspace }) => (
<section key={workspace.slug}>
<h2>
<a href={`/${org}/${workspace.slug}`}>{workspace.name}</a>
</h2>
<p>{i18n.message("workspace.projectCount", { count: inWorkspace.length })}</p>
<ul>
{inWorkspace.map((project) => (
<li key={project.slug}>
<a href={projectHref(org, workspace.slug, project.slug)}>{project.name}</a>:
{PROJECT_ENTRIES.map((entry) => (
<span key={entry.href}>
{" "}
<a href={projectHref(org, workspace.slug, project.slug, entry)}>{i18n.message(entry.label)}</a>
</span>
))}
</li>
))}
</ul>
</section>
))}
</>
)
}
+70
View File
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The organization's own settings. `tenancy.org.update` needs the admin role, so the page asks for
// it (src/server/guard.ts): a member below it gets the forbidden page, a stranger the not-found
// page. Workspaces are created on their own page (`/{org}/workspaces`).
import { settingsGet } from "@app/modules/knowledge"
import { SettingsSection } from "@app/modules/knowledge/ui"
import { orgGet } from "@conusai-kit/tenancy"
import type { ReactElement } from "react"
import { orFallback, requireRole } from "../../../src/server/guard"
import { pageContext } from "../../../src/server/page"
export const dynamic = "force-dynamic"
export default async function Settings({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
const { org } = await params
const page = await pageContext()
const { caller, i18n, path } = page
await requireRole(page, org, "admin")
const organization = await orFallback(path, () => caller.call(orgGet, { org }))
const knowledge = await orFallback(path, () => caller.call(settingsGet, { org }))
// The request's i18n already carries every module's catalogue (kit-i18n merges them from the
// manifests), so the module's own keys resolve with no import of its messages here.
const t = (key: string) => i18n.message(`knowledge.${key}`)
return (
<>
<h1>{i18n.message("app.settings")}</h1>
<dl>
<dt>{i18n.message("app.name")}</dt>
<dd>{organization.name}</dd>
<dt>{i18n.message("app.slug")}</dt>
<dd>{organization.slug}</dd>
<dt>{i18n.message("app.status")}</dt>
<dd>{organization.status}</dd>
</dl>
<p>
<a href={`/${org}/workspaces`}>{i18n.message("app.workspaces")}</a>
</p>
<SettingsSection
org={org}
initial={knowledge}
labels={{
heading: t("settings.heading"),
state: t("settings.state"),
stateOn: t("settings.stateOn"),
stateOff: t("settings.stateOff"),
backend: t("settings.backend"),
backendVectors: t("settings.backendVectors"),
source: t("settings.source"),
sourceOperator: t("settings.sourceOperator"),
sourceManaged: t("settings.sourceManaged"),
endpoint: t("settings.endpoint"),
enable: t("settings.enable"),
enabling: t("settings.enabling"),
disable: t("settings.disable"),
disabling: t("settings.disabling"),
enabledDone: t("settings.enabledDone"),
disabledDone: t("settings.disabledDone"),
planRequired: t("settings.planRequired"),
featureDisabled: t("settings.featureDisabled"),
quotaExhausted: t("settings.quotaExhausted"),
upgrade: t("settings.upgrade"),
hint: t("settings.hint"),
failed: i18n.message("app.failed"),
}}
/>
</>
)
}
+79
View File
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The workspaces of the organization, and a form to create one (spec §3.1). A workspace is where
// projects live and where a restricted group of people works apart from the rest of the
// organization; creating one is for administrators (`tenancy.workspace.manage`), so the form is shown
// to them and the kernel refuses everybody else regardless.
import { workspaceList } from "@conusai-kit/tenancy"
import type { ReactElement } from "react"
import { orFallback } from "../../../src/server/guard"
import { holdsAtLeast, pageContext } from "../../../src/server/page"
import { ProcedureForm } from "../../../src/ui/procedure-form"
export const dynamic = "force-dynamic"
export default async function Workspaces({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
const { org } = await params
const page = await pageContext()
const { caller, i18n, path } = page
const { workspaces } = await orFallback(path, () => caller.call(workspaceList, { org }))
const mayCreate = await holdsAtLeast(page, org, "admin")
return (
<>
<h1>{i18n.message("workspace.list")}</h1>
{workspaces.length === 0 ? (
<p>{i18n.message("workspace.none")}</p>
) : (
<table>
<thead>
<tr>
<th scope="col">{i18n.message("app.name")}</th>
<th scope="col">{i18n.message("workspace.visibility")}</th>
</tr>
</thead>
<tbody>
{workspaces.map((workspace) => (
<tr key={workspace.slug}>
<td>
<a href={`/${org}/${workspace.slug}`}>{workspace.name}</a>
{workspace.isDefault ? ` (${i18n.message("workspace.default")})` : ""}
</td>
<td>
{workspace.visibility === "org"
? i18n.message("workspace.visibilityOrg")
: i18n.message("workspace.visibilityRestricted")}
</td>
</tr>
))}
</tbody>
</table>
)}
{mayCreate ? (
<>
<h2>{i18n.message("workspace.create")}</h2>
<ProcedureForm
procedure="tenancy.workspace.create"
hidden={{ org }}
submitLabel={i18n.message("app.create")}
failedTemplate={i18n.message("app.failed")}
fields={[
{ name: "name", label: i18n.message("app.name"), required: true },
{ name: "slug", label: i18n.message("app.slug"), required: true },
{
name: "visibility",
label: i18n.message("workspace.visibility"),
required: true,
options: [
{ value: "org", label: i18n.message("workspace.visibilityOrg") },
{ value: "restricted", label: i18n.message("workspace.visibilityRestricted") },
],
},
]}
/>
</>
) : null}
</>
)
}