give the ability to auto upload builds
This commit is contained in:
@@ -1584,6 +1584,7 @@ form?.addEventListener("submit", async (e) => {
|
|||||||
shellcodeConsole: document.getElementById("shellcode-console")?.checked || false,
|
shellcodeConsole: document.getElementById("shellcode-console")?.checked || false,
|
||||||
useSgn: document.getElementById("sgn-mode")?.checked || false,
|
useSgn: document.getElementById("sgn-mode")?.checked || false,
|
||||||
sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1,
|
sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1,
|
||||||
|
uploadToFileShare: pendingUpload,
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAndroid = platforms.some(p => p.startsWith('android-'));
|
const hasAndroid = platforms.some(p => p.startsWith('android-'));
|
||||||
@@ -1637,6 +1638,10 @@ async function startBuild(config) {
|
|||||||
buildUpdateAllBtn.disabled = true;
|
buildUpdateAllBtn.disabled = true;
|
||||||
buildUpdateAllBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Building...</span>';
|
buildUpdateAllBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Building...</span>';
|
||||||
}
|
}
|
||||||
|
if (buildUploadBtn) {
|
||||||
|
buildUploadBtn.disabled = true;
|
||||||
|
buildUploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Building...</span>';
|
||||||
|
}
|
||||||
|
|
||||||
buildStatus.classList.remove("hidden");
|
buildStatus.classList.remove("hidden");
|
||||||
buildStatusText.textContent = "Starting build...";
|
buildStatusText.textContent = "Starting build...";
|
||||||
@@ -1693,6 +1698,11 @@ async function startBuild(config) {
|
|||||||
buildUpdateAllBtn.disabled = false;
|
buildUpdateAllBtn.disabled = false;
|
||||||
buildUpdateAllBtn.innerHTML = '<i class="fa-solid fa-arrow-up-from-bracket"></i> <span>Build & Update All</span>';
|
buildUpdateAllBtn.innerHTML = '<i class="fa-solid fa-arrow-up-from-bracket"></i> <span>Build & Update All</span>';
|
||||||
}
|
}
|
||||||
|
if (buildUploadBtn) {
|
||||||
|
buildUploadBtn.disabled = false;
|
||||||
|
buildUploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> <span>Build & Upload</span>';
|
||||||
|
}
|
||||||
|
pendingUpload = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1711,6 +1721,7 @@ async function streamBuildOutput(buildId, config = {}) {
|
|||||||
const RECONNECT_DELAY_MS = 2000;
|
const RECONNECT_DELAY_MS = 2000;
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let completed = false;
|
let completed = false;
|
||||||
|
const uploadedShareFiles = [];
|
||||||
|
|
||||||
while (!completed && attempts <= MAX_RECONNECT_ATTEMPTS) {
|
while (!completed && attempts <= MAX_RECONNECT_ATTEMPTS) {
|
||||||
if (attempts > 0) {
|
if (attempts > 0) {
|
||||||
@@ -1814,6 +1825,13 @@ async function streamBuildOutput(buildId, config = {}) {
|
|||||||
addBuildOutput(data.text, data.level || "info");
|
addBuildOutput(data.text, data.level || "info");
|
||||||
} else if (data.type === "status") {
|
} else if (data.type === "status") {
|
||||||
buildStatusText.textContent = data.text;
|
buildStatusText.textContent = data.text;
|
||||||
|
} else if (data.type === "file_share_uploaded") {
|
||||||
|
uploadedShareFiles.push({
|
||||||
|
id: data.id,
|
||||||
|
filename: data.filename,
|
||||||
|
platform: data.platform,
|
||||||
|
size: data.size,
|
||||||
|
});
|
||||||
} else if (data.type === "complete") {
|
} else if (data.type === "complete") {
|
||||||
buildStatusText.textContent = data.success
|
buildStatusText.textContent = data.success
|
||||||
? "Build completed successfully!"
|
? "Build completed successfully!"
|
||||||
@@ -1845,6 +1863,10 @@ async function streamBuildOutput(buildId, config = {}) {
|
|||||||
buildResults.classList.remove("hidden");
|
buildResults.classList.remove("hidden");
|
||||||
displayBuild(buildData);
|
displayBuild(buildData);
|
||||||
|
|
||||||
|
if (uploadedShareFiles.length > 0) {
|
||||||
|
renderShareLinksPanel(uploadedShareFiles);
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-push update if "Build & Update All" was used
|
// Auto-push update if "Build & Update All" was used
|
||||||
if (pendingUpdateAll) {
|
if (pendingUpdateAll) {
|
||||||
pendingUpdateAll = false;
|
pendingUpdateAll = false;
|
||||||
@@ -1919,6 +1941,68 @@ function addBuildOutput(text, level = "info") {
|
|||||||
buildOutputDiv.appendChild(span);
|
buildOutputDiv.appendChild(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderShareLinksPanel(items) {
|
||||||
|
const existing = document.getElementById("build-share-links");
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.id = "build-share-links";
|
||||||
|
panel.className =
|
||||||
|
"mt-3 p-3 bg-emerald-950/30 border border-emerald-700/50 rounded-lg space-y-2";
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "flex items-center gap-2 text-sm font-medium text-emerald-300";
|
||||||
|
header.innerHTML =
|
||||||
|
'<i class="fa-solid fa-cloud-arrow-up"></i><span>Uploaded to File Share</span>' +
|
||||||
|
`<span class="text-xs text-slate-400 font-normal">(${items.length})</span>`;
|
||||||
|
panel.appendChild(header);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const url = `${window.location.origin}/api/file-share/${item.id}/download`;
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "flex items-center gap-2 text-xs";
|
||||||
|
|
||||||
|
const label = document.createElement("span");
|
||||||
|
label.className = "text-slate-400 shrink-0";
|
||||||
|
label.textContent = item.platform || item.filename;
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.readOnly = true;
|
||||||
|
input.value = url;
|
||||||
|
input.className =
|
||||||
|
"flex-1 bg-slate-900 border border-slate-700 rounded px-2 py-1 text-slate-200 font-mono select-all";
|
||||||
|
input.addEventListener("focus", () => input.select());
|
||||||
|
|
||||||
|
const copyBtn = document.createElement("button");
|
||||||
|
copyBtn.type = "button";
|
||||||
|
copyBtn.className =
|
||||||
|
"px-2 py-1 rounded bg-emerald-700 hover:bg-emerald-600 text-white text-xs flex items-center gap-1 shrink-0";
|
||||||
|
copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i><span>Copy</span>';
|
||||||
|
copyBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
copyBtn.innerHTML = '<i class="fa-solid fa-check"></i><span>Copied</span>';
|
||||||
|
if (window.showToast) window.showToast("Download link copied to clipboard", "success");
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i><span>Copy</span>';
|
||||||
|
}, 1500);
|
||||||
|
} catch {
|
||||||
|
input.select();
|
||||||
|
document.execCommand?.("copy");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(label);
|
||||||
|
row.appendChild(input);
|
||||||
|
row.appendChild(copyBtn);
|
||||||
|
panel.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFilesDiv.appendChild(panel);
|
||||||
|
}
|
||||||
|
|
||||||
function showBuildFiles(files, buildId, expiresAt) {
|
function showBuildFiles(files, buildId, expiresAt) {
|
||||||
buildResults.classList.remove("hidden");
|
buildResults.classList.remove("hidden");
|
||||||
buildFilesDiv.innerHTML = "";
|
buildFilesDiv.innerHTML = "";
|
||||||
@@ -2332,6 +2416,8 @@ const updateAllConfirm = document.getElementById("update-all-confirm");
|
|||||||
|
|
||||||
let pendingUpdateAll = false;
|
let pendingUpdateAll = false;
|
||||||
let pendingUpdateHideWindow = false;
|
let pendingUpdateHideWindow = false;
|
||||||
|
let pendingUpload = false;
|
||||||
|
const buildUploadBtn = document.getElementById("build-upload-btn");
|
||||||
|
|
||||||
function showUpdateAllModal() {
|
function showUpdateAllModal() {
|
||||||
if (!updateAllModal) return;
|
if (!updateAllModal) return;
|
||||||
@@ -2428,6 +2514,19 @@ if (updateAllConfirm) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buildUploadBtn) {
|
||||||
|
buildUploadBtn.addEventListener("click", () => {
|
||||||
|
if (isBuilding) return;
|
||||||
|
const platformCheckboxes = form.querySelectorAll('input[name="platform"]:checked');
|
||||||
|
if (platformCheckboxes.length === 0) {
|
||||||
|
alert("Please select at least one platform to build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingUpload = true;
|
||||||
|
form.requestSubmit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function pushUpdateToAllClients(buildId, hideWindow) {
|
async function pushUpdateToAllClients(buildId, hideWindow) {
|
||||||
addBuildOutput("\n── Pushing update to all eligible clients ──\n", "info");
|
addBuildOutput("\n── Pushing update to all eligible clients ──\n", "info");
|
||||||
|
|
||||||
|
|||||||
@@ -761,6 +761,12 @@
|
|||||||
<i class="fa-solid fa-arrow-up-from-bracket"></i>
|
<i class="fa-solid fa-arrow-up-from-bracket"></i>
|
||||||
<span>Build & Update All</span>
|
<span>Build & Update All</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button type="button" id="build-upload-btn"
|
||||||
|
class="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||||
|
<span>Build & Upload</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ const startBuildProcess = (buildId: string, buildConfig: any) =>
|
|||||||
runBuildProcess(buildId, buildConfig, {
|
runBuildProcess(buildId, buildConfig, {
|
||||||
generateBuildMutex,
|
generateBuildMutex,
|
||||||
sanitizeOutputName,
|
sanitizeOutputName,
|
||||||
|
fileShareRoot: FILE_SHARE_ROOT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pendingHttpDownloads = new Map<string, PendingHttpDownload>();
|
const pendingHttpDownloads = new Map<string, PendingHttpDownload>();
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { $ } from "bun";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { saveBuild } from "../db";
|
import { saveBuild, insertSharedFile, type SharedFileRecord } from "../db";
|
||||||
|
import { getUserById, canUploadFiles } from "../users";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { getConfig } from "../config";
|
import { getConfig } from "../config";
|
||||||
import { signBuildToken } from "./build-signing";
|
import { signBuildToken } from "./build-signing";
|
||||||
@@ -117,6 +118,7 @@ type BuildProcessConfig = {
|
|||||||
solRpcEndpoints?: string;
|
solRpcEndpoints?: string;
|
||||||
iosBundleId?: string;
|
iosBundleId?: string;
|
||||||
fetchPublicIP?: boolean;
|
fetchPublicIP?: boolean;
|
||||||
|
uploadToFileShare?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -148,8 +150,128 @@ function stripUpxHeaders(filePath: string): boolean {
|
|||||||
type BuildProcessDeps = {
|
type BuildProcessDeps = {
|
||||||
generateBuildMutex: (length?: number) => string;
|
generateBuildMutex: (length?: number) => string;
|
||||||
sanitizeOutputName: (name: string) => string;
|
sanitizeOutputName: (name: string) => string;
|
||||||
|
fileShareRoot?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function guessMimeTypeForUpload(filename: string): string {
|
||||||
|
const ext = path.extname(filename).toLowerCase();
|
||||||
|
switch (ext) {
|
||||||
|
case ".exe":
|
||||||
|
case ".dll":
|
||||||
|
case ".scr":
|
||||||
|
case ".com":
|
||||||
|
case ".pif":
|
||||||
|
return "application/vnd.microsoft.portable-executable";
|
||||||
|
case ".bat":
|
||||||
|
case ".cmd":
|
||||||
|
return "application/bat";
|
||||||
|
case ".bin":
|
||||||
|
return "application/octet-stream";
|
||||||
|
case ".ipa":
|
||||||
|
return "application/octet-stream";
|
||||||
|
case ".sgn":
|
||||||
|
return "application/octet-stream";
|
||||||
|
default:
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadBuildFilesToFileShare(
|
||||||
|
build: BuildStream,
|
||||||
|
outDir: string,
|
||||||
|
fileShareRoot: string,
|
||||||
|
userId: number,
|
||||||
|
sendToStream: (data: any) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
const dbUser = getUserById(userId);
|
||||||
|
if (!dbUser) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: "WARNING: Skipping file-share upload — uploading user not found.\n",
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!canUploadFiles(dbUser.id, dbUser.role)) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: "WARNING: Skipping file-share upload — you do not have upload permission.\n",
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.mkdir(fileShareRoot, { recursive: true });
|
||||||
|
|
||||||
|
sendToStream({ type: "status", text: "Uploading build to file share..." });
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `\n── Uploading ${build.files.length} file(s) to file share ──\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const file of build.files as any[]) {
|
||||||
|
const filename: string = file.filename || file.name;
|
||||||
|
if (!filename) continue;
|
||||||
|
const sourcePath = path.join(outDir, filename);
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `WARNING: Build output not found on disk for upload: ${filename}\n`,
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const id = uuidv4();
|
||||||
|
const folder = path.join(fileShareRoot, id);
|
||||||
|
await fs.promises.mkdir(folder, { recursive: true });
|
||||||
|
const targetPath = path.join(folder, filename);
|
||||||
|
await fs.promises.copyFile(sourcePath, targetPath);
|
||||||
|
const size = fs.statSync(targetPath).size;
|
||||||
|
|
||||||
|
const record: SharedFileRecord = {
|
||||||
|
id,
|
||||||
|
filename,
|
||||||
|
storedPath: targetPath,
|
||||||
|
size,
|
||||||
|
mimeType: guessMimeTypeForUpload(filename),
|
||||||
|
uploadedBy: dbUser.id,
|
||||||
|
uploadedByUsername: dbUser.username,
|
||||||
|
passwordHash: null,
|
||||||
|
maxDownloads: null,
|
||||||
|
downloadCount: 0,
|
||||||
|
expiresAt: null,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
description: `Build ${build.id.substring(0, 8)} — ${file.platform || "unknown"}`,
|
||||||
|
};
|
||||||
|
insertSharedFile(record);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[file-share] ${dbUser.username} uploaded "${filename}" (${size} bytes) id=${id} via build ${build.id.substring(0, 8)}`,
|
||||||
|
);
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `Uploaded ${filename} (${size} bytes) → file share id ${id}\n`,
|
||||||
|
level: "success",
|
||||||
|
});
|
||||||
|
sendToStream({
|
||||||
|
type: "file_share_uploaded",
|
||||||
|
id,
|
||||||
|
filename,
|
||||||
|
platform: file.platform || null,
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `WARNING: Failed to upload ${filename} to file share: ${err.message || err}\n`,
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function detectAgentVersion(clientDir: string): string {
|
function detectAgentVersion(clientDir: string): string {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(clientDir, "cmd", "agent", "config", "config.go");
|
const configPath = path.join(clientDir, "cmd", "agent", "config", "config.go");
|
||||||
@@ -1267,6 +1389,31 @@ func runBoundFiles() {
|
|||||||
build.status = "completed";
|
build.status = "completed";
|
||||||
logger.info(`[build:${buildId.substring(0, 8)}] Build completed successfully! Built ${build.files.length} file(s)`);
|
logger.info(`[build:${buildId.substring(0, 8)}] Build completed successfully! Built ${build.files.length} file(s)`);
|
||||||
sendToStream({ type: "output", text: `\n[OK] Build completed successfully!\n`, level: "success" });
|
sendToStream({ type: "output", text: `\n[OK] Build completed successfully!\n`, level: "success" });
|
||||||
|
|
||||||
|
if (config.uploadToFileShare && config.builtByUserId && deps.fileShareRoot) {
|
||||||
|
try {
|
||||||
|
await uploadBuildFilesToFileShare(
|
||||||
|
build,
|
||||||
|
outDir,
|
||||||
|
deps.fileShareRoot,
|
||||||
|
config.builtByUserId,
|
||||||
|
sendToStream,
|
||||||
|
);
|
||||||
|
} catch (uploadErr: any) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `WARNING: File-share upload failed: ${uploadErr.message || uploadErr}\n`,
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (config.uploadToFileShare && !deps.fileShareRoot) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: "WARNING: File-share upload requested but file share is not configured on this server.\n",
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sendToStream({ type: "complete", success: true, files: build.files, buildId, expiresAt: build.expiresAt });
|
sendToStream({ type: "complete", success: true, files: build.files, buildId, expiresAt: build.expiresAt });
|
||||||
|
|
||||||
saveBuild({
|
saveBuild({
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { logger } from "../../logger";
|
|||||||
import { normalizeClientOs } from "../deploy-utils";
|
import { normalizeClientOs } from "../deploy-utils";
|
||||||
import { canUserBuild, recordBuildStart, recordBuildEnd } from "../../build-rate-limit";
|
import { canUserBuild, recordBuildStart, recordBuildEnd } from "../../build-rate-limit";
|
||||||
import { getConfig } from "../../config";
|
import { getConfig } from "../../config";
|
||||||
|
import { canUploadFiles } from "../../users";
|
||||||
import { addBuildToBanlist, removeBuildFromBanlist } from "../build-signing";
|
import { addBuildToBanlist, removeBuildFromBanlist } from "../build-signing";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -118,6 +119,7 @@ export async function handleBuildRoutes(
|
|||||||
useSgn,
|
useSgn,
|
||||||
sgnIterations,
|
sgnIterations,
|
||||||
fetchPublicIP,
|
fetchPublicIP,
|
||||||
|
uploadToFileShare,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
|
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
|
||||||
@@ -345,6 +347,14 @@ export async function handleBuildRoutes(
|
|||||||
safeBoundFiles = validated;
|
safeBoundFiles = validated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const safeUploadToFileShare = !!uploadToFileShare;
|
||||||
|
if (safeUploadToFileShare && !canUploadFiles(user.userId, user.role)) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "You do not have permission to upload files" },
|
||||||
|
{ status: 403 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const rateLimitActive = getConfig().registration.mode !== "off";
|
const rateLimitActive = getConfig().registration.mode !== "off";
|
||||||
if (rateLimitActive) recordBuildStart(user.userId);
|
if (rateLimitActive) recordBuildStart(user.userId);
|
||||||
|
|
||||||
@@ -392,6 +402,7 @@ export async function handleBuildRoutes(
|
|||||||
useSgn: !!useSgn,
|
useSgn: !!useSgn,
|
||||||
sgnIterations: Math.max(1, Math.min(50, Math.floor(Number(sgnIterations) || 1))),
|
sgnIterations: Math.max(1, Math.min(50, Math.floor(Number(sgnIterations) || 1))),
|
||||||
fetchPublicIP: !!fetchPublicIP,
|
fetchPublicIP: !!fetchPublicIP,
|
||||||
|
uploadToFileShare: safeUploadToFileShare,
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
if (rateLimitActive) recordBuildEnd(user.userId);
|
if (rateLimitActive) recordBuildEnd(user.userId);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user