feat(api): comprehensive QA hardening — security gates, chat improvements, beta scaffolds
Closes checklist items F-01..F-06, D-01..D-28, S-01..S-10, C-01..C-07, B-01..B-07, R-01..R-02, O-03. Security (28 deletions + 10 auth gates): - Delete 28 unauthenticated debug/cursor/firebase/test routes - Gate ai/chat, ai/conversation, context/summarize, work-completed with withTenantProject/withAuth - Add HMAC-SHA256 signature verification to webhooks/coolify - Switch all admin secret comparisons to timingSafeStringEq Foundations (lib/server/*): - api-handler.ts: withAuth, withTenantProject, withWorkspace, withAdminSecret, withRateLimit - logger.ts: structured request-scoped logging with turnId - audit-log.ts: writeAuditLog helper + audit_log table - rate-limit.ts: Postgres sliding window rate limiter - coolify-webhook.ts: verifyCoolifySignature - timing-safe.ts: timingSafeStringEq Chat hardening (chat/route.ts): - MAX_TOOL_ROUNDS 15 → 8 (C-01) - Loop detection: hard-break at 3 identical fingerprints (was 5) (C-02) - Add 6-consecutive-tool-call hard-break (C-02) - Mode: respond first, act second prompt block (C-03) - SSE heartbeat every 25s via setInterval (C-04) - Per-tool 45s timeout via Promise.race (C-05) - turnId per-turn UUID for log correlation (C-06) - Recovery fires when roundsSinceText >= 4 (C-07) - SSE plan event on plan_task_add/edit (B-05) Beta features: - invites table + GET/POST /api/invites (P4.8) - invites/[token] validate + redeem (P4.8) - fs_project_dev_servers table + lib/server/dev-server-state.ts (P6.B1) - fs_project_secrets table + CRUD routes (P6.D2) - lib/integrations/brief-extract.ts (P3.7) Documentation: - app/api/ROUTES.md: full route map with auth + tenant
This commit is contained in:
134
new-site/onboarding-fork.jsx
Normal file
134
new-site/onboarding-fork.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
// Step 1: the only branching question — "which describes you?"
|
||||
// Quiet radio-style cards. No quotes, no marketing, no glow theatrics.
|
||||
|
||||
const FORKS = [
|
||||
{
|
||||
id: "entrepreneur",
|
||||
label: "I'm building my own thing",
|
||||
hint: "Idea → live → first customer. You're the founder.",
|
||||
icon: (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="3"/>
|
||||
<path d="M9 2.5v2M9 13.5v2M2.5 9h2M13.5 9h2"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "owner",
|
||||
label: "I run a business",
|
||||
hint: "Replace the stack of tools you currently rent.",
|
||||
icon: (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M3 6h12l-1 9H4L3 6Z"/>
|
||||
<path d="M6 6V4.5a3 3 0 0 1 6 0V6"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "consultant",
|
||||
label: "I build for clients",
|
||||
hint: "A workspace per client. Bill for the system, not the hours.",
|
||||
icon: (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M2.5 15 9 3l6.5 12"/>
|
||||
<path d="M5.5 12h7"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function ForkScreen({ name, value, onChange, onClose, onNext }) {
|
||||
return (
|
||||
<>
|
||||
<WizardTop
|
||||
onBack={null}
|
||||
onClose={onClose}
|
||||
stepText="Pick your lane"
|
||||
current={1}
|
||||
total={5}
|
||||
/>
|
||||
<WizardBody>
|
||||
<WizardQ
|
||||
title={name ? `Welcome, ${name}. Which sounds like you?` : "Which one sounds like you?"}
|
||||
sub="Vibn asks different questions on the next screens depending on the answer. You can change this later."
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{FORKS.map((f) => {
|
||||
const active = value === f.id;
|
||||
return (
|
||||
<button
|
||||
key={f.id}
|
||||
type="button"
|
||||
onClick={() => onChange(f.id)}
|
||||
onDoubleClick={() => { onChange(f.id); onNext(); }}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 14,
|
||||
padding: "14px 16px",
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${active ? "var(--accent)" : "var(--hairline)"}`,
|
||||
background: active ? "oklch(0.20 0.04 35 / 0.4)" : "oklch(0.18 0.009 60 / 0.6)",
|
||||
boxShadow: active ? "0 0 0 3px oklch(0.74 0.175 35 / 0.1)" : "none",
|
||||
textAlign: "left",
|
||||
color: "var(--fg)",
|
||||
transition: "border-color .15s, background .15s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 36, height: 36, flexShrink: 0,
|
||||
borderRadius: 9,
|
||||
background: active ? "oklch(0.74 0.175 35 / 0.18)" : "oklch(0.22 0.011 60)",
|
||||
border: "1px solid var(--hairline)",
|
||||
color: active ? "var(--accent)" : "var(--fg-mute)",
|
||||
display: "grid", placeItems: "center",
|
||||
}}>
|
||||
{f.icon}
|
||||
</span>
|
||||
<span style={{ display: "flex", flexDirection: "column", gap: 2, flex: 1 }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 500, letterSpacing: "-0.008em" }}>
|
||||
{f.label}
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: "var(--fg-mute)", lineHeight: 1.4 }}>
|
||||
{f.hint}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
style={{
|
||||
width: 18, height: 18, flexShrink: 0,
|
||||
borderRadius: "50%",
|
||||
border: `1.5px solid ${active ? "var(--accent)" : "var(--hairline-2)"}`,
|
||||
background: active ? "var(--accent)" : "transparent",
|
||||
display: "grid", placeItems: "center",
|
||||
color: "var(--accent-fg)",
|
||||
transition: "border-color .15s, background .15s",
|
||||
}}
|
||||
>
|
||||
{active && (
|
||||
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
||||
strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="m3 8.5 3.2 3.2L13 5"/>
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<WizardFooter
|
||||
canNext={!!value}
|
||||
onNext={onNext}
|
||||
nextLabel="Continue"
|
||||
hint={value ? "Press ⌘↵" : null}
|
||||
/>
|
||||
</WizardBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { ForkScreen });
|
||||
Reference in New Issue
Block a user