git-paid 2.1.3 → 2.1.5

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.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * SSRF-Safe Outbound HTTP Fetcher for Postbuild (postbuild.dev)
3
+ *
4
+ * Enforces:
5
+ * 1. Protocol allowlist: http: and https: only
6
+ * 2. Pre-request DNS resolution and private/loopback/link-local/metadata IP blocking
7
+ * 3. Manual redirect following (max 3 hops) with full SSRF validation on every hop
8
+ * 4. Strict 5-second timeout and 2 MB response body cap
9
+ */
10
+
11
+ import dns from "node:dns";
12
+ import net from "node:net";
13
+
14
+ const MAX_REDIRECTS = 3;
15
+ const DEFAULT_TIMEOUT_MS = 5000;
16
+ const MAX_RESPONSE_BYTES = 2 * 1024 * 1024; // 2 MB
17
+
18
+ export function isPrivateOrReservedIp(ip) {
19
+ if (!ip || typeof ip !== "string") return true;
20
+ const trimmed = ip.trim().toLowerCase().replace(/^\[|\]$/g, "");
21
+
22
+ // Handle IPv4-mapped IPv6 addresses in dotted-quad (::ffff:127.0.0.1) or hex-quartet (::ffff:7f00:1) form
23
+ const v4MappedPrefix = /^(?:::ffff:|0:0:0:0:0:ffff:)/i;
24
+ if (v4MappedPrefix.test(trimmed)) {
25
+ const suffix = trimmed.replace(v4MappedPrefix, "");
26
+ if (net.isIPv4(suffix)) {
27
+ return isPrivateOrReservedIp(suffix);
28
+ }
29
+ const hexMatch = suffix.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
30
+ if (hexMatch) {
31
+ const hi = parseInt(hexMatch[1], 16);
32
+ const lo = parseInt(hexMatch[2], 16);
33
+ const dotted = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
34
+ return isPrivateOrReservedIp(dotted);
35
+ }
36
+ return true;
37
+ }
38
+
39
+ if (net.isIPv4(trimmed)) {
40
+ const parts = trimmed.split(".").map((n) => Number(n));
41
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
42
+ return true;
43
+ }
44
+ const [a, b, c] = parts;
45
+ if (a === 0) return true; // 0.0.0.0/8
46
+ if (a === 10) return true; // 10.0.0.0/8 (RFC1918)
47
+ if (a === 127) return true; // 127.0.0.0/8 (Loopback)
48
+ if (a === 169 && b === 254) return true; // 169.254.0.0/16 (Link-local / AWS & GCP metadata)
49
+ if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12 (RFC1918)
50
+ if (a === 192 && b === 168) return true; // 192.168.0.0/16 (RFC1918)
51
+ if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10 (Carrier-grade NAT / Tailscale)
52
+ if (a === 192 && b === 0 && (c === 0 || c === 2)) return true; // IETF Protocol Assignments & TEST-NET-1
53
+ if (a === 198 && (b === 18 || b === 19)) return true; // Benchmarking (198.18.0.0/15)
54
+ if (a === 198 && b === 51 && c === 100) return true; // TEST-NET-2
55
+ if (a === 203 && b === 0 && c === 113) return true; // TEST-NET-3
56
+ if (a >= 224) return true; // Multicast & Reserved (224.0.0.0/4+)
57
+ return false;
58
+ }
59
+
60
+ if (net.isIPv6(trimmed)) {
61
+ if (trimmed === "::" || trimmed === "::1" || trimmed === "0:0:0:0:0:0:0:1" || trimmed === "0:0:0:0:0:0:0:0") {
62
+ return true; // Unspecified or loopback
63
+ }
64
+ if (trimmed.startsWith("fc") || trimmed.startsWith("fd")) return true; // Unique local fc00::/7
65
+ if (
66
+ trimmed.startsWith("fe8") ||
67
+ trimmed.startsWith("fe9") ||
68
+ trimmed.startsWith("fea") ||
69
+ trimmed.startsWith("feb")
70
+ ) {
71
+ return true; // Link-local fe80::/10
72
+ }
73
+ if (trimmed.startsWith("ff")) return true; // Multicast ff00::/8
74
+ if (trimmed.startsWith("64:ff9b:")) return true; // NAT64 well-known prefix 64:ff9b::/96
75
+ if (trimmed.startsWith("2001:db8:")) return true; // Documentation prefix 2001:db8::/32
76
+ if (trimmed.startsWith("2001:0:") || trimmed.startsWith("2001::")) return true; // Teredo 2001::/32
77
+ if (trimmed.startsWith("2002:")) return true; // 6to4 2002::/16
78
+ return false;
79
+ }
80
+
81
+ return true;
82
+ }
83
+
84
+ function ssrfError(message) {
85
+ const err = new Error(message);
86
+ err.statusCode = 422;
87
+ return err;
88
+ }
89
+
90
+ export async function assertSafeUrl(rawUrl, options = {}) {
91
+ let parsed;
92
+ try {
93
+ parsed = rawUrl instanceof URL ? rawUrl : new URL(String(rawUrl));
94
+ } catch {
95
+ throw ssrfError("Invalid URL format");
96
+ }
97
+
98
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
99
+ throw ssrfError(`Disallowed protocol: ${parsed.protocol}`);
100
+ }
101
+
102
+ if (parsed.username || parsed.password) {
103
+ throw ssrfError("URLs with embedded credentials are not allowed");
104
+ }
105
+
106
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
107
+ if (
108
+ !hostname ||
109
+ hostname === "localhost" ||
110
+ hostname.endsWith(".localhost") ||
111
+ hostname.endsWith(".local") ||
112
+ hostname.endsWith(".internal") ||
113
+ hostname === "metadata.google.internal"
114
+ ) {
115
+ throw ssrfError(`Blocked internal hostname: ${hostname}`);
116
+ }
117
+
118
+ if (net.isIP(hostname)) {
119
+ if (isPrivateOrReservedIp(hostname)) {
120
+ throw ssrfError(`Blocked private or reserved IP address: ${hostname}`);
121
+ }
122
+ return parsed;
123
+ }
124
+
125
+ const lookupFn = options.lookupFn || ((host) => dns.promises.lookup(host, { all: true }));
126
+ let records;
127
+ try {
128
+ records = await lookupFn(hostname);
129
+ } catch {
130
+ throw ssrfError(`Unable to resolve hostname: ${hostname}`);
131
+ }
132
+
133
+ const list = Array.isArray(records) ? records : [records];
134
+ if (list.length === 0) {
135
+ throw ssrfError(`No DNS records resolved for hostname: ${hostname}`);
136
+ }
137
+
138
+ for (const rec of list) {
139
+ const addr = typeof rec === "string" ? rec : rec?.address;
140
+ if (!addr || isPrivateOrReservedIp(addr)) {
141
+ throw ssrfError(`Blocked hostname resolving to private/reserved IP (${addr || "unknown"})`);
142
+ }
143
+ }
144
+
145
+ return parsed;
146
+ }
147
+
148
+ async function readResponseWithCap(response, maxBytes = MAX_RESPONSE_BYTES) {
149
+ if (!response.body) {
150
+ return Buffer.alloc(0);
151
+ }
152
+ const reader = response.body.getReader();
153
+ const chunks = [];
154
+ let total = 0;
155
+
156
+ while (true) {
157
+ const { done, value } = await reader.read();
158
+ if (done) break;
159
+ const buf = Buffer.from(value);
160
+ total += buf.length;
161
+ if (total > maxBytes) {
162
+ await reader.cancel();
163
+ throw new Error(`Response exceeded maximum size limit of ${maxBytes} bytes`);
164
+ }
165
+ chunks.push(buf);
166
+ }
167
+
168
+ return Buffer.concat(chunks);
169
+ }
170
+
171
+ export async function safeFetch(rawUrl, options = {}) {
172
+ const {
173
+ method = "GET",
174
+ headers = {},
175
+ body,
176
+ timeoutMs = DEFAULT_TIMEOUT_MS,
177
+ maxRedirects = MAX_REDIRECTS,
178
+ maxBytes = MAX_RESPONSE_BYTES,
179
+ allowedDomains = null,
180
+ lookupFn
181
+ } = options;
182
+
183
+ let currentUrl = String(rawUrl);
184
+
185
+ for (let hop = 0; hop <= maxRedirects; hop++) {
186
+ const validatedUrl = await assertSafeUrl(currentUrl, { lookupFn });
187
+
188
+ if (Array.isArray(allowedDomains) && allowedDomains.length > 0) {
189
+ const host = validatedUrl.hostname.replace(/^www\./i, "").toLowerCase();
190
+ const allowed = allowedDomains.some((d) => {
191
+ const cleanD = String(d).replace(/^www\./i, "").toLowerCase();
192
+ return host === cleanD || host.endsWith(`.${cleanD}`);
193
+ });
194
+ if (!allowed) {
195
+ throw new Error(`Domain ${host} is not in the allowed outbound verification list`);
196
+ }
197
+ }
198
+
199
+ const controller = new AbortController();
200
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
201
+
202
+ try {
203
+ const resp = await fetch(validatedUrl.toString(), {
204
+ method: hop === 0 ? method : "GET",
205
+ headers,
206
+ body: hop === 0 ? body : undefined,
207
+ redirect: "manual",
208
+ signal: controller.signal
209
+ });
210
+
211
+ if ([301, 302, 303, 307, 308].includes(resp.status)) {
212
+ const location = resp.headers.get("location");
213
+ if (!location) {
214
+ throw new Error(`Redirect response (${resp.status}) missing Location header`);
215
+ }
216
+ if (hop === maxRedirects) {
217
+ throw new Error(`Maximum redirect limit (${maxRedirects}) exceeded`);
218
+ }
219
+ currentUrl = new URL(location, validatedUrl).toString();
220
+ continue;
221
+ }
222
+
223
+ const bodyBuffer = await readResponseWithCap(resp, maxBytes);
224
+ return {
225
+ ok: resp.ok,
226
+ status: resp.status,
227
+ url: validatedUrl.toString(),
228
+ headers: resp.headers,
229
+ bodyBuffer,
230
+ text: () => bodyBuffer.toString("utf8"),
231
+ json: () => JSON.parse(bodyBuffer.toString("utf8"))
232
+ };
233
+ } finally {
234
+ clearTimeout(timer);
235
+ }
236
+ }
237
+
238
+ throw new Error("Redirect loop terminated");
239
+ }
@@ -12,9 +12,21 @@
12
12
 
13
13
  import crypto from "node:crypto";
14
14
 
15
- const DEFAULT_SECRET =
16
- process.env.POSTBUILD_SECRET ||
17
- "postbuild_mcp_stateful_occ_hmac_secret";
15
+ const EPHEMERAL_MCP_SECRET = crypto.randomBytes(32).toString("hex");
16
+
17
+ export function requireMcpSecret() {
18
+ const value = (process.env.POSTBUILD_SECRET || process.env.POSTBUILD_AUTH_SECRET || "").trim();
19
+ if (value.length < 32) {
20
+ throw new Error("POSTBUILD_SECRET or POSTBUILD_AUTH_SECRET is missing or too short (minimum 32 characters required)");
21
+ }
22
+ return value;
23
+ }
24
+
25
+ function getMcpSecret() {
26
+ const configured = (process.env.POSTBUILD_SECRET || process.env.POSTBUILD_AUTH_SECRET || "").trim();
27
+ if (configured.length >= 32) return configured;
28
+ return EPHEMERAL_MCP_SECRET;
29
+ }
18
30
 
19
31
  export function spotlightUntrustedText(value) {
20
32
  if (value === null || value === undefined) return value;
@@ -369,7 +381,7 @@ export function withStatefulMcp(options = {}) {
369
381
  const nonce = crypto.randomBytes(6).toString("hex");
370
382
  const rawPayload = `${sessionId}|${principalId}|${entityId}|${liveWatermark}|${changeHash}|${nonce}`;
371
383
  const sig = crypto
372
- .createHmac("sha256", DEFAULT_SECRET)
384
+ .createHmac("sha256", getMcpSecret())
373
385
  .update(rawPayload)
374
386
  .digest("hex")
375
387
  .slice(0, 20);
package/lib/submitter.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Local Headless Directory Form Submitter & Auto-Verification Pipeline
3
- * (`git paid submit <url>` / `postbuild_submit` MCP tool)
2
+ * Local Headless Directory Form Inspector & Launch Submitter
3
+ * (`npx git-paid submit <url>`)
4
4
  */
5
5
 
6
6
  import fs from "node:fs";
@@ -9,6 +9,7 @@ import { execFile } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
10
  import { scanTargetUrl, dispatchDirectories, DIRECTORIES } from "./engine.js";
11
11
  import { autoVerifyInboxLinks } from "./mcp.js";
12
+ import { assertSafeUrl, safeFetch } from "./safe-fetch.js";
12
13
 
13
14
  const execFileAsync = promisify(execFile);
14
15
 
@@ -34,9 +35,11 @@ export function detectHeadlessBrowser() {
34
35
 
35
36
  /**
36
37
  * 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
+ * or SSRF-safe HTTP fetch to detect form inputs (`url`, `name`, `email`, `description`) and prefill.
38
39
  */
39
- async function inspectAndSubmitDirectoryForm(dir, launchKit, browserPath, useHeadless = true) {
40
+ export async function inspectAndSubmitDirectoryForm(dir, launchKit, browserPath, useHeadless = true, options = {}) {
41
+ await assertSafeUrl(dir.url, { lookupFn: options.lookupFn });
42
+
40
43
  const payload = {
41
44
  product_name: launchKit.productName,
42
45
  url: launchKit.utmUrl || launchKit.targetUrl,
@@ -56,7 +59,6 @@ async function inspectAndSubmitDirectoryForm(dir, launchKit, browserPath, useHea
56
59
  [
57
60
  "--headless=new",
58
61
  "--disable-gpu",
59
- "--no-sandbox",
60
62
  "--virtual-time-budget=2500",
61
63
  "--dump-dom",
62
64
  dir.url
@@ -72,23 +74,20 @@ async function inspectAndSubmitDirectoryForm(dir, launchKit, browserPath, useHea
72
74
 
73
75
  if (!html) {
74
76
  try {
75
- const controller = new AbortController();
76
- const timer = setTimeout(() => controller.abort(), 3500);
77
- const resp = await fetch(dir.url, {
77
+ const resp = await safeFetch(dir.url, {
78
+ timeoutMs: 3500,
78
79
  headers: {
79
80
  "User-Agent":
80
81
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36"
81
82
  },
82
- signal: controller.signal
83
+ lookupFn: options.lookupFn
83
84
  });
84
- clearTimeout(timer);
85
85
  if (resp.ok) {
86
- html = await resp.text();
86
+ html = resp.text();
87
87
  }
88
88
  } catch {}
89
89
  }
90
90
 
91
- // Parse detected <input>, <textarea>, and <form> elements from the live DOM
92
91
  const inputNames = [];
93
92
  const inputRegex = /<(?:input|textarea|select)\b[^>]*?(?:name|id|placeholder)=["']([^"']+)["'][^>]*>/gi;
94
93
  let m;
@@ -100,7 +99,54 @@ async function inspectAndSubmitDirectoryForm(dir, launchKit, browserPath, useHea
100
99
  }
101
100
 
102
101
  const formActionMatch = html.match(/<form\b[^>]*?action=["']([^"']+)["']/i);
103
- const detectedAction = formActionMatch ? formActionMatch[1] : dir.url;
102
+ let detectedAction = formActionMatch ? formActionMatch[1] : dir.url;
103
+ if (detectedAction && detectedAction.startsWith("/")) {
104
+ try {
105
+ detectedAction = new URL(detectedAction, dir.url).toString();
106
+ } catch {
107
+ detectedAction = dir.url;
108
+ }
109
+ }
110
+
111
+ const hasCaptcha =
112
+ /cf-turnstile|g-recaptcha|hcaptcha/i.test(html) ||
113
+ inputNames.some((n) => /turnstile|captcha/i.test(n));
114
+ let httpSubmitStatus = null;
115
+ let submissionMode = hasCaptcha ? "prefilled_launch_url (captcha_gated)" : "prefilled_launch_url";
116
+
117
+ if (!hasCaptcha && formActionMatch && /^https?:\/\//i.test(detectedAction) && !process.env.VERCEL && options.allowFormPost !== false) {
118
+ try {
119
+ const formBody = new URLSearchParams();
120
+ for (const f of inputNames) {
121
+ const lower = f.toLowerCase();
122
+ if (lower.includes("email")) formBody.set(f, payload.email);
123
+ else if (lower.includes("url") || lower.includes("link") || lower.includes("website")) formBody.set(f, payload.url);
124
+ else if (lower.includes("tag") || lower.includes("headline") || lower.includes("short")) formBody.set(f, payload.tagline);
125
+ else if (lower.includes("desc") || lower.includes("about") || lower.includes("pitch")) formBody.set(f, payload.description);
126
+ else if (lower.includes("name") || lower.includes("title")) formBody.set(f, payload.product_name);
127
+ }
128
+ for (const [k, v] of Object.entries(payload)) {
129
+ if (!formBody.has(k)) formBody.set(k, v);
130
+ }
131
+
132
+ const postResp = await safeFetch(detectedAction, {
133
+ method: "POST",
134
+ headers: {
135
+ "Content-Type": "application/x-www-form-urlencoded",
136
+ "User-Agent": "Postbuild-Form-Inspector/2.1 (+https://www.postbuild.dev)"
137
+ },
138
+ body: formBody.toString(),
139
+ maxRedirects: 0,
140
+ timeoutMs: 4000,
141
+ lookupFn: options.lookupFn
142
+ });
143
+ httpSubmitStatus = postResp.status;
144
+ submissionMode = `form_post_attempted (HTTP ${postResp.status})`;
145
+ } catch {}
146
+ }
147
+
148
+ // Only count 2xx responses as submitted; 3xx redirects (e.g. to login pages) require manual confirmation
149
+ const postSucceeded = Boolean(httpSubmitStatus && httpSubmitStatus >= 200 && httpSubmitStatus < 300);
104
150
 
105
151
  return {
106
152
  id: dir.id,
@@ -112,34 +158,44 @@ async function inspectAndSubmitDirectoryForm(dir, launchKit, browserPath, useHea
112
158
  prefilledUrl: dir.prefilledUrl,
113
159
  engineUsed,
114
160
  detectedFormAction: detectedAction,
161
+ hasCaptcha,
162
+ httpSubmitStatus,
163
+ submissionMode,
115
164
  mappedFields: inputNames.length
116
165
  ? inputNames
117
166
  : ["name", "url", "tagline", "description", "maker_email"],
118
167
  submittedPayload: payload,
119
- status: "dispatched_and_queued_for_otp",
168
+ status: hasCaptcha
169
+ ? "prefilled_ready_captcha_protected"
170
+ : postSucceeded
171
+ ? "submitted_awaiting_verification"
172
+ : "prefilled_ready_manual_confirm",
120
173
  timestamp: new Date().toISOString()
121
174
  };
122
175
  }
123
176
 
124
177
  /**
125
- * Runs the end-to-end Headless Directory Submitter + Inbox Auto-Verification pipeline.
178
+ * Runs the Headless Directory Form Inspector + optional Inbox Verification pipeline.
126
179
  */
127
180
  export async function runHeadlessDirectorySubmit({
128
- targetUrl = "https://postbuild.dev",
181
+ targetUrl = "https://www.postbuild.dev",
129
182
  limit = 6,
130
183
  useHeadless = true,
131
- cwd = process.cwd()
184
+ autoVerify = false,
185
+ cwd = process.cwd(),
186
+ lookupFn
132
187
  } = {}) {
133
- const report = await scanTargetUrl(targetUrl);
188
+ const report = await scanTargetUrl(targetUrl, { lookupFn });
134
189
  const browserPath = detectHeadlessBrowser();
135
190
 
136
- // 1. Fire IndexNow, Sitemap & WebSub API dispatches across all 85 directories
191
+ // 1. Fire IndexNow & WebSub dispatches across all 85 directories
137
192
  const dispatchSummary = await dispatchDirectories({
138
193
  targetUrl: report.targetUrl,
139
- directoryIds: []
194
+ directoryIds: [],
195
+ lookupFn
140
196
  });
141
197
 
142
- // 2. Run live Headless DOM Form Inspection & Prefill on the top direct-form directories
198
+ // 2. Run Headless DOM Form Inspection & Prefill on the top direct-form directories
143
199
  const directFormDirs = (report.directories || DIRECTORIES)
144
200
  .filter((d) => d.autoSubmit)
145
201
  .slice(0, Math.max(1, Math.min(Number(limit) || 6, 15)));
@@ -150,13 +206,17 @@ export async function runHeadlessDirectorySubmit({
150
206
  dir,
151
207
  report.launchKit,
152
208
  browserPath,
153
- useHeadless
209
+ useHeadless,
210
+ { lookupFn }
154
211
  );
155
212
  formReceipts.push(receipt);
156
213
  }
157
214
 
158
- // 3. Poll <slug>-bot@postbuild.dev and auto-verify any confirmation links / extract OTPs
159
- const inboxVerification = await autoVerifyInboxLinks(report.botEmail);
215
+ // 3. Check dedicated @postbuild.dev inbox (auto-verify is opt-in only)
216
+ const inboxVerification = await autoVerifyInboxLinks(report.botEmail, {
217
+ autoVerify: Boolean(autoVerify),
218
+ lookupFn
219
+ });
160
220
 
161
221
  const result = {
162
222
  ok: true,
@@ -164,7 +224,7 @@ export async function runHeadlessDirectorySubmit({
164
224
  targetUrl: report.targetUrl,
165
225
  botEmail: report.botEmail,
166
226
  headlessBrowser: browserPath ? path.basename(browserPath) : "http-fallback",
167
- totalDirectoriesDispatched: DIRECTORIES.length,
227
+ totalDirectoriesPrepared: DIRECTORIES.length,
168
228
  headlessInspectedCount: formReceipts.length,
169
229
  dispatchSummary: dispatchSummary.summary,
170
230
  formReceipts,
@@ -177,10 +237,11 @@ export async function runHeadlessDirectorySubmit({
177
237
  completedAt: new Date().toISOString()
178
238
  };
179
239
 
180
- // Save local receipt log if running inside a project workspace
240
+ // P1-7: Always save local receipt log to .postbuild/ (never public/)
181
241
  try {
182
- const outDir = fs.existsSync(path.join(cwd, "public")) ? path.join(cwd, "public") : cwd;
183
- const receiptPath = path.join(outDir, "postbuild-submissions.json");
242
+ const pbDir = path.join(cwd, ".postbuild");
243
+ fs.mkdirSync(pbDir, { recursive: true });
244
+ const receiptPath = path.join(pbDir, "postbuild-submissions.json");
184
245
  fs.writeFileSync(receiptPath, JSON.stringify(result, null, 2), "utf8");
185
246
  result.receiptFile = receiptPath;
186
247
  } catch {}
package/package.json CHANGED
@@ -1,44 +1,53 @@
1
- {
2
- "name": "git-paid",
3
- "version": "2.1.3",
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": "bin/git-paid.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
- "LICENSE"
17
- ],
18
- "scripts": {
19
- "start": "node server.js",
20
- "dev": "node server.js",
21
- "sola:build": "node scripts/build-sola.mjs",
22
- "cli": "node ./bin/git-paid.js"
23
- },
24
- "keywords": [
25
- "postbuild",
26
- "git-paid",
27
- "mcp",
28
- "seo",
29
- "programmatic-seo",
30
- "stripe",
31
- "monetization",
32
- "directory-submission"
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": "Apache-2.0",
41
- "dependencies": {
42
- "stripe": "^23.1.0"
43
- }
44
- }
1
+ {
2
+ "name": "git-paid",
3
+ "version": "2.1.5",
4
+ "description": "git push ships your code. git paid sells it. Autonomous SEO, Paywall, MCP & Directory Launch Agent (www.postbuild.dev).",
5
+ "type": "module",
6
+ "main": "bin/git-paid.js",
7
+ "bin": {
8
+ "git-paid": "bin/git-paid.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/auth.js",
13
+ "lib/directories.js",
14
+ "lib/engine.js",
15
+ "lib/fixer.js",
16
+ "lib/inbox.js",
17
+ "lib/mcp.js",
18
+ "lib/pseo.js",
19
+ "lib/safe-fetch.js",
20
+ "lib/stateful-mcp.js",
21
+ "lib/submitter.js",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "start": "node server.js",
27
+ "dev": "node server.js",
28
+ "sola:build": "node scripts/build-sola.mjs",
29
+ "cli": "node ./bin/git-paid.js",
30
+ "test": "node --test test/security.test.js test/smoke.test.js",
31
+ "smoke": "node scripts/smoke.mjs"
32
+ },
33
+ "keywords": [
34
+ "postbuild",
35
+ "git-paid",
36
+ "mcp",
37
+ "seo",
38
+ "programmatic-seo",
39
+ "stripe",
40
+ "monetization",
41
+ "directory-submission"
42
+ ],
43
+ "author": "Bennett Moore (https://www.postbuild.dev)",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/Rbm3267/postbuild.git"
47
+ },
48
+ "homepage": "https://www.postbuild.dev",
49
+ "license": "Apache-2.0",
50
+ "dependencies": {
51
+ "stripe": "^23.1.0"
52
+ }
53
+ }
package/brand/README.md DELETED
@@ -1,54 +0,0 @@
1
- # `postbuild` (`git paid`) — Official Brand Kit
2
-
3
- > **`git push` ships your code. `git paid` sells it.**
4
-
5
- ## 1. Brand Assets
6
-
7
- All assets are available in both `/brand/` (repository root) and `/public/brand/` (served live at `https://postbuild.dev/brand/...`):
8
-
9
- | Asset | File | Dimensions | Format | Usage |
10
- | :--- | :--- | :--- | :--- | :--- |
11
- | **Brand Icon (PNG)** | [`postbuild-icon.png`](./postbuild-icon.png) | `512 × 512 px` | PNG | Stripe Icon, App Store / Directory listings, OAuth apps |
12
- | **Horizontal Logo (PNG)** | [`postbuild-logo.png`](./postbuild-logo.png) | `1000 × 260 px` | PNG | Stripe Checkout Header Logo, Invoices, Receipts, Press Kits |
13
- | **Brand Icon (SVG)** | [`postbuild-icon.svg`](./postbuild-icon.svg) | Vector (`64 × 64`) | SVG | Web UI, Favicons, Scalable vector contexts |
14
- | **Horizontal Logo (SVG)** | [`postbuild-logo.svg`](./postbuild-logo.svg) | Vector (`1000 × 260`) | SVG | High-DPI headers, documentation, print |
15
-
16
- ---
17
-
18
- ## 2. Visual Symbolism (`2 × 2` Grid)
19
-
20
- The `postbuild` mark represents the two-stage developer lifecycle inside a clean `2 × 2` architectural grid:
21
- 1. **Top Row (`git push` — Ship the build)**: Deep Slate build module (`#0F172A`) on the left pushing outward into the Sky Blue deployment arrow (`#0284C7`) on the right.
22
- 2. **Bottom Row (`git paid` — Monetize & distribute)**: Muted Slate post-build artifact (`#64748B`) on the left converting into the Emerald Green revenue badge (`$`, `#10B981`) on the right.
23
-
24
- ---
25
-
26
- ## 3. Color Palette
27
-
28
- | Role | Color Name | Hex Code | RGB | Usage |
29
- | :--- | :--- | :--- | :--- | :--- |
30
- | **Primary / Brand** | Deep Slate | `#0F172A` | `rgb(15, 23, 42)` | Wordmark, primary CTAs, Stripe **Brand Color** |
31
- | **Accent (`git paid`)** | Emerald Green | `#10B981` | `rgb(16, 185, 129)` | `$` revenue badge, `git paid` pill, Stripe **Accent Color** |
32
- | **Secondary (`git push`)** | Sky Blue | `#0284C7` | `rgb(2, 132, 199)` | Deployment arrow (`→`), technical indicators |
33
- | **Structural Muted** | Slate Gray | `#64748B` | `rgb(100, 116, 139)` | Secondary build block, supporting labels |
34
- | **Surface / Canvas** | Crisp Alabaster | `#F8FAFC` | `rgb(248, 250, 252)` | App background, card surfaces |
35
- | **Pure White** | White | `#FFFFFF` | `rgb(255, 255, 255)` | Icon & Logo container surface |
36
-
37
- ---
38
-
39
- ## 4. Typography
40
-
41
- - **Wordmark (`postbuild`)**: Always lowercase (`postbuild`), set in **Plus Jakarta Sans / Inter ExtraBold (`800`)** with `-0.045em` letter-spacing.
42
- - **CLI Companion (`git paid`)**: Always lowercase (`git paid`), set in **JetBrains Mono Bold (`700`)** inside an emerald tint pill (`#ECFDF5` fill, `#A7F3D0` border, `#047857` text).
43
-
44
- ---
45
-
46
- ## 5. Official Stripe & Directory Copy
47
-
48
- - **Business Name**: `postbuild`
49
- - **Website**: `https://postbuild.dev`
50
- - **Statement Descriptor**: `POSTBUILD.DEV` (Shortened: `POSTBUILD`)
51
- - **One-Line Tagline**:
52
- > Automated post-build technical SEO, programmatic metadata, and directory launch kits for software developers.
53
- - **Full Business Description**:
54
- > `postbuild` (`https://postbuild.dev`) is a developer SaaS platform and CLI tool (`git paid` / `npx postbuild`) that automates post-build technical SEO audits, structured metadata (JSON-LD, OpenGraph, robots.txt, sitemaps), programmatic SEO route packs, and curated SaaS directory launch kits for software developers and product teams.
Binary file
@@ -1,14 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 64 64" fill="none">
2
- <rect width="64" height="64" rx="14" fill="#FFFFFF"/>
3
-
4
- <!-- Top-left build block (git push source) -->
5
- <rect x="7" y="7" width="20" height="20" rx="5.5" stroke="#0F172A" stroke-width="5" fill="#F8FAFC"/>
6
- <!-- Bottom-left build block (git paid source) -->
7
- <rect x="7" y="37" width="20" height="20" rx="5.5" stroke="#64748B" stroke-width="5" fill="#F8FAFC"/>
8
- <!-- Top-right arrow (git push) -->
9
- <path d="M36 17H56M56 17L47.5 8.5M56 17L47.5 25.5" stroke="#0284C7" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
10
- <!-- Bottom-right emerald badge + crisp typographic $ (git paid) -->
11
- <rect x="35" y="35" width="22" height="22" rx="6.5" fill="#10B981"/>
12
- <text x="46" y="52.5" text-anchor="middle" fill="#FFFFFF" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif" font-size="18.5" font-weight="800">$</text>
13
-
14
- </svg>
Binary file