ework-web 0.10.60 → 0.10.62
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/package.json +1 -1
- package/src/giteaApi.ts +33 -3
- package/src/index.ts +16 -0
- package/src/webhooks.ts +8 -4
package/package.json
CHANGED
package/src/giteaApi.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// bg audit) plus a few cheap stubs (/version, /user, /repos/:o/:r) that
|
|
13
13
|
// Gitea clients commonly hit during connection bootstrap.
|
|
14
14
|
|
|
15
|
-
import { getDB } from "./db";
|
|
15
|
+
import { getDB, getConfigAll } from "./db";
|
|
16
16
|
import {
|
|
17
17
|
StoreError,
|
|
18
18
|
getProject,
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
listReactionsFor,
|
|
33
33
|
canWriteProject,
|
|
34
34
|
canReadProject,
|
|
35
|
+
ensureUser,
|
|
36
|
+
getUserByLogin,
|
|
35
37
|
type UserRow,
|
|
36
38
|
} from "./store";
|
|
37
39
|
import {
|
|
@@ -95,6 +97,12 @@ function asContent(v: unknown): string | undefined {
|
|
|
95
97
|
return trimmed;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
async function canSudo(u: UserRow): Promise<boolean> {
|
|
101
|
+
const cfg = await getConfigAll();
|
|
102
|
+
const allow = (cfg["sudoLogins"] ?? "").split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
103
|
+
return allow.includes(u.login);
|
|
104
|
+
}
|
|
105
|
+
|
|
98
106
|
export async function handleGiteaApi(
|
|
99
107
|
req: Request,
|
|
100
108
|
url: URL,
|
|
@@ -103,8 +111,30 @@ export async function handleGiteaApi(
|
|
|
103
111
|
const path = url.pathname;
|
|
104
112
|
if (!path.startsWith("/api/v1/")) return null;
|
|
105
113
|
const origin = new URL(req.url).origin;
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
114
|
+
const caller = ctx.user;
|
|
115
|
+
if (!caller) return giteaError(401, "requires authentication");
|
|
116
|
+
|
|
117
|
+
// ─── Sudo: Gitea convention for impersonation ───
|
|
118
|
+
// Allows bridge/integration processes to post on behalf of real users.
|
|
119
|
+
// Permission gated by config KV `sudoLogins` whitelist (not is_admin — bridges
|
|
120
|
+
// should not be admins). Sudo-Kind header controls kind for first-creation
|
|
121
|
+
// only; existing users' kind is never changed via sudo.
|
|
122
|
+
const sudoLoginRaw = req.headers.get("Sudo") ?? url.searchParams.get("sudo");
|
|
123
|
+
const sudoLogin = sudoLoginRaw?.trim() || null;
|
|
124
|
+
let user: UserRow = caller;
|
|
125
|
+
if (sudoLogin && sudoLogin !== caller.login) {
|
|
126
|
+
if (!(await canSudo(caller))) return giteaError(403, "sudo requires permission");
|
|
127
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(sudoLogin)) return giteaError(400, "invalid sudo login");
|
|
128
|
+
const sudoKindRaw = req.headers.get("Sudo-Kind")?.trim();
|
|
129
|
+
const existing = await getUserByLogin(sudoLogin);
|
|
130
|
+
if (existing) {
|
|
131
|
+
user = existing;
|
|
132
|
+
} else {
|
|
133
|
+
const kind = sudoKindRaw === "bot" || sudoKindRaw === "system" ? sudoKindRaw : "human";
|
|
134
|
+
user = await ensureUser(sudoLogin, kind);
|
|
135
|
+
}
|
|
136
|
+
if (!user.is_active) return giteaError(403, "sudo target inactive");
|
|
137
|
+
}
|
|
108
138
|
|
|
109
139
|
if (ROUTES.version.test(path) && req.method === "GET") {
|
|
110
140
|
return { status: 200, body: { version: "1.22.0" } };
|
package/src/index.ts
CHANGED
|
@@ -1311,6 +1311,22 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1311
1311
|
return json({ ok: true });
|
|
1312
1312
|
}
|
|
1313
1313
|
|
|
1314
|
+
if (url.pathname === "/api/sudo-policy" && req.method === "GET") {
|
|
1315
|
+
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
1316
|
+
const cfg = await getConfigAll();
|
|
1317
|
+
const list = (k: string, d: string) => (cfg[k] ?? d).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1318
|
+
return json({ sudoLogins: list("sudoLogins", "") });
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (url.pathname === "/api/sudo-policy" && req.method === "POST") {
|
|
1322
|
+
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
1323
|
+
const payload = await req.json().catch(() => ({} as Record<string, unknown>));
|
|
1324
|
+
const join = (v: unknown) => Array.isArray(v) ? v.filter((s) => typeof s === "string").join(",") : (typeof v === "string" ? v : "");
|
|
1325
|
+
const sl = join((payload as { sudoLogins?: unknown }).sudoLogins);
|
|
1326
|
+
if (sl) await setConfig("sudoLogins", sl); else await deleteConfig("sudoLogins");
|
|
1327
|
+
return json({ ok: true });
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1314
1330
|
if (url.pathname === "/api/router/daemons" && req.method === "GET") {
|
|
1315
1331
|
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
1316
1332
|
try {
|
package/src/webhooks.ts
CHANGED
|
@@ -696,11 +696,15 @@ export async function emitCommentEvent(
|
|
|
696
696
|
log.info(`webhook: issue dispatch disabled — skipping comment_created (author=${comment.author}) for ${scopeKey}#${issue.number}`);
|
|
697
697
|
return;
|
|
698
698
|
}
|
|
699
|
-
// Global/project-level are "default no auto-dispatch" strategies — comments can still explicitly wake AI.
|
|
700
|
-
if (cfg["dispatchEnabled"] === "false" || cfg[`dispatchOff:${scopeKey}`] === "1") {
|
|
701
|
-
log.info(`webhook: dispatch disabled but waking via comment_created (author=${comment.author}) for ${scopeKey}#${issue.number}`);
|
|
702
|
-
}
|
|
703
699
|
const authorUser = await getUserByLogin(comment.author);
|
|
700
|
+
const dispatchOff = cfg["dispatchEnabled"] === "false" || cfg[`dispatchOff:${scopeKey}`] === "1";
|
|
701
|
+
if (dispatchOff && authorUser && authorUser.kind !== "human") {
|
|
702
|
+
log.info(`webhook: dispatch disabled — non-human comment blocked (author=${comment.author} kind=${authorUser.kind}) for ${scopeKey}#${issue.number}`);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (dispatchOff) {
|
|
706
|
+
log.info(`webhook: dispatch disabled but human comment waking AI (author=${comment.author}) for ${scopeKey}#${issue.number}`);
|
|
707
|
+
}
|
|
704
708
|
const appCfg = await loadConfig();
|
|
705
709
|
const cfgList = (k: string, d: string) =>
|
|
706
710
|
(cfg[k] ?? d).split(",").map((s) => s.trim()).filter(Boolean);
|