ework-web 0.10.35 → 0.10.36
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/config.ts +8 -0
- package/src/db.ts +2 -1
- package/src/index.ts +18 -6
- package/src/ratelimit.ts +4 -0
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -93,6 +93,13 @@ export const configSchema = z.object({
|
|
|
93
93
|
if (!Number.isFinite(n) || n < 1 || n > 64) return 6;
|
|
94
94
|
return Math.floor(n);
|
|
95
95
|
}, z.number().int().min(1).max(64)),
|
|
96
|
+
// Space-separated list of origins (e.g. "https://ework-web.taobao.net http://ework-web.taobao.net")
|
|
97
|
+
// injected into CSP form-action + connect-src. For reverse-proxy/gateway deployments
|
|
98
|
+
// where the browser sees a different scheme/host than the app's 'self'.
|
|
99
|
+
publicOrigins: z
|
|
100
|
+
.string()
|
|
101
|
+
.default("")
|
|
102
|
+
.transform((s) => s.split(/\s+/).filter(Boolean)),
|
|
96
103
|
});
|
|
97
104
|
|
|
98
105
|
export type Config = z.infer<typeof configSchema>;
|
|
@@ -188,6 +195,7 @@ export async function loadConfig(): Promise<Config> {
|
|
|
188
195
|
defaultModel: db.defaultModel ?? process.env.WORK_DEFAULT_MODEL,
|
|
189
196
|
autowireActive: process.env.WORK_AUTOWIRE_ACTIVE !== "false",
|
|
190
197
|
webhookMaxConcurrent: Number(process.env.WORK_WEBHOOK_MAX_CONCURRENT ?? "6"),
|
|
198
|
+
publicOrigins: process.env.WORK_PUBLIC_ORIGINS ?? "",
|
|
191
199
|
});
|
|
192
200
|
}
|
|
193
201
|
|
package/src/db.ts
CHANGED
|
@@ -301,7 +301,8 @@ function translateForMysql(sql: string): string {
|
|
|
301
301
|
return sql
|
|
302
302
|
.replace(/INSERT OR IGNORE INTO/g, "INSERT IGNORE INTO")
|
|
303
303
|
.replace(/ON CONFLICT\((\w+)\) DO UPDATE SET/g, "ON DUPLICATE KEY UPDATE")
|
|
304
|
-
.replace(/excluded\.(\w+)/g, "VALUES($1)")
|
|
304
|
+
.replace(/excluded\.(\w+)/g, "VALUES($1)")
|
|
305
|
+
.replace(/LIKE \? ESCAPE '\\'/g, "LIKE ?");
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
async function migrateMysqlSurrogateId(pool: Pool): Promise<void> {
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { buildAdminTokensPage } from "./views/adminTokens";
|
|
|
27
27
|
import { buildSessionList, buildSessionView, renderNewMessages, renderBatchHTML } from "./views/sessionLog";
|
|
28
28
|
import { buildFileView, FileViewError, readFileSince, serveRawFile } from "./fileview";
|
|
29
29
|
import { translateText, translateTextStream, TranslateError } from "./translate";
|
|
30
|
-
import { rateLimit } from "./ratelimit";
|
|
30
|
+
import { rateLimit, clearRateLimit } from "./ratelimit";
|
|
31
31
|
import {
|
|
32
32
|
StoreError,
|
|
33
33
|
getProject,
|
|
@@ -185,13 +185,20 @@ async function autoWireAllProjects(origin: string): Promise<void> {
|
|
|
185
185
|
void autoWireAllProjects(`http://${cfg.host}:${cfg.port}`);
|
|
186
186
|
|
|
187
187
|
const SEC_HEADERS: Record<string, string> = {
|
|
188
|
-
"content-security-policy":
|
|
188
|
+
"content-security-policy": buildCsp(cfg),
|
|
189
189
|
"x-content-type-options": "nosniff",
|
|
190
190
|
"x-frame-options": "DENY",
|
|
191
191
|
"referrer-policy": "same-origin",
|
|
192
192
|
"permissions-policy": "()",
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
+
function buildCsp(cfg: Config): string {
|
|
196
|
+
const origins = cfg.publicOrigins.join(" ");
|
|
197
|
+
const formAction = origins ? `'self' ${origins}` : "'self'";
|
|
198
|
+
const connectSrc = origins ? `'self' ${origins}` : "'self'";
|
|
199
|
+
return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ${connectSrc}; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action ${formAction}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
const hlCss = loadHighlightCss();
|
|
196
203
|
|
|
197
204
|
function loadHighlightCss(): string {
|
|
@@ -483,14 +490,18 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
483
490
|
if (req.method === "POST") {
|
|
484
491
|
const form = await req.formData().catch(() => new FormData());
|
|
485
492
|
const next = sanitizeNext(String(form.get("next") ?? "/"));
|
|
486
|
-
if (!rateLimit(`login:${ip}`, 5, 5 / (15 * 60))) {
|
|
487
|
-
return html(loginHTML(next, "尝试过多,15 分钟后再试", cfg), 429);
|
|
488
|
-
}
|
|
489
493
|
const login = String(form.get("login") ?? "").trim();
|
|
490
494
|
const password = String(form.get("password") ?? "");
|
|
491
495
|
const token = String(form.get("token") ?? "").trim();
|
|
492
496
|
|
|
493
|
-
|
|
497
|
+
const rlKey = `login:${ip}:${login || token || "?"}`;
|
|
498
|
+
if (!rateLimit(rlKey, 5, 5 / (15 * 60))) {
|
|
499
|
+
return new Response(loginHTML(next, "尝试过多,15 分钟后再试", cfg), {
|
|
500
|
+
status: 429,
|
|
501
|
+
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "900" },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
494
505
|
let resolvedLogin: string | null = null;
|
|
495
506
|
if (token && token === cfg.authToken) {
|
|
496
507
|
resolvedLogin = cfg.operatorLogin;
|
|
@@ -508,6 +519,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
508
519
|
}
|
|
509
520
|
|
|
510
521
|
if (resolvedLogin) {
|
|
522
|
+
clearRateLimit(rlKey);
|
|
511
523
|
const setCookie = await makeAuthCookieHeader(cfg, resolvedLogin);
|
|
512
524
|
return new Response(null, {
|
|
513
525
|
status: 302,
|
package/src/ratelimit.ts
CHANGED