git-paid 0.0.0-stage → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -2
- package/bin/git-paid.js +211 -0
- package/brand/README.md +54 -0
- package/brand/postbuild-icon.png +0 -0
- package/brand/postbuild-icon.svg +14 -0
- package/brand/postbuild-logo.png +0 -0
- package/brand/postbuild-logo.svg +17 -0
- package/lib/auth.js +208 -0
- package/lib/directories.js +123 -0
- package/lib/engine.js +502 -0
- package/lib/fixer.js +168 -0
- package/lib/inbox.js +216 -0
- package/lib/mcp.js +907 -0
- package/lib/pseo.js +491 -0
- package/lib/stateful-mcp.js +485 -0
- package/lib/stripe.js +554 -0
- package/lib/submitter.js +189 -0
- package/package.json +41 -4
- package/server.js +252 -0
package/lib/fixer.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writePseoHtmlFiles } from "./pseo.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects the static public folder of a local project (Next.js, Vite, Astro, SvelteKit, or generic).
|
|
7
|
+
*/
|
|
8
|
+
export function detectPublicDir(cwd = process.cwd()) {
|
|
9
|
+
const svelteStatic = path.join(cwd, "static");
|
|
10
|
+
const svelteConfig = path.join(cwd, "svelte.config.js");
|
|
11
|
+
if (fs.existsSync(svelteConfig) && fs.existsSync(svelteStatic)) {
|
|
12
|
+
return { framework: "SvelteKit", publicDir: svelteStatic };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let framework = "Static / Node";
|
|
16
|
+
if (fs.existsSync(path.join(cwd, "next.config.js")) || fs.existsSync(path.join(cwd, "next.config.mjs")) || fs.existsSync(path.join(cwd, "next.config.ts"))) {
|
|
17
|
+
framework = "Next.js";
|
|
18
|
+
} else if (fs.existsSync(path.join(cwd, "astro.config.mjs")) || fs.existsSync(path.join(cwd, "astro.config.ts"))) {
|
|
19
|
+
framework = "Astro";
|
|
20
|
+
} else if (fs.existsSync(path.join(cwd, "vite.config.js")) || fs.existsSync(path.join(cwd, "vite.config.ts")) || fs.existsSync(path.join(cwd, "vite.config.mjs"))) {
|
|
21
|
+
framework = "Vite";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const publicDir = path.join(cwd, "public");
|
|
25
|
+
return { framework, publicDir };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildLlmsTxt(report) {
|
|
29
|
+
const kit = report.launchKit || {};
|
|
30
|
+
const lines = [
|
|
31
|
+
`# ${kit.brandName || kit.productName || report.domain}`,
|
|
32
|
+
``,
|
|
33
|
+
`> ${kit.tagline || report.meta?.description || "Software platform and developer workflow tool."}`,
|
|
34
|
+
``,
|
|
35
|
+
`- Canonical URL: ${report.targetUrl}`,
|
|
36
|
+
`- Verification & Press Contact: ${report.botEmail}`,
|
|
37
|
+
`- Category: ${kit.categories ? kit.categories.join(", ") : "Developer Tools, SaaS"}`,
|
|
38
|
+
``,
|
|
39
|
+
`## Overview`,
|
|
40
|
+
`${kit.shortPitch || kit.longDescription || ""}`,
|
|
41
|
+
``,
|
|
42
|
+
`## Key Comparison & Programmatic SEO Guides`,
|
|
43
|
+
...(report.pseoPages || []).slice(0, 15).map(
|
|
44
|
+
(p) => `- [${p.keyword || p.path}](${report.targetUrl.replace(/\/$/, "")}${p.path || p.slug}): ${p.intent} guide (${p.volume || p.estMonthlySearches})`
|
|
45
|
+
),
|
|
46
|
+
``
|
|
47
|
+
];
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildHeadSeoSnippet(report) {
|
|
52
|
+
const title = report.launchKit?.tagline
|
|
53
|
+
? `${report.launchKit.brandName || report.launchKit.productName || report.domain} — ${report.launchKit.tagline}`
|
|
54
|
+
: report.meta?.title || report.domain;
|
|
55
|
+
const desc = report.launchKit?.shortPitch || report.meta?.description || "";
|
|
56
|
+
return `<!-- Generated by postbuild (git paid fix) for ${report.domain} -->
|
|
57
|
+
<title>${title}</title>
|
|
58
|
+
<meta name="description" content="${desc.replace(/"/g, """)}" />
|
|
59
|
+
<link rel="canonical" href="${report.targetUrl}" />
|
|
60
|
+
|
|
61
|
+
<!-- OpenGraph / Social Cards -->
|
|
62
|
+
<meta property="og:type" content="website" />
|
|
63
|
+
<meta property="og:site_name" content="${report.launchKit?.brandName || report.launchKit?.productName || report.domain}" />
|
|
64
|
+
<meta property="og:title" content="${title.replace(/"/g, """)}" />
|
|
65
|
+
<meta property="og:description" content="${desc.replace(/"/g, """)}" />
|
|
66
|
+
<meta property="og:url" content="${report.targetUrl}" />
|
|
67
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
68
|
+
<meta name="twitter:title" content="${title.replace(/"/g, """)}" />
|
|
69
|
+
<meta name="twitter:description" content="${desc.replace(/"/g, """)}" />
|
|
70
|
+
|
|
71
|
+
<!-- Structured Data (JSON-LD) -->
|
|
72
|
+
${report.artifacts?.jsonLd || ""}
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function applyLocalSeoFixes(report, options = {}) {
|
|
77
|
+
const cwd = options.cwd || process.cwd();
|
|
78
|
+
const overwritePublic = Boolean(options.overwritePublic);
|
|
79
|
+
const { framework, publicDir } = detectPublicDir(cwd);
|
|
80
|
+
fs.mkdirSync(publicDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const outDir = path.join(cwd, ".postbuild");
|
|
83
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
const writtenFiles = [];
|
|
86
|
+
|
|
87
|
+
// 1. robots.txt in public/ (if missing or overwritePublic)
|
|
88
|
+
const robotsPath = path.join(publicDir, "robots.txt");
|
|
89
|
+
if (!fs.existsSync(robotsPath) || overwritePublic) {
|
|
90
|
+
fs.writeFileSync(robotsPath, report.artifacts.robotsTxt, "utf8");
|
|
91
|
+
writtenFiles.push({ path: path.relative(cwd, robotsPath), role: "AI + Search Crawler Rules (robots.txt)" });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. sitemap.xml in public/ (if missing or overwritePublic)
|
|
95
|
+
const sitemapPath = path.join(publicDir, "sitemap.xml");
|
|
96
|
+
if (!fs.existsSync(sitemapPath) || overwritePublic) {
|
|
97
|
+
fs.writeFileSync(sitemapPath, report.artifacts.sitemapXml, "utf8");
|
|
98
|
+
writtenFiles.push({ path: path.relative(cwd, sitemapPath), role: "XML Sitemap with 25 pSEO Routes" });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. llms.txt in public/ for AI Answer Engines (ChatGPT, Claude, Perplexity)
|
|
102
|
+
const llmsPath = path.join(publicDir, "llms.txt");
|
|
103
|
+
const llmsContent = buildLlmsTxt(report);
|
|
104
|
+
if (!fs.existsSync(llmsPath) || overwritePublic) {
|
|
105
|
+
fs.writeFileSync(llmsPath, llmsContent, "utf8");
|
|
106
|
+
writtenFiles.push({ path: path.relative(cwd, llmsPath), role: "AI Answer Engine Manifest (llms.txt)" });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Complete .postbuild bundle
|
|
110
|
+
const headSnippetPath = path.join(outDir, "head-seo-snippet.html");
|
|
111
|
+
fs.writeFileSync(headSnippetPath, buildHeadSeoSnippet(report), "utf8");
|
|
112
|
+
writtenFiles.push({ path: path.relative(cwd, headSnippetPath), role: "OpenGraph + Canonical + JSON-LD <head> Snippet" });
|
|
113
|
+
|
|
114
|
+
const sitemapOut = path.join(outDir, "sitemap.xml");
|
|
115
|
+
fs.writeFileSync(sitemapOut, report.artifacts.sitemapXml, "utf8");
|
|
116
|
+
|
|
117
|
+
const robotsOut = path.join(outDir, "robots.txt");
|
|
118
|
+
fs.writeFileSync(robotsOut, report.artifacts.robotsTxt, "utf8");
|
|
119
|
+
|
|
120
|
+
const llmsOut = path.join(outDir, "llms.txt");
|
|
121
|
+
fs.writeFileSync(llmsOut, llmsContent, "utf8");
|
|
122
|
+
|
|
123
|
+
const gateOut = path.join(outDir, "PostbuildCheckoutGate.jsx");
|
|
124
|
+
fs.writeFileSync(gateOut, report.artifacts.paywallCode, "utf8");
|
|
125
|
+
writtenFiles.push({ path: path.relative(cwd, gateOut), role: "Drop-in Stripe Checkout Paywall Component" });
|
|
126
|
+
|
|
127
|
+
const pseoJsonOut = path.join(outDir, "pseo-routes.json");
|
|
128
|
+
fs.writeFileSync(pseoJsonOut, JSON.stringify(report.pseoPages, null, 2), "utf8");
|
|
129
|
+
writtenFiles.push({ path: path.relative(cwd, pseoJsonOut), role: "25 Programmatic SEO Route Definitions (JSON)" });
|
|
130
|
+
|
|
131
|
+
// Render the 25 static HTML landing pages into .postbuild/pseo (and public/ if overwritePublic)
|
|
132
|
+
const pseoOutDir = path.join(outDir, "pseo");
|
|
133
|
+
const pseoRenderOpts = {
|
|
134
|
+
origin: report.targetUrl,
|
|
135
|
+
cleanName: report.cleanName || report.launchKit?.productName || report.domain,
|
|
136
|
+
domain: report.domain
|
|
137
|
+
};
|
|
138
|
+
const renderedPseo = writePseoHtmlFiles(report.pseoPages || [], pseoOutDir, pseoRenderOpts);
|
|
139
|
+
writtenFiles.push({
|
|
140
|
+
path: path.relative(cwd, pseoOutDir),
|
|
141
|
+
role: `${renderedPseo.length} Static Programmatic SEO HTML Landing Pages`
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (overwritePublic) {
|
|
145
|
+
writePseoHtmlFiles(report.pseoPages || [], publicDir, pseoRenderOpts);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const launchKitOut = path.join(outDir, "launch-kit.md");
|
|
149
|
+
fs.writeFileSync(launchKitOut, report.launchKit.longDescription, "utf8");
|
|
150
|
+
writtenFiles.push({ path: path.relative(cwd, launchKitOut), role: "Synthesized Launch Copy (HN, Product Hunt, Directories)" });
|
|
151
|
+
|
|
152
|
+
// 5. CSV export of all 85 directories with prefilled submission links
|
|
153
|
+
const csvHeader = "ID,Directory Name,Category,Domain Rating,Submission URL,Prefilled Launch URL,Auto-Fill\n";
|
|
154
|
+
const csvRows = (report.directories || []).map((d) => {
|
|
155
|
+
const q = (val) => `"${String(val || "").replace(/"/g, '""')}"`;
|
|
156
|
+
return [d.id, q(d.name), q(d.category), d.dr, q(d.submitUrl || d.url), q(d.prefilledUrl), d.auto ? "Yes" : "Manual"].join(",");
|
|
157
|
+
});
|
|
158
|
+
const csvOut = path.join(outDir, "directories-85.csv");
|
|
159
|
+
fs.writeFileSync(csvOut, csvHeader + csvRows.join("\n") + "\n", "utf8");
|
|
160
|
+
writtenFiles.push({ path: path.relative(cwd, csvOut), role: "85 Curated Directories CSV with Prefilled URLs" });
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
framework,
|
|
164
|
+
publicDir: path.relative(cwd, publicDir) || "public",
|
|
165
|
+
outDir: path.relative(cwd, outDir) || ".postbuild",
|
|
166
|
+
writtenFiles
|
|
167
|
+
};
|
|
168
|
+
}
|
package/lib/inbox.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const INBOX_STORE_PATH = path.join(os.tmpdir(), "postbuild-verification-inbox.json");
|
|
6
|
+
|
|
7
|
+
function loadStore() {
|
|
8
|
+
try {
|
|
9
|
+
if (fs.existsSync(INBOX_STORE_PATH)) {
|
|
10
|
+
return JSON.parse(fs.readFileSync(INBOX_STORE_PATH, "utf8"));
|
|
11
|
+
}
|
|
12
|
+
} catch {}
|
|
13
|
+
return { messages: [] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function saveStore(store) {
|
|
17
|
+
try {
|
|
18
|
+
fs.writeFileSync(INBOX_STORE_PATH, JSON.stringify(store, null, 2), "utf8");
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts confirmation/verification links and 4-to-8 digit OTP codes from email text/HTML.
|
|
24
|
+
*/
|
|
25
|
+
export function extractVerificationSignals(subject = "", text = "", html = "") {
|
|
26
|
+
const combined = `${subject}\n${text}\n${html.replace(/<[^>]+>/g, " ")}`;
|
|
27
|
+
|
|
28
|
+
// Extract all https:// URLs
|
|
29
|
+
const urlMatches = `${text}\n${html}`.match(/https?:\/\/[^\s"'<>)+]+/gi) || [];
|
|
30
|
+
const cleanedUrls = [...new Set(urlMatches.map((u) => u.replace(/[.,;!?]+$/, "")))];
|
|
31
|
+
|
|
32
|
+
const verificationKeywords = /verify|confirm|activate|token|magic|auth|validate|callback|accept|claim|submission|approve/i;
|
|
33
|
+
const verificationLinks = cleanedUrls.filter((u) => verificationKeywords.test(u));
|
|
34
|
+
const primaryLink = verificationLinks[0] || cleanedUrls[0] || null;
|
|
35
|
+
|
|
36
|
+
// Extract 6-digit OTP code (or 4-8 digit code near "code", "OTP", "PIN", "verification")
|
|
37
|
+
let otpCode = null;
|
|
38
|
+
const contextualOtp = combined.match(/(?:code|otp|pin|token|verification|confirm)[:\s#-]*([0-9]{4,8})\b/i);
|
|
39
|
+
if (contextualOtp) {
|
|
40
|
+
otpCode = contextualOtp[1];
|
|
41
|
+
} else {
|
|
42
|
+
const sixDigit = combined.match(/\b([0-9]{6})\b/);
|
|
43
|
+
if (sixDigit && !/^202[0-9]/.test(sixDigit[1])) {
|
|
44
|
+
otpCode = sixDigit[1];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
primaryLink,
|
|
50
|
+
verificationLinks: verificationLinks.length ? verificationLinks : cleanedUrls.slice(0, 3),
|
|
51
|
+
otpCode
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ingests an inbound email webhook payload (Resend `email.received`, Postmark, ForwardEmail, or raw JSON).
|
|
57
|
+
*/
|
|
58
|
+
export async function ingestInboundEmail(rawPayload = {}) {
|
|
59
|
+
const payload = rawPayload?.data && typeof rawPayload.data === "object" ? rawPayload.data : rawPayload;
|
|
60
|
+
|
|
61
|
+
let textBody = String(payload.text || payload.body || payload.rawText || "").trim();
|
|
62
|
+
let htmlBody = String(payload.html || "").trim();
|
|
63
|
+
let subject = String(payload.subject || "Confirm your directory submission").trim();
|
|
64
|
+
|
|
65
|
+
const resendEmailId = payload.email_id || payload.id;
|
|
66
|
+
const resendApiKey = (process.env.RESEND_API_KEY || "").trim();
|
|
67
|
+
if (resendEmailId && resendApiKey && !textBody && !htmlBody) {
|
|
68
|
+
try {
|
|
69
|
+
const resp = await fetch(`https://api.resend.com/emails/receiving/${encodeURIComponent(resendEmailId)}`, {
|
|
70
|
+
headers: { Authorization: `Bearer ${resendApiKey}` }
|
|
71
|
+
});
|
|
72
|
+
if (resp.ok) {
|
|
73
|
+
const fullEmail = await resp.json();
|
|
74
|
+
textBody = String(fullEmail.text || textBody).trim();
|
|
75
|
+
htmlBody = String(fullEmail.html || htmlBody).trim();
|
|
76
|
+
subject = String(fullEmail.subject || subject).trim();
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const toRaw = payload.to || payload.recipient || payload.envelope?.to || "postbuild-bot@postbuild.dev";
|
|
82
|
+
const toAddress = (Array.isArray(toRaw) ? toRaw[0] : String(toRaw))
|
|
83
|
+
.replace(/^.*<([^>]+)>.*$/, "$1")
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
|
|
87
|
+
const fromRaw = payload.from || payload.sender || payload.envelope?.from || "no-reply@directory-verifier.com";
|
|
88
|
+
const fromAddress = String(fromRaw).trim();
|
|
89
|
+
|
|
90
|
+
const signals = extractVerificationSignals(subject, textBody, htmlBody);
|
|
91
|
+
|
|
92
|
+
const msg = {
|
|
93
|
+
id: resendEmailId || `msg_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
94
|
+
to: toAddress,
|
|
95
|
+
from: fromAddress,
|
|
96
|
+
subject,
|
|
97
|
+
snippet: (textBody || htmlBody.replace(/<[^>]+>/g, " ")).slice(0, 220).trim(),
|
|
98
|
+
primaryLink: signals.primaryLink,
|
|
99
|
+
verificationLinks: signals.verificationLinks,
|
|
100
|
+
otpCode: signals.otpCode,
|
|
101
|
+
status: signals.primaryLink || signals.otpCode ? "action_ready" : "received",
|
|
102
|
+
receivedAt: payload.created_at || new Date().toISOString()
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const store = loadStore();
|
|
106
|
+
const existsIdx = store.messages.findIndex((m) => m.id === msg.id);
|
|
107
|
+
if (existsIdx >= 0) {
|
|
108
|
+
store.messages[existsIdx] = msg;
|
|
109
|
+
} else {
|
|
110
|
+
store.messages.unshift(msg);
|
|
111
|
+
}
|
|
112
|
+
store.messages = store.messages.slice(0, 100);
|
|
113
|
+
saveStore(store);
|
|
114
|
+
|
|
115
|
+
return msg;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Syncs recent inbound emails directly from Resend Receiving API (`GET /emails/receiving`)
|
|
120
|
+
* so serverless cold starts never lose verification emails.
|
|
121
|
+
*/
|
|
122
|
+
async function syncFromResendReceivingApi(targetInbox = "") {
|
|
123
|
+
const resendApiKey = (process.env.RESEND_API_KEY || "").trim();
|
|
124
|
+
if (!resendApiKey) return;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timer = setTimeout(() => controller.abort(), 3500);
|
|
129
|
+
const resp = await fetch("https://api.resend.com/emails/receiving?limit=20", {
|
|
130
|
+
headers: { Authorization: `Bearer ${resendApiKey}` },
|
|
131
|
+
signal: controller.signal
|
|
132
|
+
});
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
if (!resp.ok) return;
|
|
135
|
+
|
|
136
|
+
const data = await resp.json();
|
|
137
|
+
const items = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : [];
|
|
138
|
+
const store = loadStore();
|
|
139
|
+
const knownIds = new Set(store.messages.map((m) => m.id));
|
|
140
|
+
const prefix = targetInbox ? targetInbox.split("@")[0].toLowerCase() : "";
|
|
141
|
+
|
|
142
|
+
for (const item of items.slice(0, 12)) {
|
|
143
|
+
const id = item.id || item.email_id;
|
|
144
|
+
if (!id || knownIds.has(id)) continue;
|
|
145
|
+
|
|
146
|
+
const toList = Array.isArray(item.to) ? item.to.join(" ").toLowerCase() : String(item.to || "").toLowerCase();
|
|
147
|
+
if (prefix && !toList.includes(prefix) && !toList.includes("postbuild.dev")) continue;
|
|
148
|
+
|
|
149
|
+
await ingestInboundEmail(item);
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Lists verification emails for a given bot inbox address (e.g., postbuild-bot@postbuild.dev).
|
|
156
|
+
* Queries Resend Receiving API directly when RESEND_API_KEY is present, or syncs from postbuild.dev when run via local CLI.
|
|
157
|
+
*/
|
|
158
|
+
export async function listInboxMessages(inboxAddress = "", options = {}) {
|
|
159
|
+
const normalized = String(inboxAddress || "postbuild-bot@postbuild.dev").trim().toLowerCase();
|
|
160
|
+
|
|
161
|
+
if (process.env.RESEND_API_KEY) {
|
|
162
|
+
await syncFromResendReceivingApi(normalized);
|
|
163
|
+
} else if (options.allowRemoteFallback !== false && !process.env.VERCEL) {
|
|
164
|
+
try {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
167
|
+
const remoteResp = await fetch(
|
|
168
|
+
`https://www.postbuild.dev/api/dispatch?action=inbox_list&inbox=${encodeURIComponent(normalized)}`,
|
|
169
|
+
{ signal: controller.signal }
|
|
170
|
+
);
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
if (remoteResp.ok) {
|
|
173
|
+
const remoteData = await remoteResp.json();
|
|
174
|
+
if (Array.isArray(remoteData.messages) && remoteData.messages.length > 0) {
|
|
175
|
+
const store = loadStore();
|
|
176
|
+
const existingIds = new Set(store.messages.map((m) => m.id));
|
|
177
|
+
for (const rm of remoteData.messages) {
|
|
178
|
+
if (!existingIds.has(rm.id)) store.messages.push(rm);
|
|
179
|
+
}
|
|
180
|
+
saveStore(store);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const store = loadStore();
|
|
187
|
+
const filtered = normalized
|
|
188
|
+
? store.messages.filter((m) => m.to === normalized || m.to.includes(normalized.split("@")[0]))
|
|
189
|
+
: store.messages;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
inboxAddress: normalized,
|
|
193
|
+
count: filtered.length,
|
|
194
|
+
messages: filtered
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generates a realistic test verification email so users can verify the inbox pipeline immediately.
|
|
200
|
+
*/
|
|
201
|
+
export async function sendTestVerificationEmail(inboxAddress = "postbuild-bot@postbuild.dev", domain = "postbuild.dev") {
|
|
202
|
+
const cleanInbox = String(inboxAddress || "postbuild-bot@postbuild.dev").trim().toLowerCase();
|
|
203
|
+
const otp = String(Math.floor(100000 + Math.random() * 900000));
|
|
204
|
+
const token = Buffer.from(`${domain}:${Date.now()}`).toString("base64url");
|
|
205
|
+
const verifyUrl = `https://postbuild.dev/?verified_directory=betalist&domain=${encodeURIComponent(domain)}&token=${token}`;
|
|
206
|
+
|
|
207
|
+
return ingestInboundEmail({
|
|
208
|
+
type: "email.received",
|
|
209
|
+
data: {
|
|
210
|
+
to: [cleanInbox],
|
|
211
|
+
from: "BetaList Launch Bot <verify@betalist.com>",
|
|
212
|
+
subject: `[Action Required] Verify ${domain} submission on BetaList (Code: ${otp})`,
|
|
213
|
+
text: `Hi ${domain} team,\n\nWe received your directory launch submission via postbuild. Your one-time verification code is ${otp}.\n\nOr click the instant confirmation link below to publish your listing immediately:\n${verifyUrl}\n`
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|