@ipul-labs/forge 1.16.6 → 1.16.7
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/cli/sessionStore.js
CHANGED
|
@@ -48,17 +48,14 @@ export function sessionDir(repo, iid) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
async function ensureDir(dir) {
|
|
51
|
-
await fs.mkdir(dir, { recursive: true });
|
|
51
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export async function writeAnalysis(repo, iid, analysis) {
|
|
55
55
|
const dir = sessionDir(repo, iid);
|
|
56
56
|
await ensureDir(dir);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
JSON.stringify(analysis, null, 2),
|
|
60
|
-
"utf8",
|
|
61
|
-
);
|
|
57
|
+
const file = path.join(dir, "analysis.json");
|
|
58
|
+
await fs.writeFile(file, JSON.stringify(analysis, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
export async function readAnalysis(repo, iid) {
|
|
@@ -84,7 +81,7 @@ export async function updateState(repo, iid, partial) {
|
|
|
84
81
|
createdAt: now,
|
|
85
82
|
};
|
|
86
83
|
const next = { ...prev, ...partial, updatedAt: now };
|
|
87
|
-
await fs.writeFile(file, JSON.stringify(next, null, 2), "utf8");
|
|
84
|
+
await fs.writeFile(file, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
88
85
|
return next;
|
|
89
86
|
}
|
|
90
87
|
|
package/package.json
CHANGED
|
@@ -55,9 +55,21 @@ function sanitizeRepo(repo) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function gitlabCloneUrl(project) {
|
|
58
|
+
return `https://gitlab.com/${project}.git`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function gitEnvWithToken() {
|
|
58
62
|
const { token } = getConnection("gitlab");
|
|
59
63
|
if (!token) throw new Error("GitLab token is required to clone the repo locally. Set it in Settings → Connections or via GITLAB_TOKEN env var.");
|
|
60
|
-
return
|
|
64
|
+
return {
|
|
65
|
+
GIT_ASKPASS: "echo",
|
|
66
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
67
|
+
GIT_CONFIG_COUNT: "2",
|
|
68
|
+
GIT_CONFIG_KEY_0: "credential.helper",
|
|
69
|
+
GIT_CONFIG_VALUE_0: "",
|
|
70
|
+
GIT_CONFIG_KEY_1: "http.extraHeader",
|
|
71
|
+
GIT_CONFIG_VALUE_1: `Authorization: Bearer ${token}`,
|
|
72
|
+
};
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
function resolveCloneUrl(repoInput) {
|
|
@@ -90,16 +102,18 @@ export async function ensureWorkingTree(repo, branchName, onLog) {
|
|
|
90
102
|
const hasRepo = await exists(path.join(dir, ".git"));
|
|
91
103
|
const log = (m) => onLog?.(m);
|
|
92
104
|
|
|
105
|
+
const gitEnv = gitEnvWithToken();
|
|
106
|
+
const envOpts = { env: { ...process.env, ...gitEnv } };
|
|
93
107
|
if (!hasRepo) {
|
|
94
108
|
log(`Cloning ${repo} → ${dir}`);
|
|
95
109
|
await fs.mkdir(dir, { recursive: true });
|
|
96
|
-
const clone = await runGit(["clone", "--depth=1", resolveCloneUrl(repo), dir],
|
|
110
|
+
const clone = await runGit(["clone", "--depth=1", resolveCloneUrl(repo), dir], envOpts, onLog);
|
|
97
111
|
if (clone.code !== 0) throw new Error(`git clone failed (code ${clone.code})`);
|
|
98
112
|
} else {
|
|
99
113
|
log(`Reusing clone at ${dir}`);
|
|
100
114
|
}
|
|
101
115
|
|
|
102
|
-
const fetch = await runGit(["fetch", "--depth=1", "origin", "main"], { cwd: dir }, onLog);
|
|
116
|
+
const fetch = await runGit(["fetch", "--depth=1", "origin", "main"], { cwd: dir, ...envOpts }, onLog);
|
|
103
117
|
if (fetch.code !== 0) throw new Error(`git fetch failed (code ${fetch.code})`);
|
|
104
118
|
|
|
105
119
|
const reset = await runGit(["reset", "--hard", "origin/main"], { cwd: dir }, onLog);
|
|
@@ -121,9 +135,10 @@ export async function ensureWorkingTree(repo, branchName, onLog) {
|
|
|
121
135
|
* @param {(msg: string) => void} [onLog]
|
|
122
136
|
*/
|
|
123
137
|
export async function overlayChanges(dir, changes, onLog) {
|
|
138
|
+
const realBase = await fs.realpath(dir);
|
|
124
139
|
for (const c of changes) {
|
|
125
|
-
const full = path.
|
|
126
|
-
if (!full.startsWith(
|
|
140
|
+
const full = path.resolve(dir, c.file);
|
|
141
|
+
if (!full.startsWith(realBase + path.sep) && full !== realBase) {
|
|
127
142
|
throw new Error(`Refusing to write outside clone dir: ${c.file}`);
|
|
128
143
|
}
|
|
129
144
|
if (c.action === "delete") {
|
|
@@ -26,11 +26,23 @@ function sanitizeRepo(repo) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function gitlabCloneUrl(project) {
|
|
29
|
+
return `https://gitlab.com/${project}.git`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function gitEnvWithToken() {
|
|
29
33
|
const { token } = getConnection("gitlab");
|
|
30
34
|
if (!token) {
|
|
31
35
|
throw new Error("GitLab token is required to clone the repo for validation. Set it in Settings → Connections or via GITLAB_TOKEN env var.");
|
|
32
36
|
}
|
|
33
|
-
return
|
|
37
|
+
return {
|
|
38
|
+
GIT_ASKPASS: "echo",
|
|
39
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
40
|
+
GIT_CONFIG_COUNT: "2",
|
|
41
|
+
GIT_CONFIG_KEY_0: "credential.helper",
|
|
42
|
+
GIT_CONFIG_VALUE_0: "",
|
|
43
|
+
GIT_CONFIG_KEY_1: "http.extraHeader",
|
|
44
|
+
GIT_CONFIG_VALUE_1: `Authorization: Bearer ${token}`,
|
|
45
|
+
};
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
function resolveCloneUrl(repoInput) {
|
|
@@ -54,11 +66,12 @@ export async function ensureClone(repo, branchName, emit) {
|
|
|
54
66
|
emit({ type: "progress", step: "clone", status: "running" });
|
|
55
67
|
emit({ type: "log", step: "clone", message: hasRepo ? `Reusing clone at ${dir}` : `Cloning ${repo} → ${dir}` });
|
|
56
68
|
|
|
69
|
+
const gitEnv = gitEnvWithToken();
|
|
57
70
|
if (!hasRepo) {
|
|
58
71
|
await fs.mkdir(dir, { recursive: true });
|
|
59
72
|
const clone = await runCmd(
|
|
60
73
|
"git", ["clone", "--depth=1", resolveCloneUrl(repo), dir],
|
|
61
|
-
{},
|
|
74
|
+
{ env: { ...process.env, ...gitEnv } },
|
|
62
75
|
(l) => emit({ type: "log", step: "clone", message: l }),
|
|
63
76
|
);
|
|
64
77
|
if (clone.code !== 0) throw new Error(`git clone failed (code ${clone.code})`);
|
|
@@ -66,7 +79,7 @@ export async function ensureClone(repo, branchName, emit) {
|
|
|
66
79
|
|
|
67
80
|
const fetch = await runCmd(
|
|
68
81
|
"git", ["fetch", "--depth=1", "origin", "main"],
|
|
69
|
-
{ cwd: dir },
|
|
82
|
+
{ cwd: dir, env: { ...process.env, ...gitEnv } },
|
|
70
83
|
(l) => emit({ type: "log", step: "clone", message: l }),
|
|
71
84
|
);
|
|
72
85
|
if (fetch.code !== 0) throw new Error(`git fetch failed (code ${fetch.code})`);
|
|
@@ -8,10 +8,10 @@ import path from "node:path";
|
|
|
8
8
|
|
|
9
9
|
export async function overlayChanges(dir, changes, emit) {
|
|
10
10
|
emit({ type: "progress", step: "overlay", status: "running" });
|
|
11
|
+
const realBase = await fs.realpath(dir);
|
|
11
12
|
for (const c of changes) {
|
|
12
|
-
const full = path.
|
|
13
|
-
|
|
14
|
-
if (!full.startsWith(dir + path.sep) && full !== dir) {
|
|
13
|
+
const full = path.resolve(dir, c.file);
|
|
14
|
+
if (!full.startsWith(realBase + path.sep) && full !== realBase) {
|
|
15
15
|
throw new Error(`Refusing to write outside clone dir: ${c.file}`);
|
|
16
16
|
}
|
|
17
17
|
if (c.action === "delete") {
|
package/server/forge/routes.js
CHANGED
|
@@ -21,6 +21,7 @@ import { Router } from "express";
|
|
|
21
21
|
import * as configStore from "./core/configStore.js";
|
|
22
22
|
import { listDeployments } from "./core/k8s.js";
|
|
23
23
|
import { ForgeAgent } from "./core/ForgeAgent.js";
|
|
24
|
+
import crypto from "node:crypto";
|
|
24
25
|
import { probeQuota } from "./core/quotaClient.js";
|
|
25
26
|
import { validate as runValidation } from "./core/validator.js";
|
|
26
27
|
import { createProvider } from "./core/providers/index.js";
|
|
@@ -45,7 +46,9 @@ function requireAuth(req, res, next) {
|
|
|
45
46
|
return res.status(401).json({ error: "Missing Authorization header." });
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
const provided = Buffer.from(auth.slice(7));
|
|
50
|
+
const expected = Buffer.from(token);
|
|
51
|
+
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
|
|
49
52
|
return res.status(403).json({ error: "Invalid token." });
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -54,12 +57,20 @@ function requireAuth(req, res, next) {
|
|
|
54
57
|
|
|
55
58
|
// ─── Error handler ────────────────────────────────────────────────────────────
|
|
56
59
|
|
|
60
|
+
function redactSecrets(text) {
|
|
61
|
+
return text
|
|
62
|
+
.replace(/(?:token|key|secret|password|bearer)[=:\s]+\S+/gi, (m) => m.slice(0, m.indexOf(m.match(/[=:\s]/)[0]) + 1) + "***")
|
|
63
|
+
.replace(/oauth2:[^@]+@/gi, "oauth2:***@")
|
|
64
|
+
.replace(/ghp_\w+/g, "ghp_***")
|
|
65
|
+
.replace(/glpat-\w+/g, "glpat-***");
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
function handleError(res, error, operation) {
|
|
58
69
|
const msg = error?.message ?? "Unknown error";
|
|
59
|
-
console.error(`[forge] ${operation} failed:`, msg);
|
|
70
|
+
console.error(`[forge] ${operation} failed:`, redactSecrets(msg));
|
|
60
71
|
const upstream = /^(?:GitLab|GitHub) (\d{3}) on /.exec(msg);
|
|
61
72
|
const status = error?.status ?? (upstream ? Number(upstream[1]) : 500);
|
|
62
|
-
res.status(status).json({ error: msg, operation });
|
|
73
|
+
res.status(status).json({ error: redactSecrets(msg), operation });
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
// ─── Repo helpers ─────────────────────────────────────────────────────────────
|