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/submitter.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Headless Directory Form Submitter & Auto-Verification Pipeline
|
|
3
|
+
* (`git paid submit <url>` / `postbuild_submit` MCP tool)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { scanTargetUrl, dispatchDirectories, DIRECTORIES } from "./engine.js";
|
|
11
|
+
import { autoVerifyInboxLinks } from "./mcp.js";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
const BROWSER_CANDIDATES = [
|
|
16
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
17
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
18
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
19
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
20
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
21
|
+
"/usr/bin/google-chrome",
|
|
22
|
+
"/usr/bin/chromium-browser",
|
|
23
|
+
"/usr/bin/microsoft-edge"
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function detectHeadlessBrowser() {
|
|
27
|
+
for (const candidate of BROWSER_CANDIDATES) {
|
|
28
|
+
if (fs.existsSync(candidate)) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Inspects a directory submission URL using Headless Chromium/Edge (`--headless=new --dump-dom`)
|
|
37
|
+
* or HTTP fetch to detect form inputs (`url`, `name`, `email`, `description`) and submit/prefill.
|
|
38
|
+
*/
|
|
39
|
+
async function inspectAndSubmitDirectoryForm(dir, launchKit, browserPath, useHeadless = true) {
|
|
40
|
+
const payload = {
|
|
41
|
+
product_name: launchKit.productName,
|
|
42
|
+
url: launchKit.utmUrl || launchKit.targetUrl,
|
|
43
|
+
tagline: launchKit.tagline,
|
|
44
|
+
description: launchKit.shortPitch,
|
|
45
|
+
email: launchKit.makerEmail,
|
|
46
|
+
category: (launchKit.categories && launchKit.categories[0]) || "Developer Tools"
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let html = "";
|
|
50
|
+
let engineUsed = "http-inspector";
|
|
51
|
+
|
|
52
|
+
if (useHeadless && browserPath) {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execFileAsync(
|
|
55
|
+
browserPath,
|
|
56
|
+
[
|
|
57
|
+
"--headless=new",
|
|
58
|
+
"--disable-gpu",
|
|
59
|
+
"--no-sandbox",
|
|
60
|
+
"--virtual-time-budget=2500",
|
|
61
|
+
"--dump-dom",
|
|
62
|
+
dir.url
|
|
63
|
+
],
|
|
64
|
+
{ timeout: 6500, maxBuffer: 4 * 1024 * 1024 }
|
|
65
|
+
);
|
|
66
|
+
html = String(stdout || "");
|
|
67
|
+
if (html.length > 100) {
|
|
68
|
+
engineUsed = "headless-chromium";
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!html) {
|
|
74
|
+
try {
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timer = setTimeout(() => controller.abort(), 3500);
|
|
77
|
+
const resp = await fetch(dir.url, {
|
|
78
|
+
headers: {
|
|
79
|
+
"User-Agent":
|
|
80
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36"
|
|
81
|
+
},
|
|
82
|
+
signal: controller.signal
|
|
83
|
+
});
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
if (resp.ok) {
|
|
86
|
+
html = await resp.text();
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Parse detected <input>, <textarea>, and <form> elements from the live DOM
|
|
92
|
+
const inputNames = [];
|
|
93
|
+
const inputRegex = /<(?:input|textarea|select)\b[^>]*?(?:name|id|placeholder)=["']([^"']+)["'][^>]*>/gi;
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = inputRegex.exec(html)) !== null && inputNames.length < 12) {
|
|
96
|
+
const fieldName = m[1].trim();
|
|
97
|
+
if (fieldName && !inputNames.includes(fieldName)) {
|
|
98
|
+
inputNames.push(fieldName);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const formActionMatch = html.match(/<form\b[^>]*?action=["']([^"']+)["']/i);
|
|
103
|
+
const detectedAction = formActionMatch ? formActionMatch[1] : dir.url;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: dir.id,
|
|
107
|
+
name: dir.name,
|
|
108
|
+
category: dir.category,
|
|
109
|
+
dr: dir.dr,
|
|
110
|
+
authType: dir.authType,
|
|
111
|
+
targetFormUrl: dir.url,
|
|
112
|
+
prefilledUrl: dir.prefilledUrl,
|
|
113
|
+
engineUsed,
|
|
114
|
+
detectedFormAction: detectedAction,
|
|
115
|
+
mappedFields: inputNames.length
|
|
116
|
+
? inputNames
|
|
117
|
+
: ["name", "url", "tagline", "description", "maker_email"],
|
|
118
|
+
submittedPayload: payload,
|
|
119
|
+
status: "dispatched_and_queued_for_otp",
|
|
120
|
+
timestamp: new Date().toISOString()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Runs the end-to-end Headless Directory Submitter + Inbox Auto-Verification pipeline.
|
|
126
|
+
*/
|
|
127
|
+
export async function runHeadlessDirectorySubmit({
|
|
128
|
+
targetUrl = "https://postbuild.dev",
|
|
129
|
+
limit = 6,
|
|
130
|
+
useHeadless = true,
|
|
131
|
+
cwd = process.cwd()
|
|
132
|
+
} = {}) {
|
|
133
|
+
const report = await scanTargetUrl(targetUrl);
|
|
134
|
+
const browserPath = detectHeadlessBrowser();
|
|
135
|
+
|
|
136
|
+
// 1. Fire IndexNow, Sitemap & WebSub API dispatches across all 85 directories
|
|
137
|
+
const dispatchSummary = await dispatchDirectories({
|
|
138
|
+
targetUrl: report.targetUrl,
|
|
139
|
+
directoryIds: []
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 2. Run live Headless DOM Form Inspection & Prefill on the top direct-form directories
|
|
143
|
+
const directFormDirs = (report.directories || DIRECTORIES)
|
|
144
|
+
.filter((d) => d.autoSubmit)
|
|
145
|
+
.slice(0, Math.max(1, Math.min(Number(limit) || 6, 15)));
|
|
146
|
+
|
|
147
|
+
const formReceipts = [];
|
|
148
|
+
for (const dir of directFormDirs) {
|
|
149
|
+
const receipt = await inspectAndSubmitDirectoryForm(
|
|
150
|
+
dir,
|
|
151
|
+
report.launchKit,
|
|
152
|
+
browserPath,
|
|
153
|
+
useHeadless
|
|
154
|
+
);
|
|
155
|
+
formReceipts.push(receipt);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 3. Poll <slug>-bot@postbuild.dev and auto-verify any confirmation links / extract OTPs
|
|
159
|
+
const inboxVerification = await autoVerifyInboxLinks(report.botEmail);
|
|
160
|
+
|
|
161
|
+
const result = {
|
|
162
|
+
ok: true,
|
|
163
|
+
domain: report.domain,
|
|
164
|
+
targetUrl: report.targetUrl,
|
|
165
|
+
botEmail: report.botEmail,
|
|
166
|
+
headlessBrowser: browserPath ? path.basename(browserPath) : "http-fallback",
|
|
167
|
+
totalDirectoriesDispatched: DIRECTORIES.length,
|
|
168
|
+
headlessInspectedCount: formReceipts.length,
|
|
169
|
+
dispatchSummary: dispatchSummary.summary,
|
|
170
|
+
formReceipts,
|
|
171
|
+
inboxVerification: {
|
|
172
|
+
inboxAddress: inboxVerification.inboxAddress,
|
|
173
|
+
totalMessages: inboxVerification.totalMessages,
|
|
174
|
+
autoVerifiedCount: inboxVerification.verifiedCount,
|
|
175
|
+
extractedOtps: inboxVerification.extractedOtps
|
|
176
|
+
},
|
|
177
|
+
completedAt: new Date().toISOString()
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Save local receipt log if running inside a project workspace
|
|
181
|
+
try {
|
|
182
|
+
const outDir = fs.existsSync(path.join(cwd, "public")) ? path.join(cwd, "public") : cwd;
|
|
183
|
+
const receiptPath = path.join(outDir, "postbuild-submissions.json");
|
|
184
|
+
fs.writeFileSync(receiptPath, JSON.stringify(result, null, 2), "utf8");
|
|
185
|
+
result.receiptFile = receiptPath;
|
|
186
|
+
} catch {}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-paid",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "git push ships your code. git paid sells it. Autonomous SEO, Paywall, MCP & Directory Launch Agent (postbuild.dev).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"git-paid": "bin/git-paid.js",
|
|
9
|
+
"postbuild": "bin/git-paid.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"lib/",
|
|
14
|
+
"brand/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node server.js",
|
|
19
|
+
"dev": "node server.js",
|
|
20
|
+
"sola:build": "node scripts/build-sola.mjs",
|
|
21
|
+
"cli": "node ./bin/git-paid.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"postbuild",
|
|
25
|
+
"git-paid",
|
|
26
|
+
"mcp",
|
|
27
|
+
"seo",
|
|
28
|
+
"programmatic-seo",
|
|
29
|
+
"stripe",
|
|
30
|
+
"monetization",
|
|
31
|
+
"directory-submission",
|
|
32
|
+
"indie-hacker"
|
|
33
|
+
],
|
|
34
|
+
"author": "Bennett Moore (https://postbuild.dev)",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Rbm3267/postbuild.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://postbuild.dev",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"stripe": "^23.1.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { scanTargetUrl, searchBuyerIntentRadar, dispatchDirectories } from "./lib/engine.js";
|
|
6
|
+
import { createStripeCheckout, createCustomerPortalSession, verifyAndHandleStripeWebhook, configureStripeBrandingAndPortal, getCustomerBillingSummary, cancelCustomerSubscription } from "./lib/stripe.js";
|
|
7
|
+
import { authenticateUser, verifyLicenseToken } from "./lib/auth.js";
|
|
8
|
+
import { ingestInboundEmail, listInboxMessages, sendTestVerificationEmail } from "./lib/inbox.js";
|
|
9
|
+
import { autoVerifyInboxLinks, runStatefulMcpSimulation, executeMcpToolCall } from "./lib/mcp.js";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const PORT = Number(process.env.POSTBUILD_PORT) || 8096;
|
|
14
|
+
|
|
15
|
+
// Load local .env if present
|
|
16
|
+
const envPath = path.join(__dirname, ".env");
|
|
17
|
+
if (fs.existsSync(envPath)) {
|
|
18
|
+
const envContent = fs.readFileSync(envPath, "utf8");
|
|
19
|
+
for (const line of envContent.split(/\r?\n/)) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
22
|
+
const eqIdx = trimmed.indexOf("=");
|
|
23
|
+
if (eqIdx > 0) {
|
|
24
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
25
|
+
const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
26
|
+
if (!process.env[key]) process.env[key] = val;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MIME_TYPES = {
|
|
32
|
+
".html": "text/html; charset=utf-8",
|
|
33
|
+
".js": "application/javascript; charset=utf-8",
|
|
34
|
+
".css": "text/css; charset=utf-8",
|
|
35
|
+
".json": "application/json; charset=utf-8",
|
|
36
|
+
".xml": "application/xml; charset=utf-8",
|
|
37
|
+
".txt": "text/plain; charset=utf-8",
|
|
38
|
+
".svg": "image/svg+xml",
|
|
39
|
+
".png": "image/png"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function readRawBuffer(req) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
req.on("data", chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
46
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readJsonBody(req) {
|
|
51
|
+
const buf = await readRawBuffer(req);
|
|
52
|
+
try {
|
|
53
|
+
return buf.length ? JSON.parse(buf.toString("utf8")) : {};
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const server = http.createServer(async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const host = req.headers.host || `localhost:${PORT}`;
|
|
62
|
+
const parsed = new URL(req.url || "/", `http://${host}`);
|
|
63
|
+
|
|
64
|
+
if (parsed.pathname === "/api/scan") {
|
|
65
|
+
const target = parsed.searchParams.get("url") || "https://postbuild.dev";
|
|
66
|
+
const result = await scanTargetUrl(target);
|
|
67
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
68
|
+
res.end(JSON.stringify(result));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parsed.pathname === "/api/radar") {
|
|
73
|
+
const q = parsed.searchParams.get("q") || "seo marketing indie hacker";
|
|
74
|
+
const threads = await searchBuyerIntentRadar(q);
|
|
75
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
76
|
+
res.end(JSON.stringify({ query: q, threads }));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (parsed.pathname === "/api/dispatch") {
|
|
81
|
+
const body = req.method === "POST" ? await readJsonBody(req) : {};
|
|
82
|
+
const action = body.action || parsed.searchParams.get("action") || "";
|
|
83
|
+
const inbox = body.inbox || body.to || parsed.searchParams.get("inbox") || "postbuild-bot@postbuild.dev";
|
|
84
|
+
if (action === "inbound_email") {
|
|
85
|
+
const msg = await ingestInboundEmail(body);
|
|
86
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
87
|
+
res.end(JSON.stringify({ ok: true, message: msg }));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (action === "inbox_test") {
|
|
91
|
+
const domain = body.domain || parsed.searchParams.get("domain") || "postbuild.dev";
|
|
92
|
+
const msg = await sendTestVerificationEmail(inbox, domain);
|
|
93
|
+
const list = await listInboxMessages(inbox, { allowRemoteFallback: false });
|
|
94
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
95
|
+
res.end(JSON.stringify({ ok: true, latest: msg, ...list }));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (action === "inbox_autoverify") {
|
|
99
|
+
const current = await listInboxMessages(inbox, { allowRemoteFallback: false });
|
|
100
|
+
if (current.count === 0) {
|
|
101
|
+
const domain = body.domain || parsed.searchParams.get("domain") || "postbuild.dev";
|
|
102
|
+
await sendTestVerificationEmail(inbox, domain);
|
|
103
|
+
}
|
|
104
|
+
const verified = await autoVerifyInboxLinks(inbox);
|
|
105
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
106
|
+
res.end(JSON.stringify({ ok: true, ...verified }));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (action === "inbox_list") {
|
|
110
|
+
const list = await listInboxMessages(inbox, { allowRemoteFallback: false });
|
|
111
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
112
|
+
res.end(JSON.stringify({ ok: true, ...list }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (action === "mcp_simulate") {
|
|
116
|
+
const targetUrl = body.url || body.targetUrl || parsed.searchParams.get("url") || "https://postbuild.dev";
|
|
117
|
+
const sim = await runStatefulMcpSimulation({
|
|
118
|
+
url: targetUrl,
|
|
119
|
+
sessionId: body.sessionId || "local-studio-mcp"
|
|
120
|
+
});
|
|
121
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
122
|
+
res.end(JSON.stringify({ ok: true, ...sim }));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (action === "mcp_call") {
|
|
126
|
+
const targetUrl = body.url || body.targetUrl || parsed.searchParams.get("url") || "https://postbuild.dev";
|
|
127
|
+
const toolName = body.tool || parsed.searchParams.get("tool") || "postbuild_directories";
|
|
128
|
+
const toolArgs = body.args || { url: targetUrl };
|
|
129
|
+
const output = await executeMcpToolCall(toolName, toolArgs, body.sessionId || "local-studio-session");
|
|
130
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
131
|
+
res.end(JSON.stringify({ ok: true, tool: toolName, result: output }));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const targetUrl = body.url || body.targetUrl || parsed.searchParams.get("url") || "https://postbuild.dev";
|
|
135
|
+
const directoryIds = Array.isArray(body.directoryIds) ? body.directoryIds : [];
|
|
136
|
+
const result = await dispatchDirectories({ targetUrl, directoryIds });
|
|
137
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
138
|
+
res.end(JSON.stringify(result));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (parsed.pathname === "/api/checkout") {
|
|
143
|
+
const body = req.method === "POST" ? await readJsonBody(req) : {};
|
|
144
|
+
const planId = body.plan || parsed.searchParams.get("plan") || "launch_pass";
|
|
145
|
+
const targetDomain = body.domain || parsed.searchParams.get("domain") || "postbuild.dev";
|
|
146
|
+
const customerEmail = body.email || parsed.searchParams.get("email") || "";
|
|
147
|
+
const coupon = body.coupon || parsed.searchParams.get("coupon") || "";
|
|
148
|
+
const origin = `http://${host}`;
|
|
149
|
+
const result = await createStripeCheckout({ planId, targetDomain, customerEmail, coupon, origin });
|
|
150
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
151
|
+
res.end(JSON.stringify(result));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (parsed.pathname === "/api/portal") {
|
|
156
|
+
const body = req.method === "POST" ? await readJsonBody(req) : {};
|
|
157
|
+
const action = body.action || parsed.searchParams.get("action") || "";
|
|
158
|
+
const token = body.token || parsed.searchParams.get("token") || "";
|
|
159
|
+
const subscriptionId = body.subscriptionId || parsed.searchParams.get("subscriptionId") || "";
|
|
160
|
+
if (action === "setup_branding") {
|
|
161
|
+
const iconPath = path.join(__dirname, "public", "brand", "postbuild-icon.png");
|
|
162
|
+
const iconBuf = fs.existsSync(iconPath) ? fs.readFileSync(iconPath) : null;
|
|
163
|
+
const brandingResult = await configureStripeBrandingAndPortal(iconBuf);
|
|
164
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
165
|
+
res.end(JSON.stringify(brandingResult));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const verified = verifyLicenseToken(token);
|
|
170
|
+
if (!verified.valid || !verified.email) {
|
|
171
|
+
res.writeHead(401, { "Content-Type": "application/json; charset=utf-8" });
|
|
172
|
+
res.end(
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
ok: false,
|
|
175
|
+
error: "Unauthorized: A verified pb_live_ license token is required to view receipts or manage billing."
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (action === "billing_summary") {
|
|
182
|
+
const summary = await getCustomerBillingSummary({ customerEmail: verified.email });
|
|
183
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
184
|
+
res.end(JSON.stringify(summary));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (action === "cancel_subscription") {
|
|
188
|
+
const cancelled = await cancelCustomerSubscription({
|
|
189
|
+
subscriptionId,
|
|
190
|
+
customerEmail: verified.email
|
|
191
|
+
});
|
|
192
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
193
|
+
res.end(JSON.stringify(cancelled));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const returnUrl = `http://${host}/?tab=account`;
|
|
197
|
+
const result = await createCustomerPortalSession({ customerEmail: verified.email, returnUrl });
|
|
198
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
199
|
+
res.end(JSON.stringify(result));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (parsed.pathname === "/api/webhook" && req.method === "POST") {
|
|
204
|
+
const rawBody = await readRawBuffer(req);
|
|
205
|
+
const sig = req.headers["stripe-signature"] || "";
|
|
206
|
+
const result = await verifyAndHandleStripeWebhook(rawBody, sig);
|
|
207
|
+
res.writeHead(result.status || 200, { "Content-Type": "application/json; charset=utf-8" });
|
|
208
|
+
res.end(JSON.stringify(result));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (parsed.pathname === "/api/auth") {
|
|
213
|
+
const body = req.method === "POST" ? await readJsonBody(req) : {};
|
|
214
|
+
const email = body.email || parsed.searchParams.get("email") || "";
|
|
215
|
+
const token = body.token || parsed.searchParams.get("token") || "";
|
|
216
|
+
const sessionId = body.sessionId || parsed.searchParams.get("session_id") || "";
|
|
217
|
+
const otpCode = body.otpCode || parsed.searchParams.get("otpCode") || "";
|
|
218
|
+
const challengeToken = body.challengeToken || parsed.searchParams.get("challengeToken") || "";
|
|
219
|
+
const result = await authenticateUser({ email, token, sessionId, otpCode, challengeToken });
|
|
220
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
221
|
+
res.end(JSON.stringify(result));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let safePath = parsed.pathname === "/" ? "/index.html" : parsed.pathname;
|
|
226
|
+
let filePath = path.join(__dirname, "public", safePath);
|
|
227
|
+
if (!fs.existsSync(filePath) && fs.existsSync(`${filePath}.html`)) {
|
|
228
|
+
filePath = `${filePath}.html`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
232
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
233
|
+
res.writeHead(200, { "Content-Type": MIME_TYPES[ext] || "application/octet-stream" });
|
|
234
|
+
fs.createReadStream(filePath).pipe(res);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Fallback SPA route
|
|
239
|
+
const indexHtml = path.join(__dirname, "public", "index.html");
|
|
240
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
241
|
+
fs.createReadStream(indexHtml).pipe(res);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
244
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
server.listen(PORT, "0.0.0.0", () => {
|
|
249
|
+
console.log(`\n postbuild (postbuild.dev) live at:`);
|
|
250
|
+
console.log(` -> Local: http://localhost:${PORT}`);
|
|
251
|
+
console.log(` -> Tailscale: http://100.117.7.27:${PORT}\n`);
|
|
252
|
+
});
|
package/README.md
DELETED