Ship Phases 1–3 of the multi-tenant AI access plan so an AI agent can
act on a Vibn workspace with one bearer token and zero admin reach.
Phase 1 — Gitea bot per workspace
- Add gitea_bot_username / gitea_bot_user_id / gitea_bot_token_encrypted
columns to vibn_workspaces (migrate route).
- New lib/auth/secret-box.ts (AES-256-GCM, VIBN_SECRETS_KEY) for PAT at rest.
- Extend lib/gitea.ts with createUser, createAccessTokenFor (Sudo PAT),
createOrgTeam, addOrgTeamMember, ensureOrgTeamMembership.
- ensureWorkspaceProvisioned now mints a vibn-bot-<slug> user, adds it to
a Writers team (write perms only) on the workspace's org, and stores
its PAT encrypted.
- GET /api/workspaces/[slug]/gitea-credentials returns a workspace-scoped
bot PAT + clone URL template; session or vibn_sk_ bearer auth.
Phase 2 — Tenant-safe Coolify proxy + real MCP
- lib/coolify.ts: projectUuidOf, listApplicationsInProject,
getApplicationInProject, TenantError, env CRUD, deployments list.
- Workspace-scoped REST endpoints (all filtered by coolify_project_uuid):
GET/POST /api/workspaces/[slug]/apps/[uuid](/deploy|/envs|/deployments),
GET /api/workspaces/[slug]/deployments/[deploymentUuid]/logs.
- Full rewrite of /api/mcp off legacy Firebase onto Postgres vibn_sk_
keys, exposing workspace.describe, gitea.credentials, projects.*,
apps.* (list/get/deploy/deployments, envs.list/upsert/delete).
Phase 3 — Settings UI AI bundle
- GET /api/workspaces/[slug]/bootstrap.sh: curl|sh installer that writes
.cursor/rules, .cursor/mcp.json and appends VIBN_* to .env.local.
Embeds the caller's vibn_sk_ token when invoked with bearer auth.
- WorkspaceKeysPanel: single AiAccessBundleCard with system-prompt block,
one-line bootstrap, Reveal-bot-PAT button, collapsible manual-setup
fallback. Minted-key modal also shows the bootstrap one-liner.
Ops prerequisites:
- Set VIBN_SECRETS_KEY (>=16 chars) on the frontend.
- Run /api/admin/migrate to add the three bot columns.
- GITEA_API_TOKEN must be a site-admin token (needed for admin/users
+ Sudo PAT mint); otherwise provision_status lands on 'partial'.
Made-with: Cursor
58 lines
2.4 KiB
TypeScript
58 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
export default function GrowPage() {
|
|
const items = [
|
|
{ icon: "📣", title: "Marketing copy", desc: "AI-generated landing page, emails, and social posts tailored to your product." },
|
|
{ icon: "🎯", title: "Launch channels", desc: "Recommended channels based on your target audience and business model." },
|
|
{ icon: "👥", title: "User acquisition", desc: "Onboarding flows, referral mechanics, and early adopter campaigns." },
|
|
{ icon: "💬", title: "Community", desc: "Discord, Slack, or forum setup recommendations for your user base." },
|
|
];
|
|
|
|
return (
|
|
<div
|
|
className="vibn-enter"
|
|
style={{ padding: "28px 32px", overflow: "auto", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}
|
|
>
|
|
<div style={{ maxWidth: 560 }}>
|
|
<h3 style={{ fontFamily: "var(--font-lora), ui-serif, serif", fontSize: "1.2rem", fontWeight: 400, color: "#1a1a1a", marginBottom: 4 }}>
|
|
Grow
|
|
</h3>
|
|
<p style={{ fontSize: "0.8rem", color: "#a09a90", marginBottom: 28 }}>
|
|
Marketing, launch strategy, and user acquisition — coming once your product is live.
|
|
</p>
|
|
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
{items.map((item, i) => (
|
|
<div
|
|
key={i}
|
|
className="vibn-enter"
|
|
style={{
|
|
display: "flex", alignItems: "flex-start", gap: 16,
|
|
padding: "18px 20px", background: "#fff",
|
|
border: "1px solid #e8e4dc", borderRadius: 10,
|
|
boxShadow: "0 1px 2px #1a1a1a05",
|
|
animationDelay: `${i * 0.06}s`,
|
|
}}
|
|
>
|
|
<div style={{ fontSize: "1.2rem", flexShrink: 0, marginTop: 2 }}>{item.icon}</div>
|
|
<div>
|
|
<div style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 4 }}>{item.title}</div>
|
|
<div style={{ fontSize: "0.8rem", color: "#6b6560", lineHeight: 1.55 }}>{item.desc}</div>
|
|
</div>
|
|
<span style={{
|
|
marginLeft: "auto", flexShrink: 0,
|
|
display: "inline-flex", alignItems: "center",
|
|
padding: "3px 9px", borderRadius: 4,
|
|
fontSize: "0.68rem", fontWeight: 600,
|
|
color: "#9a7b3a", background: "#d4a04a12",
|
|
}}>
|
|
Soon
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|