mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 08:25:57 +00:00
27b66d6753
Introduce an always-on auth layer with auto-created admin on first boot, multi-tenant isolation for threads/stores, and a full setup/login flow. Backend - JWT access tokens with `ver` field for stale-token rejection; bump on password/email change - Password hashing, HttpOnly+Secure cookies (Secure derived from request scheme at runtime) - CSRF middleware covering both REST and LangGraph routes - IP-based login rate limiting (5 attempts / 5-min lockout) with bounded dict growth and X-Forwarded-For bypass fix - Multi-worker-safe admin auto-creation (single DB write, WAL once) - needs_setup + token_version on User model; SQLite schema migration - Thread/store isolation by owner; orphan thread migration on first admin registration - thread_id validated as UUID to prevent log injection - CLI tool to reset admin password - Decorator-based authz module extracted from auth core Frontend - Login and setup pages with SSR guard for needs_setup flow - Account settings page (change password / email) - AuthProvider + route guards; skips redirect when no users registered - i18n (en-US / zh-CN) for auth surfaces - Typed auth API client; parseAuthError unwraps FastAPI detail envelope Infra & tooling - Unified `serve.sh` with gateway mode + auto dep install - Public PyPI uv.toml pin for CI compatibility - Regenerated uv.lock with public index Tests - HTTP vs HTTPS cookie security tests - Auth middleware, rate limiter, CSRF, setup flow coverage
151 lines
4.1 KiB
JavaScript
151 lines
4.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
async function loadModule() {
|
|
try {
|
|
return await import("./prompt-input-files.ts");
|
|
} catch (error) {
|
|
return { error };
|
|
}
|
|
}
|
|
|
|
test("exports the prompt-input file conversion helper", async () => {
|
|
const loaded = await loadModule();
|
|
|
|
assert.ok(
|
|
!("error" in loaded),
|
|
loaded.error instanceof Error
|
|
? loaded.error.message
|
|
: "prompt-input-files module is missing",
|
|
);
|
|
assert.equal(typeof loaded.promptInputFilePartToFile, "function");
|
|
});
|
|
|
|
test("reuses the original File when a prompt attachment already has one", async () => {
|
|
const { promptInputFilePartToFile } = await import("./prompt-input-files.ts");
|
|
const file = new File(["hello"], "note.txt", { type: "text/plain" });
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("fetch should not run when File is already present");
|
|
};
|
|
|
|
try {
|
|
const converted = await promptInputFilePartToFile({
|
|
type: "file",
|
|
filename: file.name,
|
|
mediaType: file.type,
|
|
url: "blob:http://localhost:2026/stale-preview-url",
|
|
file,
|
|
});
|
|
|
|
assert.equal(converted, file);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("reconstructs a File from a data URL when no original File is present", async () => {
|
|
const { promptInputFilePartToFile } = await import("./prompt-input-files.ts");
|
|
const converted = await promptInputFilePartToFile({
|
|
type: "file",
|
|
filename: "note.txt",
|
|
mediaType: "text/plain",
|
|
url: "data:text/plain;base64,aGVsbG8=",
|
|
});
|
|
|
|
assert.ok(converted);
|
|
assert.equal(converted.name, "note.txt");
|
|
assert.equal(converted.type, "text/plain");
|
|
assert.equal(await converted.text(), "hello");
|
|
});
|
|
|
|
test("rewraps the original File when the prompt metadata changes", async () => {
|
|
const { promptInputFilePartToFile } = await import("./prompt-input-files.ts");
|
|
const file = new File(["hello"], "note.txt", { type: "text/plain" });
|
|
|
|
const converted = await promptInputFilePartToFile({
|
|
type: "file",
|
|
filename: "renamed.txt",
|
|
mediaType: "text/markdown",
|
|
file,
|
|
});
|
|
|
|
assert.ok(converted);
|
|
assert.notEqual(converted, file);
|
|
assert.equal(converted.name, "renamed.txt");
|
|
assert.equal(converted.type, "text/markdown");
|
|
assert.equal(await converted.text(), "hello");
|
|
});
|
|
|
|
test("returns null when upload preparation is missing required data", async () => {
|
|
const { promptInputFilePartToFile } = await import("./prompt-input-files.ts");
|
|
|
|
const converted = await promptInputFilePartToFile({
|
|
type: "file",
|
|
mediaType: "text/plain",
|
|
});
|
|
|
|
assert.equal(converted, null);
|
|
});
|
|
|
|
test("returns null when the URL fallback fetch fails", async () => {
|
|
const { promptInputFilePartToFile } = await import("./prompt-input-files.ts");
|
|
const originalFetch = globalThis.fetch;
|
|
const originalWarn = console.warn;
|
|
const warnCalls = [];
|
|
|
|
console.warn = (...args) => {
|
|
warnCalls.push(args);
|
|
};
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("network down");
|
|
};
|
|
|
|
try {
|
|
const converted = await promptInputFilePartToFile({
|
|
type: "file",
|
|
filename: "note.txt",
|
|
url: "blob:http://localhost:2026/missing-preview-url",
|
|
});
|
|
|
|
assert.equal(converted, null);
|
|
assert.equal(warnCalls.length, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
console.warn = originalWarn;
|
|
}
|
|
});
|
|
|
|
test("returns null when the URL fallback fetch response is non-ok", async () => {
|
|
const { promptInputFilePartToFile } = await import("./prompt-input-files.ts");
|
|
const originalFetch = globalThis.fetch;
|
|
const originalWarn = console.warn;
|
|
const warnCalls = [];
|
|
|
|
console.warn = (...args) => {
|
|
warnCalls.push(args);
|
|
};
|
|
|
|
globalThis.fetch = async () =>
|
|
new Response("missing", {
|
|
status: 404,
|
|
statusText: "Not Found",
|
|
});
|
|
|
|
try {
|
|
const converted = await promptInputFilePartToFile({
|
|
type: "file",
|
|
filename: "note.txt",
|
|
url: "blob:http://localhost:2026/missing-preview-url",
|
|
});
|
|
|
|
assert.equal(converted, null);
|
|
assert.equal(warnCalls.length, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
console.warn = originalWarn;
|
|
}
|
|
});
|