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}
</>
)
}
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Every HTTP entry point of this app but the sign-in redirects: /api/health, /api/ready,
// /api/openapi.json and /api/v1/<procedure>. One file, because there is one mount (spec §6.2).
import { handle } from "../../../src/server/router"
// A procedure call reads a session and a database; nothing here may be cached or prerendered.
export const dynamic = "force-dynamic"
export const runtime = "nodejs"
export const GET = handle
export const POST = handle
// A procedure whose payload is a byte stream (kit-core .stream(field)) is answered on PUT.
export const PUT = handle
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// /api/auth/*: the same-origin route from this app's pages to Better Auth on its Convex deployment
// (AUTH_PROVIDER=convex-better-auth). Only the endpoints a sign-in, sign-up, sign-out, password
// change, device revocation or account deletion needs are forwarded, and no bearer comes back
// (@conusai-kit/auth-convex-better-auth's proxy). A deployment that selected another provider has no
// such route.
import { loadAuthConfig } from "@conusai-kit/auth"
import { betterAuthRuntime } from "../../../../src/server/app"
export const dynamic = "force-dynamic"
export const runtime = "nodejs"
async function handle(request: Request): Promise<Response> {
if (loadAuthConfig().provider !== "convex-better-auth") return new Response(null, { status: 404 })
return betterAuthRuntime().proxy(request)
}
export const GET = handle
export const POST = handle
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The app's own share image (Open Graph and Twitter card, spec §10.1): the app brand's name and logo in
// the brandbook's treatment, 1200x630. It is public, because a link preview is fetched without a session,
// and it carries nothing of any organization. The image only changes when the brand file does, so it is
// drawn once per process.
import { appBrand } from "@app/config"
import { renderShareImage, shareBrand } from "@app/modules/branding"
import { appIdentity, appOnlyReaders } from "../../../../src/server/brand-assets"
export const dynamic = "force-dynamic"
let drawn: Promise<Buffer<ArrayBuffer>> | undefined
export async function GET(): Promise<Response> {
try {
drawn ??= (async () => {
const share = await shareBrand({ app: appIdentity(), tenant: null, readers: appOnlyReaders() })
return Buffer.from(await renderShareImage(share, { title: appBrand.name }))
})()
return new Response(await drawn, {
headers: {
"content-type": "image/png",
"x-content-type-options": "nosniff",
"cache-control": "public, max-age=3600",
},
})
} catch {
drawn = undefined
return new Response(null, { status: 500 })
}
}
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// GET /auth/callback: the provider sends the browser back here (MIB I3's path). The code is
// exchanged on the server, the tokens are sealed into the session row, and the browser receives
// one sealed cookie whose plaintext is a random handle. A failure of any step ends at the sign-in
// page with a code for the person to read, never at a 500 (src/server/auth-response.ts).
import { authRoutes } from "../../../src/server/app"
import { authResponse } from "../../../src/server/auth-response"
export const dynamic = "force-dynamic"
export const runtime = "nodejs"
export async function GET(request: Request): Promise<Response> {
return authResponse(request, async () => (await authRoutes()).callback(request))
}
+31
View File
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// GET /auth/dev-login?email=…&return_to=…: a sign-in for a developer with no identity provider.
// @conusai-kit/auth refuses to build it in a production deployment, and refuses again at boot if
// AUTH_DEV_LOGIN is set there, so this route cannot become a way in (spec §7).
import { createDevLogin, loadAuthConfig } from "@conusai-kit/auth"
import { authRoutes, webRuntime } from "../../../src/server/app"
import { safeReturnTo, signInErrorHref } from "../../../src/server/auth-errors"
import { authResponse, redirectTo } from "../../../src/server/auth-response"
export const dynamic = "force-dynamic"
export const runtime = "nodejs"
export async function GET(request: Request): Promise<Response> {
const config = loadAuthConfig()
if (!config.devLogin) return new Response("dev login is not enabled for this deployment", { status: 404 })
return authResponse(request, async () => {
const runtime = webRuntime()
const data = runtime.authData ?? runtime.data
if (!data) throw new Error("The selected profile has no auth data source")
const { sessions } = await authRoutes()
const response = await createDevLogin({ config, data, sessions }).signIn(request)
// An address the adapter would not take is a failed sign-in like any other, not a bare 400.
if (response.status === 400) return redirectTo(signInErrorHref("failed"))
// The adapter always lands on `/`; the person was going somewhere (the invitation they were sent).
const target = safeReturnTo(new URL(request.url).searchParams.get("return_to"))
if (response.status !== 302 || target === "/") return response
const headers = new Headers(response.headers)
headers.set("location", target)
return new Response(null, { status: 302, headers })
})
}
+79
View File
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The sign-in page (spec §7): where a visitor lands when they need to be signed in, and where a
// failed sign-in ends (src/server/auth-response.ts). It says what went wrong in the person's
// language -- from a fixed set of codes, never from the provider's text -- and offers the way in the
// deployment has: its identity provider, or, in development with none, any e-mail address.
import { loadAuthConfig } from "@conusai-kit/auth"
import { redirect } from "next/navigation"
import type { ReactElement } from "react"
import { AUTH_ERROR_MESSAGES, parseAuthError, safeReturnTo } from "../../../src/server/auth-errors"
import { betterAuthFormLabels, MIN_PASSWORD_LENGTH } from "../../../src/server/better-auth" // kit:convex-better-auth
import { pageContext } from "../../../src/server/page"
import { BetterAuthForm } from "../../../src/ui/better-auth-form" // kit:convex-better-auth
import { StateView } from "../../../src/ui/state-view"
export const dynamic = "force-dynamic"
export default async function SignIn({
searchParams,
}: {
searchParams: Promise<{ error?: string; return_to?: string }>
}): Promise<ReactElement> {
const { error, return_to: returnToParam } = await searchParams
const { i18n, signedIn } = await pageContext()
const returnTo = safeReturnTo(returnToParam)
// Somebody who is already in has nothing to do here, unless they were sent back with an error.
const code = parseAuthError(error)
if (signedIn && !code) redirect(returnTo)
const config = loadAuthConfig()
const next = returnTo === "/" ? "" : `?return_to=${encodeURIComponent(returnTo)}`
const betterAuth = config.provider === "convex-better-auth" // kit:convex-better-auth
return (
<StateView title={i18n.message("signin.title")}>
{code ? (
<p className="app-error" role="alert">
{i18n.message(AUTH_ERROR_MESSAGES[code])}
</p>
) : (
<p>{i18n.message("signin.intro")}</p>
)}
{config.oidc ? (
<p>
<a href={`/auth/sign-in/start${next}`}>{i18n.message("signin.continue")}</a>
</p>
) : null}
{!config.oidc && config.devLogin ? (
<form method="get" action="/auth/dev-login">
<h2>{i18n.message("signin.devTitle")}</h2>
<p>{i18n.message("signin.devHelp")}</p>
{returnTo === "/" ? null : <input type="hidden" name="return_to" value={returnTo} />}
<label className="kit-field" htmlFor="dev-email">
{i18n.message("app.email")}
<input id="dev-email" name="email" type="email" required autoComplete="email" />
</label>
<button type="submit">{i18n.message("signin.devSubmit")}</button>
</form>
) : null}
{/* kit:convex-better-auth:begin */}
{betterAuth ? (
<>
<BetterAuthForm
mode="sign-in"
returnTo={returnTo}
minPasswordLength={MIN_PASSWORD_LENGTH}
labels={betterAuthFormLabels(i18n, "sign-in")}
/>
<p>
<a href={`/auth/sign-up${next}`}>{i18n.message("signin.account.toSignUp")}</a>
</p>
</>
) : null}
{/* kit:convex-better-auth:end */}
{!config.oidc && !config.devLogin && config.provider === "oidc" ? (
<p role="status">{i18n.message("signin.notConfigured")}</p>
) : null}
</StateView>
)
}
+16
View File
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// GET /auth/sign-in/start: the redirect to the identity provider (spec §7). The transaction cookie it
// sets carries the state, the nonce and the PKCE verifier, sealed; the browser gets no token. The
// page people see before it is /auth/sign-in. A deployment with no provider has nothing to redirect
// to, and sends the visitor back to that page, which says so.
import { loadAuthConfig } from "@conusai-kit/auth"
import { authRoutes } from "../../../../src/server/app"
import { authResponse, redirectTo } from "../../../../src/server/auth-response"
export const dynamic = "force-dynamic"
export const runtime = "nodejs"
export async function GET(request: Request): Promise<Response> {
if (!loadAuthConfig().oidc) return redirectTo("/auth/sign-in")
return authResponse(request, async () => (await authRoutes()).signIn(request))
}
+29
View File
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// POST /auth/sign-out: revokes the session row and redirects to the provider's end-session
// endpoint, which sends the browser to /auth/signed-out.
//
// POST, with the fetch-metadata half of csrf.ts's check: revoking a session is a state change, so
// it must not hang on a GET, which a prefetch, a crawler or an <img> fires unprompted, and a
// cross-site form POST is refused the same way the API refuses one. The synchronizer token itself
// cannot be checked here -- a plain HTML form cannot send a header -- so the metadata a browser
// always sends is the check.
import { authRoutes } from "../../../src/server/app"
import { authResponse } from "../../../src/server/auth-response"
import { betterAuthSignOut } from "../../../src/server/better-auth-session" // kit:convex-better-auth
export const dynamic = "force-dynamic"
export const runtime = "nodejs"
export async function GET(): Promise<Response> {
return new Response("signing out is a POST", { status: 405, headers: { allow: "POST" } })
}
export async function POST(request: Request): Promise<Response> {
const site = request.headers.get("sec-fetch-site")
if (site !== null && site !== "same-origin" && site !== "none") {
return new Response("a cross-site sign-out is refused", { status: 403 })
}
const betterAuth = await betterAuthSignOut(request) // kit:convex-better-auth
if (betterAuth) return betterAuth // kit:convex-better-auth
return authResponse(request, async () => (await authRoutes()).signOut(request))
}
+42
View File
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The sign-up page of the Convex Better Auth adapter (AUTH_PROVIDER=convex-better-auth): a new
// account with an e-mail address and a password. An OIDC deployment has its identity provider's own
// registration, so this page is not found there.
import { loadAuthConfig } from "@conusai-kit/auth"
import { notFound, redirect } from "next/navigation"
import type { ReactElement } from "react"
import { safeReturnTo } from "../../../src/server/auth-errors"
import { betterAuthFormLabels, MIN_PASSWORD_LENGTH } from "../../../src/server/better-auth"
import { pageContext } from "../../../src/server/page"
import { BetterAuthForm } from "../../../src/ui/better-auth-form"
import { StateView } from "../../../src/ui/state-view"
export const dynamic = "force-dynamic"
export default async function SignUp({
searchParams,
}: {
searchParams: Promise<{ return_to?: string }>
}): Promise<ReactElement> {
if (loadAuthConfig().provider !== "convex-better-auth") notFound()
const { return_to: returnToParam } = await searchParams
const { i18n, signedIn } = await pageContext()
const returnTo = safeReturnTo(returnToParam)
if (signedIn) redirect(returnTo)
const next = returnTo === "/" ? "" : `?return_to=${encodeURIComponent(returnTo)}`
return (
<StateView title={i18n.message("signin.account.signUpTitle")}>
<p>{i18n.message("signin.account.signUpIntro")}</p>
<BetterAuthForm
mode="sign-up"
returnTo={returnTo}
minPasswordLength={MIN_PASSWORD_LENGTH}
labels={betterAuthFormLabels(i18n, "sign-up")}
/>
<p>
<a href={`/auth/sign-in${next}`}>{i18n.message("signin.account.toSignIn")}</a>
</p>
</StateView>
)
}
+18
View File
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Where the identity provider sends the browser after an RP-initiated sign-out (MIB I3's path).
import type { ReactElement } from "react"
import { pageContext } from "../../../src/server/page"
export const dynamic = "force-dynamic"
export default async function SignedOut(): Promise<ReactElement> {
const { i18n } = await pageContext()
return (
<main>
<h1>{i18n.message("app.signedOut")}</h1>
<p>{i18n.message("app.signedOutBody")}</p>
<a href="/auth/sign-in">{i18n.message("app.signIn")}</a>
</main>
)
}
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
"use client"
// What a person sees when a page throws (spec §10.4). The strings come from the root layout's
// `BoundaryMessages`, in the person's language; the error is reported to observability from here
// because React swallows what a boundary catches (src/ui/boundary.tsx).
import { ErrorView } from "../src/ui/boundary"
export default function ErrorPage({ error, retry }: { error: Error & { digest?: string }; retry: () => void }) {
return <ErrorView error={error} retry={retry} />
}
+20
View File
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The page for a member whose role does not allow what they asked for (spec §3.2). It is only ever
// shown to somebody who is in the organization -- a stranger gets the not-found page -- so it says
// nothing that the person did not already know.
import type { ReactElement } from "react"
import { pageContext } from "../src/server/page"
import { StateView } from "../src/ui/state-view"
export default async function Forbidden(): Promise<ReactElement> {
const { i18n } = await pageContext()
return (
<StateView
title={i18n.message("boundary.forbiddenTitle")}
actions={<a href="/">{i18n.message("boundary.home")}</a>}
>
<p>{i18n.message("boundary.forbiddenBody")}</p>
</StateView>
)
}
+39
View File
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
"use client"
// The last boundary: the root layout itself threw, so there is no document, no stylesheet and no
// provider of strings -- this file renders its own <html> and carries its own two languages, chosen
// from the browser's. The server has already logged the error; this reports the browser's side.
import { useEffect } from "react"
import { reportClientError } from "../src/observe/browser"
const STRINGS = {
en: { title: "Something went wrong", body: "The app could not be shown. Try again.", retry: "Try again" },
lt: {
title: "Kažkas nepavyko",
body: "Programos parodyti nepavyko. Bandykite dar kartą.",
retry: "Bandyti dar kartą",
},
} as const
export default function GlobalError({ error, retry }: { error: Error & { digest?: string }; retry: () => void }) {
useEffect(() => {
reportClientError(error)
}, [error])
const locale = typeof navigator !== "undefined" && navigator.language.toLowerCase().startsWith("lt") ? "lt" : "en"
const strings = STRINGS[locale]
return (
<html lang={locale}>
<body>
<main>
<h1>{strings.title}</h1>
<p role="alert">{strings.body}</p>
<button type="button" onClick={retry}>
{strings.retry}
</button>
</main>
</body>
</html>
)
}
+281
View File
@@ -0,0 +1,281 @@
/* SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 */
/* The app's stylesheet. Every colour, font and radius is a brand token: this file declares none
of its own, so a white-label change is a change in config/brand.ts and nothing else
(decision AT-D11). The fonts are the self-hosted woff2 files of @conusai-kit/brand; no external
stylesheet or font host is referenced anywhere (self-hosted D20). */
@import "@conusai-kit/brand/tokens.css";
@import "@conusai-kit/ui/styles.css";
:root {
/* The brand's tokens are dark by default and `.light` carries the light variant, so the browser's
own colour scheme follows the class: a tenant brand's `light-dark()` ink (modules/branding)
resolves against the same choice the tokens make, and never against the operating system's. */
color-scheme: dark;
}
.light {
color-scheme: light;
}
body {
margin: 0;
background: var(--brand-background);
color: var(--brand-foreground);
font-family: var(--brand-font-sans);
font-size: var(--brand-font-size-root, 16px);
line-height: 1.5;
}
a {
/* The ink, not the fill: the brand's own teal is 1.8:1 as text on white, which is why the ink exists. */
color: var(--brand-accent-ink);
}
main {
padding: 1.5rem;
max-width: 72rem;
margin: 0 auto;
}
h1,
h2,
h3 {
font-family: var(--brand-font-display);
margin-block: 0 0.75rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: start;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--brand-border-soft);
}
.app-shell-header {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--brand-border-soft);
}
.app-error {
border: 1px solid var(--brand-border-soft);
border-radius: var(--brand-radius-panel, 8px);
padding: 0.75rem 1rem;
}
/* The app's logo and an organization's, one image per theme: the other is hidden by the theme class. */
.app-logo {
height: 1.75rem;
width: auto;
vertical-align: middle;
}
.app-logo-light {
display: none;
}
.light .app-logo-light {
display: inline;
}
.light .app-logo-dark {
display: none;
}
.app-brand-link {
display: inline-flex;
gap: 0.5rem;
align-items: center;
}
/* The brand settings' preview: the page's own tokens on a panel per theme, with the brand layer the
organization layout will write drawn on the panel itself. */
.brand-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1rem;
}
.brand-preview-panel {
background: var(--brand-background);
color: var(--brand-foreground);
border: 1px solid var(--brand-border-strong);
border-radius: var(--brand-radius-panel);
padding: 1rem;
}
.brand-preview-action {
background: var(--brand-accent);
color: var(--brand-accent-foreground);
border: 0;
border-radius: var(--brand-radius-button);
padding: 0.5rem 1rem;
font: inherit;
}
.brand-preview-link {
color: var(--brand-accent-ink);
}
/* The shell's controls, the form controls the pages use, and the states a page can be in. Brand
tokens only: a white-label change is still a change in config/brand.ts and nothing else. */
.app-shell-controls {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
.app-control {
display: inline-flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
button,
select,
input {
font: inherit;
color: var(--brand-foreground);
background: var(--brand-panel);
border: 1px solid var(--brand-border-strong);
border-radius: var(--brand-radius-button);
padding: 0.4rem 0.75rem;
}
button {
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: default;
}
button[aria-pressed="true"] {
border-color: var(--brand-accent);
}
:focus-visible {
outline: 2px solid var(--brand-accent-ink);
outline-offset: 2px;
}
.kit-field {
margin-block-end: 0.75rem;
}
.app-state {
max-width: 40rem;
margin: 3rem auto;
}
.app-breadcrumb ol {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
margin: 0 0 1rem;
padding: 0;
}
.app-breadcrumb li + li::before {
content: "/";
margin-inline-end: 0.5rem;
color: var(--brand-text-70);
}
.app-link-field {
width: 100%;
box-sizing: border-box;
font-family: var(--brand-font-mono);
}
.app-loading {
display: flex;
gap: 0.75rem;
align-items: center;
color: var(--brand-text-80);
}
.app-spinner {
width: 1rem;
height: 1rem;
border: 2px solid var(--brand-border-strong);
border-top-color: var(--brand-accent);
border-radius: 50%;
animation: app-spin 0.8s linear infinite;
}
@keyframes app-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.app-spinner {
animation: none;
}
}
/* The organization shell of @conusai-kit/ui: the bar with the organization's name, its switcher and
its links, and the navigation of the modules, as two rows rather than a pile of unstyled elements. */
.kit-shell-bar {
display: flex;
flex-wrap: wrap;
gap: 1rem 1.5rem;
align-items: center;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--brand-border-soft);
}
.kit-shell nav {
padding: 0.5rem 1.5rem;
border-bottom: 1px solid var(--brand-border-soft);
}
.kit-shell nav ul {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.5rem;
}
/* The account page's analytics panel (ADR-049): its two actions side by side, the irreversible one
marked with the brand's warning tokens, and the erasure's confirmation set apart. */
.app-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* The text stays the foreground colour, which keeps its contrast on the button's panel fill in both
themes (the warning ink on that fill falls under WCAG's 4.5:1); the danger is carried by the
heavier warning border and weight, not by colour alone, and the confirmation states it. */
.app-danger {
border: 2px solid var(--brand-warning);
color: var(--brand-foreground);
font-weight: 600;
}
.app-confirm {
border: 1px solid var(--brand-warning);
border-radius: var(--brand-radius-panel, 8px);
padding: 0.75rem 1rem;
margin-block: 0.75rem;
}
.app-stack {
display: grid;
gap: 0.5rem;
margin-block: 0.75rem;
}
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The page an invitation link leads to: /invitations/<organization>/<token> (spec §3.1).
//
// Opening it changes nothing. A visitor who is not signed in is asked to sign in -- with the address
// the invitation was sent to -- and comes back here; a signed-in person sees who they are signed in
// as and a button, and the button is what accepts. The page does not look the token up: only the
// invitee's own account may learn anything about it, and the answer to accepting is worded per state
// (used, expired, already a member) for exactly that account and as a plain "not valid" for everyone
// else (kit-tenancy), so this page cannot be used to find out which tokens or organizations exist.
import type { Metadata } from "next"
import type { ReactElement } from "react"
import { signInHref } from "../../../../src/server/guard"
import { pageContext } from "../../../../src/server/page"
import { InviteAccept } from "../../../../src/ui/invite-accept"
import { SignOut } from "../../../../src/ui/sign-out"
import { StateView } from "../../../../src/ui/state-view"
export const dynamic = "force-dynamic"
// The token is the credential: it must not leave in a Referer header, and the page is not for search.
export const metadata: Metadata = { referrer: "no-referrer", robots: { index: false, follow: false } }
export default async function Invitation({
params,
}: {
params: Promise<{ org: string; token: string }>
}): Promise<ReactElement> {
const { org, token } = await params
const { i18n, signedIn, user, path } = await pageContext()
const message = (key: string, values?: Record<string, string | number>): string => i18n.message(key, values)
return (
<StateView title={message("invite.title")}>
<p>{message("invite.intro", { org })}</p>
{signedIn ? (
<>
{user ? <p>{message("invite.signedInAs", { email: user.email })}</p> : null}
<InviteAccept
org={org}
token={token}
labels={{
accept: message("invite.accept"),
accepted: message("invite.accepted", { org }),
open: message("invite.open", { org }),
invalid: message("invite.error.invalid"),
used: message("invite.error.used"),
expired: message("invite.error.expired"),
member: message("invite.error.member"),
failed: message("invite.error.failed"),
}}
/>
<SignOut label={message("invite.switchAccount")} />
</>
) : (
<>
<p>{message("invite.signInFirst")}</p>
<p>
<a href={signInHref(path)}>{message("app.signIn")}</a>
</p>
</>
)}
</StateView>
)
}
+83
View File
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The root layout: the document, the brand layer and the app shell (spec §10.2).
//
// The brand's `--brand-*` declarations are written as a style attribute on <html> (src/ui/document.tsx),
// which is the layer a tenant brand overrides at runtime without any stylesheet being rebuilt.
import { appBrand, publicUrlOf } from "@app/config"
import { ACCOUNT_PATH } from "@app/modules/account/contract"
import type { Metadata, Viewport } from "next"
import type { ReactNode } from "react"
import { pageContext } from "../src/server/page"
import { BoundaryMessages } from "../src/ui/boundary"
import { brandMetadata, Document } from "../src/ui/document"
import { LanguageSwitch, ThemeToggle } from "../src/ui/shell-controls"
import { SignOut } from "../src/ui/sign-out"
import "./globals.css"
/**
* The app's own name and share image. `metadataBase` makes the image's address absolute, as a link preview needs, from
* the public URL the deployment states; it is read when the page is asked for, never at build (one image, every environment).
*/
export function generateMetadata(): Metadata {
return {
...brandMetadata(appBrand),
metadataBase: new URL(publicUrlOf()),
}
}
export const viewport: Viewport = { width: "device-width", initialScale: 1 }
export default async function RootLayout({ children }: { children: ReactNode }): Promise<ReactNode> {
const { i18n, signedIn, csrf, theme, observe } = await pageContext()
const message = (key: string): string => i18n.message(key)
return (
<Document
brand={appBrand}
locale={i18n.locale}
theme={theme}
signedIn={signedIn}
{...(csrf ? { csrf } : {})}
{...(observe ? { observe } : {})}
signIn={message("app.signIn")}
signOut={
<>
<a href={ACCOUNT_PATH}>{message("account.link")}</a>
<SignOut label={message("app.signOut")} />
</>
}
controls={
<>
<LanguageSwitch
current={i18n.locale}
signedIn={signedIn}
label={message("shell.language")}
options={[
{ value: "en", label: message("shell.languageEn") },
{ value: "lt", label: message("shell.languageLt") },
]}
failedTemplate={message("app.failed")}
/>
<ThemeToggle
initial={theme}
label={message("shell.theme")}
darkLabel={message("shell.themeDark")}
lightLabel={message("shell.themeLight")}
/>
</>
}
>
<BoundaryMessages
strings={{
title: message("boundary.errorTitle"),
body: message("boundary.errorBody"),
code: message("boundary.errorCode"),
retry: message("boundary.retry"),
home: message("boundary.home"),
}}
>
{children}
</BoundaryMessages>
</Document>
)
}
+18
View File
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The page for an address that leads nowhere (spec §3.3). It is the one answer for an organization
// that does not exist, an organization the person is not in, a workspace or a project of another
// organization, and a path no module answers: the words and the 404 status are the same for all of
// them, so the page cannot be used to find out which organizations exist.
import type { ReactElement } from "react"
import { pageContext } from "../src/server/page"
import { StateView } from "../src/ui/state-view"
export default async function NotFound(): Promise<ReactElement> {
const { i18n } = await pageContext()
return (
<StateView title={i18n.message("boundary.notFoundTitle")} actions={<a href="/">{i18n.message("boundary.home")}</a>}>
<p>{i18n.message("boundary.notFoundBody")}</p>
</StateView>
)
}
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Onboarding: the first organization, which creates its default workspace and project in one
// transaction (kit-tenancy's use case), with the signed-in user as its owner. Joining an existing
// organization is not done by name (spec §3.3): it is an invitation, which has its own page.
import { redirect } from "next/navigation"
import type { ReactElement } from "react"
import { signInHref } from "../../src/server/guard"
import { pageContext } from "../../src/server/page"
import { ProcedureForm } from "../../src/ui/procedure-form"
export const dynamic = "force-dynamic"
export default async function Onboarding(): Promise<ReactElement> {
const { i18n, signedIn, path } = await pageContext()
if (!signedIn) redirect(signInHref(path))
return (
<main>
<h1>{i18n.message("app.createOrganization")}</h1>
<ProcedureForm
procedure="tenancy.org.create"
submitLabel={i18n.message("app.create")}
failedTemplate={i18n.message("app.failed")}
after="/{slug}"
fields={[
{ name: "name", label: i18n.message("app.name"), required: true },
{ name: "slug", label: i18n.message("app.slug"), required: true },
]}
/>
</main>
)
}
+44
View File
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The entry page: the organizations this user belongs to, or the way in (spec §7).
import { meGet } from "@conusai-kit/tenancy"
import type { ReactElement } from "react"
import { orFallback } from "../src/server/guard"
import { pageContext } from "../src/server/page"
export const dynamic = "force-dynamic"
export default async function Home(): Promise<ReactElement> {
const { caller, signedIn, i18n, path } = await pageContext()
if (!signedIn) {
return (
<main>
<h1>{i18n.message("app.title")}</h1>
<p>{i18n.message("app.needsSignIn")}</p>
<a href="/auth/sign-in">{i18n.message("app.signIn")}</a>
</main>
)
}
// The typed in-process client: the contract decides the input and the output types, and the
// same procedure answers a browser, a CLI and a partner (spec §6.2).
const me = await orFallback(path, () => caller.call(meGet, undefined))
return (
<main>
<h1>{i18n.message("app.organizations")}</h1>
{me.organizations.length === 0 ? (
<p>{i18n.message("app.noOrganizations")}</p>
) : (
<ul>
{me.organizations.map((organization) => (
<li key={organization.slug}>
<a href={`/${organization.slug}`}>{organization.name}</a>{" "}
<span>{i18n.message(`roles.${organization.role}`)}</span>
</li>
))}
</ul>
)}
<a href="/onboarding">{i18n.message("app.createOrganization")}</a>
</main>
)
}
+72
View File
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The person's own account (joint contract §12): what the app keeps about them, the export of it, and
// the deletion of the account. The page is a user page, under `settings`, which is a reserved
// organization slug, so no organization's path can shadow it.
//
// The export and the deletion are the account module's procedures; this page only renders them, and
// the client host beside it ends the browser modules' context once a deletion answered. The analytics
// panel (ADR-049) erases or exports what the platform's own stores keep about the person's visits,
// through the account.analytics procedures: the deployment token stays on the server. With the fake
// platform its procedures answer "unavailable".
import { ACCOUNT_PATH, accountDeletionStatus } from "@app/modules/account/contract"
import { analyticsLabels, deletionLabels, exportLabels, pageLabels } from "@app/modules/account/ui"
import { loadAuthConfig } from "@conusai-kit/auth"
import { meGet } from "@conusai-kit/tenancy"
import { redirect } from "next/navigation"
import type { ReactElement } from "react"
import { orFallback, signInHref } from "../../../src/server/guard"
import { pageContext } from "../../../src/server/page"
import { AccountPanels } from "../../../src/ui/account"
export const dynamic = "force-dynamic"
/**
* Where a person signs in again to delete their account: straight to the provider (the sign-in page
* sends somebody already signed in away), or, in development, the dev login with their own address.
* Either opens a new session, whose sign-in is then recent.
*/
function reauthHref(email: string): string {
const back = `return_to=${encodeURIComponent(ACCOUNT_PATH)}`
const config = loadAuthConfig()
if (config.oidc) return `/auth/sign-in/start?${back}`
if (config.devLogin) return `/auth/dev-login?email=${encodeURIComponent(email)}&${back}`
return `/auth/sign-in?${back}`
}
export default async function AccountPage(): Promise<ReactElement> {
const { caller, i18n, signedIn, path } = await pageContext()
if (!signedIn) redirect(signInHref(path))
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`account.${key}`, values)
const page = pageLabels(t)
const [me, status] = await orFallback(path, () =>
Promise.all([caller.call(meGet, undefined), caller.call(accountDeletionStatus, undefined)]),
)
const failed = i18n.message("app.failed")
return (
<main>
<h1>{page.title}</h1>
<p>{page.intro}</p>
<section aria-labelledby="account-profile">
<h2 id="account-profile">{page.profileHeading}</h2>
<dl>
<dt>{page.name}</dt>
<dd>{me.displayName}</dd>
<dt>{page.email}</dt>
<dd>{me.email}</dd>
</dl>
</section>
<AccountPanels
exportLabels={exportLabels(t, failed)}
analyticsLabels={analyticsLabels(t, failed)}
deletion={{
labels: deletionLabels(t, failed, status, me.email),
interactive: status.interactive,
recentSignIn: status.recentSignIn,
reauthHref: reauthHref(me.email),
deletion: status.deletion,
}}
/>
</main>
)
}