feat(saas): full SquareMCP SaaS platform v1

- JWT auth with bcrypt password hashing, cookie sessions, forgot/reset password
- Per-user encrypted credential storage (Redis + AES-256-GCM) for all 9 platforms
- Usage tracking with monthly limits per plan (free/starter/growth/enterprise)
- Invoice generation and retrieval (admin + user views)
- Admin panel with customer listing (role-based access)
- Web app UI at app.squaremcp.com — login, dashboard, connections, usage, invoices
- Unified auth middleware: API key, OAuth Bearer, and JWT cookie support
- Facebook Graph API fixes: published_posts endpoint, photo/video post support
- TikTok sandbox compliance: SELF_ONLY privacy for unaudited apps
- URL verification files for TikTok app review
This commit is contained in:
Garfield
2026-05-13 08:42:33 -04:00
parent 7796de12bf
commit a5e4c55885
46 changed files with 4054 additions and 171 deletions

View File

@@ -2,6 +2,14 @@ import "./index.css";
import { Composition } from "remotion";
import { SquareMCPLinkedIn } from "./SquareMCPLinkedIn";
import { SquareMCPHeroLoop } from "./SquareMCPHeroLoop";
import {
SquareMCPTikTokCTA,
SquareMCPTikTokDemo,
SquareMCPTikTokFull,
SquareMCPTikTokHook,
SquareMCPTikTokProblem,
SquareMCPTikTokProof,
} from "./SquareMCPTikTok";
export const RemotionRoot = () => {
return (
@@ -22,6 +30,54 @@ export const RemotionRoot = () => {
width={1920}
height={1080}
/>
<Composition
id="SquareMCPTikTokFull"
component={SquareMCPTikTokFull}
durationInFrames={30 * 30}
fps={30}
width={1080}
height={1920}
/>
<Composition
id="SquareMCPTikTokHook"
component={SquareMCPTikTokHook}
durationInFrames={3 * 30}
fps={30}
width={1080}
height={1920}
/>
<Composition
id="SquareMCPTikTokProblem"
component={SquareMCPTikTokProblem}
durationInFrames={5 * 30}
fps={30}
width={1080}
height={1920}
/>
<Composition
id="SquareMCPTikTokDemo"
component={SquareMCPTikTokDemo}
durationInFrames={12 * 30}
fps={30}
width={1080}
height={1920}
/>
<Composition
id="SquareMCPTikTokProof"
component={SquareMCPTikTokProof}
durationInFrames={5 * 30}
fps={30}
width={1080}
height={1920}
/>
<Composition
id="SquareMCPTikTokCTA"
component={SquareMCPTikTokCTA}
durationInFrames={5 * 30}
fps={30}
width={1080}
height={1920}
/>
</>
);
};

View File

@@ -0,0 +1,81 @@
import type { ReactNode } from "react";
import { AbsoluteFill, Sequence, useVideoConfig } from "remotion";
import { TikTokBackground } from "./scenes/tiktok/TikTokBackground";
import { TikTokCTA } from "./scenes/tiktok/TikTokCTA";
import { TikTokDemo } from "./scenes/tiktok/TikTokDemo";
import { TikTokHook } from "./scenes/tiktok/TikTokHook";
import { TikTokProblem } from "./scenes/tiktok/TikTokProblem";
import { TikTokProof } from "./scenes/tiktok/TikTokProof";
const TikTokShell = ({ children }: { children: ReactNode }) => {
return (
<AbsoluteFill>
<TikTokBackground />
{children}
</AbsoluteFill>
);
};
export const SquareMCPTikTokFull = () => {
const { fps } = useVideoConfig();
return (
<TikTokShell>
<Sequence durationInFrames={3 * fps}>
<TikTokHook />
</Sequence>
<Sequence from={3 * fps} durationInFrames={5 * fps}>
<TikTokProblem />
</Sequence>
<Sequence from={8 * fps} durationInFrames={12 * fps}>
<TikTokDemo />
</Sequence>
<Sequence from={20 * fps} durationInFrames={5 * fps}>
<TikTokProof />
</Sequence>
<Sequence from={25 * fps} durationInFrames={5 * fps}>
<TikTokCTA />
</Sequence>
</TikTokShell>
);
};
export const SquareMCPTikTokHook = () => {
return (
<TikTokShell>
<TikTokHook />
</TikTokShell>
);
};
export const SquareMCPTikTokProblem = () => {
return (
<TikTokShell>
<TikTokProblem />
</TikTokShell>
);
};
export const SquareMCPTikTokDemo = () => {
return (
<TikTokShell>
<TikTokDemo />
</TikTokShell>
);
};
export const SquareMCPTikTokProof = () => {
return (
<TikTokShell>
<TikTokProof />
</TikTokShell>
);
};
export const SquareMCPTikTokCTA = () => {
return (
<TikTokShell>
<TikTokCTA />
</TikTokShell>
);
};

View File

@@ -0,0 +1,26 @@
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
import { COLORS } from "../../styles";
export const TikTokBackground = () => {
const frame = useCurrentFrame();
const orbit = interpolate(frame % 180, [0, 90, 180], [0, 1, 0]);
return (
<AbsoluteFill
style={{
background: `radial-gradient(circle at ${20 + orbit * 45}% ${18 + orbit * 30}%, rgba(125, 182, 255, 0.18), transparent 28%), radial-gradient(circle at 80% 78%, rgba(14, 99, 246, 0.22), transparent 30%), linear-gradient(180deg, #070812 0%, ${COLORS.bg} 100%)`,
}}
>
<div
style={{
position: "absolute",
inset: 0,
backgroundImage:
"linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px)",
backgroundSize: "60px 60px",
maskImage: "linear-gradient(180deg, rgba(0,0,0,0.65), rgba(0,0,0,0.95))",
}}
/>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,69 @@
import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
import { COLORS, FONT, SPRING_CFG } from "../../styles";
export const TikTokCTA = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const logoIn = spring({ fps, frame, config: SPRING_CFG });
const glow = interpolate(frame % 60, [0, 30, 60], [0.32, 0.7, 0.32]);
return (
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
<div
style={{
position: "absolute",
width: 520,
height: 520,
borderRadius: 999,
background: `radial-gradient(circle, rgba(14,99,246,${glow}) 0%, rgba(14,99,246,0) 68%)`,
filter: "blur(12px)",
}}
/>
<div
style={{
opacity: logoIn,
transform: `scale(${0.9 + logoIn * 0.1}) translateY(${(1 - logoIn) * 50}px)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Img src={staticFile("squaremcp-logo.svg")} style={{ width: 240, height: 240 }} />
<div
style={{
marginTop: 26,
fontFamily: FONT,
fontSize: 82,
fontWeight: 800,
color: COLORS.text,
letterSpacing: -2.2,
}}
>
SquareMCP
</div>
<div
style={{
marginTop: 12,
fontFamily: FONT,
fontSize: 34,
fontWeight: 600,
color: COLORS.accentLight,
letterSpacing: -0.6,
}}
>
squaremcp.com
</div>
<div
style={{
marginTop: 22,
fontFamily: FONT,
fontSize: 24,
color: COLORS.textSecondary,
}}
>
Early access now open
</div>
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,124 @@
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { COLORS, FONT, SPRING_CFG } from "../../styles";
export const TikTokDemo = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const chatIn = spring({ fps, frame, config: SPRING_CFG });
const jsonIn = spring({ fps, frame: Math.max(0, frame - 30), config: SPRING_CFG });
const chipsIn = spring({ fps, frame: Math.max(0, frame - 70), config: SPRING_CFG });
const pulse = interpolate(frame % 45, [0, 22, 45], [0.75, 1, 0.75]);
return (
<AbsoluteFill style={{ padding: 58 }}>
<div style={{ fontFamily: FONT, color: COLORS.text, fontSize: 68, fontWeight: 800, lineHeight: 1, marginTop: 48 }}>
One prompt.
<br />
Real execution.
</div>
<div
style={{
marginTop: 42,
borderRadius: 36,
border: `1px solid ${COLORS.borderBlue}`,
background: "rgba(9, 12, 22, 0.94)",
padding: 30,
opacity: chatIn,
transform: `translateY(${(1 - chatIn) * 60}px)`,
}}
>
<div style={{ fontFamily: FONT, color: COLORS.textSecondary, fontSize: 22, marginBottom: 18 }}>Chat</div>
<div
style={{
marginLeft: "auto",
maxWidth: 760,
borderRadius: 28,
background: "linear-gradient(135deg, rgba(14,99,246,0.95), rgba(125,182,255,0.9))",
padding: "24px 26px",
color: "#08111f",
fontFamily: FONT,
fontSize: 34,
fontWeight: 700,
lineHeight: 1.25,
}}
>
Post my launch video to LinkedIn, X, and Instagram.
</div>
</div>
<div
style={{
marginTop: 28,
borderRadius: 36,
border: "1px solid rgba(34, 197, 94, 0.24)",
background: "rgba(5, 14, 10, 0.9)",
padding: 30,
opacity: jsonIn,
transform: `translateY(${(1 - jsonIn) * 60}px)`,
}}
>
<div style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", color: "#86efac", fontSize: 22, marginBottom: 18 }}>
response.json
</div>
{[
'{',
' "status": "success",',
' "published": ["linkedin", "x", "instagram"],',
' "duration_seconds": 12,',
' "urls": 3',
'}',
].map((line) => (
<div
key={line}
style={{
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
color: line.includes("success") ? "#86efac" : "#dbeafe",
fontSize: 24,
lineHeight: 1.55,
}}
>
{line}
</div>
))}
</div>
<div
style={{
marginTop: 28,
display: "grid",
gridTemplateColumns: "1fr",
gap: 16,
opacity: chipsIn,
transform: `scale(${0.96 + chipsIn * 0.04})`,
}}
>
{["linkedin.com/post/7459...", "x.com/squaremcp/status/...", "instagram.com/reel/C..."].map((url) => (
<div
key={url}
style={{
borderRadius: 22,
padding: "18px 22px",
background: "rgba(255,255,255,0.06)",
border: `1px solid ${COLORS.borderBlue}`,
display: "flex",
alignItems: "center",
gap: 16,
boxShadow: `0 0 24px rgba(34, 197, 94, ${0.12 * pulse})`,
}}
>
<div
style={{
width: 18,
height: 18,
borderRadius: 999,
background: "#22c55e",
}}
/>
<div style={{ fontFamily: FONT, color: COLORS.text, fontSize: 24, fontWeight: 600 }}>{url}</div>
</div>
))}
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,113 @@
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { COLORS, FONT, SPRING_CFG } from "../../styles";
const TOOL_LABELS = [
"Email",
"Notes",
"CRM",
"LinkedIn",
"X",
"WhatsApp",
"Docs",
"Sheets",
"Tickets",
"Calendar",
"DB",
"API",
];
export const TikTokHook = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const terminalIn = spring({ fps, frame, config: SPRING_CFG });
const gridIn = spring({ fps, frame: Math.max(0, frame - 18), config: SPRING_CFG });
const captionIn = interpolate(frame, [20, 34], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ padding: 72 }}>
<div
style={{
marginTop: 120,
borderRadius: 32,
border: `1px solid ${COLORS.borderBlue}`,
background: "rgba(7, 8, 18, 0.92)",
boxShadow: `0 20px 60px ${COLORS.accentGlow}`,
padding: "28px 32px",
opacity: terminalIn,
transform: `translateY(${(1 - terminalIn) * 80}px)`,
}}
>
<div style={{ display: "flex", gap: 12, marginBottom: 26 }}>
{["#ff5f57", "#febc2e", "#28c840"].map((dot) => (
<div key={dot} style={{ width: 14, height: 14, borderRadius: 999, background: dot }} />
))}
</div>
<div
style={{
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
fontWeight: 700,
fontSize: 44,
color: COLORS.accentLight,
letterSpacing: -1.2,
}}
>
{"> connect squaremcp"}
<span style={{ color: COLORS.text, opacity: (frame % 20) < 10 ? 1 : 0 }}>|</span>
</div>
</div>
<div
style={{
marginTop: 48,
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: 18,
opacity: gridIn,
transform: `scale(${0.9 + gridIn * 0.1})`,
}}
>
{TOOL_LABELS.map((label, index) => (
<div
key={label}
style={{
borderRadius: 24,
border: `1px solid ${COLORS.borderBlue}`,
background: index % 2 === 0 ? "rgba(18, 18, 31, 0.95)" : "rgba(10, 17, 38, 0.92)",
padding: "26px 16px",
minHeight: 112,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: COLORS.text,
fontFamily: FONT,
fontWeight: 700,
fontSize: 28,
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.03)",
}}
>
{label}
</div>
))}
</div>
<div
style={{
marginTop: "auto",
marginBottom: 68,
fontFamily: FONT,
fontSize: 54,
fontWeight: 800,
color: COLORS.text,
lineHeight: 1.05,
letterSpacing: -1.8,
opacity: captionIn,
transform: `translateY(${(1 - captionIn) * 40}px)`,
}}
>
One API key.
<br />
One workflow layer.
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,131 @@
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { COLORS, FONT, SPRING_CFG } from "../../styles";
const LEFT_APPS = ["Email", "LinkedIn", "Slack", "Notes", "Sheets", "Docs"];
export const TikTokProblem = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const leftIn = spring({ fps, frame, config: SPRING_CFG });
const rightIn = spring({ fps, frame: Math.max(0, frame - 12), config: SPRING_CFG });
const divider = interpolate(frame, [4, 24], [0.25, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ padding: 60, flexDirection: "column" }}>
<div
style={{
fontFamily: FONT,
fontSize: 70,
fontWeight: 800,
color: COLORS.text,
letterSpacing: -2,
lineHeight: 1,
marginTop: 44,
marginBottom: 42,
}}
>
8 tabs vs
<br />
1 gateway
</div>
<div style={{ display: "flex", flex: 1, gap: 28 }}>
<div
style={{
flex: 1,
opacity: leftIn,
transform: `translateX(${(1 - leftIn) * -80}px) rotate(${-4 + (1 - leftIn) * -8}deg)`,
}}
>
<div
style={{
borderRadius: 32,
border: `1px solid rgba(239, 68, 68, 0.4)`,
background: "rgba(35, 10, 16, 0.88)",
padding: 28,
height: "100%",
}}
>
<div style={{ fontFamily: FONT, color: "#fca5a5", fontSize: 30, fontWeight: 700, marginBottom: 20 }}>
Before
</div>
<div style={{ display: "grid", gap: 16 }}>
{LEFT_APPS.map((app, index) => (
<div
key={app}
style={{
borderRadius: 20,
padding: "18px 20px",
background: `rgba(255,255,255,${0.06 + index * 0.01})`,
border: "1px solid rgba(255,255,255,0.06)",
fontFamily: FONT,
color: COLORS.text,
fontSize: 28,
fontWeight: 600,
transform: `translateX(${index % 2 === 0 ? -12 : 18}px)`,
}}
>
{app}
</div>
))}
</div>
</div>
</div>
<div
style={{
width: 6,
borderRadius: 999,
background: `linear-gradient(180deg, rgba(255,255,255,0.05), rgba(14, 99, 246, ${divider}), rgba(255,255,255,0.05))`,
}}
/>
<div
style={{
flex: 1,
opacity: rightIn,
transform: `translateX(${(1 - rightIn) * 80}px)`,
}}
>
<div
style={{
borderRadius: 32,
border: `1px solid ${COLORS.borderBlue}`,
background: "rgba(9, 16, 36, 0.92)",
padding: 28,
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 20,
}}
>
<div style={{ fontFamily: FONT, color: COLORS.accentLight, fontSize: 30, fontWeight: 700 }}>
After
</div>
{["AI agent", "SquareMCP", "Connected tools"].map((item, index) => (
<div
key={item}
style={{
borderRadius: 22,
padding: "24px 22px",
background: index === 1 ? "rgba(14, 99, 246, 0.22)" : "rgba(255,255,255,0.05)",
border: `1px solid ${index === 1 ? COLORS.borderBlue : "rgba(255,255,255,0.08)"}`,
fontFamily: FONT,
color: COLORS.text,
fontSize: 30,
fontWeight: index === 1 ? 800 : 600,
textAlign: "center",
}}
>
{item}
</div>
))}
<div style={{ fontFamily: FONT, color: COLORS.textSecondary, fontSize: 24, lineHeight: 1.35 }}>
One entry point for auth, permissions, posting, notes, and internal systems.
</div>
</div>
</div>
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,58 @@
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { COLORS, FONT, SPRING_CFG } from "../../styles";
const CHANNELS = [
{ name: "LinkedIn", time: "12:04:03", tint: "rgba(10, 102, 194, 0.28)" },
{ name: "X", time: "12:04:05", tint: "rgba(255, 255, 255, 0.08)" },
{ name: "Instagram", time: "12:04:07", tint: "rgba(225, 48, 108, 0.24)" },
{ name: "TikTok", time: "12:04:09", tint: "rgba(37, 244, 238, 0.18)" },
];
export const TikTokProof = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const inView = spring({ fps, frame, config: SPRING_CFG });
return (
<AbsoluteFill style={{ padding: 60 }}>
<div style={{ fontFamily: FONT, fontSize: 62, fontWeight: 800, color: COLORS.text, letterSpacing: -1.8, marginTop: 50 }}>
Publish once.
<br />
Watch it land.
</div>
<div
style={{
marginTop: 52,
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 22,
opacity: inView,
transform: `scale(${0.92 + inView * 0.08})`,
}}
>
{CHANNELS.map((channel, index) => (
<div
key={channel.name}
style={{
borderRadius: 30,
border: `1px solid ${COLORS.borderBlue}`,
background: channel.tint,
minHeight: 260,
padding: 24,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
transform: `translateY(${index % 2 === 0 ? -8 : 12}px)`,
}}
>
<div style={{ fontFamily: FONT, color: COLORS.text, fontSize: 30, fontWeight: 700 }}>{channel.name}</div>
<div style={{ fontFamily: FONT, color: "#86efac", fontSize: 26, fontWeight: 800 }}>LIVE</div>
<div style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", color: COLORS.textSecondary, fontSize: 22 }}>
{channel.time}
</div>
</div>
))}
</div>
</AbsoluteFill>
);
};