mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-20 19:18:08 +00:00
Add provider webhook event store
This commit is contained in:
@@ -73,9 +73,22 @@ initialize Sentry. Keep auth tokens server-side only.
|
|||||||
- `ADMIN_ALERT_EMAIL=emplusbodyworks@gmail.com`
|
- `ADMIN_ALERT_EMAIL=emplusbodyworks@gmail.com`
|
||||||
- `CUSTOMER_SUPPORT_EMAIL=emplusbodyworks@gmail.com`
|
- `CUSTOMER_SUPPORT_EMAIL=emplusbodyworks@gmail.com`
|
||||||
- `PRODUCTION_APP_URL=https://polymarket-site-eta.vercel.app`
|
- `PRODUCTION_APP_URL=https://polymarket-site-eta.vercel.app`
|
||||||
|
- `WEBHOOK_BASE_URL=https://polymarket-site-eta.vercel.app/api`
|
||||||
- `RESTRICTED_JURISDICTIONS=US,CA-NY,CA-ON`
|
- `RESTRICTED_JURISDICTIONS=US,CA-NY,CA-ON`
|
||||||
- `LIVE_TRADING_ENABLED=false`
|
- `LIVE_TRADING_ENABLED=false`
|
||||||
|
|
||||||
|
## Webhook URLs to paste into provider dashboards
|
||||||
|
|
||||||
|
These endpoints store provider events in Neon for audit/reconciliation prep.
|
||||||
|
They do not move money or place orders.
|
||||||
|
|
||||||
|
- Clerk: `https://polymarket-site-eta.vercel.app/api/webhooks/clerk`
|
||||||
|
- Veriff: `https://polymarket-site-eta.vercel.app/api/webhooks/veriff`
|
||||||
|
- Circle: `https://polymarket-site-eta.vercel.app/api/webhooks/circle`
|
||||||
|
|
||||||
|
Provider event counts are visible at
|
||||||
|
`https://polymarket-site-eta.vercel.app/api/provider-events`.
|
||||||
|
|
||||||
## What is still intentionally locked
|
## What is still intentionally locked
|
||||||
|
|
||||||
- Real deposits.
|
- Real deposits.
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ interfaces visible without moving funds or placing orders.
|
|||||||
Sentry browser DSN and Clerk publishable key once configured.
|
Sentry browser DSN and Clerk publishable key once configured.
|
||||||
- `/api/live` now checks the selected provider stack: Clerk, Neon, Veriff,
|
- `/api/live` now checks the selected provider stack: Clerk, Neon, Veriff,
|
||||||
Circle, and Sentry.
|
Circle, and Sentry.
|
||||||
|
- `/api/webhooks/clerk`, `/api/webhooks/veriff`, and `/api/webhooks/circle`
|
||||||
|
receive provider events and store them in Neon.
|
||||||
|
- `/api/provider-events` reports provider event counts for a quick health check.
|
||||||
- The Live Money tab shows launch checks, wallet/deposit settings, agent risk
|
- The Live Money tab shows launch checks, wallet/deposit settings, agent risk
|
||||||
limits, a dry-run order console, and an audit preview.
|
limits, provider webhook URLs, a dry-run order console, and an audit preview.
|
||||||
|
|
||||||
The app still refuses to place live orders. That is intentional until the legal,
|
The app still refuses to place live orders. That is intentional until the legal,
|
||||||
provider, wallet-signing, reconciliation, and monitoring steps below are done.
|
provider, wallet-signing, reconciliation, and monitoring steps below are done.
|
||||||
|
|||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
import { neon } from "@neondatabase/serverless";
|
||||||
|
|
||||||
|
let sqlClient;
|
||||||
|
let schemaReady;
|
||||||
|
|
||||||
|
export function databaseUrl() {
|
||||||
|
return process.env.DATABASE_URL || process.env.NEON_DATABASE_URL || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasDatabase() {
|
||||||
|
return Boolean(databaseUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sql() {
|
||||||
|
if (!sqlClient) {
|
||||||
|
const url = databaseUrl();
|
||||||
|
if (!url) throw new Error("Database is not configured");
|
||||||
|
sqlClient = neon(url);
|
||||||
|
}
|
||||||
|
return sqlClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureSchema() {
|
||||||
|
if (schemaReady) return;
|
||||||
|
const db = sql();
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create table if not exists provider_events (
|
||||||
|
id bigserial primary key,
|
||||||
|
provider text not null,
|
||||||
|
event_type text not null,
|
||||||
|
external_id text,
|
||||||
|
signature_valid boolean not null default false,
|
||||||
|
payload jsonb not null,
|
||||||
|
headers jsonb not null default '{}'::jsonb,
|
||||||
|
received_at timestamptz not null default now()
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create unique index if not exists provider_events_provider_external_id_idx
|
||||||
|
on provider_events (provider, external_id)
|
||||||
|
where external_id is not null
|
||||||
|
`;
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create table if not exists readiness_audit (
|
||||||
|
id bigserial primary key,
|
||||||
|
event_type text not null,
|
||||||
|
detail jsonb not null,
|
||||||
|
created_at timestamptz not null default now()
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
schemaReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recordProviderEvent(event) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
||||||
|
const headers = event.headers && typeof event.headers === "object" ? event.headers : {};
|
||||||
|
|
||||||
|
const rows = await db`
|
||||||
|
insert into provider_events (
|
||||||
|
provider,
|
||||||
|
event_type,
|
||||||
|
external_id,
|
||||||
|
signature_valid,
|
||||||
|
payload,
|
||||||
|
headers
|
||||||
|
) values (
|
||||||
|
${event.provider},
|
||||||
|
${event.eventType || "unknown"},
|
||||||
|
${event.externalId || null},
|
||||||
|
${Boolean(event.signatureValid)},
|
||||||
|
${JSON.stringify(payload)}::jsonb,
|
||||||
|
${JSON.stringify(headers)}::jsonb
|
||||||
|
)
|
||||||
|
on conflict do nothing
|
||||||
|
returning id, received_at
|
||||||
|
`;
|
||||||
|
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recordAuditEvent(eventType, detail) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
await db`
|
||||||
|
insert into readiness_audit (event_type, detail)
|
||||||
|
values (${eventType}, ${JSON.stringify(detail || {})}::jsonb)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function providerEventSummary() {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
const rows = await db`
|
||||||
|
select
|
||||||
|
provider,
|
||||||
|
count(*)::int as total,
|
||||||
|
max(received_at) as latest_received_at,
|
||||||
|
count(*) filter (where signature_valid)::int as signature_valid_count
|
||||||
|
from provider_events
|
||||||
|
group by provider
|
||||||
|
order by provider
|
||||||
|
`;
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
export async function rawBody(req) {
|
||||||
|
if (typeof req.body === "string") return req.body;
|
||||||
|
if (req.body && typeof req.body === "object") return JSON.stringify(req.body);
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
||||||
|
return Buffer.concat(chunks).toString("utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseJson(raw) {
|
||||||
|
if (!raw) return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeHeaders(req, names) {
|
||||||
|
const allowed = new Set(names.map((name) => name.toLowerCase()));
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(req.headers || {})
|
||||||
|
.filter(([key]) => allowed.has(key.toLowerCase()))
|
||||||
|
.map(([key, value]) => [key.toLowerCase(), Array.isArray(value) ? value.join(",") : String(value)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function timingSafeEqualText(a, b) {
|
||||||
|
const left = Buffer.from(String(a || ""));
|
||||||
|
const right = Buffer.from(String(b || ""));
|
||||||
|
return left.length === right.length && crypto.timingSafeEqual(left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureCandidates(signature) {
|
||||||
|
return String(signature || "")
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.map((piece) => piece.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.flatMap((piece) => {
|
||||||
|
const idx = piece.indexOf("=");
|
||||||
|
return idx === -1 ? [piece] : [piece.slice(idx + 1), piece];
|
||||||
|
})
|
||||||
|
.map((piece) => piece.replace(/^sha256=/i, "").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyHmacSignature(raw, secret, signature) {
|
||||||
|
if (!secret || !signature) return false;
|
||||||
|
const hex = crypto.createHmac("sha256", secret).update(raw).digest("hex");
|
||||||
|
const base64 = crypto.createHmac("sha256", secret).update(raw).digest("base64");
|
||||||
|
return signatureCandidates(signature).some((candidate) => (
|
||||||
|
timingSafeEqualText(candidate, hex) || timingSafeEqualText(candidate, base64)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function firstHeader(req, names) {
|
||||||
|
for (const name of names) {
|
||||||
|
const value = req.headers?.[name.toLowerCase()];
|
||||||
|
if (value) return Array.isArray(value) ? value.join(",") : String(value);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendMethodNotAllowed(res) {
|
||||||
|
res.setHeader("Allow", "POST");
|
||||||
|
return res.status(405).json({ ok: false, error: "Method not allowed" });
|
||||||
|
}
|
||||||
+13
@@ -107,6 +107,12 @@ const LAUNCH_REQUIREMENTS = [
|
|||||||
["testing", "Paper-to-live parity, dry-runs, test wallets, webhook tests, and edge-case QA completed"],
|
["testing", "Paper-to-live parity, dry-runs, test wallets, webhook tests, and edge-case QA completed"],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const WEBHOOK_ROUTES = [
|
||||||
|
["clerk", "Clerk user and session events", "/api/webhooks/clerk"],
|
||||||
|
["veriff", "Veriff verification and review events", "/api/webhooks/veriff"],
|
||||||
|
["circle", "Circle payment, wallet, and transfer events", "/api/webhooks/circle"],
|
||||||
|
];
|
||||||
|
|
||||||
function envValue(key) {
|
function envValue(key) {
|
||||||
if (process.env[key]) return process.env[key];
|
if (process.env[key]) return process.env[key];
|
||||||
return (ENV_ALIASES[key] || []).map((alias) => process.env[alias]).find(Boolean);
|
return (ENV_ALIASES[key] || []).map((alias) => process.env[alias]).find(Boolean);
|
||||||
@@ -143,6 +149,7 @@ function liveTradingReady() {
|
|||||||
|
|
||||||
function baseStatus() {
|
function baseStatus() {
|
||||||
const providers = providerStatus();
|
const providers = providerStatus();
|
||||||
|
const origin = (envValue("WEBHOOK_BASE_URL") || envValue("PRODUCTION_APP_URL") || "").replace(/\/api\/?$/, "").replace(/\/$/, "");
|
||||||
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
|
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
|
||||||
const signatureTypeOk = String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3";
|
const signatureTypeOk = String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3";
|
||||||
const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137";
|
const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137";
|
||||||
@@ -162,6 +169,12 @@ function baseStatus() {
|
|||||||
providers,
|
providers,
|
||||||
provider_stack: stackStatus(),
|
provider_stack: stackStatus(),
|
||||||
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
|
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
|
||||||
|
webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({
|
||||||
|
provider,
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
url: origin ? `${origin}${path}` : path,
|
||||||
|
})),
|
||||||
next_required: missing,
|
next_required: missing,
|
||||||
docs: {
|
docs: {
|
||||||
polymarket_overview: "https://docs.polymarket.com/trading/overview",
|
polymarket_overview: "https://docs.polymarket.com/trading/overview",
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { hasDatabase, providerEventSummary } from "./_db.js";
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
res.setHeader("Allow", "GET");
|
||||||
|
return res.status(405).json({ ok: false, error: "Method not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasDatabase()) {
|
||||||
|
return res.status(503).json({ ok: false, error: "Database is not configured" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const providers = await providerEventSummary();
|
||||||
|
return res.status(200).json({ ok: true, providers });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ ok: false, error: err?.message || "Provider event summary failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { recordProviderEvent } from "../_db.js";
|
||||||
|
import { firstHeader, parseJson, rawBody, safeHeaders, sendMethodNotAllowed, verifyHmacSignature } from "../_webhook.js";
|
||||||
|
|
||||||
|
const SIGNATURE_HEADERS = [
|
||||||
|
"circle-signature",
|
||||||
|
"x-circle-signature",
|
||||||
|
"x-signature",
|
||||||
|
"webhook-signature",
|
||||||
|
];
|
||||||
|
|
||||||
|
function eventType(payload) {
|
||||||
|
return payload.type || payload.eventType || payload.notificationType || "circle.webhook";
|
||||||
|
}
|
||||||
|
|
||||||
|
function externalId(payload) {
|
||||||
|
return payload.id || payload.notificationId || payload.data?.id || payload.resource?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
if (req.method !== "POST") return sendMethodNotAllowed(res);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await rawBody(req);
|
||||||
|
const payload = parseJson(body);
|
||||||
|
const headers = safeHeaders(req, SIGNATURE_HEADERS.concat(["user-agent"]));
|
||||||
|
const secret = process.env.CIRCLE_WEBHOOK_SECRET;
|
||||||
|
const signature = firstHeader(req, SIGNATURE_HEADERS);
|
||||||
|
const signatureValid = verifyHmacSignature(body, secret, signature);
|
||||||
|
|
||||||
|
const recorded = await recordProviderEvent({
|
||||||
|
provider: "circle",
|
||||||
|
eventType: eventType(payload),
|
||||||
|
externalId: externalId(payload),
|
||||||
|
signatureValid,
|
||||||
|
payload,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(202).json({
|
||||||
|
ok: true,
|
||||||
|
recorded: Boolean(recorded),
|
||||||
|
provider: "circle",
|
||||||
|
event_type: eventType(payload),
|
||||||
|
signature_valid: signatureValid,
|
||||||
|
warning: signatureValid ? undefined : "Event recorded, but no recognized Circle signature was verified.",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ ok: false, error: err?.message || "Circle webhook failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Webhook } from "svix";
|
||||||
|
import { recordProviderEvent } from "../_db.js";
|
||||||
|
import { rawBody, parseJson, safeHeaders, sendMethodNotAllowed } from "../_webhook.js";
|
||||||
|
|
||||||
|
function eventType(payload) {
|
||||||
|
return payload.type || "clerk.webhook";
|
||||||
|
}
|
||||||
|
|
||||||
|
function externalId(payload) {
|
||||||
|
return payload.id || payload.data?.id || payload.data?.user_id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
if (req.method !== "POST") return sendMethodNotAllowed(res);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secret = process.env.CLERK_WEBHOOK_SIGNING_SECRET;
|
||||||
|
if (!secret) return res.status(503).json({ ok: false, error: "Clerk webhook secret is not configured" });
|
||||||
|
|
||||||
|
const body = await rawBody(req);
|
||||||
|
const headers = safeHeaders(req, ["svix-id", "svix-timestamp", "svix-signature"]);
|
||||||
|
let payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = new Webhook(secret).verify(body, headers);
|
||||||
|
} catch {
|
||||||
|
return res.status(401).json({ ok: false, error: "Invalid Clerk webhook signature" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const recorded = await recordProviderEvent({
|
||||||
|
provider: "clerk",
|
||||||
|
eventType: eventType(payload),
|
||||||
|
externalId: externalId(payload),
|
||||||
|
signatureValid: true,
|
||||||
|
payload,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
ok: true,
|
||||||
|
recorded: Boolean(recorded),
|
||||||
|
provider: "clerk",
|
||||||
|
event_type: eventType(payload),
|
||||||
|
signature_valid: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const payload = parseJson(req.body);
|
||||||
|
return res.status(500).json({
|
||||||
|
ok: false,
|
||||||
|
error: err?.message || "Clerk webhook failed",
|
||||||
|
provider: "clerk",
|
||||||
|
event_type: eventType(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { recordProviderEvent } from "../_db.js";
|
||||||
|
import { firstHeader, parseJson, rawBody, safeHeaders, sendMethodNotAllowed, verifyHmacSignature } from "../_webhook.js";
|
||||||
|
|
||||||
|
const SIGNATURE_HEADERS = [
|
||||||
|
"x-hmac-signature",
|
||||||
|
"x-veriff-signature",
|
||||||
|
"veriff-signature",
|
||||||
|
"x-signature",
|
||||||
|
];
|
||||||
|
|
||||||
|
function eventType(payload) {
|
||||||
|
return payload.action || payload.eventType || payload.type || payload.status || "veriff.webhook";
|
||||||
|
}
|
||||||
|
|
||||||
|
function externalId(payload) {
|
||||||
|
return payload.id || payload.sessionId || payload.verification?.id || payload.verification?.vendorData || payload.vendorData || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
if (req.method !== "POST") return sendMethodNotAllowed(res);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await rawBody(req);
|
||||||
|
const payload = parseJson(body);
|
||||||
|
const headers = safeHeaders(req, SIGNATURE_HEADERS.concat(["user-agent"]));
|
||||||
|
const secret = process.env.VERIFF_WEBHOOK_SECRET || process.env.VERIFF_SECRET_KEY;
|
||||||
|
const signature = firstHeader(req, SIGNATURE_HEADERS);
|
||||||
|
const signatureValid = verifyHmacSignature(body, secret, signature);
|
||||||
|
|
||||||
|
const recorded = await recordProviderEvent({
|
||||||
|
provider: "veriff",
|
||||||
|
eventType: eventType(payload),
|
||||||
|
externalId: externalId(payload),
|
||||||
|
signatureValid,
|
||||||
|
payload,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(202).json({
|
||||||
|
ok: true,
|
||||||
|
recorded: Boolean(recorded),
|
||||||
|
provider: "veriff",
|
||||||
|
event_type: eventType(payload),
|
||||||
|
signature_valid: signatureValid,
|
||||||
|
warning: signatureValid ? undefined : "Event recorded, but no recognized Veriff signature was verified.",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ ok: false, error: err?.message || "Veriff webhook failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -598,6 +598,10 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
<div id="liveAuditTrail"></div>
|
<div id="liveAuditTrail"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-h"><h3>Provider webhooks</h3><span class="small muted">events are stored in Neon</span></div>
|
||||||
|
<div id="liveWebhookStatus" class="small muted">Checking webhook setup...</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ ABOUT ============ -->
|
<!-- ============ ABOUT ============ -->
|
||||||
@@ -2352,13 +2356,18 @@ async function fetchLiveBackendStatus(){
|
|||||||
}
|
}
|
||||||
function renderLiveBackendStatus(){
|
function renderLiveBackendStatus(){
|
||||||
const root=$("liveBackendStatus");if(!root)return;
|
const root=$("liveBackendStatus");if(!root)return;
|
||||||
|
const hookRoot=$("liveWebhookStatus");
|
||||||
const s=LIVE_BACKEND_STATUS;
|
const s=LIVE_BACKEND_STATUS;
|
||||||
if(!s){root.innerHTML="Checking backend readiness...";return;}
|
if(!s){root.innerHTML="Checking backend readiness...";if(hookRoot)hookRoot.innerHTML="Checking webhook setup...";return;}
|
||||||
const providers=(s.providers||[]).map(p=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"configured":"missing"}</b></div>`).join("");
|
const providers=(s.providers||[]).map(p=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"configured":"missing"}</b></div>`).join("");
|
||||||
const stack=(s.provider_stack||[]).map(p=>`<div class="leader-row"><span><b>${esc(p.provider)}</b><div class="small muted">${esc(p.role)}</div><div class="small muted">${(p.checks||[]).map(c=>`${esc(c.key)}: ${c.configured?"ok":"missing"}`).join(" · ")}</div></span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"needs keys"}</b></div>`).join("");
|
const stack=(s.provider_stack||[]).map(p=>`<div class="leader-row"><span><b>${esc(p.provider)}</b><div class="small muted">${esc(p.role)}</div><div class="small muted">${(p.checks||[]).map(c=>`${esc(c.key)}: ${c.configured?"ok":"missing"}`).join(" · ")}</div></span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"needs keys"}</b></div>`).join("");
|
||||||
const next=(s.next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join("");
|
const next=(s.next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join("");
|
||||||
const reqs=(s.launch_requirements||[]).map(x=>`<li>${esc(x.label)}</li>`).join("");
|
const reqs=(s.launch_requirements||[]).map(x=>`<li>${esc(x.label)}</li>`).join("");
|
||||||
root.innerHTML=`<b>${s.live_trading_enabled?"Live trading unlocked":"Live trading locked"}</b><div class="small muted" style="margin:6px 0">${esc(s.locked_reason||"All required backend providers are configured.")}</div>${stack?`<div class="locked-panel" style="margin-top:10px"><b>Your provider stack</b>${stack}</div>`:""}${providers}${next?`<div class="locked-panel" style="margin-top:10px"><b>Next setup items</b><ul>${next}</ul></div>`:""}${reqs?`<div class="locked-panel" style="margin-top:10px"><b>Full launch requirements</b><ol>${reqs}</ol></div>`:""}<div class="small muted" style="margin-top:8px">Live execution stays disabled until every provider is configured, every launch requirement is complete, and final approval flips LIVE_TRADING_ENABLED=true.</div>`;
|
root.innerHTML=`<b>${s.live_trading_enabled?"Live trading unlocked":"Live trading locked"}</b><div class="small muted" style="margin:6px 0">${esc(s.locked_reason||"All required backend providers are configured.")}</div>${stack?`<div class="locked-panel" style="margin-top:10px"><b>Your provider stack</b>${stack}</div>`:""}${providers}${next?`<div class="locked-panel" style="margin-top:10px"><b>Next setup items</b><ul>${next}</ul></div>`:""}${reqs?`<div class="locked-panel" style="margin-top:10px"><b>Full launch requirements</b><ol>${reqs}</ol></div>`:""}<div class="small muted" style="margin-top:8px">Live execution stays disabled until every provider is configured, every launch requirement is complete, and final approval flips LIVE_TRADING_ENABLED=true.</div>`;
|
||||||
|
if(hookRoot){
|
||||||
|
const hooks=(s.webhooks||[]).map(h=>`<div class="leader-row"><span><b>${esc(h.provider)}</b><div class="small muted">${esc(h.label)}</div></span><a class="market-link" href="${esc(h.url)}" target="_blank" rel="noopener">${esc(h.url)}</a></div>`).join("");
|
||||||
|
hookRoot.innerHTML=hooks||"Webhook URLs will appear after backend readiness loads.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function dryRunLiveOrderIntent(){
|
async function dryRunLiveOrderIntent(){
|
||||||
const s=loadLiveState();
|
const s=loadLiveState();
|
||||||
|
|||||||
Generated
+43
-1
@@ -5,9 +5,26 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/blob": "2.5.0"
|
"@neondatabase/serverless": "^1.1.0",
|
||||||
|
"@vercel/blob": "2.5.0",
|
||||||
|
"svix": "^1.96.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@neondatabase/serverless": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@stablelib/base64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vercel/blob": {
|
"node_modules/@vercel/blob": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.5.0.tgz",
|
||||||
@@ -107,6 +124,12 @@
|
|||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-sha256": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/get-stream": {
|
"node_modules/get-stream": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||||
@@ -280,6 +303,16 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/standardwebhooks": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-final-newline": {
|
"node_modules/strip-final-newline": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||||
@@ -289,6 +322,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svix": {
|
||||||
|
"version": "1.96.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svix/-/svix-1.96.1.tgz",
|
||||||
|
"integrity": "sha512-l+GyPS6gjL0okiXplLazEY2GbBsv1jZ4UIwtjp553YkDDv635xMKbM5sABpIdiWy1BYxgyV8M6tHeTwY0gyXlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"standardwebhooks": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/throttleit": {
|
"node_modules/throttleit": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/blob": "2.5.0"
|
"@neondatabase/serverless": "^1.1.0",
|
||||||
|
"@vercel/blob": "2.5.0",
|
||||||
|
"svix": "^1.96.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user