Files
hermes-mcp/product/app/index.html
Garfield 61dab40585 feat(saas): SquareMCP v2 — multi-tenant MCP platform complete
Steps 0–10 of the v2 plan, 194 tests passing.

Core infrastructure
- Shared Redis client (src/redis.ts); all four Redis consumers migrated
- Vitest test harness with vitest.config.ts and npm test/test:watch scripts

Billing & invoicing (Steps 1–2)
- Monthly invoice generation with idempotency (MySQL uq_customer_period unique key)
- Cron job with Redis distributed lock (Lua compare-delete, 1-hr TTL)
- Invoice emailer via nodemailer (FETCHERPAY SMTP)
- Billing middleware: checkLimit gate in handleToolCall; platform attribution fix

Email multi-tenancy (Step 3)
- EmailCtx = Account | EmailCredentials; imap.ts + smtp.ts accept both
- resolveEmailCtx helper in tools.ts; all email tools use customer credentials

Analytics + platform health (Steps 4–5)
- Chart.js bar charts for platform breakdown and daily activity
- Token expiry check in getCredential with dynamic import refresh
- platform-health.ts: per-platform health probe with 10-min Redis cache
- GET /api/health/platforms; "Token expired" amber badge in dashboard

Tool schema filtering (Step 6)
- stripAccountParam deep-clones tool schemas; multi-tenant sessions never
  see the internal account enum

OAuth hardening (Step 7)
- Atomic auth code consumption: UPDATE SET used=TRUE, check affectedRows
- customer_id threaded through oauth_auth_codes → oauth_tokens
- getTokenCustomer(); requireAuth resolves req.customer from Bearer token
- Consent page requires authenticated session; redirect_uri validated
  against registered URIs; http://localhost:* loopback wildcard

DCR browser flow (Step 8)
- ensureOAuthAppRegistered() upserts pre-registered SquareMCP OAuth app
  on startup with redirect URIs for mcp-callback, localhost:*, claude-desktop,
  opencode
- GET /oauth/connect-mcp → server-side redirect (client_id off frontend)
- GET /oauth/mcp-callback → exchanges code, renders config snippet page
  with copy buttons for Claude Desktop and Codex CLI

Webhooks (Step 9)
- webhook_url + webhook_secret columns on customers
- deliverWebhook(): HMAC-SHA256 signing, 3× exponential retry (1s/4s/16s),
  Redis DLQ with 7-day TTL on total failure
- isValidWebhookUrl(): SSRF protection (blocks RFC-1918, localhost, .local)
- POST /api/webhooks/config (secret returned once), GET, DELETE
- GET /api/admin/webhooks/dlq/:customerId
- WhatsApp POST route uses express.raw() for raw body preservation
- Dashboard Webhooks tab with secret-once display and copy button

Developer docs (Step 10)
- docs/ static HTML site (GitHub Pages, no build pipeline)
- index.html: landing page with client + platform overview
- getting-started.html: tabbed MCP config for Claude Desktop, Codex CLI, opencode
- platforms.html: LinkedIn, TikTok, WhatsApp, Instagram, Twitter, Telegram guides
- agent-tutorial.html: complete Node.js agent (Anthropic SDK + MCP SDK),
  LinkedIn posting loop, extensions for multi-platform + inbound webhook reaction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 23:43:56 -04:00

276 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SquareMCP — AI Social Gateway</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="app">
<!-- Password Reset Request View -->
<div id="reset-request-view" class="view hidden">
<div class="auth-card">
<div class="logo">
<div class="logo-mark">S</div>
<h1>Reset Password</h1>
<p>Enter your email to receive a reset link</p>
</div>
<form id="reset-request-form" class="auth-form">
<input type="email" name="email" placeholder="Email" required>
<button type="submit" class="btn btn-primary">Send Reset Link</button>
<p class="error-msg" id="reset-request-error"></p>
<p class="success-msg" id="reset-request-success"></p>
<a href="#" id="back-to-login" style="color:#888;font-size:13px;text-align:center;display:block;margin-top:12px;">Back to login</a>
</form>
</div>
</div>
<!-- Password Reset Confirm View -->
<div id="reset-confirm-view" class="view hidden">
<div class="auth-card">
<div class="logo">
<div class="logo-mark">S</div>
<h1>New Password</h1>
<p>Enter your new password below</p>
</div>
<form id="reset-confirm-form" class="auth-form">
<input type="password" name="password" placeholder="New password (min 8 chars)" required minlength="8">
<button type="submit" class="btn btn-primary">Update Password</button>
<p class="error-msg" id="reset-confirm-error"></p>
<p class="success-msg" id="reset-confirm-success"></p>
</form>
</div>
</div>
<!-- Login View -->
<div id="login-view" class="view">
<div class="auth-card">
<div class="logo">
<div class="logo-mark">S</div>
<h1>SquareMCP</h1>
<p>AI Social Media Gateway</p>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="login">Sign In</button>
<button class="tab-btn" data-tab="signup">Create Account</button>
</div>
<form id="login-form" class="auth-form">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required minlength="8">
<button type="submit" class="btn btn-primary">Sign In</button>
<p class="error-msg" id="login-error"></p>
</form>
<form id="signup-form" class="auth-form hidden">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password (min 8 chars)" required minlength="8">
<button type="submit" class="btn btn-primary">Create Account</button>
<p class="error-msg" id="signup-error"></p>
</form>
</div>
</div>
<!-- Dashboard View -->
<div id="dashboard-view" class="view hidden">
<header class="app-header">
<div class="header-left">
<div class="logo-mark small">S</div>
<span class="app-title">SquareMCP</span>
</div>
<div class="header-right">
<nav class="header-nav">
<button class="nav-link active" data-view="platforms">Platforms</button>
<button class="nav-link" data-view="analytics">Analytics</button>
<button class="nav-link" data-view="invoices">Invoices</button>
<button class="nav-link" data-view="webhooks">Webhooks</button>
<button class="nav-link hidden" data-view="admin" id="admin-nav">Admin</button>
</nav>
<span id="user-email" class="user-email"></span>
<button id="logout-btn" class="btn btn-ghost">Logout</button>
</div>
</header>
<main class="dashboard">
<section class="welcome">
<h2>Connect your platforms</h2>
<p>Link your social accounts to publish, analyze, and manage content from one place.</p>
<button id="connect-mcp-btn" class="btn btn-primary" style="margin-top:16px;">Connect MCP Client</button>
</section>
<section class="usage-bar" id="usage-bar">
<div class="usage-info">
<span class="plan-badge" id="plan-badge">Free</span>
<span class="usage-text" id="usage-text">0 / 100 calls this month</span>
</div>
<div class="usage-bar-track"><div class="usage-bar-fill" id="usage-bar-fill" style="width:0%"></div></div>
</section>
<section class="platform-grid">
<!-- TikTok -->
<div class="platform-card" data-platform="tiktok">
<div class="platform-icon" style="background:#000;">🎵</div>
<div class="platform-info">
<h3>TikTok</h3>
<p class="platform-desc">Publish videos and view analytics</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="tiktok">Connect</button>
</div>
<!-- Facebook -->
<div class="platform-card" data-platform="facebook">
<div class="platform-icon" style="background:#1877f2;">f</div>
<div class="platform-info">
<h3>Facebook</h3>
<p class="platform-desc">Post to pages and manage content</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="facebook">Connect</button>
</div>
<!-- Instagram -->
<div class="platform-card" data-platform="instagram">
<div class="platform-icon" style="background:linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888);">📷</div>
<div class="platform-info">
<h3>Instagram</h3>
<p class="platform-desc">Publish reels and images</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="instagram">Connect</button>
</div>
<!-- LinkedIn -->
<div class="platform-card" data-platform="linkedin">
<div class="platform-icon" style="background:#0a66c2;">in</div>
<div class="platform-info">
<h3>LinkedIn</h3>
<p class="platform-desc">Share posts, images, and videos</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="linkedin">Connect</button>
</div>
<!-- Twitter/X -->
<div class="platform-card" data-platform="twitter">
<div class="platform-icon" style="background:#000;">𝕏</div>
<div class="platform-info">
<h3>Twitter / X</h3>
<p class="platform-desc">Tweet with media support</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="twitter">Connect</button>
</div>
<!-- Telegram -->
<div class="platform-card" data-platform="telegram">
<div class="platform-icon" style="background:#0088cc;">✈️</div>
<div class="platform-info">
<h3>Telegram</h3>
<p class="platform-desc">Send messages via bot</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="telegram">Connect</button>
</div>
<!-- Discord -->
<div class="platform-card" data-platform="discord">
<div class="platform-icon" style="background:#5865f2;">🎮</div>
<div class="platform-info">
<h3>Discord</h3>
<p class="platform-desc">Send messages to channels</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="discord">Connect</button>
</div>
<!-- WhatsApp -->
<div class="platform-card" data-platform="whatsapp">
<div class="platform-icon" style="background:#25d366;">💬</div>
<div class="platform-info">
<h3>WhatsApp</h3>
<p class="platform-desc">Business messaging</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="whatsapp">Connect</button>
</div>
<!-- Email -->
<div class="platform-card" data-platform="email">
<div class="platform-icon" style="background:#ea4335;">✉️</div>
<div class="platform-info">
<h3>Email</h3>
<p class="platform-desc">IMAP/SMTP accounts</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="email">Connect</button>
</div>
</section>
<section class="analytics-section hidden" id="analytics-section">
<h3 class="section-title">Analytics</h3>
<p class="section-subtitle">Tool calls this month, by platform and day</p>
<div class="charts-grid">
<div class="chart-card">
<h4>By platform</h4>
<div class="chart-container"><canvas id="platform-chart"></canvas></div>
</div>
<div class="chart-card">
<h4>Daily activity</h4>
<div class="chart-container"><canvas id="daily-chart"></canvas></div>
</div>
</div>
<div id="analytics-empty" class="analytics-empty hidden">
<p>No activity yet this month. Make your first tool call to see data here.</p>
</div>
</section>
<section class="invoices-section hidden" id="invoices-section">
<h3>Invoices</h3>
<div id="invoices-list" class="invoices-list"></div>
</section>
<section class="admin-section hidden" id="admin-section">
<h3>Admin Panel</h3>
<div id="admin-customers" class="admin-customers"></div>
</section>
<section class="webhooks-section hidden" id="webhooks-section">
<h3 class="section-title">Webhook</h3>
<p class="section-subtitle">Receive real-time events when messages arrive on your connected platforms.</p>
<div class="webhook-card" id="webhook-card">
<div id="webhook-status-row" class="webhook-status-row">
<span id="webhook-url-display" class="webhook-url-display">No webhook configured</span>
<button id="webhook-delete-btn" class="btn btn-ghost hidden">Remove</button>
</div>
<form id="webhook-form" class="webhook-form">
<input type="url" id="webhook-url-input" placeholder="https://your-server.com/webhook" required>
<button type="submit" class="btn btn-primary">Save &amp; generate secret</button>
</form>
<div id="webhook-secret-box" class="webhook-secret-box hidden">
<p class="webhook-secret-label">Signing secret — copy now, not shown again:</p>
<div class="webhook-secret-value" id="webhook-secret-value"></div>
<button class="btn btn-ghost" onclick="copyWebhookSecret()">Copy secret</button>
</div>
<div class="webhook-instructions">
<p>Verify each request by computing <code>sha256=HMAC-SHA256(secret, rawBody)</code> and comparing to the <code>X-SquareMCP-Signature</code> header.</p>
</div>
</div>
</section>
</main>
</div>
<!-- Connection Modal -->
<div id="connect-modal" class="modal hidden">
<div class="modal-backdrop"></div>
<div class="modal-content">
<button class="modal-close">&times;</button>
<div id="modal-body"></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script src="app.js"></script>
</body>
</html>