@drantoniou/uploadcheck 0.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/index.mjs +89 -0
- package/launch-evidence.mjs +42 -0
- package/package.json +28 -0
- package/request-builder.mjs +553 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { buildCostBasisRequest, buildEstimateRequest, buildJobRequest, buildLaunchDoctorRequest, buildLaunchEvidenceRequest, buildLaunchHandoffRequest, buildLaunchStatusRequest, buildNpoPipelineHandoffRequest, buildPipelineHandoffRequest, buildPipelineRecipesRequest, buildRemoteLaunchEvidence, buildUsageRequest, formatCostBasisSummary, formatJobSummary, formatLaunchDoctorSummary, formatLaunchEvidenceSummary, formatLaunchHandoffSummary, formatLaunchStatusSummary, formatNpoPipelineHandoffSummary, formatPipelineHandoffSummary, formatPipelineRecipesSummary, formatUsageSummary, parseArgs } from "./request-builder.mjs";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const { command, target, options } = parseArgs(process.argv.slice(2));
|
|
7
|
+
const apiKey = options.apiKey || process.env.UPLOADCHECK_API_KEY || process.env.QCGENIE_API_KEY;
|
|
8
|
+
|
|
9
|
+
const request = command === "estimate"
|
|
10
|
+
? buildEstimateRequest(options)
|
|
11
|
+
: (command === "usage" ? buildUsageRequest(options) : (command === "launch-status" ? buildLaunchStatusRequest(options) : (command === "launch-handoff" ? buildLaunchHandoffRequest(options) : (command === "launch-doctor" ? buildLaunchDoctorRequest(options) : (command === "launch-evidence" ? buildLaunchEvidenceRequest(options) : (command === "pipeline-handoff" ? buildPipelineHandoffRequest(options) : (command === "npo-pipeline-handoff" ? buildNpoPipelineHandoffRequest(options) : (command === "recipes" ? buildPipelineRecipesRequest(options) : (command === "cost-basis" ? buildCostBasisRequest(options) : buildJobRequest(target, options))))))))));
|
|
12
|
+
if (!request.public && !apiKey) throw new Error("Set UPLOADCHECK_API_KEY or pass --api-key.");
|
|
13
|
+
const rawPayload = request.kind === "signed_upload"
|
|
14
|
+
? await runSignedUploadJob(request, apiKey)
|
|
15
|
+
: (request.method === "GET" ? await getJson(request.apiBaseUrl, request.path, apiKey) : await postJson(request.apiBaseUrl, request.path, request.payload, apiKey));
|
|
16
|
+
const payload = request.kind === "launch_evidence" && rawPayload.name !== "UploadCheck.app Remote Launch Evidence"
|
|
17
|
+
? buildRemoteLaunchEvidence(rawPayload, { source: `${request.apiBaseUrl}${request.path}` })
|
|
18
|
+
: rawPayload;
|
|
19
|
+
|
|
20
|
+
console.log(options.json ? JSON.stringify(payload, null, 2) : formatSummary(request.kind, payload));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(error.message);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatSummary(kind, payload) {
|
|
27
|
+
if (kind === "usage") return formatUsageSummary(payload);
|
|
28
|
+
if (kind === "launch_status") return formatLaunchStatusSummary(payload);
|
|
29
|
+
if (kind === "launch_handoff") return formatLaunchHandoffSummary(payload);
|
|
30
|
+
if (kind === "launch_doctor") return formatLaunchDoctorSummary(payload);
|
|
31
|
+
if (kind === "launch_evidence") return formatLaunchEvidenceSummary(payload);
|
|
32
|
+
if (kind === "pipeline_handoff") return formatPipelineHandoffSummary(payload);
|
|
33
|
+
if (kind === "npo_pipeline_handoff") return formatNpoPipelineHandoffSummary(payload);
|
|
34
|
+
if (kind === "pipeline_recipes") return formatPipelineRecipesSummary(payload);
|
|
35
|
+
if (kind === "cost_basis") return formatCostBasisSummary(payload);
|
|
36
|
+
return formatJobSummary(payload);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runSignedUploadJob(request, apiKey) {
|
|
40
|
+
const upload = await postJson(request.apiBaseUrl, request.createUpload.path, request.createUpload.payload, apiKey);
|
|
41
|
+
const putResponse = await fetch(upload.signedPutUrl, {
|
|
42
|
+
method: "PUT",
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": request.contentType,
|
|
45
|
+
"content-length": String(request.sizeBytes)
|
|
46
|
+
},
|
|
47
|
+
body: createReadStream(request.filePath),
|
|
48
|
+
duplex: "half"
|
|
49
|
+
});
|
|
50
|
+
if (!putResponse.ok) {
|
|
51
|
+
const text = await putResponse.text();
|
|
52
|
+
throw new Error(`UploadCheck upload ${putResponse.status}: ${text}`);
|
|
53
|
+
}
|
|
54
|
+
const jobPayload = {
|
|
55
|
+
...request.createJob.payload,
|
|
56
|
+
upload_id: upload.uploadId
|
|
57
|
+
};
|
|
58
|
+
return postJson(request.apiBaseUrl, request.createJob.path, jobPayload, apiKey);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function postJson(apiBaseUrl, path, payload, apiKey) {
|
|
62
|
+
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": "application/json",
|
|
66
|
+
authorization: `Bearer ${apiKey}`
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(payload)
|
|
69
|
+
});
|
|
70
|
+
const body = await response.json();
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`UploadCheck API ${response.status}: ${JSON.stringify(body)}`);
|
|
73
|
+
}
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getJson(apiBaseUrl, path, apiKey) {
|
|
78
|
+
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
79
|
+
method: "GET",
|
|
80
|
+
headers: apiKey ? {
|
|
81
|
+
authorization: `Bearer ${apiKey}`
|
|
82
|
+
} : undefined
|
|
83
|
+
});
|
|
84
|
+
const body = await response.json();
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`UploadCheck API ${response.status}: ${JSON.stringify(body)}`);
|
|
87
|
+
}
|
|
88
|
+
return body;
|
|
89
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const LAUNCH_PROOF_CONTRACT_VERSION = "2026-06-06.render-web-proof";
|
|
2
|
+
|
|
3
|
+
export function buildRemoteLaunchEvidence(payload = {}, options = {}) {
|
|
4
|
+
const blockers = (payload.remainingBlockers || []).map((blocker) => blocker.id).filter(Boolean);
|
|
5
|
+
const commands = Array.isArray(payload.launchDoctorCommands) ? payload.launchDoctorCommands : [];
|
|
6
|
+
const phases = Array.isArray(payload.blockerFixPlan?.phases) ? payload.blockerFixPlan.phases : [];
|
|
7
|
+
return {
|
|
8
|
+
name: "UploadCheck.app Remote Launch Evidence",
|
|
9
|
+
contractVersion: LAUNCH_PROOF_CONTRACT_VERSION,
|
|
10
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
11
|
+
source: options.source || "https://api.uploadcheck.app/v1/launch-doctor",
|
|
12
|
+
productHuntReady: Boolean(payload.productHuntReady),
|
|
13
|
+
status: payload.productHuntReady && blockers.length === 0 ? "ready" : "blocked",
|
|
14
|
+
blockers,
|
|
15
|
+
redaction: {
|
|
16
|
+
rawStdoutIncluded: false,
|
|
17
|
+
rawStderrIncluded: false,
|
|
18
|
+
protectedValues: ["API bearer tokens", "checkout URL paths", "Lemon Squeezy variant ids", "local temp paths"]
|
|
19
|
+
},
|
|
20
|
+
commandCoverage: commands.map((command) => redactLaunchText(command)),
|
|
21
|
+
fixPhases: phases.map((phase) => ({
|
|
22
|
+
id: phase.id,
|
|
23
|
+
title: phase.title,
|
|
24
|
+
blockers: phase.blockers || [],
|
|
25
|
+
proofCommands: (phase.proof_commands || []).map((command) => redactLaunchText(command))
|
|
26
|
+
})),
|
|
27
|
+
completionRule: payload.blockerFixPlan?.completionRule || payload.rule || "Only launch when Product Hunt readiness is true and no blockers remain."
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function redactLaunchText(value = "") {
|
|
32
|
+
return String(value)
|
|
33
|
+
.replace(/UPLOADCHECK_API_KEY=([^\s"']+)/g, "UPLOADCHECK_API_KEY=<private_bearer>")
|
|
34
|
+
.replace(/(authorization:\s*bearer\s+)[^\s"']+/gi, "$1<private_bearer>")
|
|
35
|
+
.replace(/(https:\/\/[^/\s"']+\/checkout\/buy\/)[A-Za-z0-9_-]+/g, "$1<variant_id>")
|
|
36
|
+
.replace(/(https:\/\/[^/\s"']+)<checkout_path>/g, "$1<checkout_path>")
|
|
37
|
+
.replace(/(https:\/\/[^/\s"']+)\/[^\s"']*(checkout|creator|studio|network)[^\s"']*/gi, (match, origin) => {
|
|
38
|
+
if (match.includes("/checkout/buy/<variant_id>")) return match;
|
|
39
|
+
return `${origin}<checkout_path>`;
|
|
40
|
+
})
|
|
41
|
+
.replace(/\/tmp\/uploadcheck[-/][^\s"']+/g, "/tmp/uploadcheck/<redacted>");
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drantoniou/uploadcheck",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Quality check videos, podcasts, and clips before you upload.",
|
|
5
|
+
"homepage": "https://uploadcheck.app",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ajantoniou/uploadcheck.git",
|
|
9
|
+
"directory": "cli"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./index.mjs",
|
|
14
|
+
"./request-builder": "./request-builder.mjs"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"uploadcheck": "./index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.mjs",
|
|
21
|
+
"launch-evidence.mjs",
|
|
22
|
+
"request-builder.mjs"
|
|
23
|
+
],
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { extname, basename, join, relative } from "node:path";
|
|
2
|
+
import { statSync, readFileSync, existsSync, readdirSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_API_BASE_URL = "https://api.uploadcheck.app";
|
|
5
|
+
const DEFAULT_MAX_INLINE_MB = 128;
|
|
6
|
+
const LAUNCH_PROOF_CONTRACT_VERSION = "2026-06-06.render-web-proof";
|
|
7
|
+
|
|
8
|
+
const CONTENT_TYPES = new Map([
|
|
9
|
+
[".mp4", "video/mp4"],
|
|
10
|
+
[".mov", "video/quicktime"],
|
|
11
|
+
[".m4v", "video/x-m4v"],
|
|
12
|
+
[".webm", "video/webm"],
|
|
13
|
+
[".mp3", "audio/mpeg"],
|
|
14
|
+
[".m4a", "audio/mp4"],
|
|
15
|
+
[".wav", "audio/wav"],
|
|
16
|
+
[".aac", "audio/aac"],
|
|
17
|
+
[".ogg", "audio/ogg"],
|
|
18
|
+
[".jpg", "image/jpeg"],
|
|
19
|
+
[".jpeg", "image/jpeg"],
|
|
20
|
+
[".png", "image/png"],
|
|
21
|
+
[".webp", "image/webp"]
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function buildJobRequest(target, options = {}) {
|
|
25
|
+
if (!target) throw new Error("Usage: uploadcheck check <file-or-url>");
|
|
26
|
+
|
|
27
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
28
|
+
const maxInlineMb = Number(options.maxInlineMb || process.env.UPLOADCHECK_INLINE_MEDIA_MAX_MB || DEFAULT_MAX_INLINE_MB);
|
|
29
|
+
const payload = {};
|
|
30
|
+
|
|
31
|
+
if (isHttpUrl(target)) {
|
|
32
|
+
if (isYouTubeUrl(target)) {
|
|
33
|
+
payload.youtube_url = target;
|
|
34
|
+
} else {
|
|
35
|
+
payload.signed_url = target;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
if (!existsSync(target)) throw new Error(`File not found: ${target}`);
|
|
39
|
+
const fileStat = statSync(target);
|
|
40
|
+
const maxBytes = maxInlineMb * 1024 * 1024;
|
|
41
|
+
const uploadMode = options.uploadMode || "auto";
|
|
42
|
+
if (uploadMode === "signed" || (uploadMode === "auto" && fileStat.size > maxBytes)) {
|
|
43
|
+
return buildSignedUploadPlan(target, options, fileStat);
|
|
44
|
+
}
|
|
45
|
+
if (uploadMode === "inline" && fileStat.size > maxBytes) {
|
|
46
|
+
throw new Error(`File is ${formatMb(fileStat.size)} MB; inline CLI payload limit is ${maxInlineMb} MB. Use a signed URL or raise --max-inline-mb.`);
|
|
47
|
+
}
|
|
48
|
+
const contentType = inferContentType(target);
|
|
49
|
+
const mediaKind = inferMediaKind(contentType);
|
|
50
|
+
payload.media_base64 = readFileSync(target).toString("base64");
|
|
51
|
+
payload.media_content_type = contentType;
|
|
52
|
+
payload.media_kind = mediaKind;
|
|
53
|
+
payload.filename = basename(target);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.checks) payload.checks = options.checks;
|
|
57
|
+
attachManifest(payload, options);
|
|
58
|
+
attachTranscript(payload, options);
|
|
59
|
+
attachWatchlist(payload, options);
|
|
60
|
+
attachExpectedScript(payload, options);
|
|
61
|
+
attachChunkSidecars(payload, options);
|
|
62
|
+
if (options.callbackUrl) payload.callback_url = options.callbackUrl;
|
|
63
|
+
if (options.idempotencyKey) payload.idempotency_key = options.idempotencyKey;
|
|
64
|
+
attachCostOptions(payload, options);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
apiBaseUrl,
|
|
68
|
+
path: "/v1/qc/jobs",
|
|
69
|
+
method: "POST",
|
|
70
|
+
kind: "job",
|
|
71
|
+
payload
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildSignedUploadPlan(target, options = {}, fileStat = null) {
|
|
76
|
+
if (!existsSync(target)) throw new Error(`File not found: ${target}`);
|
|
77
|
+
const stat = fileStat || statSync(target);
|
|
78
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
79
|
+
const contentType = inferContentType(target);
|
|
80
|
+
const jobPayload = {};
|
|
81
|
+
if (options.checks) jobPayload.checks = options.checks;
|
|
82
|
+
attachManifest(jobPayload, options);
|
|
83
|
+
attachTranscript(jobPayload, options);
|
|
84
|
+
attachWatchlist(jobPayload, options);
|
|
85
|
+
attachExpectedScript(jobPayload, options);
|
|
86
|
+
attachChunkSidecars(jobPayload, options);
|
|
87
|
+
if (options.callbackUrl) jobPayload.callback_url = options.callbackUrl;
|
|
88
|
+
if (options.idempotencyKey) jobPayload.idempotency_key = options.idempotencyKey;
|
|
89
|
+
attachCostOptions(jobPayload, options);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
apiBaseUrl,
|
|
93
|
+
kind: "signed_upload",
|
|
94
|
+
filePath: target,
|
|
95
|
+
contentType,
|
|
96
|
+
sizeBytes: stat.size,
|
|
97
|
+
createUpload: {
|
|
98
|
+
path: "/v1/uploads",
|
|
99
|
+
method: "POST",
|
|
100
|
+
payload: {
|
|
101
|
+
filename: basename(target),
|
|
102
|
+
content_type: contentType,
|
|
103
|
+
size_bytes: stat.size
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
createJob: {
|
|
107
|
+
path: "/v1/qc/jobs",
|
|
108
|
+
method: "POST",
|
|
109
|
+
payload: jobPayload
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function parseArgs(argv) {
|
|
115
|
+
const args = [...argv];
|
|
116
|
+
const command = args.shift();
|
|
117
|
+
if (!["check", "estimate", "usage", "launch-status", "launch-handoff", "launch-doctor", "launch-evidence", "pipeline-handoff", "npo-pipeline-handoff", "recipes", "cost-basis"].includes(command)) throw new Error("Usage: uploadcheck check <file-or-url> | uploadcheck estimate --minutes N | uploadcheck usage | uploadcheck launch-status | uploadcheck launch-handoff | uploadcheck launch-doctor | uploadcheck launch-evidence | uploadcheck pipeline-handoff | uploadcheck npo-pipeline-handoff | uploadcheck recipes | uploadcheck cost-basis");
|
|
118
|
+
|
|
119
|
+
const target = command === "check" ? args.shift() : null;
|
|
120
|
+
const options = { json: false };
|
|
121
|
+
|
|
122
|
+
while (args.length) {
|
|
123
|
+
const arg = args.shift();
|
|
124
|
+
if (arg === "--json") {
|
|
125
|
+
options.json = true;
|
|
126
|
+
} else if (arg === "--api-base") {
|
|
127
|
+
options.apiBaseUrl = requireValue(arg, args.shift());
|
|
128
|
+
} else if (arg === "--api-key") {
|
|
129
|
+
options.apiKey = requireValue(arg, args.shift());
|
|
130
|
+
} else if (arg === "--checks") {
|
|
131
|
+
options.checks = requireValue(arg, args.shift());
|
|
132
|
+
} else if (arg === "--manifest") {
|
|
133
|
+
options.manifestPath = requireValue(arg, args.shift());
|
|
134
|
+
} else if (arg === "--transcript") {
|
|
135
|
+
options.transcriptPath = requireValue(arg, args.shift());
|
|
136
|
+
} else if (arg === "--output") {
|
|
137
|
+
options.outputPath = requireValue(arg, args.shift());
|
|
138
|
+
} else if (arg === "--model") {
|
|
139
|
+
options.model = requireValue(arg, args.shift());
|
|
140
|
+
} else if (arg === "--keep-file") {
|
|
141
|
+
options.keepFile = true;
|
|
142
|
+
} else if (arg === "--watchlist") {
|
|
143
|
+
options.watchlistPath = requireValue(arg, args.shift());
|
|
144
|
+
} else if (arg === "--expected-script") {
|
|
145
|
+
options.expectedScriptPath = requireValue(arg, args.shift());
|
|
146
|
+
} else if (arg === "--sidecar-dir") {
|
|
147
|
+
options.sidecarDir = requireValue(arg, args.shift());
|
|
148
|
+
} else if (arg === "--callback-url") {
|
|
149
|
+
options.callbackUrl = requireValue(arg, args.shift());
|
|
150
|
+
} else if (arg === "--idempotency-key") {
|
|
151
|
+
options.idempotencyKey = requireValue(arg, args.shift());
|
|
152
|
+
} else if (arg === "--plan") {
|
|
153
|
+
options.planId = requireValue(arg, args.shift());
|
|
154
|
+
} else if (arg === "--plan-price-cents") {
|
|
155
|
+
options.planPriceCents = requireValue(arg, args.shift());
|
|
156
|
+
} else if (arg === "--included-minutes") {
|
|
157
|
+
options.includedMinutes = requireValue(arg, args.shift());
|
|
158
|
+
} else if (arg === "--ai-review-seconds") {
|
|
159
|
+
options.aiReviewSeconds = requireValue(arg, args.shift());
|
|
160
|
+
} else if (arg === "--cost-guardrail") {
|
|
161
|
+
const mode = requireValue(arg, args.shift());
|
|
162
|
+
if (!["downgrade", "block", "off"].includes(mode)) throw new Error("--cost-guardrail must be downgrade, block, or off");
|
|
163
|
+
options.costGuardrail = mode;
|
|
164
|
+
} else if (arg === "--minutes") {
|
|
165
|
+
options.minutes = requireValue(arg, args.shift());
|
|
166
|
+
} else if (arg === "--duration-seconds") {
|
|
167
|
+
options.durationSeconds = requireValue(arg, args.shift());
|
|
168
|
+
} else if (arg === "--billing-period") {
|
|
169
|
+
options.billingPeriod = requireValue(arg, args.shift());
|
|
170
|
+
} else if (arg === "--limit") {
|
|
171
|
+
options.limit = requireValue(arg, args.shift());
|
|
172
|
+
} else if (arg === "--max-inline-mb") {
|
|
173
|
+
options.maxInlineMb = requireValue(arg, args.shift());
|
|
174
|
+
} else if (arg === "--upload-mode") {
|
|
175
|
+
const mode = requireValue(arg, args.shift());
|
|
176
|
+
if (!["auto", "inline", "signed"].includes(mode)) throw new Error("--upload-mode must be auto, inline, or signed");
|
|
177
|
+
options.uploadMode = mode;
|
|
178
|
+
} else {
|
|
179
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { command, target, options };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function buildLaunchStatusRequest(options = {}) {
|
|
187
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
188
|
+
return {
|
|
189
|
+
apiBaseUrl,
|
|
190
|
+
path: "/v1/launch-status",
|
|
191
|
+
method: "GET",
|
|
192
|
+
kind: "launch_status",
|
|
193
|
+
public: true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function buildLaunchHandoffRequest(options = {}) {
|
|
198
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
199
|
+
return {
|
|
200
|
+
apiBaseUrl,
|
|
201
|
+
path: "/v1/launch-handoff",
|
|
202
|
+
method: "GET",
|
|
203
|
+
kind: "launch_handoff",
|
|
204
|
+
public: true
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function buildLaunchDoctorRequest(options = {}) {
|
|
209
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
210
|
+
return {
|
|
211
|
+
apiBaseUrl,
|
|
212
|
+
path: "/v1/launch-doctor",
|
|
213
|
+
method: "GET",
|
|
214
|
+
kind: "launch_doctor",
|
|
215
|
+
public: true
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function buildLaunchEvidenceRequest(options = {}) {
|
|
220
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
221
|
+
return {
|
|
222
|
+
apiBaseUrl,
|
|
223
|
+
path: "/v1/launch-evidence",
|
|
224
|
+
method: "GET",
|
|
225
|
+
kind: "launch_evidence",
|
|
226
|
+
public: true
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildPipelineHandoffRequest(options = {}) {
|
|
231
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
232
|
+
return {
|
|
233
|
+
apiBaseUrl,
|
|
234
|
+
path: "/pipeline-handoff.json",
|
|
235
|
+
method: "GET",
|
|
236
|
+
kind: "pipeline_handoff",
|
|
237
|
+
public: true
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function buildNpoPipelineHandoffRequest(options = {}) {
|
|
242
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
243
|
+
return {
|
|
244
|
+
apiBaseUrl,
|
|
245
|
+
path: "/npo-pipeline-handoff.json",
|
|
246
|
+
method: "GET",
|
|
247
|
+
kind: "npo_pipeline_handoff",
|
|
248
|
+
public: true
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function buildPipelineRecipesRequest(options = {}) {
|
|
253
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
254
|
+
return {
|
|
255
|
+
apiBaseUrl,
|
|
256
|
+
path: "/pipeline-recipes.json",
|
|
257
|
+
method: "GET",
|
|
258
|
+
kind: "pipeline_recipes",
|
|
259
|
+
public: true
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function buildCostBasisRequest(options = {}) {
|
|
264
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
265
|
+
return {
|
|
266
|
+
apiBaseUrl,
|
|
267
|
+
path: "/cost-basis.json",
|
|
268
|
+
method: "GET",
|
|
269
|
+
kind: "cost_basis",
|
|
270
|
+
public: true
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function buildUsageRequest(options = {}) {
|
|
275
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
276
|
+
const params = new URLSearchParams();
|
|
277
|
+
if (options.billingPeriod) params.set("billing_period", options.billingPeriod);
|
|
278
|
+
if (options.limit) params.set("limit", Number(options.limit));
|
|
279
|
+
const query = params.toString();
|
|
280
|
+
return {
|
|
281
|
+
apiBaseUrl,
|
|
282
|
+
path: `/v1/usage/margins${query ? `?${query}` : ""}`,
|
|
283
|
+
method: "GET",
|
|
284
|
+
kind: "usage"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function buildEstimateRequest(options = {}) {
|
|
289
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl || process.env.UPLOADCHECK_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
290
|
+
const payload = {};
|
|
291
|
+
if (options.checks) payload.checks = options.checks;
|
|
292
|
+
if (options.minutes) payload.minutes = Number(options.minutes);
|
|
293
|
+
if (options.durationSeconds) payload.duration_seconds = Number(options.durationSeconds);
|
|
294
|
+
attachCostOptions(payload, options);
|
|
295
|
+
return {
|
|
296
|
+
apiBaseUrl,
|
|
297
|
+
path: "/v1/qc/estimate",
|
|
298
|
+
method: "POST",
|
|
299
|
+
kind: "estimate",
|
|
300
|
+
payload
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function formatJobSummary(payload) {
|
|
305
|
+
const status = payload.status || "unknown";
|
|
306
|
+
const verdict = payload.verdict || "pending";
|
|
307
|
+
const minutes = payload.minutesMetered ?? 0;
|
|
308
|
+
const ingressSuffix = formatMediaIngress(payload.mediaIngress);
|
|
309
|
+
const cost = payload.costEstimate?.estimatedCogsUsd ?? (
|
|
310
|
+
payload.costEstimate?.estimatedCogsCents == null ? null : payload.costEstimate.estimatedCogsCents / 100
|
|
311
|
+
);
|
|
312
|
+
const observed = payload.costEstimate?.observedTotalCogsCents;
|
|
313
|
+
const observedSuffix = observed == null ? "" : ` | observed COGS $${(Number(observed) / 100).toFixed(4)}`;
|
|
314
|
+
const suffix = cost == null ? observedSuffix : ` | est. COGS $${Number(cost).toFixed(4)}${observedSuffix}`;
|
|
315
|
+
return `UploadCheck job ${payload.jobId || payload.id || "(unknown)"}: ${status} / ${verdict} | ${minutes} min${ingressSuffix}${suffix}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function formatUsageSummary(payload) {
|
|
319
|
+
const summary = payload.summary || {};
|
|
320
|
+
const minutes = Number(summary.minutes || 0);
|
|
321
|
+
const cogs = Number(summary.estimatedCogsCents || 0) / 100;
|
|
322
|
+
const costPerMinuteCents = Number(summary.estimatedCostPerMinuteCents || 0);
|
|
323
|
+
const grossMarginPct = Number(summary.estimatedGrossMarginPct || 0);
|
|
324
|
+
const status = summary.marginSafe === false ? "MARGIN RISK" : "MARGIN SAFE";
|
|
325
|
+
const observed = summary.observedProviderUsageEntries > 0
|
|
326
|
+
? ` | observed cost/min ${Number(summary.observedCostPerMinuteCents || 0).toFixed(4)}c | observed margin ${Number(summary.observedGrossMarginPct || 0).toFixed(2)}%`
|
|
327
|
+
: "";
|
|
328
|
+
return `UploadCheck usage: ${status} | ${minutes} min | est. COGS $${cogs.toFixed(4)} | cost/min ${costPerMinuteCents.toFixed(4)}c | margin ${grossMarginPct.toFixed(2)}%${observed}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function formatLaunchStatusSummary(payload) {
|
|
332
|
+
const status = payload.product_hunt_ready ? "READY" : "NOT READY";
|
|
333
|
+
const blockers = (payload.remaining_blockers || []).map((blocker) => blocker.id).filter(Boolean);
|
|
334
|
+
return `UploadCheck launch status: ${status}${blockers.length ? ` | blockers ${blockers.join(", ")}` : ""}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function formatLaunchHandoffSummary(payload) {
|
|
338
|
+
const status = payload.productHuntReady ? "READY" : "NOT READY";
|
|
339
|
+
const blockers = (payload.remainingBlockers || []).map((blocker) => blocker.id).filter(Boolean);
|
|
340
|
+
const actions = (payload.requiredActions || []).map((action) => action.id).filter(Boolean);
|
|
341
|
+
return `UploadCheck launch handoff: ${status}${blockers.length ? ` | blockers ${blockers.join(", ")}` : ""}${actions.length ? ` | required actions ${actions.join(", ")}` : ""}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function formatLaunchDoctorSummary(payload) {
|
|
345
|
+
const status = payload.productHuntReady ? "READY" : "NOT READY";
|
|
346
|
+
const blockers = (payload.remainingBlockers || []).map((blocker) => blocker.id).filter(Boolean);
|
|
347
|
+
const phases = payload.blockerFixPlan?.phases?.length || 0;
|
|
348
|
+
const commands = payload.launchDoctorCommands?.length || 0;
|
|
349
|
+
return `UploadCheck launch doctor: ${status}${blockers.length ? ` | blockers ${blockers.join(", ")}` : ""}${phases ? ` | fix phases ${phases}` : ""}${commands ? ` | doctor commands ${commands}` : ""}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function formatLaunchEvidenceSummary(payload) {
|
|
353
|
+
const evidence = payload.name === "UploadCheck.app Remote Launch Evidence" ? payload : buildRemoteLaunchEvidence(payload);
|
|
354
|
+
const status = evidence.status === "ready" ? "READY" : "NOT READY";
|
|
355
|
+
const blockers = evidence.blockers || [];
|
|
356
|
+
const phases = evidence.fixPhases?.length || 0;
|
|
357
|
+
const commands = evidence.commandCoverage?.length || 0;
|
|
358
|
+
return `UploadCheck launch evidence: ${status}${blockers.length ? ` | blockers ${blockers.join(", ")}` : ""}${phases ? ` | fix phases ${phases}` : ""}${commands ? ` | commands ${commands}` : ""}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function formatPipelineRecipesSummary(payload) {
|
|
362
|
+
const profiles = Object.keys(payload.profiles || {});
|
|
363
|
+
const checks = payload.nto_replacement_qc?.implemented_gates?.length || 0;
|
|
364
|
+
return `UploadCheck pipeline recipes: ${profiles.length} profiles${profiles.length ? ` (${profiles.join(", ")})` : ""} | ${checks} implemented NTO/NPO replacement gates`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function formatPipelineHandoffSummary(payload) {
|
|
368
|
+
const steps = payload.call_sequence?.length || 0;
|
|
369
|
+
const profiles = payload.profiles || [];
|
|
370
|
+
const ingress = Object.keys(payload.media_ingress || {});
|
|
371
|
+
return `UploadCheck pipeline handoff: ${steps} steps${profiles.length ? ` | profiles ${profiles.join(", ")}` : ""}${ingress.length ? ` | media ingress ${ingress.join(", ")}` : ""}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function formatNpoPipelineHandoffSummary(payload) {
|
|
375
|
+
const checks = payload.cost_preflight?.checks || "";
|
|
376
|
+
const steps = payload.mcp_sequence?.length || 0;
|
|
377
|
+
const sidecars = Object.keys(payload.required_sidecars || {});
|
|
378
|
+
return `UploadCheck NPO pipeline handoff: ${steps} MCP steps | checks ${checks}${sidecars.length ? ` | sidecars ${sidecars.join(", ")}` : ""}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function formatCostBasisSummary(payload) {
|
|
382
|
+
const target = Number(payload.target_gross_margin_pct || 0);
|
|
383
|
+
const stress = (payload.plans || []).find((plan) => plan.plan_id === "stress_99_5000");
|
|
384
|
+
const remaining = stress?.remaining_cost_per_minute_after_deterministic_full_allowance_cents;
|
|
385
|
+
const verdict = payload.verdict?.stress_99_5000 || "";
|
|
386
|
+
const remainingText = remaining == null ? "unknown" : `${Number(remaining).toFixed(4)}c/min`;
|
|
387
|
+
return `UploadCheck cost basis: target margin ${target}% | $99/5,000 remaining post-deterministic COGS ${remainingText}${verdict ? ` | ${verdict}` : ""}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function requireValue(flag, value) {
|
|
391
|
+
if (!value) throw new Error(`${flag} requires a value`);
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function trimTrailingSlash(value) {
|
|
396
|
+
return value.replace(/\/+$/, "");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isHttpUrl(value) {
|
|
400
|
+
return /^https?:\/\//i.test(value);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isYouTubeUrl(value) {
|
|
404
|
+
return /(^https?:\/\/)?([^/]+\.)?(youtube\.com|youtu\.be)\//i.test(value);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function inferContentType(filePath) {
|
|
408
|
+
const ext = extname(filePath).toLowerCase();
|
|
409
|
+
return CONTENT_TYPES.get(ext) || "application/octet-stream";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function inferMediaKind(contentType) {
|
|
413
|
+
if (contentType.startsWith("audio/")) return "audio";
|
|
414
|
+
if (contentType.startsWith("image/")) return "image";
|
|
415
|
+
return "video";
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function attachManifest(payload, options) {
|
|
419
|
+
if (!options.manifestPath) return;
|
|
420
|
+
if (!existsSync(options.manifestPath)) throw new Error(`Manifest not found: ${options.manifestPath}`);
|
|
421
|
+
const text = readFileSync(options.manifestPath, "utf8");
|
|
422
|
+
payload.manifest_json = JSON.parse(text);
|
|
423
|
+
payload.manifest_filename = basename(options.manifestPath);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function attachTranscript(payload, options) {
|
|
427
|
+
if (!options.transcriptPath) return;
|
|
428
|
+
if (!existsSync(options.transcriptPath)) throw new Error(`Transcript not found: ${options.transcriptPath}`);
|
|
429
|
+
const text = readFileSync(options.transcriptPath, "utf8");
|
|
430
|
+
if (options.transcriptPath.toLowerCase().endsWith(".json")) {
|
|
431
|
+
payload.transcript_json = JSON.parse(text);
|
|
432
|
+
} else {
|
|
433
|
+
payload.transcript_text = text;
|
|
434
|
+
}
|
|
435
|
+
payload.transcript_filename = basename(options.transcriptPath);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function attachWatchlist(payload, options) {
|
|
439
|
+
if (!options.watchlistPath) return;
|
|
440
|
+
if (!existsSync(options.watchlistPath)) throw new Error(`Watchlist not found: ${options.watchlistPath}`);
|
|
441
|
+
const text = readFileSync(options.watchlistPath, "utf8");
|
|
442
|
+
payload.watchlist_json = JSON.parse(text);
|
|
443
|
+
payload.watchlist_filename = basename(options.watchlistPath);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function attachExpectedScript(payload, options) {
|
|
447
|
+
if (!options.expectedScriptPath) return;
|
|
448
|
+
if (!existsSync(options.expectedScriptPath)) throw new Error(`Expected script not found: ${options.expectedScriptPath}`);
|
|
449
|
+
const text = readFileSync(options.expectedScriptPath, "utf8");
|
|
450
|
+
if (options.expectedScriptPath.toLowerCase().endsWith(".json")) {
|
|
451
|
+
payload.expected_script_json = JSON.parse(text);
|
|
452
|
+
} else {
|
|
453
|
+
payload.expected_script_text = text;
|
|
454
|
+
}
|
|
455
|
+
payload.expected_script_filename = basename(options.expectedScriptPath);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function attachChunkSidecars(payload, options) {
|
|
459
|
+
if (!options.sidecarDir) return;
|
|
460
|
+
if (!existsSync(options.sidecarDir)) throw new Error(`Sidecar dir not found: ${options.sidecarDir}`);
|
|
461
|
+
if (!statSync(options.sidecarDir).isDirectory()) throw new Error(`Sidecar dir is not a directory: ${options.sidecarDir}`);
|
|
462
|
+
const files = collectJsonFiles(options.sidecarDir).slice(0, 200);
|
|
463
|
+
payload.chunk_sidecars_json = files.map((filePath) => ({
|
|
464
|
+
relative_path: relative(options.sidecarDir, filePath),
|
|
465
|
+
filename: basename(filePath),
|
|
466
|
+
json: JSON.parse(readFileSync(filePath, "utf8"))
|
|
467
|
+
}));
|
|
468
|
+
payload.chunk_sidecar_dirname = basename(options.sidecarDir);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function collectJsonFiles(dir) {
|
|
472
|
+
const out = [];
|
|
473
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
474
|
+
const filePath = join(dir, entry.name);
|
|
475
|
+
if (entry.isDirectory()) out.push(...collectJsonFiles(filePath));
|
|
476
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".json")) out.push(filePath);
|
|
477
|
+
}
|
|
478
|
+
return out.sort();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function attachCostOptions(payload, options) {
|
|
482
|
+
if (options.planId) payload.plan_id = options.planId;
|
|
483
|
+
if (options.planPriceCents) payload.plan_price_cents = Number(options.planPriceCents);
|
|
484
|
+
if (options.includedMinutes) payload.included_minutes = Number(options.includedMinutes);
|
|
485
|
+
if (options.aiReviewSeconds) payload.ai_review_seconds = Number(options.aiReviewSeconds);
|
|
486
|
+
if (options.costGuardrail) payload.cost_guardrail = options.costGuardrail;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function formatMediaIngress(mediaIngress) {
|
|
490
|
+
if (!mediaIngress?.mode) return "";
|
|
491
|
+
const detail = [mediaIngress.mode, mediaIngress.contentType, formatBytes(mediaIngress.bytes), formatSha256(mediaIngress.sha256)].filter(Boolean).join(" ");
|
|
492
|
+
return detail ? ` | media ${detail}` : "";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function formatSha256(value) {
|
|
496
|
+
const text = String(value || "");
|
|
497
|
+
return /^[a-f0-9]{64}$/i.test(text) ? `sha256 ${text.slice(0, 12).toLowerCase()}...` : "";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function formatBytes(bytes) {
|
|
501
|
+
const value = Number(bytes);
|
|
502
|
+
if (!Number.isFinite(value) || value <= 0) return "";
|
|
503
|
+
if (value < 1024) return `${value} B`;
|
|
504
|
+
if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
|
|
505
|
+
return `${(value / 1024 / 1024).toFixed(value < 10 * 1024 * 1024 ? 2 : 1)} MB`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function formatMb(bytes) {
|
|
509
|
+
return (bytes / 1024 / 1024).toFixed(1);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function buildRemoteLaunchEvidence(payload = {}, options = {}) {
|
|
513
|
+
const blockers = (payload.remainingBlockers || []).map((blocker) => blocker.id).filter(Boolean);
|
|
514
|
+
const commands = Array.isArray(payload.launchDoctorCommands) ? payload.launchDoctorCommands : [];
|
|
515
|
+
const phases = Array.isArray(payload.blockerFixPlan?.phases) ? payload.blockerFixPlan.phases : [];
|
|
516
|
+
return {
|
|
517
|
+
name: "UploadCheck.app Remote Launch Evidence",
|
|
518
|
+
contractVersion: LAUNCH_PROOF_CONTRACT_VERSION,
|
|
519
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
520
|
+
source: options.source || "https://api.uploadcheck.app/v1/launch-doctor",
|
|
521
|
+
productHuntReady: Boolean(payload.productHuntReady),
|
|
522
|
+
status: payload.productHuntReady && blockers.length === 0 ? "ready" : "blocked",
|
|
523
|
+
blockers,
|
|
524
|
+
redaction: {
|
|
525
|
+
rawStdoutIncluded: false,
|
|
526
|
+
rawStderrIncluded: false,
|
|
527
|
+
protectedValues: ["API bearer tokens", "checkout URL paths", "Lemon Squeezy variant ids", "local temp paths"]
|
|
528
|
+
},
|
|
529
|
+
commandCoverage: commands.map((command) => redactLaunchText(command)),
|
|
530
|
+
fixPhases: phases.map((phase) => ({
|
|
531
|
+
id: phase.id,
|
|
532
|
+
title: phase.title,
|
|
533
|
+
blockers: phase.blockers || [],
|
|
534
|
+
proofCommands: (phase.proof_commands || []).map((command) => redactLaunchText(command))
|
|
535
|
+
})),
|
|
536
|
+
completionRule: payload.blockerFixPlan?.completionRule || payload.rule || "Only launch when Product Hunt readiness is true and no blockers remain."
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function redactLaunchText(value = "") {
|
|
541
|
+
return String(value)
|
|
542
|
+
.replace(/UPLOADCHECK_API_KEY=([^\s"']+)/g, "UPLOADCHECK_API_KEY=<private_bearer>")
|
|
543
|
+
.replace(/(authorization:\s*bearer\s+)[^\s"']+/gi, "$1<private_bearer>")
|
|
544
|
+
.replace(/(https:\/\/[^/\s"']+\/checkout\/buy\/)[A-Za-z0-9_-]+/g, "$1<variant_id>")
|
|
545
|
+
.replace(/(https:\/\/[^/\s"']+)<checkout_path>/g, "$1<checkout_path>")
|
|
546
|
+
.replace(/(https:\/\/[^/\s"']+)\/[^\s"']*(checkout|creator|studio|network)[^\s"']*/gi, (match, origin) => {
|
|
547
|
+
if (match.includes("/checkout/buy/<variant_id>")) return match;
|
|
548
|
+
return `${origin}<checkout_path>`;
|
|
549
|
+
})
|
|
550
|
+
.replace(/\/tmp\/uploadcheck[-/][^\s"']+/g, "/tmp/uploadcheck/<redacted>");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export { buildRemoteLaunchEvidence };
|