diff --git a/apps/server/spec/etapi/auth-login.spec.ts b/apps/server/spec/etapi/auth-login.spec.ts
index da7588dd5e..6213ede085 100644
--- a/apps/server/spec/etapi/auth-login.spec.ts
+++ b/apps/server/spec/etapi/auth-login.spec.ts
@@ -24,9 +24,8 @@ describe("etapi/auth/login", () => {
expect(response.body.authToken).toBeTruthy();
});
- // Regression test for the auth-bypass where `verifyPassword` (async) was used
- // without `await`, so `!verifyPassword(...)` evaluated a truthy Promise and the
- // wrong-password branch never executed — any password yielded a full-access token.
+ // verifyPassword is async; this guards that a wrong password is rejected and the
+ // async verification result actually gates token issuance.
it("rejects a wrong password and issues no token", async () => {
const response = await supertest(app)
.post(LOGIN_URL)
@@ -46,20 +45,19 @@ describe("etapi/auth/login", () => {
expect(response.body.authToken).toBeUndefined();
});
- // A token minted from a wrong password must not grant access to data. Before the
- // fix, the bypass returned a real, fully-privileged token here.
+ // A token must never be issued from a failed login, and even if one were, it must not
+ // grant access to data.
it("does not grant data access from a wrong-password login attempt", async () => {
const response = await supertest(app)
.post(LOGIN_URL)
.send({ password: "definitely-not-the-password", tokenName: "test" });
- const leakedToken = response.body.authToken;
- // If the bypass is present, `leakedToken` is a usable token; assert it cannot
- // be used to read the root note.
- if (leakedToken) {
+ const token = response.body.authToken;
+ // Defense in depth: if any token came back, assert it cannot read the root note.
+ if (token) {
await supertest(app)
.get("/etapi/notes/root")
- .auth("etapi", leakedToken, { type: "basic" })
+ .auth("etapi", token, { type: "basic" })
.expect(401);
}
});
diff --git a/apps/server/src/anonymize.ts b/apps/server/src/anonymize.ts
index aea817831e..d78c5ae97b 100644
--- a/apps/server/src/anonymize.ts
+++ b/apps/server/src/anonymize.ts
@@ -2,7 +2,7 @@ import anonymizationService from "./services/anonymization.js";
import sqlInit from "./services/sql_init.js";
await import("@triliumnext/core");
-sqlInit.dbReady.then(async () => {
+void sqlInit.dbReady.then(async () => {
try {
console.log("Starting anonymization...");
diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts
index 2996c32174..ab8cfcba7c 100644
--- a/apps/server/src/app.ts
+++ b/apps/server/src/app.ts
@@ -1,4 +1,4 @@
-import("@triliumnext/core");
+void import("@triliumnext/core");
import { erase } from "@triliumnext/core";
import compression from "compression";
diff --git a/apps/server/src/etapi/auth.ts b/apps/server/src/etapi/auth.ts
index 22b1fcfdee..65f5c817d1 100644
--- a/apps/server/src/etapi/auth.ts
+++ b/apps/server/src/etapi/auth.ts
@@ -6,12 +6,26 @@ import etapiTokenService from "../services/etapi_tokens.js";
import eu from "./etapi_utils.js";
function register(router: Router, loginMiddleware: RequestHandler[]) {
- eu.NOT_AUTHENTICATED_ROUTE(router, "post", "/etapi/auth/login", loginMiddleware, (req, res, next) => {
- const { password, tokenName } = req.body;
+ // Password verification is async (scrypt), so it runs as middleware: the synchronous
+ // transactional route handler below cannot await, and the check must complete before the
+ // token is issued.
+ const verifyPasswordMiddleware: RequestHandler = async (req, res, next) => {
+ try {
+ const { password } = req.body;
- if (!passwordEncryptionService.verifyPassword(password)) {
- throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password.");
+ if (!(await passwordEncryptionService.verifyPassword(password))) {
+ eu.sendError(res, 401, "WRONG_PASSWORD", "Wrong password.");
+ return;
+ }
+
+ next();
+ } catch (e: any) {
+ eu.sendError(res, 500, eu.GENERIC_CODE, e.message);
}
+ };
+
+ eu.NOT_AUTHENTICATED_ROUTE(router, "post", "/etapi/auth/login", [...loginMiddleware, verifyPasswordMiddleware], (req, res) => {
+ const { tokenName } = req.body;
const { authToken } = etapiTokenService.createToken(tokenName || "ETAPI login");
diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts
index 8dd4ef746c..cd0db083a9 100644
--- a/apps/server/src/etapi/notes.ts
+++ b/apps/server/src/etapi/notes.ts
@@ -147,7 +147,7 @@ function register(router: Router) {
noteService.saveRevisionIfNeeded(note);
note.setContent(req.body);
- noteService.asyncPostProcessContent(note, req.body);
+ void noteService.asyncPostProcessContent(note, req.body);
return res.sendStatus(204);
});
@@ -166,14 +166,14 @@ function register(router: Router) {
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
const branch = note.getParentBranches()[0];
- zipExportService.exportToZip(taskContext, branch, format as ExportFormat, res);
+ void zipExportService.exportToZip(taskContext, branch, format as ExportFormat, res);
});
eu.route<{ noteId: string }>(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const taskContext = new TaskContext("no-progress-reporting", "importNotes", null);
- zipImportService.importZip(taskContext, req.body, note).then((importedNote) => {
+ void zipImportService.importZip(taskContext, req.body, note).then((importedNote) => {
res.status(201).json({
note: mappers.mapNoteToPojo(importedNote),
branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0])
diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
index b97ff85375..e203789efb 100644
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -43,23 +43,26 @@ async function startApplication() {
dbConfig: {
provider: dbProvider,
isReadOnly: config.General.readOnly,
- async onTransactionCommit() {
- const { ws } = await import("@triliumnext/core");
- ws.sendTransactionEntityChangesToAllClients();
+ onTransactionCommit() {
+ // Core types these hooks as synchronous (() => void) and invokes them without
+ // awaiting, so the dynamic import must stay fire-and-forget rather than async.
+ void import("@triliumnext/core").then(({ ws }) => {
+ ws.sendTransactionEntityChangesToAllClients();
+ });
},
- async onTransactionRollback() {
- const { cls, becca_loader, entity_changes } = await import("@triliumnext/core");
+ onTransactionRollback() {
+ void import("@triliumnext/core").then(({ cls, becca_loader, entity_changes }) => {
+ const entityChangeIds = cls.getAndClearEntityChangeIds();
- const entityChangeIds = cls.getAndClearEntityChangeIds();
+ if (entityChangeIds.length > 0) {
+ logService.info("Transaction rollback dirtied the becca, forcing reload.");
- if (entityChangeIds.length > 0) {
- logService.info("Transaction rollback dirtied the becca, forcing reload.");
+ becca_loader.load();
+ }
- becca_loader.load();
- }
-
- // the maxEntityChangeId has been incremented during failed transaction, need to recalculate
- entity_changes.recalculateMaxEntityChangeId();
+ // the maxEntityChangeId has been incremented during failed transaction, need to recalculate
+ entity_changes.recalculateMaxEntityChangeId();
+ });
}
},
crypto: new NodejsCryptoProvider(),
@@ -93,4 +96,4 @@ async function startApplication() {
}
}
-startApplication();
+void startApplication();
diff --git a/apps/server/src/routes/api/clipper.ts b/apps/server/src/routes/api/clipper.ts
index 4aa6a7020e..cb80914777 100644
--- a/apps/server/src/routes/api/clipper.ts
+++ b/apps/server/src/routes/api/clipper.ts
@@ -129,7 +129,7 @@ async function createNote(req: Request) {
const newContent = `${existingContent}${existingContent.trim() ? "
" : ""}${rewrittenContent}`;
note.setContent(newContent);
- noteService.asyncPostProcessContent(note, newContent); // to mark attachments as used
+ void noteService.asyncPostProcessContent(note, newContent); // to mark attachments as used
return {
noteId: note.noteId
diff --git a/apps/server/src/routes/api/database.ts b/apps/server/src/routes/api/database.ts
index 4ca4447f25..9bfe261d03 100644
--- a/apps/server/src/routes/api/database.ts
+++ b/apps/server/src/routes/api/database.ts
@@ -27,7 +27,7 @@ function vacuumDatabase() {
}
function findAndFixConsistencyIssues() {
- consistencyChecksService.runOnDemandChecks(true);
+ void consistencyChecksService.runOnDemandChecks(true);
}
async function rebuildIntegrationTestDatabase() {
diff --git a/apps/server/src/routes/api/files.ts b/apps/server/src/routes/api/files.ts
index 1484d26092..4cd5f438de 100644
--- a/apps/server/src/routes/api/files.ts
+++ b/apps/server/src/routes/api/files.ts
@@ -32,7 +32,7 @@ function updateFile(req: Request<{ noteId: string }>) {
note.setLabel("originalFileName", file.originalname);
- noteService.asyncPostProcessContent(note, file.buffer);
+ void noteService.asyncPostProcessContent(note, file.buffer);
return {
uploaded: true
diff --git a/apps/server/src/routes/api/link_embed.ts b/apps/server/src/routes/api/link_embed.ts
index 492a88a3b4..7399450cda 100644
--- a/apps/server/src/routes/api/link_embed.ts
+++ b/apps/server/src/routes/api/link_embed.ts
@@ -29,7 +29,7 @@ async function readResponseText(response: Response, maxBytes: number): Promise MAX_FAVICON_SIZE) {
- reader.cancel();
+ void reader.cancel();
return undefined;
}
chunks.push(value);
diff --git a/apps/server/src/routes/login.ts b/apps/server/src/routes/login.ts
index d184fdf77b..11f5215663 100644
--- a/apps/server/src/routes/login.ts
+++ b/apps/server/src/routes/login.ts
@@ -98,7 +98,7 @@ async function setPassword(req: Request, res: Response) {
*/
async function login(req: Request, res: Response) {
if (openID.isOpenIDEnabled()) {
- res.oidc.login({
+ void res.oidc.login({
returnTo: '/',
authorizationParams: {
prompt: 'consent',
@@ -176,7 +176,7 @@ function logout(req: Request, res: Response) {
req.session.loggedIn = false;
if (openID.isOpenIDEnabled() && openIDEncryption.isSubjectIdentifierSaved()) {
- res.oidc.logout({ returnTo: '/' });
+ void res.oidc.logout({ returnTo: '/' });
}
res.redirect('login');
diff --git a/apps/server/src/routes/mcp.ts b/apps/server/src/routes/mcp.ts
index 300c4df049..cd293af171 100644
--- a/apps/server/src/routes/mcp.ts
+++ b/apps/server/src/routes/mcp.ts
@@ -47,8 +47,8 @@ async function handleMcpRequest(req: express.Request, res: express.Response) {
});
res.on("close", () => {
- transport.close();
- server.close();
+ void transport.close();
+ void server.close();
});
await server.connect(transport);
diff --git a/apps/server/src/services/auth.spec.ts b/apps/server/src/services/auth.spec.ts
index 219cf49cb4..24b6f1cf2a 100644
--- a/apps/server/src/services/auth.spec.ts
+++ b/apps/server/src/services/auth.spec.ts
@@ -339,18 +339,18 @@ describe("Auth", () => {
expect(res.statusCode).toBe(401);
});
- it("checkCredentials walks DB/password/header/verification branches", () => {
+ it("checkCredentials walks DB/password/header/verification branches", async () => {
// DB not initialized -> 400
const dbSpy = vi.spyOn(sqlInit, "isDbInitialized").mockReturnValue(false);
const res1 = makeRes();
- auth.checkCredentials(makeReq(), res1 as never, vi.fn());
+ await auth.checkCredentials(makeReq(), res1 as never, vi.fn());
expect(res1.statusCode).toBe(400);
dbSpy.mockRestore();
// password not set -> 400
const unsetSpy = vi.spyOn(passwordService, "isPasswordSet").mockReturnValue(false);
const res2 = makeRes();
- auth.checkCredentials(makeReq(), res2 as never, vi.fn());
+ await auth.checkCredentials(makeReq(), res2 as never, vi.fn());
expect(res2.statusCode).toBe(400);
unsetSpy.mockRestore();
@@ -359,31 +359,64 @@ describe("Auth", () => {
// non-string trilium-cred header -> 400
const res3 = makeRes();
- auth.checkCredentials(makeReq({ headers: { "trilium-cred": ["a", "b"] } }), res3 as never, vi.fn());
+ await auth.checkCredentials(makeReq({ headers: { "trilium-cred": ["a", "b"] } }), res3 as never, vi.fn());
expect(res3.statusCode).toBe(400);
- // wrong password -> 401
- const verifySpy = vi.spyOn(passwordEncryptionService, "verifyPassword").mockReturnValue(false as never);
+ // wrong password -> 401. verifyPassword is async, so it's mocked to resolve a
+ // boolean and the call is awaited.
+ const verifySpy = vi.spyOn(passwordEncryptionService, "verifyPassword").mockResolvedValue(false as never);
const cred = Buffer.from("user:wrongpass").toString("base64");
const res4 = makeRes();
- auth.checkCredentials(makeReq({ headers: { "trilium-cred": cred } }), res4 as never, vi.fn());
+ await auth.checkCredentials(makeReq({ headers: { "trilium-cred": cred } }), res4 as never, vi.fn());
expect(res4.statusCode).toBe(401);
// The username before the colon is stripped; only the password is verified.
expect(verifySpy).toHaveBeenLastCalledWith("wrongpass");
// correct password (no colon in decoded cred path also exercised) -> next
- verifySpy.mockReturnValue(true as never);
+ verifySpy.mockResolvedValue(true as never);
const credNoColon = Buffer.from("justpassword").toString("base64");
const next = vi.fn();
- auth.checkCredentials(makeReq({ headers: { "trilium-cred": credNoColon } }), makeRes() as never, next);
+ await auth.checkCredentials(makeReq({ headers: { "trilium-cred": credNoColon } }), makeRes() as never, next);
expect(next).toHaveBeenCalled();
// No colon → the whole cred is treated as username and the password is "".
expect(verifySpy).toHaveBeenLastCalledWith("");
// missing trilium-cred header -> falls back to "" -> next (with verify mocked true)
const nextNoHeader = vi.fn();
- auth.checkCredentials(makeReq({ headers: {} }), makeRes() as never, nextNoHeader);
+ await auth.checkCredentials(makeReq({ headers: {} }), makeRes() as never, nextNoHeader);
expect(nextNoHeader).toHaveBeenCalled();
});
+
+ // verifyPassword is async, so its resolved value — not the Promise object — must drive
+ // the result. These two cases mock it the way it really behaves (resolving a boolean)
+ // and await the call, asserting the verification outcome actually gates the response;
+ // a synchronous mock would not exercise that.
+ it("checkCredentials rejects a password that fails async verification", async () => {
+ vi.spyOn(sqlInit, "isDbInitialized").mockReturnValue(true);
+ vi.spyOn(passwordService, "isPasswordSet").mockReturnValue(true);
+ vi.spyOn(passwordEncryptionService, "verifyPassword").mockResolvedValue(false as never);
+
+ const cred = Buffer.from("user:wrongpass").toString("base64");
+ const res = makeRes();
+ const next = vi.fn();
+
+ await auth.checkCredentials(makeReq({ headers: { "trilium-cred": cred } }), res as never, next);
+
+ expect(next).not.toHaveBeenCalled();
+ expect(res.statusCode).toBe(401);
+ });
+
+ it("checkCredentials calls next when async verification succeeds", async () => {
+ vi.spyOn(sqlInit, "isDbInitialized").mockReturnValue(true);
+ vi.spyOn(passwordService, "isPasswordSet").mockReturnValue(true);
+ vi.spyOn(passwordEncryptionService, "verifyPassword").mockResolvedValue(true as never);
+
+ const cred = Buffer.from("user:correctpass").toString("base64");
+ const next = vi.fn();
+
+ await auth.checkCredentials(makeReq({ headers: { "trilium-cred": cred } }), makeRes() as never, next);
+
+ expect(next).toHaveBeenCalled();
+ });
});
}, 60_000);
diff --git a/apps/server/src/services/auth.ts b/apps/server/src/services/auth.ts
index a29d285cc5..1b316b5d09 100644
--- a/apps/server/src/services/auth.ts
+++ b/apps/server/src/services/auth.ts
@@ -152,7 +152,7 @@ function reject(req: Request, res: Response, message: string) {
res.setHeader("Content-Type", "text/plain").status(401).send(message);
}
-function checkCredentials(req: Request, res: Response, next: NextFunction) {
+async function checkCredentials(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.setHeader("Content-Type", "text/plain").status(400).send("Database is not initialized yet.");
return;
@@ -174,7 +174,7 @@ function checkCredentials(req: Request, res: Response, next: NextFunction) {
const password = colonIndex === -1 ? "" : auth.substr(colonIndex + 1);
// username is ignored
- if (!passwordEncryptionService.verifyPassword(password)) {
+ if (!(await passwordEncryptionService.verifyPassword(password))) {
res.setHeader("Content-Type", "text/plain").status(401).send("Incorrect password");
getLog().info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
} else {
diff --git a/apps/server/src/services/image.ts b/apps/server/src/services/image.ts
index 7b099de9ba..e4da4bf776 100644
--- a/apps/server/src/services/image.ts
+++ b/apps/server/src/services/image.ts
@@ -8,24 +8,20 @@ import ocrService from "./ocr/ocr_service.js";
function scheduleOcrForNote(noteId: string) {
if (optionService.getOptionBool("ocrAutoProcessImages")) {
- setImmediate(async () => {
- try {
- await ocrService.processNoteOCR(noteId);
- } catch (error) {
+ setImmediate(() => {
+ void ocrService.processNoteOCR(noteId).catch((error) => {
getLog().error(`Failed to process OCR for note ${noteId}: ${error}`);
- }
+ });
});
}
}
function scheduleOcrForAttachment(attachmentId: string | undefined) {
if (attachmentId && optionService.getOptionBool("ocrAutoProcessImages")) {
- setImmediate(async () => {
- try {
- await ocrService.processAttachmentOCR(attachmentId);
- } catch (error) {
+ setImmediate(() => {
+ void ocrService.processAttachmentOCR(attachmentId).catch((error) => {
getLog().error(`Failed to process OCR for attachment ${attachmentId}: ${error}`);
- }
+ });
});
}
}
diff --git a/apps/server/src/services/ocr/ocr_service.spec.ts b/apps/server/src/services/ocr/ocr_service.spec.ts
index 8c21888413..22fdb51c9b 100644
--- a/apps/server/src/services/ocr/ocr_service.spec.ts
+++ b/apps/server/src/services/ocr/ocr_service.spec.ts
@@ -505,7 +505,7 @@ describe('OCRService', () => {
});
mockWorker.recognize.mockResolvedValue({ data: { text: 'text', confidence: 90, words: [] } });
- ocrService.startBatchProcessing();
+ void ocrService.startBatchProcessing();
const result = await ocrService.startBatchProcessing();
@@ -613,7 +613,7 @@ describe('OCRService', () => {
Array.from({ length: 10 }, (_, i) => ({ entityId: `note${i}`, mimeType: 'image/jpeg' }))
);
- ocrService.startBatchProcessing();
+ void ocrService.startBatchProcessing();
const progress = ocrService.getBatchProgress();
@@ -631,7 +631,7 @@ describe('OCRService', () => {
[{ entityId: 'note1', mimeType: 'image/jpeg' }]
);
- ocrService.startBatchProcessing();
+ void ocrService.startBatchProcessing();
expect(ocrService.getBatchProgress().inProgress).toBe(true);
diff --git a/apps/server/src/services/request.ts b/apps/server/src/services/request.ts
index f416698dae..0ff7630bd4 100644
--- a/apps/server/src/services/request.ts
+++ b/apps/server/src/services/request.ts
@@ -94,8 +94,9 @@ export default class NodeRequestProvider implements RequestProvider {
const proxyAgent = await getProxyAgent(opts);
const parsedTargetUrl = url.parse(opts.url);
+ const resolvedClient = await client;
- return new Promise(async (resolve, reject) => {
+ return new Promise((resolve, reject) => {
try {
const headers: Record = {
Cookie: (opts.cookieJar && opts.cookieJar.header) || "",
@@ -109,7 +110,7 @@ export default class NodeRequestProvider implements RequestProvider {
headers["trilium-cred"] = Buffer.from(`dummy:${opts.auth.password}`).toString("base64");
}
- const request = (await client).request({
+ const request = resolvedClient.request({
method: opts.method,
// url is used by electron net module
url: opts.url,
diff --git a/apps/server/src/services/safe_fetch.ts b/apps/server/src/services/safe_fetch.ts
index 7909dc0558..483c825238 100644
--- a/apps/server/src/services/safe_fetch.ts
+++ b/apps/server/src/services/safe_fetch.ts
@@ -112,7 +112,7 @@ function createPinnedLookup(validatedAddresses: dns.LookupAddress[]) {
function withDispatcherCleanup(response: Response, dispatcher: Agent): Response {
const originalBody = response.body;
if (!originalBody) {
- dispatcher.close();
+ void dispatcher.close();
return response;
}
@@ -120,7 +120,7 @@ function withDispatcherCleanup(response: Response, dispatcher: Agent): Response
const cleanup = () => {
if (!closed) {
closed = true;
- dispatcher.close();
+ void dispatcher.close();
}
};
@@ -141,7 +141,7 @@ function withDispatcherCleanup(response: Response, dispatcher: Agent): Response
}
},
cancel() {
- reader.cancel();
+ void reader.cancel();
cleanup();
}
});
@@ -187,7 +187,7 @@ async function safeFetch(url: string, options: RequestInit = {}): Promise {
- const message = JSON.parse(messageJson as any);
+ ws.on("message", (messageJson) => {
+ void (async () => {
+ const message = JSON.parse(messageJson as any);
- if (this.clientMessageHandler) {
- await this.clientMessageHandler(id, message);
- }
+ if (this.clientMessageHandler) {
+ await this.clientMessageHandler(id, message);
+ }
+ })();
});
ws.on("close", () => {
diff --git a/apps/server/src/share/sql.ts b/apps/server/src/share/sql.ts
index 8d41210c76..e14483ec6a 100644
--- a/apps/server/src/share/sql.ts
+++ b/apps/server/src/share/sql.ts
@@ -27,7 +27,7 @@ function resolveDbPath(): string | null {
return null;
}
-sql_init.dbReady.then(() => {
+void sql_init.dbReady.then(() => {
const dbPath = resolveDbPath();
if (!dbPath) {
return;
diff --git a/apps/server/src/sql_provider.ts b/apps/server/src/sql_provider.ts
index 8195e08060..fb29a2faa3 100644
--- a/apps/server/src/sql_provider.ts
+++ b/apps/server/src/sql_provider.ts
@@ -43,7 +43,7 @@ export default class BetterSqlite3Provider implements DatabaseProvider {
unlinkSync(destinationFile);
} catch (e) { } // unlink throws exception if the file did not exist
- this.dbConnection?.backup(destinationFile);
+ void this.dbConnection?.backup(destinationFile);
}
prepare(query: string): Statement {
diff --git a/apps/server/src/zip_provider.ts b/apps/server/src/zip_provider.ts
index 973ca5128a..17924c3642 100644
--- a/apps/server/src/zip_provider.ts
+++ b/apps/server/src/zip_provider.ts
@@ -51,18 +51,20 @@ export default class NodejsZipProvider implements ZipProvider {
}
zipfile.readEntry();
});
- zipfile.on("end", async () => {
- if (samples.length === 0) {
- return res("utf-8");
- }
- const combined = Buffer.concat(samples);
- try {
- const chardet = await import("chardet");
- const detected = chardet.default.detect(combined);
- res(detected || "utf-8");
- } catch {
- res("utf-8");
- }
+ zipfile.on("end", () => {
+ void (async () => {
+ if (samples.length === 0) {
+ return res("utf-8");
+ }
+ const combined = Buffer.concat(samples);
+ try {
+ const chardet = await import("chardet");
+ const detected = chardet.default.detect(combined);
+ res(detected || "utf-8");
+ } catch {
+ res("utf-8");
+ }
+ })();
});
zipfile.on("error", rej);
});
@@ -95,33 +97,35 @@ export default class NodejsZipProvider implements ZipProvider {
if (!zipfile) { rej(new Error("Unable to read zip file.")); return; }
zipfile.readEntry();
- zipfile.on("entry", async (entry: yauzl.Entry) => {
- try {
- // yauzl with decodeStrings: false returns fileName as a Buffer.
- // Use the detected encoding for non-UTF-8-flagged entries,
- // falling back to UTF-8.
- let fileName: string;
- if (Buffer.isBuffer(entry.fileName)) {
- const isUtf8Flagged = !!(entry.generalPurposeBitFlag & 0x800);
- const encoding = isUtf8Flagged ? "utf-8" : (filenameEncoding || "utf-8");
- fileName = decodeBuffer(entry.fileName as Buffer, encoding);
- } else {
- fileName = entry.fileName;
- }
+ zipfile.on("entry", (entry: yauzl.Entry) => {
+ void (async () => {
+ try {
+ // yauzl with decodeStrings: false returns fileName as a Buffer.
+ // Use the detected encoding for non-UTF-8-flagged entries,
+ // falling back to UTF-8.
+ let fileName: string;
+ if (Buffer.isBuffer(entry.fileName)) {
+ const isUtf8Flagged = !!(entry.generalPurposeBitFlag & 0x800);
+ const encoding = isUtf8Flagged ? "utf-8" : (filenameEncoding || "utf-8");
+ fileName = decodeBuffer(entry.fileName as Buffer, encoding);
+ } else {
+ fileName = entry.fileName;
+ }
- const readContent = () => new Promise((res, rej) => {
- zipfile.openReadStream(entry, (err, readStream) => {
- if (err) { rej(err); return; }
- if (!readStream) { rej(new Error("Unable to read content.")); return; }
- streamToBuffer(readStream).then(res, rej);
+ const readContent = () => new Promise((res, rej) => {
+ zipfile.openReadStream(entry, (err, readStream) => {
+ if (err) { rej(err); return; }
+ if (!readStream) { rej(new Error("Unable to read content.")); return; }
+ streamToBuffer(readStream).then(res, rej);
+ });
});
- });
- await processEntry({ fileName }, readContent);
- } catch (e) {
- rej(e);
- }
- zipfile.readEntry();
+ await processEntry({ fileName }, readContent);
+ } catch (e) {
+ rej(e);
+ }
+ zipfile.readEntry();
+ })();
});
zipfile.on("end", res);
zipfile.on("error", rej);
diff --git a/eslint.config.mjs b/eslint.config.mjs
index b3ee4da530..a2f5cc84e6 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -49,7 +49,23 @@ const mainConfig = [
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
- ]
+ ],
+ // Catch mishandled promises at the type level. `no-misused-promises` flags a Promise used
+ // where a non-Promise is expected (e.g. a boolean conditional or a void-returning callback);
+ // `no-floating-promises` flags promises whose result/rejection is silently discarded.
+ // Warn repo-wide (large existing backlog, mostly in apps/client); errored on the server below.
+ "@typescript-eslint/no-misused-promises": "warn",
+ "@typescript-eslint/no-floating-promises": "warn"
+ }
+ },
+ {
+ // The server is the security-sensitive surface (auth, sync, API), so enforce the promise
+ // rules as errors here — an unawaited async call in a conditional or callback can silently
+ // change control flow.
+ files: ["apps/server/**/*.{js,ts,mjs,cjs,tsx}"],
+ rules: {
+ "@typescript-eslint/no-misused-promises": "error",
+ "@typescript-eslint/no-floating-promises": "error"
}
},
{