mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Every import, the catalog pin and the docs name @hanzo/app and github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and components/rooms.tsx is components/app.tsx.
531 lines
22 KiB
TypeScript
531 lines
22 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* /security — verified security architecture and mechanisms.
|
|
*
|
|
* NO TAILWIND AND NO STYLE OBJECTS. The five section frames are
|
|
* `components/marketing/band`'s `Band` and `Head`; @hanzo/ui's marketing
|
|
* components carry the type and the controls (`Display`, `Lede`, `Title`,
|
|
* `Chip`, `Action`, `More`, `tap`); everything left is gui props over
|
|
* @hanzo/design's tokens.
|
|
*
|
|
* Four things the swap moves, each the shared shape winning over this page's
|
|
* own copy of it:
|
|
*
|
|
* - the section headings take `Title quiet` and the `h1` takes
|
|
* `Display`, so this page's ladder is the site's.
|
|
* - every control is `Action` at one of its two weights, and the three quiet
|
|
* links are `More`, whose `:hover` to white is what each of them was
|
|
* spelling out. `tap` stays on those three because a standalone link IS a
|
|
* tap target and `More` states no floor; the buttons drop it, because
|
|
* `Action` already carries the 44px minimum.
|
|
* - the two side-by-side sections were a `lg:grid-cols-2` with `order-*`
|
|
* classes swapping the halves at 1024. They are a stack that becomes a row —
|
|
* `row` where the picture is on the right, `row-reverse` where it is on the
|
|
* left — so the DOM order is the reading order at every width and no class
|
|
* is needed to say it twice.
|
|
* - `flex-1` is spelt `grow={1} shrink={1} flexBasis="0%"`, because gui's
|
|
* `flex={1}` writes `flex-basis: 0px` and collapses the pane.
|
|
*/
|
|
|
|
import Link from "next/link";
|
|
|
|
import { motion } from "@/components/motion";
|
|
import type { ReactNode } from "react";
|
|
import {
|
|
Shield,
|
|
Lock,
|
|
CheckCircle,
|
|
Server,
|
|
Eye,
|
|
Key,
|
|
FileCheck,
|
|
Globe,
|
|
ArrowRight,
|
|
ExternalLink,
|
|
Fingerprint,
|
|
Database,
|
|
Network,
|
|
Clock,
|
|
Users,
|
|
Bug
|
|
} from "lucide-react";
|
|
import { Box, Text, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { Band, Head } from '@/components/marketing/band'
|
|
import { Glow } from '@/components/shared/Glow'
|
|
import { Action, Chip, Display, Lede, More, Title, Line } from '@hanzo/ui/marketing'
|
|
import { mix } from '@hanzo/app/lib/mix'
|
|
|
|
/*
|
|
* There was a badge row here: four names — SOC 2 Type II, GDPR, CCPA, HIPAA —
|
|
* each rendered with a green checkmark. The qualifiers that made them honest
|
|
* ("assessment planned", "BAA available") were carried in a `description` field
|
|
* the markup never rendered, so what shipped was four certifications with a tick
|
|
* beside them and nothing anywhere saying we hold none of them.
|
|
*
|
|
* The certification question now has exactly one answer, at /trust: continual
|
|
* internal audits, the report on request, and a scoping conversation for a
|
|
* reviewer who runs on a named framework. This page describes mechanisms; that
|
|
* one holds the position. Two pages answering it is how the qualifier got lost
|
|
* the first time.
|
|
*/
|
|
|
|
/** The inks this page draws in. */
|
|
const INK = 'var(--foreground)'
|
|
const DIM = 'var(--muted-foreground)'
|
|
const SOFT = '$color/80'
|
|
|
|
/** The entrance a block makes as it scrolls in. */
|
|
const rise = (delay?: number) => (
|
|
<motion.div initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay }} />
|
|
)
|
|
|
|
/** The badge above a heading, in the tint this page has always drawn it. */
|
|
const BADGE = { mb: 24, px: 12, py: 4, fontSize: "$1", lineHeight: "1.3333", fontWeight: "500", borderColor: "transparent", bg: "color-mix(in srgb, var(--primary) 15%, transparent)", color: "var(--primary)" } as const
|
|
|
|
/** A pane of a two-up section: `flex-1`, spelt so gui does not collapse it. */
|
|
const PANE = { grow: 1, shrink: 1, flexBasis: '0%' } as const
|
|
|
|
/** The disc a mark sits on. */
|
|
const DISC = { width: 48, height: 48, items: 'center', justify: 'center', rounded: 'var(--radius-lg)' } as const
|
|
|
|
/** A heading inside a panel, and the grey paragraph under it. */
|
|
const PanelTitle = ({ mb = 16, children }: { mb?: number; children: ReactNode }) => (
|
|
<Line render="h3" display="block" mb={mb} fontSize="$6" fontWeight="700" color={INK} lineHeight="1.4">
|
|
{children}
|
|
</Line>
|
|
)
|
|
|
|
const Body = ({ mb, children }: { mb?: number; children: ReactNode }) => (
|
|
<Text render="p" display="block" mb={mb} color={DIM}>
|
|
{children}
|
|
</Text>
|
|
)
|
|
|
|
/** A small line, on the leading its utility paired with `text-sm`. */
|
|
const SMALL = { fontSize: '$2', lineHeight: '1.4286' } as const
|
|
|
|
const SECURITY_FEATURES = [
|
|
{
|
|
icon: Lock,
|
|
title: "Encryption at Rest and in Transit",
|
|
description:
|
|
"At rest, each organization is its own file on Hanzo Base, and where a master key is configured each file opens under its own key, derived through HKDF-SHA256. In transit the edge speaks TLS 1.3 and offers hybrid ML-KEM-768 key exchange to clients that support it, so their traffic captured today cannot be read by a quantum computer later.",
|
|
},
|
|
{
|
|
icon: Fingerprint,
|
|
title: "Second Factors",
|
|
description:
|
|
"An authenticator app, or a code by SMS or email, with recovery codes. Enrolment hands out the material and demands it back before it writes anything, and adding or dropping a factor signs the account out of every other browser — a stolen session that outlives the change is the same as not having the factor.",
|
|
},
|
|
{
|
|
icon: Key,
|
|
title: "Keys and Tokens",
|
|
description:
|
|
"A key is told apart from a token by its prefix, so nothing has to guess what it was handed. Tokens are refused outright if they carry an HMAC signature or no algorithm at all, and a refresh token is single-use — presenting a spent one revokes its whole family rather than the one token.",
|
|
},
|
|
{
|
|
icon: Users,
|
|
title: "Who May Reach What",
|
|
description:
|
|
"Access is a grant at a place: an organization, a workspace under it, a project under that. A check asks whether some grant the caller holds covers the path and admits the verb. One rule, no special cases, and the decision is a function call rather than a service that can be down.",
|
|
},
|
|
{
|
|
icon: Network,
|
|
title: "The Edge Throws Identity Away",
|
|
description:
|
|
"Anything that arrives claiming to be an org, a user or an email is deleted at the gateway before a single handler reads it. Identity is written back only from a verified token. Inside the cluster, services reach each other over a binary protocol rather than the public internet.",
|
|
},
|
|
{
|
|
icon: Database,
|
|
title: "Where It Sits",
|
|
description:
|
|
"An organization is a tenancy boundary, and on Hanzo Base it is a physical one — a different organization is a different database file, so no query can reach across two. Dedicated and self-hosted deployments pin that file to a region you choose.",
|
|
},
|
|
];
|
|
|
|
const ENTERPRISE_FEATURES = [
|
|
"Single sign-on through Hanzo IAM",
|
|
"Federation to your own OpenID Connect issuer",
|
|
"SCIM 2.0 provisioning from your directory",
|
|
"A named engineer who knows your deployment",
|
|
"Priority incident response",
|
|
"Retention set by you, not by us",
|
|
"Audit records exported where you want them",
|
|
"Security questionnaire support",
|
|
];
|
|
|
|
const EDGE_FEATURES = [
|
|
{ name: "Run it where you like", description: "Our cloud, your cluster, your rack" },
|
|
{ name: "Scales to nothing", description: "Idle should cost what idle is worth" },
|
|
{ name: "Your region", description: "Pin the data and keep it there" },
|
|
];
|
|
|
|
const Security = () => {
|
|
return (
|
|
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
|
|
<main>
|
|
{/* Hero Section */}
|
|
<Box
|
|
render="section"
|
|
position="relative"
|
|
overflow="hidden"
|
|
pt={96}
|
|
pb={80}
|
|
px={16}
|
|
$md={{ px: 32 }}
|
|
$lg={{ px: 48 }}
|
|
>
|
|
{/* Background gradient */}
|
|
<Box
|
|
aria-hidden="true"
|
|
position="absolute"
|
|
t={0}
|
|
l="50%"
|
|
x="-50%"
|
|
width={800}
|
|
height={420}
|
|
background="radial-gradient(circle, color-mix(in srgb, var(--primary) 12%, transparent) 0%, transparent 70%)"
|
|
filter="blur(80px)"
|
|
pointerEvents="none"
|
|
/>
|
|
|
|
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1024} mx="auto">
|
|
<Box $platform-web={{ textAlign: 'center' }}>
|
|
{/* Badge */}
|
|
<Chip
|
|
render={<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }} />}
|
|
{...BADGE}
|
|
>
|
|
<Shield width={14} height={14} />
|
|
Security Architecture
|
|
</Chip>
|
|
|
|
<Display
|
|
render={<motion.h1 initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.05 }} />}
|
|
mb={24}
|
|
>
|
|
What protects your data,
|
|
<br />
|
|
and how.
|
|
</Display>
|
|
|
|
<Lede
|
|
render={<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.1 }} />}
|
|
mb={40}
|
|
width="100%"
|
|
maxW={768}
|
|
mx="auto"
|
|
>
|
|
Clear security mechanisms: which key encrypts your data, where tenancy boundaries are
|
|
enforced, and how every request is verified.
|
|
</Lede>
|
|
|
|
{/* CTAs */}
|
|
<XStack
|
|
render={<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.15 }} />}
|
|
flexWrap="wrap"
|
|
items="center"
|
|
justify="center"
|
|
gap={16}
|
|
mb={48}
|
|
>
|
|
<Action
|
|
href="/contact"
|
|
fill
|
|
>
|
|
Talk to Security Team
|
|
<ArrowRight width={16} height={16} />
|
|
</Action>
|
|
<Action
|
|
href="https://docs.hanzo.ai/docs/cli/security"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
Security Documentation
|
|
<ExternalLink width={16} height={16} />
|
|
</Action>
|
|
</XStack>
|
|
|
|
<Line
|
|
render={<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.2 }} />}
|
|
display="block"
|
|
{...SMALL}
|
|
color={DIM}
|
|
>
|
|
Continual internal audits, with the report on request.{" "}
|
|
<Line render={<Link href="/trust" />} color={INK} whiteSpace="normal" textDecorationLine="underline" $platform-web={{ textUnderlineOffset: '4px' }}>
|
|
The controls, and how to check them
|
|
</Line>
|
|
.
|
|
</Line>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Security Features Grid */}
|
|
<Band rule={false} pad="$12" measure={1152} ground="linear-gradient(to bottom in oklab, var(--background), color-mix(in oklab, var(--neutral-900) 50%, transparent))">
|
|
<Head
|
|
title="The Controls, Named"
|
|
lede="Each of these is a decision in code you can point at, not a posture."
|
|
/>
|
|
|
|
<Grid columns={{ min: 300, max: 3 }} gap={24}>
|
|
{SECURITY_FEATURES.map((feature, index) => {
|
|
const Icon = feature.icon;
|
|
return (
|
|
<Box
|
|
key={feature.title}
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: index * 0.1 }} />}
|
|
p={24}
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
bg={mix('var(--secondary)', 80)}
|
|
>
|
|
<XStack {...DISC} mb={16} bg={mix('var(--primary)', 15, 'srgb')}>
|
|
<Icon width={24} height={24} />
|
|
</XStack>
|
|
<Line render="h3" display="block" mb={8} fontSize="$4" fontWeight="600" color={INK} lineHeight="1.5556">
|
|
{feature.title}
|
|
</Line>
|
|
<Line render="p" display="block" {...SMALL} color={DIM}>{feature.description}</Line>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* Infrastructure Security */}
|
|
<Band rule={false} pad="$12" measure={1152}>
|
|
<YStack gap={64} $lg={{ flexDirection: 'row', items: 'flex-start' }}>
|
|
<Box render={rise()} {...PANE}>
|
|
{/* A plain span, not a Box: `Chip` decides the display, and a
|
|
gui element states one of its own that the class would have
|
|
to out-rank. */}
|
|
<Chip {...BADGE}>
|
|
Infrastructure
|
|
</Chip>
|
|
<Title quiet mb={24}>
|
|
What runs underneath
|
|
</Title>
|
|
<Body mb={32}>
|
|
Workloads sit in hardware-virtualised machines on managed Kubernetes, in data centres with
|
|
physical access control and redundant power. Secrets never reach a manifest — they are fetched
|
|
from Hanzo KMS at boot, and outside development a service refuses to start without a verified
|
|
issuer, audience and key set rather than falling back to running open.
|
|
</Body>
|
|
|
|
<YStack gap={16}>
|
|
{[
|
|
{ icon: Server, text: "Hardware-virtualised machines, one tenant to a boundary" },
|
|
{ icon: Eye, text: "Errors, traces and release health watched around the clock" },
|
|
{ icon: Clock, text: "Replicated, with failover that does not need a person" },
|
|
{ icon: FileCheck, text: "Every image pinned to a version, every deploy reconciled" },
|
|
].map((item, index) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<XStack key={index} items="center" gap={12}>
|
|
<Icon width={20} height={20} color={DIM} />
|
|
<Text color={SOFT}>{item.text}</Text>
|
|
</XStack>
|
|
);
|
|
})}
|
|
</YStack>
|
|
</Box>
|
|
|
|
<Box
|
|
render={rise(0.2)}
|
|
{...PANE}
|
|
p={32}
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
background="linear-gradient(to bottom right in oklab, var(--neutral-900), color-mix(in oklab, var(--neutral-900) 50%, transparent))"
|
|
>
|
|
<View render={<Globe width={40} height={40} color={DIM} />} display="inline" mb={24} />
|
|
<PanelTitle>Or none of our infrastructure at all</PanelTitle>
|
|
<Body mb={24}>
|
|
Every piece named on this page is a binary you can run yourself. The strongest answer to a
|
|
question about our infrastructure is that you do not have to use it.
|
|
</Body>
|
|
|
|
<YStack gap={16}>
|
|
{EDGE_FEATURES.map((feature) => (
|
|
<Box
|
|
key={feature.name}
|
|
p={16}
|
|
rounded="var(--radius-lg)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
bg={mix('var(--neutral-800)', 50)}
|
|
>
|
|
<XStack items="center" justify="space-between">
|
|
<Text fontWeight="500" color={INK}>{feature.name}</Text>
|
|
</XStack>
|
|
<Line {...SMALL} color={DIM}>{feature.description}</Line>
|
|
</Box>
|
|
))}
|
|
</YStack>
|
|
</Box>
|
|
</YStack>
|
|
</Band>
|
|
|
|
{/* Enterprise Security */}
|
|
<Band rule={false} pad="$12" measure={1152} ground="linear-gradient(to bottom in oklab, color-mix(in oklab, var(--neutral-900) 50%, transparent), var(--background))">
|
|
<YStack gap={64} $lg={{ flexDirection: 'row-reverse', items: 'center' }}>
|
|
<Box render={rise()} {...PANE}>
|
|
<Chip {...BADGE}>
|
|
Enterprise
|
|
</Chip>
|
|
<Title quiet mb={24}>
|
|
If you are the one who has to sign off
|
|
</Title>
|
|
<Body mb={24}>
|
|
Bring the control you are required to demonstrate and we will show you the code that
|
|
implements it, or tell you plainly that it does not exist yet. A control we cannot point at is
|
|
one you should not put your name to.
|
|
</Body>
|
|
<Body>
|
|
Where a requirement is not met, the honest options are usually self-hosting, a dedicated
|
|
deployment, or waiting. Our engineers will say which one applies rather than selling you the
|
|
gap.
|
|
</Body>
|
|
</Box>
|
|
|
|
<Box render={rise(0.2)} {...PANE}>
|
|
<Box p={32} rounded="var(--radius-xl)" borderWidth={1} borderColor="var(--border)" background="linear-gradient(to bottom right in oklab, var(--white-10), transparent)">
|
|
<View render={<Shield width={40} height={40} />} display="inline" mb={24} />
|
|
<PanelTitle>
|
|
What comes with Enterprise
|
|
</PanelTitle>
|
|
<Body mb={24}>
|
|
An engineer who has read your deployment, retention you set rather than inherit, and
|
|
somebody to answer the questionnaire your procurement team is about to send.
|
|
</Body>
|
|
|
|
<Grid columns={{ min: 220, max: 2 }} gap={12}>
|
|
{ENTERPRISE_FEATURES.map((feature) => (
|
|
<XStack key={feature} items="flex-start" gap={8}>
|
|
<View render={<CheckCircle width={16} height={16} color={mix('var(--foreground)', 70)} />} mt={2} shrink={0} />
|
|
<Line {...SMALL} color={SOFT}>{feature}</Line>
|
|
</XStack>
|
|
))}
|
|
</Grid>
|
|
|
|
<More
|
|
href="/contact-sales"
|
|
mt={32}
|
|
$touchable={{ minH: 44, minW: 44 }}
|
|
>
|
|
Contact Sales
|
|
<ArrowRight width={16} height={16} />
|
|
</More>
|
|
</Box>
|
|
</Box>
|
|
</YStack>
|
|
</Band>
|
|
|
|
{/* Vulnerability Disclosure */}
|
|
<Band rule={false} pad="$12" measure={896}>
|
|
<Box
|
|
render={rise()}
|
|
p={32}
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
background="linear-gradient(to bottom right in oklab, var(--white-20), transparent)"
|
|
>
|
|
<XStack items="flex-start" gap={16}>
|
|
<XStack {...DISC} shrink={0} bg="var(--white-10)">
|
|
<Bug width={24} height={24} color={mix('var(--foreground)', 60)} />
|
|
</XStack>
|
|
<Box>
|
|
<PanelTitle mb={8}>
|
|
Found something
|
|
</PanelTitle>
|
|
<Body mb={16}>
|
|
Email us rather than opening a public issue, and include a reproduction if you have one. A
|
|
real report from a stranger is worth more than an internal review, and we answer quickly.
|
|
</Body>
|
|
<More
|
|
href="mailto:[email protected]"
|
|
$touchable={{ minH: 44, minW: 44 }}
|
|
>
|
|
security@hanzo.ai
|
|
<ExternalLink width={16} height={16} />
|
|
</More>
|
|
</Box>
|
|
</XStack>
|
|
</Box>
|
|
</Band>
|
|
|
|
{/* CTA Section */}
|
|
<Band rule={false} pad="$12" measure={896} ground="linear-gradient(to bottom in oklab, color-mix(in oklab, var(--neutral-900) 30%, transparent), var(--background))">
|
|
<Glow corner="top" pct={10} />
|
|
<Glow corner="bottom" pct={5} />
|
|
|
|
<Box position="relative" z="var(--z-raised)" $platform-web={{ textAlign: 'center' }}>
|
|
<Title
|
|
render={<motion.h2 initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} />}
|
|
mb={16}
|
|
>
|
|
Ask us the hard question
|
|
</Title>
|
|
|
|
<Lede
|
|
render={<motion.p initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.1 }} />}
|
|
mb={40}
|
|
width="100%"
|
|
maxW={672}
|
|
mx="auto"
|
|
mt="$3.5"
|
|
>
|
|
Bring the requirement you cannot get past, and we will show you the code or tell you it is not
|
|
built. Both answers are more useful than a brochure.
|
|
</Lede>
|
|
|
|
<XStack
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.2 }} />}
|
|
flexWrap="wrap"
|
|
items="center"
|
|
justify="center"
|
|
gap={16}
|
|
>
|
|
<Action
|
|
href="/login"
|
|
fill
|
|
>
|
|
Try Hanzo
|
|
<ArrowRight width={20} height={20} />
|
|
</Action>
|
|
<Action
|
|
href="/contact-sales"
|
|
>
|
|
Schedule Security Review
|
|
</Action>
|
|
</XStack>
|
|
|
|
<Box
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.3 }} />}
|
|
mt={32}
|
|
>
|
|
<More
|
|
href="https://docs.hanzo.ai/docs/cli/security"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
$touchable={{ minH: 44, minW: 44 }}
|
|
>
|
|
Read the security documentation
|
|
</More>
|
|
</Box>
|
|
</Box>
|
|
</Band>
|
|
</main>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default Security;
|