feat(canary): landing — specimen requisition page + Turnstile widget

Landing route (/) is the token-creation page, framed as a "specimen
requisition" intake form per the vector-minimalism ethos. Mind: cataloging
a thing that resists cataloging — a trap baited for an unseen visitor.

Page composition:
- Top archive Strip (FIELD STATION / VOL / ISSUE pill)
- Hero block: large blocky "CANARIES" headline, Latin subtitle, purpose line
- Halftone separator
- Five sections (01 species / 02 annotate / 03 route / 04 species config / 05 verify)
- Bottom Strip with routing/license metadata

Form sub-sections (each a small component to keep complexity under biome's
maxAllowedComplexity=25 cap on FormView):
- SpeciesSection — 7-card grid via TypePicker (radio fieldset, sr-only inputs)
- AnnotateSection — memo textarea + conditional filename (when file-kind)
- RouteSection — Telegram | Webhook radio fieldset + conditional credentials
- ConfigSection — slowredirect destination_url or envfile include_keys
- VerifySection — Turnstile widget if VITE_TURNSTILE_SITE_KEY present
- SubmitFooter — release button + privacy note

Validation: client uses createTokenRequestSchema.safeParse, surfaces field
errors inline (mono caps in alarm color); server-side ApiError.fields are
mapped back into the same FieldErrors shape via buildSubmissionState (Phase 14
single-source envelope contract preserved).

Result view: SpecimenCard with full token dossier (DataRows), kind-discriminated
artifact display (url → CopyField; file/text → download Blob via object-URL;
connection_string → CopyField with note), Manage + Trigger CopyFields, link to
the dossier route /m/:manage_id (route wired in next chunk).

Turnstile: explicit-render widget, lazy script-load, registers token via
setTurnstileTokenProvider on success; cleans up on unmount; no key = no widget
(matches backend dev-bypass at internal/turnstile/verifier.go).

Gates green: typecheck + biome + stylelint + build (406ms, landing chunk
123KB / 38KB gzip).
This commit is contained in:
CarterPerez-dev 2026-05-17 14:49:37 -04:00
parent f5d893ab8c
commit 5562f82f34
8 changed files with 1946 additions and 1 deletions

View File

@ -0,0 +1,136 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import { useEffect, useRef, useState } from 'react'
import { setTurnstileTokenProvider } from '@/api'
declare global {
interface Window {
turnstile?: {
render: (
container: HTMLElement,
options: {
sitekey: string
callback?: (token: string) => void
'expired-callback'?: () => void
'error-callback'?: () => void
theme?: 'light' | 'dark' | 'auto'
appearance?: 'always' | 'execute' | 'interaction-only'
size?: 'normal' | 'flexible' | 'compact' | 'invisible'
}
) => string
reset: (widgetId?: string) => void
remove: (widgetId?: string) => void
}
onloadTurnstileCallback?: () => void
}
}
const TURNSTILE_SRC =
'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback'
const TURNSTILE_SCRIPT_ID = 'cf-turnstile-script'
function loadTurnstileScript(): Promise<void> {
if (typeof window === 'undefined') {
return Promise.resolve()
}
if (window.turnstile) {
return Promise.resolve()
}
return new Promise((resolve, reject) => {
const existing = document.getElementById(TURNSTILE_SCRIPT_ID)
if (existing) {
existing.addEventListener('load', () => resolve(), { once: true })
existing.addEventListener(
'error',
() => reject(new Error('turnstile script failed to load')),
{ once: true }
)
return
}
window.onloadTurnstileCallback = () => resolve()
const script = document.createElement('script')
script.id = TURNSTILE_SCRIPT_ID
script.src = TURNSTILE_SRC
script.async = true
script.defer = true
script.addEventListener(
'error',
() => reject(new Error('turnstile script failed to load')),
{ once: true }
)
document.head.appendChild(script)
})
}
type TurnstileProps = {
siteKey: string
appearance?: 'always' | 'execute' | 'interaction-only'
}
export function Turnstile({
siteKey,
appearance = 'always',
}: TurnstileProps): React.ReactElement {
const containerRef = useRef<HTMLDivElement | null>(null)
const widgetIdRef = useRef<string | null>(null)
const tokenRef = useRef<string | null>(null)
const [status, setStatus] = useState<'idle' | 'ready' | 'error'>('idle')
useEffect(() => {
let cancelled = false
setTurnstileTokenProvider(() => tokenRef.current)
loadTurnstileScript()
.then(() => {
if (cancelled || !window.turnstile || !containerRef.current) {
return
}
widgetIdRef.current = window.turnstile.render(containerRef.current, {
sitekey: siteKey,
appearance,
callback: (token: string) => {
tokenRef.current = token
setStatus('ready')
},
'expired-callback': () => {
tokenRef.current = null
setStatus('idle')
},
'error-callback': () => {
tokenRef.current = null
setStatus('error')
},
})
})
.catch(() => {
if (!cancelled) {
setStatus('error')
}
})
return () => {
cancelled = true
setTurnstileTokenProvider(null)
if (widgetIdRef.current && window.turnstile) {
window.turnstile.remove(widgetIdRef.current)
widgetIdRef.current = null
}
tokenRef.current = null
}
}, [siteKey, appearance])
return (
<div data-turnstile-status={status}>
<div ref={containerRef} />
</div>
)
}
export function getTurnstileSiteKey(): string | null {
const key = import.meta.env.VITE_TURNSTILE_SITE_KEY
if (typeof key !== 'string' || key.length === 0) {
return null
}
return key
}

View File

@ -0,0 +1,163 @@
// ===================
// ©AngelaMos | 2026
// artifact.tsx
// ===================
import { useMemo } from 'react'
import { toast } from 'sonner'
import type { Artifact } from '@/api'
import { CopyField, Pill } from '@/components'
import styles from './landing.module.scss'
type ArtifactDisplayProps = {
artifact: Artifact
filenameFallback: string
}
export function ArtifactDisplay({
artifact,
filenameFallback,
}: ArtifactDisplayProps): React.ReactElement {
if (artifact.kind === 'url') {
return <UrlArtifact artifact={artifact} />
}
if (artifact.kind === 'file') {
return (
<FileArtifact artifact={artifact} filenameFallback={filenameFallback} />
)
}
if (artifact.kind === 'text') {
return (
<TextArtifact artifact={artifact} filenameFallback={filenameFallback} />
)
}
return <ConnectionStringArtifact artifact={artifact} />
}
function UrlArtifact({
artifact,
}: {
artifact: Extract<Artifact, { kind: 'url' }>
}): React.ReactElement {
const url = artifact.url ?? ''
return (
<div className={styles.artifactStack}>
<CopyField label="TRIGGER URL" value={url} fullWidth />
{artifact.destination_url ? (
<CopyField
label="DESTINATION"
value={artifact.destination_url}
fullWidth
/>
) : null}
</div>
)
}
function FileArtifact({
artifact,
filenameFallback,
}: {
artifact: Extract<Artifact, { kind: 'file' }>
filenameFallback: string
}): React.ReactElement {
const filename = artifact.filename ?? filenameFallback
const contentType = artifact.content_type ?? 'application/octet-stream'
const href = useMemo(() => {
if (!artifact.content_b64) {
return null
}
try {
return base64ToObjectUrl(artifact.content_b64, contentType)
} catch (_err) {
return null
}
}, [artifact.content_b64, contentType])
if (!href) {
return <Pill tone="alarm">artifact unreadable</Pill>
}
return (
<div className={styles.artifactStack}>
<div className={styles.fileBar}>
<span className={styles.fileMeta}>
<span className={styles.fileLabel}>FILE</span>
<span className={styles.fileName}>{filename}</span>
<span className={styles.fileType}>{contentType}</span>
</span>
<a
className={styles.downloadBtn}
href={href}
download={filename}
onClick={() => toast.success(`Issuing ${filename}`)}
>
download
</a>
</div>
</div>
)
}
function TextArtifact({
artifact,
filenameFallback,
}: {
artifact: Extract<Artifact, { kind: 'text' }>
filenameFallback: string
}): React.ReactElement {
const filename = artifact.filename ?? filenameFallback
const content = artifact.content ?? ''
const contentType = artifact.content_type ?? 'text/plain'
const href = useMemo(() => {
if (content.length === 0) {
return null
}
const blob = new Blob([content], { type: contentType })
return URL.createObjectURL(blob)
}, [content, contentType])
return (
<div className={styles.artifactStack}>
<div className={styles.fileBar}>
<span className={styles.fileMeta}>
<span className={styles.fileLabel}>TEXT</span>
<span className={styles.fileName}>{filename}</span>
<span className={styles.fileType}>{contentType}</span>
</span>
{href ? (
<a className={styles.downloadBtn} href={href} download={filename}>
download
</a>
) : null}
</div>
<pre className={styles.textPreview}>{content}</pre>
</div>
)
}
function ConnectionStringArtifact({
artifact,
}: {
artifact: Extract<Artifact, { kind: 'connection_string' }>
}): React.ReactElement {
const value = artifact.connection_string ?? ''
return (
<div className={styles.artifactStack}>
<CopyField label="CONNECTION" value={value} fullWidth />
<p className={styles.note}>
Open in any MySQL client. The handshake will trip the trap.
</p>
</div>
)
}
function base64ToObjectUrl(b64: string, contentType: string): string {
const binary = atob(b64)
const len = binary.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i += 1) {
bytes[i] = binary.charCodeAt(i)
}
return URL.createObjectURL(new Blob([bytes], { type: contentType }))
}

View File

@ -0,0 +1,61 @@
// ===================
// ©AngelaMos | 2026
// copy.ts
// ===================
import type { TokenType } from '@/api'
export const PAGE_COPY = {
HEADLINE: 'CANARIES',
HEADLINE_LATIN: 'Serinus Canaria · token-form (n.)',
HEADLINE_PURPOSE:
'Field-deployable trip-wires for the quiet hours. Each specimen waits in place, reports when touched, and leaves a forensic note behind.',
STRIP_FIELD_STATION: 'FIELD STATION',
STRIP_ISSUE: 'ISSUE',
STRIP_VOLUME: 'VOL.001',
STRIP_FOLIO: 'FOLIO',
SECTION_01_INDEX: '01 / SPECIES',
SECTION_01_TITLE: 'SELECT SPECIES',
SECTION_01_BODY:
'Each type lives in a different habitat. Pick the one that fits where the trap will be set.',
SECTION_02_INDEX: '02 / FIELD LABEL',
SECTION_02_TITLE: 'ANNOTATE THE SPECIMEN',
SECTION_02_BODY:
'A short note describing where this specimen will be deployed. You will see it again when the trap reports.',
SECTION_03_INDEX: '03 / ALERT ROUTE',
SECTION_03_TITLE: 'ROUTE THE REPORT',
SECTION_03_BODY:
'When the trap is touched, where should the message land? Choose one channel.',
SECTION_04_INDEX: '04 / SPECIES CONFIG',
SECTION_04_BODY: 'Configuration specific to the chosen species.',
SECTION_05_INDEX: '05 / VERIFY',
SECTION_05_BODY:
'A one-touch human check from Cloudflare. Skipped in development.',
SUBMIT_LABEL: 'Release specimen',
ISSUE_ANOTHER_LABEL: 'Release another',
RESULT_HEADLINE: 'SPECIMEN ISSUED',
RESULT_BODY:
'The trap is live. Keep the manage URL safe — it is the only way back to this dossier.',
} as const
export const TOKEN_BLURB: Record<TokenType, string> = {
webbug:
'1×1 transparent pixel. Embed anywhere an <img> works — emails, wiki pages, slide decks.',
slowredirect:
'A redirect that pauses to fingerprint the visitor before delivering them. Drop-in short link replacement.',
docx: 'A Word document that phones home the moment Word renders its footer.',
pdf: 'A PDF whose embedded URI action fires in Adobe Reader. Quiet against Chromium / PDF.js.',
kubeconfig:
'A kubeconfig pointing at a fake Kubernetes API. kubectl calls trigger and are recorded with their verb + path.',
envfile:
'A plausible-looking .env file with bait credentials and one disguised canary URL.',
mysql:
'A fake MySQL server that answers the handshake, captures the username, then 1045 ACCESS DENIED.',
}
export const ARTIFACT_LABEL = {
url: 'TRIGGER URL',
file: 'DOWNLOAD',
text: 'CONTENT',
connection_string: 'CONNECTION STRING',
} as const

View File

@ -0,0 +1,136 @@
// ===================
// ©AngelaMos | 2026
// form-state.ts
// ===================
import {
type CreateTokenInput,
createTokenRequestSchema,
type EnvfileIncludeKey,
type TokenType,
type TypeDescriptor,
} from '@/api'
import { ApiError } from '@/core/api'
export type FormState = {
type: TokenType | ''
memo: string
filename: string
alertChannel: 'telegram' | 'webhook'
telegramBot: string
telegramChat: string
webhookUrl: string
slowredirectDestination: string
envfileIncludeKeys: EnvfileIncludeKey[]
}
export const INITIAL_FORM: FormState = {
type: '',
memo: '',
filename: '',
alertChannel: 'telegram',
telegramBot: '',
telegramChat: '',
webhookUrl: '',
slowredirectDestination: '',
envfileIncludeKeys: ['aws', 'stripe', 'db'],
}
export const FILE_KIND_TYPES = new Set<TokenType>([
'docx',
'pdf',
'kubeconfig',
'envfile',
])
export function descriptorFor(
descriptors: TypeDescriptor[],
type: TokenType | ''
): TypeDescriptor | undefined {
if (type === '') {
return undefined
}
return descriptors.find((d) => d.type === type)
}
export function buildPayload(form: FormState): CreateTokenInput {
if (form.type === '') {
throw new Error('species not selected')
}
const payload: CreateTokenInput = {
type: form.type,
memo: form.memo,
alert_channel: form.alertChannel,
}
if (form.filename.length > 0) {
payload.filename = form.filename
}
if (form.alertChannel === 'telegram') {
payload.telegram_bot = form.telegramBot
payload.telegram_chat = form.telegramChat
} else {
payload.webhook_url = form.webhookUrl
}
if (form.type === 'slowredirect') {
payload.metadata = { destination_url: form.slowredirectDestination }
} else if (form.type === 'envfile') {
payload.metadata = { include_keys: form.envfileIncludeKeys }
}
return payload
}
export type FieldErrors = Partial<Record<string, string>>
const BACKEND_TO_FORM_FIELD: Readonly<Record<string, string>> = {
type: 'type',
memo: 'memo',
filename: 'filename',
alert_channel: 'alert_channel',
telegram_bot: 'telegram_bot',
telegram_chat: 'telegram_chat',
webhook_url: 'webhook_url',
'metadata.destination_url': 'metadata.destination_url',
'metadata.include_keys': 'metadata.include_keys',
}
export function buildSubmissionState(error: unknown): {
errors: FieldErrors
} {
if (!(error instanceof ApiError) || !error.fields) {
return { errors: { form: 'submission failed — try again' } }
}
const errors: FieldErrors = {}
for (const [key, value] of Object.entries(error.fields)) {
const mapped = BACKEND_TO_FORM_FIELD[key] ?? key
errors[mapped] = value
}
return { errors }
}
export function validateForm(form: FormState): {
ok: boolean
errors: FieldErrors
payload: CreateTokenInput | null
} {
if (form.type === '') {
return { ok: false, errors: { type: 'select a species' }, payload: null }
}
let payload: CreateTokenInput
try {
payload = buildPayload(form)
} catch (_err) {
return { ok: false, errors: { type: 'select a species' }, payload: null }
}
const parsed = createTokenRequestSchema.safeParse(payload)
if (parsed.success) {
return { ok: true, errors: {}, payload }
}
const errors: FieldErrors = {}
for (const issue of parsed.error.issues) {
const key = issue.path.join('.') || 'form'
if (errors[key] === undefined) {
errors[key] = issue.message
}
}
return { ok: false, errors, payload: null }
}

View File

@ -3,8 +3,611 @@
// index.tsx
// ===================
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
type CreateTokenResponse,
type EnvfileIncludeKey,
envfileIncludeKeySchema,
type TokenType,
type TypeDescriptor,
useCreateToken,
useTokenTypes,
} from '@/api'
import { Button, Halftone, Strip, StripItem } from '@/components'
import { getTurnstileSiteKey, Turnstile } from '@/core/turnstile'
import { ArtifactDisplay } from './artifact'
import { PAGE_COPY, TOKEN_BLURB } from './copy'
import {
buildSubmissionState,
FILE_KIND_TYPES,
type FieldErrors,
type FormState,
INITIAL_FORM,
validateForm,
} from './form-state'
import styles from './landing.module.scss'
import { ResultView } from './result'
import { TypePicker } from './type-picker'
const ENVFILE_KEYS = envfileIncludeKeySchema.options
export function Component(): React.ReactElement {
return <div />
const typesQuery = useTokenTypes()
const createMutation = useCreateToken()
const [form, setForm] = useState<FormState>(INITIAL_FORM)
const [errors, setErrors] = useState<FieldErrors>({})
const [created, setCreated] = useState<CreateTokenResponse | null>(null)
const turnstileSiteKey = useMemo(() => getTurnstileSiteKey(), [])
function patchForm(next: Partial<FormState>): void {
setForm((prev) => ({ ...prev, ...next }))
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>): void {
event.preventDefault()
const result = validateForm(form)
setErrors(result.errors)
if (!result.ok || !result.payload) {
toast.error('Some fields need attention')
return
}
createMutation.mutate(result.payload, {
onSuccess: (data) => {
setCreated(data)
toast.success('Specimen released')
},
onError: (error) => {
const submission = buildSubmissionState(error)
setErrors(submission.errors)
},
})
}
function handleAnother(): void {
setCreated(null)
setErrors({})
setForm((prev) => ({ ...prev, memo: '', filename: '' }))
}
return (
<div className={styles.page}>
<Strip>
<StripItem label={PAGE_COPY.STRIP_FIELD_STATION}>canary</StripItem>
<StripItem label={PAGE_COPY.STRIP_VOLUME}>2026·05</StripItem>
<StripItem label={PAGE_COPY.STRIP_ISSUE} inverted>
{created ? 'DOSSIER' : 'INTAKE'}
</StripItem>
</Strip>
<header className={styles.hero}>
<h1 className={styles.headline}>{PAGE_COPY.HEADLINE}</h1>
<p className={styles.latin}>{PAGE_COPY.HEADLINE_LATIN}</p>
<p className={styles.purpose}>{PAGE_COPY.HEADLINE_PURPOSE}</p>
</header>
<Halftone density="sparse" height={18} />
<main className={styles.main}>
{created ? (
<ResultView
data={created}
onAnother={handleAnother}
filenameFallback={form.filename || defaultFilename(form.type)}
/>
) : (
<FormView
form={form}
errors={errors}
descriptors={typesQuery.data ?? []}
descriptorsLoading={typesQuery.isLoading}
descriptorsError={typesQuery.error}
turnstileSiteKey={turnstileSiteKey}
submitting={createMutation.isPending}
onChange={patchForm}
onSubmit={handleSubmit}
/>
)}
</main>
<Strip align="left" border="top">
<StripItem label="ROUTING">cloudflare tunnel</StripItem>
<StripItem label="ALERTS">telegram / webhook</StripItem>
<StripItem label="LICENSE">©AngelaMos 2026</StripItem>
</Strip>
</div>
)
}
Component.displayName = 'Landing'
type FormViewProps = {
form: FormState
errors: FieldErrors
descriptors: TypeDescriptor[]
descriptorsLoading: boolean
descriptorsError: Error | null
turnstileSiteKey: string | null
submitting: boolean
onChange: (next: Partial<FormState>) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
}
function FormView({
form,
errors,
descriptors,
descriptorsLoading,
descriptorsError,
turnstileSiteKey,
submitting,
onChange,
onSubmit,
}: FormViewProps): React.ReactElement {
const showTypeMetadata = form.type === 'slowredirect' || form.type === 'envfile'
return (
<form className={styles.form} onSubmit={onSubmit} noValidate>
<SpeciesSection
form={form}
errors={errors}
descriptors={descriptors}
descriptorsLoading={descriptorsLoading}
descriptorsError={descriptorsError}
onChange={onChange}
/>
<AnnotateSection form={form} errors={errors} onChange={onChange} />
<RouteSection form={form} errors={errors} onChange={onChange} />
{showTypeMetadata ? (
<ConfigSection form={form} errors={errors} onChange={onChange} />
) : null}
{turnstileSiteKey ? <VerifySection siteKey={turnstileSiteKey} /> : null}
<SubmitFooter submitting={submitting} />
</form>
)
}
type SectionPropsBase = {
form: FormState
errors: FieldErrors
onChange: (next: Partial<FormState>) => void
}
type SpeciesSectionProps = SectionPropsBase & {
descriptors: TypeDescriptor[]
descriptorsLoading: boolean
descriptorsError: Error | null
}
function SpeciesSection({
form,
errors,
descriptors,
descriptorsLoading,
descriptorsError,
onChange,
}: SpeciesSectionProps): React.ReactElement {
return (
<Section
index={PAGE_COPY.SECTION_01_INDEX}
title={PAGE_COPY.SECTION_01_TITLE}
body={PAGE_COPY.SECTION_01_BODY}
>
{descriptorsError ? (
<p className={styles.errorBlock}>
Could not load species catalog. Try again in a moment.
</p>
) : descriptorsLoading ? (
<p className={styles.statusBlock}>Loading species catalog</p>
) : (
<TypePicker
descriptors={descriptors}
value={form.type}
onChange={(t) => onChange({ type: t })}
invalid={errors.type !== undefined}
/>
)}
{errors.type ? <p className={styles.fieldError}>{errors.type}</p> : null}
</Section>
)
}
function AnnotateSection({
form,
errors,
onChange,
}: SectionPropsBase): React.ReactElement {
const showFilename =
form.type !== '' && FILE_KIND_TYPES.has(form.type as TokenType)
return (
<Section
index={PAGE_COPY.SECTION_02_INDEX}
title={PAGE_COPY.SECTION_02_TITLE}
body={PAGE_COPY.SECTION_02_BODY}
>
<div className={styles.fieldRow}>
<FieldLabel index="A">MEMO</FieldLabel>
<textarea
className={styles.textarea}
value={form.memo}
maxLength={256}
placeholder="Q4 bonuses spreadsheet on file-server"
onChange={(e) => onChange({ memo: e.target.value })}
data-invalid={errors.memo !== undefined}
/>
{errors.memo ? (
<p className={styles.fieldError}>{errors.memo}</p>
) : (
<p className={styles.fieldHint}>{form.memo.length} / 256 characters</p>
)}
</div>
{showFilename ? (
<FilenameField form={form} errors={errors} onChange={onChange} />
) : null}
</Section>
)
}
function FilenameField({
form,
errors,
onChange,
}: SectionPropsBase): React.ReactElement {
return (
<div className={styles.fieldRow}>
<FieldLabel index="B">FILENAME (OPTIONAL)</FieldLabel>
<input
className={styles.input}
value={form.filename}
maxLength={128}
placeholder={defaultFilename(form.type)}
onChange={(e) => onChange({ filename: e.target.value })}
data-invalid={errors.filename !== undefined}
/>
{errors.filename ? (
<p className={styles.fieldError}>{errors.filename}</p>
) : (
<p className={styles.fieldHint}>
What the bait file will be called when it lands somewhere it shouldn't.
</p>
)}
</div>
)
}
function RouteSection({
form,
errors,
onChange,
}: SectionPropsBase): React.ReactElement {
return (
<Section
index={PAGE_COPY.SECTION_03_INDEX}
title="ROUTE THE REPORT"
body={PAGE_COPY.SECTION_03_BODY}
>
<ChannelToggle
value={form.alertChannel}
onChange={(c) => onChange({ alertChannel: c })}
/>
{form.alertChannel === 'telegram' ? (
<TelegramFields form={form} errors={errors} onChange={onChange} />
) : (
<WebhookField form={form} errors={errors} onChange={onChange} />
)}
</Section>
)
}
function TelegramFields({
form,
errors,
onChange,
}: SectionPropsBase): React.ReactElement {
return (
<div className={styles.subgrid}>
<div className={styles.fieldRow}>
<FieldLabel index="A">BOT TOKEN</FieldLabel>
<input
className={styles.input}
value={form.telegramBot}
placeholder="123456789:ABCdefGHIjklMNO..."
onChange={(e) => onChange({ telegramBot: e.target.value })}
data-invalid={errors.telegram_bot !== undefined}
/>
{errors.telegram_bot ? (
<p className={styles.fieldError}>{errors.telegram_bot}</p>
) : null}
</div>
<div className={styles.fieldRow}>
<FieldLabel index="B">CHAT ID</FieldLabel>
<input
className={styles.input}
value={form.telegramChat}
placeholder="-1001234567890"
onChange={(e) => onChange({ telegramChat: e.target.value })}
data-invalid={errors.telegram_chat !== undefined}
/>
{errors.telegram_chat ? (
<p className={styles.fieldError}>{errors.telegram_chat}</p>
) : null}
</div>
</div>
)
}
function WebhookField({
form,
errors,
onChange,
}: SectionPropsBase): React.ReactElement {
return (
<div className={styles.fieldRow}>
<FieldLabel index="A">WEBHOOK URL</FieldLabel>
<input
className={styles.input}
type="url"
value={form.webhookUrl}
placeholder="https://hooks.example.com/canary"
onChange={(e) => onChange({ webhookUrl: e.target.value })}
data-invalid={errors.webhook_url !== undefined}
/>
{errors.webhook_url ? (
<p className={styles.fieldError}>{errors.webhook_url}</p>
) : (
<p className={styles.fieldHint}>
We POST the alert envelope JSON. Optionally HMAC-signed if the server
has WEBHOOK_HMAC_SECRET set.
</p>
)}
</div>
)
}
function ConfigSection({
form,
errors,
onChange,
}: SectionPropsBase): React.ReactElement {
const body =
form.type === 'slowredirect'
? 'Visitors will be fingerprinted, then redirected here.'
: 'Bait credentials to ship alongside the disguised canary URL.'
return (
<Section
index={PAGE_COPY.SECTION_04_INDEX}
title={descriptorTitle(form.type)}
body={body}
>
{form.type === 'slowredirect' ? (
<SlowRedirectField form={form} errors={errors} onChange={onChange} />
) : (
<IncludeKeysPicker
value={form.envfileIncludeKeys}
onChange={(k) => onChange({ envfileIncludeKeys: k })}
/>
)}
</Section>
)
}
function SlowRedirectField({
form,
errors,
onChange,
}: SectionPropsBase): React.ReactElement {
const err = errors['metadata.destination_url']
return (
<div className={styles.fieldRow}>
<FieldLabel index="A">DESTINATION URL</FieldLabel>
<input
className={styles.input}
type="url"
value={form.slowredirectDestination}
placeholder="https://example.com/landing"
onChange={(e) => onChange({ slowredirectDestination: e.target.value })}
data-invalid={err !== undefined}
/>
{err ? <p className={styles.fieldError}>{err}</p> : null}
</div>
)
}
function VerifySection({ siteKey }: { siteKey: string }): React.ReactElement {
return (
<Section
index={PAGE_COPY.SECTION_05_INDEX}
title="VERIFY"
body={PAGE_COPY.SECTION_05_BODY}
>
<div className={styles.turnstile}>
<Turnstile siteKey={siteKey} />
</div>
</Section>
)
}
function SubmitFooter({
submitting,
}: {
submitting: boolean
}): React.ReactElement {
return (
<div className={styles.submitRow}>
<p className={styles.submitNote}>
We never see the alert payload it routes directly from server to your
channel.
</p>
<Button type="submit" size="lg" busy={submitting}>
{PAGE_COPY.SUBMIT_LABEL}
</Button>
</div>
)
}
type SectionProps = React.PropsWithChildren<{
index: string
title: string
body: string
}>
function Section({
index,
title,
body,
children,
}: SectionProps): React.ReactElement {
return (
<section className={styles.section}>
<header className={styles.sectionHead}>
<span className={styles.sectionIndex}>{index}</span>
<span className={styles.sectionRule} aria-hidden="true" />
</header>
<h2 className={styles.sectionTitle}>{title}</h2>
<p className={styles.sectionBody}>{body}</p>
<div className={styles.sectionContent}>{children}</div>
</section>
)
}
function FieldLabel({
index,
children,
}: React.PropsWithChildren<{ index?: string }>): React.ReactElement {
return (
<div className={styles.fieldLabel}>
{index ? <span className={styles.fieldIndex}>{index}</span> : null}
<span className={styles.fieldName}>{children}</span>
<span className={styles.fieldRule} aria-hidden="true" />
</div>
)
}
type ChannelToggleProps = {
value: 'telegram' | 'webhook'
onChange: (next: 'telegram' | 'webhook') => void
}
function ChannelToggle({
value,
onChange,
}: ChannelToggleProps): React.ReactElement {
return (
<fieldset className={styles.channelToggle}>
<legend className={styles.srOnly}>Alert channel</legend>
<ChannelOption
name="alert-channel"
value="telegram"
active={value === 'telegram'}
onChange={() => onChange('telegram')}
label="Telegram"
note="message lands in a chat"
/>
<ChannelOption
name="alert-channel"
value="webhook"
active={value === 'webhook'}
onChange={() => onChange('webhook')}
label="Webhook"
note="JSON POST to your URL"
/>
</fieldset>
)
}
function ChannelOption({
name,
value,
active,
onChange,
label,
note,
}: {
name: string
value: string
active: boolean
onChange: () => void
label: string
note: string
}): React.ReactElement {
return (
<label className={styles.channelOption} data-active={active}>
<input
className={styles.srOnly}
type="radio"
name={name}
value={value}
checked={active}
onChange={onChange}
/>
<span className={styles.channelLabel}>{label}</span>
<span className={styles.channelNote}>{note}</span>
</label>
)
}
type IncludeKeysPickerProps = {
value: EnvfileIncludeKey[]
onChange: (next: EnvfileIncludeKey[]) => void
}
function IncludeKeysPicker({
value,
onChange,
}: IncludeKeysPickerProps): React.ReactElement {
function toggle(key: EnvfileIncludeKey): void {
if (value.includes(key)) {
onChange(value.filter((k) => k !== key))
} else {
onChange([...value, key])
}
}
return (
<fieldset className={styles.keyGrid}>
<legend className={styles.srOnly}>Include keys</legend>
{ENVFILE_KEYS.map((key) => {
const checked = value.includes(key)
return (
<label key={key} className={styles.keyChip} data-selected={checked}>
<input
className={styles.srOnly}
type="checkbox"
name="envfile-keys"
value={key}
checked={checked}
onChange={() => toggle(key)}
/>
<span className={styles.keyName}>{key}</span>
</label>
)
})}
</fieldset>
)
}
function defaultFilename(type: TokenType | ''): string {
switch (type) {
case 'docx':
return 'Q4_Bonuses_2024.docx'
case 'pdf':
return 'Confidential_Report.pdf'
case 'kubeconfig':
return 'kubeconfig'
case 'envfile':
return '.env'
default:
return ''
}
}
function descriptorTitle(type: TokenType | ''): string {
if (type === 'slowredirect') {
return 'SLOWREDIRECT CONFIG'
}
if (type === 'envfile') {
return 'ENVFILE CONFIG'
}
return 'CONFIG'
}
export { TOKEN_BLURB }
export { ArtifactDisplay }

View File

@ -0,0 +1,669 @@
// ===================
// ©AngelaMos | 2026
// landing.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
@use '../../styles/mixins' as *;
.page {
display: flex;
flex-direction: column;
flex: 1 1 auto;
inline-size: 100%;
max-inline-size: 88rem;
margin-inline: auto;
padding-inline: $space-4;
padding-block-end: $space-8;
gap: $space-6;
@include breakpoint-up('md') {
padding-inline: $space-8;
gap: $space-8;
}
}
.hero {
display: flex;
flex-direction: column;
gap: $space-3;
padding-block-start: $space-6;
max-inline-size: 56rem;
@include breakpoint-up('md') {
padding-block-start: $space-10;
}
}
.headline {
font-family: $font-sans;
font-weight: 900;
font-size: clamp(3rem, 11vw, 8.5rem);
letter-spacing: -0.06em;
line-height: 0.85;
color: $ink;
margin: 0;
text-transform: uppercase;
}
.latin {
font-family: $font-mono;
font-size: $font-size-xs;
letter-spacing: $tracking-wider;
color: $ink-soft;
margin: 0;
text-transform: lowercase;
font-style: italic;
}
.purpose {
font-family: $font-sans;
font-size: clamp(0.95rem, 1.6vw, 1.125rem);
line-height: $line-height-snug;
color: $ink;
max-inline-size: 42rem;
margin: 0;
margin-block-start: $space-3;
text-wrap: balance;
}
.main {
display: flex;
flex-direction: column;
flex: 1 1 auto;
gap: $space-8;
padding-block-start: $space-2;
}
.form {
display: flex;
flex-direction: column;
gap: $space-10;
max-inline-size: 56rem;
inline-size: 100%;
}
.section {
display: flex;
flex-direction: column;
gap: $space-3;
}
.sectionHead {
display: flex;
align-items: center;
gap: $space-3;
}
.sectionIndex {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
white-space: nowrap;
}
.sectionRule {
flex: 1 1 auto;
block-size: $hairline;
background-color: $rule-hair;
}
.sectionTitle {
font-family: $font-sans;
font-weight: 800;
font-size: clamp(1.5rem, 3vw, 2.25rem);
letter-spacing: -0.03em;
line-height: $line-height-tight;
text-transform: uppercase;
color: $ink;
margin: 0;
}
.sectionBody {
font-family: $font-sans;
font-size: $font-size-sm;
line-height: $line-height-snug;
color: $ink-soft;
max-inline-size: 38rem;
margin: 0;
}
.sectionContent {
display: flex;
flex-direction: column;
gap: $space-5;
padding-block-start: $space-3;
}
.typeGrid {
display: grid;
grid-template-columns: 1fr;
gap: $space-3;
border: none;
padding: 0;
margin: 0;
min-inline-size: 0;
&[data-invalid='true'] {
outline: $hairline solid $alarm;
outline-offset: $space-2;
}
@include breakpoint-up('sm') {
grid-template-columns: repeat(2, 1fr);
}
@include breakpoint-up('md') {
grid-template-columns: repeat(3, 1fr);
}
@include breakpoint-up('lg') {
grid-template-columns: repeat(4, 1fr);
}
}
.srOnly {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.typeCard {
position: relative;
display: grid;
grid-template-areas:
'idx glyph'
'name name'
'code code'
'blur blur';
grid-template-columns: auto 1fr;
align-items: start;
gap: $space-2 $space-3;
padding: $space-4;
background-color: $paper;
border: $hairline solid $rule;
text-align: left;
cursor: pointer;
transition: background-color $duration-fast $ease-out,
color $duration-fast $ease-out;
&:hover {
background-color: $paper-deep;
}
&:focus-visible {
outline: $hairline-bold solid $ink;
outline-offset: $space-1;
}
&[data-selected='true'] {
background-color: $ink;
color: $paper;
border-color: $ink;
.typeIndex,
.typeCode {
color: $paper-edge;
}
.typeBlurb {
color: $paper-edge;
}
}
}
.typeIndex {
grid-area: idx;
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
color: $ink-mute;
}
.typeGlyph {
grid-area: glyph;
justify-self: end;
color: inherit;
}
.typeName {
grid-area: name;
font-family: $font-sans;
font-weight: 700;
font-size: $font-size-lg;
line-height: $line-height-tight;
letter-spacing: -0.02em;
color: inherit;
}
.typeCode {
grid-area: code;
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
}
.typeBlurb {
grid-area: blur;
font-size: $font-size-xs;
line-height: $line-height-snug;
color: $ink-soft;
}
.fieldRow {
display: flex;
flex-direction: column;
gap: $space-2;
}
.fieldLabel {
display: flex;
align-items: center;
gap: $space-2;
}
.fieldIndex {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
color: $ink-mute;
white-space: nowrap;
}
.fieldName {
font-family: $font-mono;
font-size: $font-size-3xs;
font-weight: $font-weight-semibold;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink;
white-space: nowrap;
}
.fieldRule {
flex: 1 1 auto;
block-size: $hairline;
background-color: $rule-hair;
}
.input,
.textarea {
inline-size: 100%;
padding-block: $space-3;
padding-inline: $space-3;
border: $hairline solid $rule;
background-color: $paper;
font-family: $font-sans;
font-size: $font-size-base;
color: $ink;
appearance: none;
border-radius: 0;
transition: border-color $duration-fast $ease-out;
&::placeholder {
color: $ink-faint;
}
&:focus {
outline: none;
box-shadow: inset 0 0 0 $hairline $ink;
border-color: $ink;
}
&[data-invalid='true'] {
border-color: $alarm;
}
}
.textarea {
min-block-size: 5lh;
resize: vertical;
line-height: $line-height-normal;
}
.fieldHint {
font-size: $font-size-xs;
color: $ink-mute;
margin: 0;
}
.fieldError {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $alarm;
margin: 0;
}
.errorBlock,
.statusBlock {
padding: $space-4;
border: $hairline solid $rule;
background-color: $paper-deep;
font-family: $font-mono;
font-size: $font-size-xs;
letter-spacing: $tracking-wide;
color: $ink-soft;
}
.errorBlock {
border-color: $alarm;
color: $alarm-deep;
}
.channelToggle {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border: $hairline solid $rule;
padding: 0;
margin: 0;
min-inline-size: 0;
}
.channelOption {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $space-1;
padding-block: $space-3;
padding-inline: $space-4;
background-color: $paper;
text-align: left;
cursor: pointer;
border: none;
transition: background-color $duration-fast $ease-out;
&:hover {
background-color: $paper-deep;
}
& + & {
border-inline-start: $hairline solid $rule;
}
&[data-active='true'] {
background-color: $ink;
color: $paper;
}
}
.channelLabel {
font-family: $font-mono;
font-size: $font-size-2xs;
font-weight: $font-weight-semibold;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: inherit;
}
.channelNote {
font-size: $font-size-xs;
color: inherit;
opacity: 0.7;
}
.subgrid {
display: grid;
grid-template-columns: 1fr;
gap: $space-4;
@include breakpoint-up('md') {
grid-template-columns: 1fr 1fr;
}
}
.keyGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $space-2;
border: none;
padding: 0;
margin: 0;
min-inline-size: 0;
@include breakpoint-up('sm') {
grid-template-columns: repeat(4, 1fr);
}
}
.keyChip {
padding-block: $space-3;
padding-inline: $space-3;
border: $hairline solid $rule;
background-color: $paper;
font-family: $font-mono;
font-size: $font-size-2xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink;
cursor: pointer;
transition: background-color $duration-fast $ease-out;
&:hover {
background-color: $paper-deep;
}
&[data-selected='true'] {
background-color: $ink;
color: $paper;
border-color: $ink;
}
}
.keyName {
display: inline-block;
}
.turnstile {
display: flex;
align-items: center;
padding: $space-3;
border: $hairline solid $rule-hair;
background-color: $paper-deep;
}
.submitRow {
display: flex;
flex-direction: column;
gap: $space-3;
align-items: stretch;
padding-block-start: $space-4;
border-block-start: $hairline solid $rule;
@include breakpoint-up('md') {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.submitNote {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wide;
text-transform: uppercase;
color: $ink-mute;
margin: 0;
max-inline-size: 28rem;
}
// ----------------------------------------------------------------------------
// RESULT
// ----------------------------------------------------------------------------
.result {
display: flex;
flex-direction: column;
gap: $space-6;
max-inline-size: 56rem;
}
.resultHead {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
align-items: baseline;
gap: $space-2 $space-3;
}
.resultMarker {
grid-row: 1 / span 2;
font-size: clamp(2.5rem, 5vw, 4rem);
line-height: 1;
color: $alarm;
align-self: start;
font-family: $font-sans;
}
.resultHeadline {
grid-row: 1;
grid-column: 2;
font-family: $font-sans;
font-weight: 900;
font-size: clamp(2rem, 5vw, 3.5rem);
letter-spacing: -0.04em;
line-height: 0.9;
text-transform: uppercase;
color: $ink;
margin: 0;
}
.resultBody {
grid-row: 2;
grid-column: 2;
font-family: $font-sans;
font-size: $font-size-sm;
line-height: $line-height-snug;
color: $ink-soft;
margin: 0;
}
.resultFoot {
display: flex;
align-items: center;
justify-content: space-between;
gap: $space-3;
flex-wrap: wrap;
}
.resultLink {
font-family: $font-mono;
font-size: $font-size-xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink;
text-decoration: none;
border-block-end: $hairline solid $ink;
padding-block-end: $space-0-5;
transition: color $duration-fast $ease-out;
&:hover {
color: $alarm;
border-block-end-color: $alarm;
}
}
// ----------------------------------------------------------------------------
// ARTIFACT
// ----------------------------------------------------------------------------
.artifactStack {
display: flex;
flex-direction: column;
gap: $space-2;
}
.fileBar {
display: grid;
grid-template-columns: 1fr auto;
align-items: stretch;
border: $hairline solid $rule;
background-color: $paper-deep;
}
.fileMeta {
display: flex;
flex-direction: column;
gap: $space-1;
padding-block: $space-3;
padding-inline: $space-3;
}
.fileLabel {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
}
.fileName {
font-family: $font-mono;
font-size: $font-size-sm;
color: $ink;
overflow-wrap: anywhere;
}
.fileType {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wide;
color: $ink-soft;
overflow-wrap: anywhere;
}
.downloadBtn {
display: inline-flex;
align-items: center;
padding-block: $space-3;
padding-inline: $space-5;
background-color: $ink;
color: $paper;
font-family: $font-mono;
font-size: $font-size-2xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
text-decoration: none;
border-inline-start: $hairline solid $ink;
transition: background-color $duration-fast $ease-out,
color $duration-fast $ease-out;
&:hover {
background-color: $paper;
color: $ink;
}
}
.textPreview {
margin: 0;
padding: $space-3;
max-block-size: 24rem;
overflow: auto;
background-color: $paper-deep;
border: $hairline solid $rule-hair;
font-family: $font-mono;
font-size: $font-size-xs;
line-height: $line-height-snug;
color: $ink;
white-space: pre;
}
.note {
font-size: $font-size-xs;
color: $ink-soft;
margin: 0;
}

View File

@ -0,0 +1,99 @@
// ===================
// ©AngelaMos | 2026
// result.tsx
// ===================
import { Link } from 'react-router-dom'
import type { CreateTokenResponse } from '@/api'
import {
Button,
CopyField,
DataRow,
Pill,
SerialBar,
SpecimenCard,
SpecimenCardSection,
} from '@/components'
import { ArtifactDisplay } from './artifact'
import { ARTIFACT_LABEL, PAGE_COPY } from './copy'
import styles from './landing.module.scss'
type ResultViewProps = {
data: CreateTokenResponse
onAnother: () => void
filenameFallback: string
}
export function ResultView({
data,
onAnother,
filenameFallback,
}: ResultViewProps): React.ReactElement {
const { token, artifact } = data
const managePath = `/m/${token.manage_id}`
return (
<div className={styles.result}>
<header className={styles.resultHead}>
<span className={styles.resultMarker}></span>
<h2 className={styles.resultHeadline}>{PAGE_COPY.RESULT_HEADLINE}</h2>
<p className={styles.resultBody}>{PAGE_COPY.RESULT_BODY}</p>
</header>
<SpecimenCard
tag={`SPECIES // ${token.type.toUpperCase()}`}
serial={<SerialBar value={token.id} prefix="SN" />}
>
<SpecimenCardSection label="DOSSIER">
<DataRow label="memo">{token.memo || '—'}</DataRow>
<DataRow label="filename" mono>
{token.filename ?? '—'}
</DataRow>
<DataRow label="alert" emphasize>
{token.alert_channel}
</DataRow>
<DataRow label="created" mono>
{formatIso(token.created_at)}
</DataRow>
<DataRow label="status">
<Pill tone={token.enabled ? 'signal' : 'alarm'} size="sm">
{token.enabled ? 'armed' : 'disabled'}
</Pill>
</DataRow>
</SpecimenCardSection>
<SpecimenCardSection label={ARTIFACT_LABEL[artifact.kind]}>
<ArtifactDisplay
artifact={artifact}
filenameFallback={filenameFallback}
/>
</SpecimenCardSection>
<SpecimenCardSection label="ENDPOINTS">
<CopyField label="MANAGE" value={token.manage_url} fullWidth />
<CopyField label="TRIGGER" value={token.trigger_url} fullWidth />
</SpecimenCardSection>
</SpecimenCard>
<footer className={styles.resultFoot}>
<Link to={managePath} className={styles.resultLink}>
open dossier
</Link>
<Button variant="ghost" onClick={onAnother}>
{PAGE_COPY.ISSUE_ANOTHER_LABEL}
</Button>
</footer>
</div>
)
}
function formatIso(iso: string): string {
try {
const dt = new Date(iso)
return dt
.toISOString()
.replace('T', ' ')
.replace(/\.\d+Z$/, ' Z')
} catch (_err) {
return iso
}
}

View File

@ -0,0 +1,78 @@
// ===================
// ©AngelaMos | 2026
// type-picker.tsx
// ===================
import type { TokenType, TypeDescriptor } from '@/api'
import { Glyph } from '@/components'
import { TOKEN_BLURB } from './copy'
import styles from './landing.module.scss'
type TypePickerProps = {
descriptors: TypeDescriptor[]
value: TokenType | ''
onChange: (next: TokenType) => void
invalid?: boolean
name?: string
}
export function TypePicker({
descriptors,
value,
onChange,
invalid = false,
name = 'token-type',
}: TypePickerProps): React.ReactElement {
return (
<fieldset className={styles.typeGrid} data-invalid={invalid}>
<legend className={styles.srOnly}>Token species</legend>
{descriptors.map((d, idx) => (
<TypeCard
key={d.type}
name={name}
descriptor={d}
index={idx + 1}
selected={value === d.type}
onSelect={() => onChange(d.type)}
/>
))}
</fieldset>
)
}
type TypeCardProps = {
name: string
descriptor: TypeDescriptor
index: number
selected: boolean
onSelect: () => void
}
function TypeCard({
name,
descriptor,
index,
selected,
onSelect,
}: TypeCardProps): React.ReactElement {
const indexStr = String(index).padStart(2, '0')
return (
<label className={styles.typeCard} data-selected={selected}>
<input
className={styles.srOnly}
type="radio"
name={name}
value={descriptor.type}
checked={selected}
onChange={onSelect}
/>
<span className={styles.typeIndex}>{indexStr}</span>
<span className={styles.typeGlyph} aria-hidden="true">
<Glyph type={descriptor.type} size={36} />
</span>
<span className={styles.typeName}>{descriptor.name}</span>
<span className={styles.typeCode}>{descriptor.type}</span>
<span className={styles.typeBlurb}>{TOKEN_BLURB[descriptor.type]}</span>
</label>
)
}