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
+84
View File
@@ -0,0 +1,84 @@
// The public API of the branding module (plan item AT2.6, spec §10.3): an organization's own brand
// -- its display name, two colours and two logos -- as the end customers of the app see it. It is a
// layer over the app brand (config/src/brand.ts) and never replaces it: layout, type and the neutral
// tokens stay the app's, so one tenant cannot break the app's accessibility.
//
// The data is `organizations.branding`, a column of the tenancy module's own table; this module
// writes it through the tenancy module's entity and owns no table of its own.
import { LOGO_UPLOAD_LIMIT_BYTES } from "@conusai-kit/brand/logo/check"
import { procedure } from "@conusai-kit/core"
import { z } from "zod"
import { LOGO_SIZES } from "./logo.js"
import { brandViewSchema, LOGO_VARIANTS } from "./view.js"
export { LOGO_SIZES, type LogoChoice, type LogoSize, logoUrl, resolveLogo, tenantLogo } from "./logo.js"
export { type BrandView, brandViewSchema, LOGO_VARIANTS, type LogoVariant } from "./view.js"
/** The audit event of every change: one per call, whether a colour, a name or a logo. */
export const BRAND_CHANGED = "branding.brand_changed"
const inOrg = { org: z.string() }
export const brandGet = procedure("branding.brand.get")
.kind("read")
.scope("org")
.permission("branding.brand.read")
.input(z.object(inOrg))
.output(brandViewSchema)
.contract()
export const brandSet = procedure("branding.brand.set")
.kind("write")
.scope("org")
.permission("branding.brand.write")
.input(
z.object({
...inOrg,
// Absent leaves a field as it is; null clears it; a value sets it.
displayName: z.string().max(60).nullable().optional(),
primary: z.string().max(32).nullable().optional(),
accent: z.string().max(32).nullable().optional(),
}),
)
.output(brandViewSchema)
.audit(BRAND_CHANGED)
.contract()
export const logoSet = procedure("branding.logo.set")
.kind("effect")
.scope("org")
.permission("branding.brand.write")
.input(
z.object({
...inOrg,
variant: z.enum(LOGO_VARIANTS),
contentType: z.string().max(64),
/** The image, standard base64: a PNG or WebP up to the upload limit (it is resized), an SVG up to the stored one. */
body: z.string().max(Math.ceil((LOGO_UPLOAD_LIMIT_BYTES * 4) / 3) + 4),
}),
)
.output(brandViewSchema)
.audit(BRAND_CHANGED)
.contract()
// Removing a logo deletes its stored files, which only an effect may do, so it is not a field of `brand.set`.
export const logoClear = procedure("branding.logo.clear")
.kind("effect")
.scope("org")
.permission("branding.brand.write")
.input(z.object({ ...inOrg, variant: z.enum(LOGO_VARIANTS) }))
.output(brandViewSchema)
.audit(BRAND_CHANGED)
.contract()
export { tenantStyle } from "./style.js"
export const logoGet = procedure("branding.logo.get")
.kind("effect")
.scope("org")
.permission("branding.brand.read")
// `size` picks the 1x or the 2x rendition of a raster logo (2x when absent); an SVG is one file and has one size.
.input(z.object({ ...inOrg, variant: z.enum(LOGO_VARIANTS), size: z.enum(LOGO_SIZES).optional() }))
.output(z.object({ contentType: z.string(), body: z.string() }))
.contract()
+52
View File
@@ -0,0 +1,52 @@
// Which logo an organization's pages show, and where one is fetched from. A file of the contract
// because the organization layout, the settings page's preview and the share image and email all
// need the same answer, and a rule that three places each re-derive is a rule that drifts.
//
// An organization has up to two logos, one for each theme: `light` is the logo for a light page,
// `dark` the one for a dark page. The fallback is one sentence and this file is where it is kept:
//
// A theme shows its own logo. When only one is uploaded it shows on both themes. When none is,
// the app brand's logo for that theme shows (config/src/brand.ts).
//
// The first two sentences are `tenantLogo`; the third is what `resolveLogo` adds.
import type { BrandView, LogoVariant } from "./view.js"
/** The two stored sizes of a raster logo, 1x and 2x (`@conusai-kit/brand/logo`'s standard set). */
export const LOGO_SIZES = ["1x", "2x"] as const
export type LogoSize = (typeof LOGO_SIZES)[number]
export interface LogoChoice {
/** Whose logo it is: the organization's own, or the app brand's. */
readonly source: "tenant" | "app"
/** Which of that source's two logos was chosen; it differs from the theme asked for when the other one stood in. */
readonly variant: LogoVariant
/** The path of that logo on this app's origin. */
readonly path: string
}
const other = (variant: LogoVariant): LogoVariant => (variant === "light" ? "dark" : "light")
const pathOf = (brand: Pick<BrandView, "logoLight" | "logoDark">, variant: LogoVariant): string | null =>
variant === "light" ? brand.logoLight : brand.logoDark
/** The organization's logo for a theme: its own, else the other theme's, else null. */
export function tenantLogo(brand: Pick<BrandView, "logoLight" | "logoDark">, theme: LogoVariant): LogoChoice | null {
for (const variant of [theme, other(theme)]) {
const path = pathOf(brand, variant)
if (path !== null) return { source: "tenant", variant, path }
}
return null
}
/** The logo for a theme: the organization's (`tenantLogo`), or, when it has none, the app brand's own for that theme. */
export function resolveLogo(
brand: Pick<BrandView, "logoLight" | "logoDark"> | null,
app: Readonly<Record<LogoVariant, string>>,
theme: LogoVariant,
): LogoChoice {
return (brand === null ? null : tenantLogo(brand, theme)) ?? { source: "app", variant: theme, path: app[theme] }
}
/** The address of one stored size of an organization's logo: its path, which carries the content tag, and the size. */
export const logoUrl = (path: string, size: LogoSize): string => `${path}${path.includes("?") ? "&" : "?"}s=${size}`
+18
View File
@@ -0,0 +1,18 @@
// An organization's brand as the stylesheet layer it becomes, and the numbers behind it. A file of
// the contract because both halves of the module use it: the organization layout writes the style on
// the server (so the first paint is already branded), and the settings page's preview computes the
// same thing in the browser as a person chooses a colour. One function, so the preview cannot say
// something the page then does not do.
import { type BrandReport, themeStyle } from "@conusai-kit/brand"
import type { BrandView } from "./view.js"
export function tenantStyle(brand: Pick<BrandView, "primary" | "accent">): {
style: Record<string, string>
report: BrandReport
} {
return themeStyle({
...(brand.primary === null ? {} : { primary: brand.primary }),
...(brand.accent === null ? {} : { accent: brand.accent }),
})
}
+18
View File
@@ -0,0 +1,18 @@
// The shapes both halves of the branding contract use: the procedures (index.ts) and the stylesheet
// layer a brand becomes (style.ts). A file of its own so neither imports the other.
import { z } from "zod"
export const LOGO_VARIANTS = ["light", "dark"] as const
export type LogoVariant = (typeof LOGO_VARIANTS)[number]
/** `null` is "not set": the app's own brand shows through. */
export const brandViewSchema = z.object({
displayName: z.string().nullable(),
primary: z.string().nullable(),
accent: z.string().nullable(),
logoLight: z.string().nullable(),
logoDark: z.string().nullable(),
})
export type BrandView = z.output<typeof brandViewSchema>