git-paid 0.0.0-stage → 2.1.0
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/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 +42 -4
- package/server.js +252 -0
- package/README.md +0 -3
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
|
+
}
|