2 Commits
Author SHA1 Message Date
ksv741 ec934a34b5 chore: bump Node to 24.22.1 (WealthLedger/backend#122)
Backend verify / commits (pull_request) Successful in 28s
Backend verify / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Successful in 4m58s
Fixes the better-sqlite3 GC abort (nodejs/node#63985) in Node
24.19.0-24.21.x. Keeps all repositories on the same Node version.
2026-10-23 08:15:41 +03:00
ksv741 4ae2db225d fix: persist refresh cookie across browser restarts (WealthLedger/backend#123)
The refresh cookie had no Max-Age, so browsers dropped it on close
while the Session stayed valid for 7 days. Set maxAge from
REFRESH_TTL_MS, the same constant behind Session.expiresAt.
2026-10-23 08:15:41 +03:00
8 changed files with 20 additions and 12 deletions
+1 -1
View File
@@ -1 +1 @@
24.21.0
24.22.1
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24.21.0-bookworm-slim AS base
FROM node:24.22.1-bookworm-slim AS base
WORKDIR /app
FROM base AS dependencies
+1 -1
View File
@@ -37,7 +37,7 @@ export default defineConfig(
linterOptions: { reportUnusedDisableDirectives: 'error' },
settings: {
'import-x/resolver-next': [createTypeScriptImportResolver()],
'node': { tryExtensions: ['.ts', '.js', '.json', '.node'], version: '24.21.0' },
'node': { tryExtensions: ['.ts', '.js', '.json', '.node'], version: '24.22.1' },
},
rules: {
'curly': ['error', 'multi-line'],
+1 -1
View File
@@ -75,7 +75,7 @@
"typescript-eslint": "8.70.1"
},
"engines": {
"node": "v24.21.0"
"node": "v24.22.1"
}
},
"node_modules/@angular-devkit/schematics": {
+1 -1
View File
@@ -6,7 +6,7 @@
"private": true,
"license": "UNLICENSED",
"engines": {
"node": "v24.21.0"
"node": "v24.22.1"
},
"scripts": {
"build": "prisma generate && nest build",
+1 -1
View File
@@ -17,7 +17,7 @@ import { AuthenticationResult, AuthUser, RefreshPayload } from './auth.types';
const EXPECTED_ROTATED_SESSIONS = 1;
const PASSWORD_HASH_ROUNDS = 12;
const REFRESH_TTL_MS = 604_800_000;
export const REFRESH_TTL_MS = 604_800_000;
const ACCESS_TTL = '15m';
const REFRESH_TTL = '7d';
@@ -22,7 +22,7 @@ import {
import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator';
import { HttpErrorCode } from '../../../common/error-code';
import { AuthService } from '../application/auth.service';
import { AuthService, REFRESH_TTL_MS } from '../application/auth.service';
import { AccessPayload, AuthenticationResult } from '../application/auth.types';
import { LoginDto, RegisterDto } from './auth.dto';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -134,6 +134,7 @@ export class AuthController {
sameSite: 'lax' as const,
secure: process.env['NODE_ENV'] !== 'development',
path: COOKIE_PATH,
maxAge: REFRESH_TTL_MS,
};
}
}
+12 -5
View File
@@ -131,6 +131,9 @@ describe('Authentication (e2e)', () => {
.filter((attribute) => !attribute.startsWith('Expires='))
.sort((a, b) => a.localeCompare(b));
const host = ['HttpOnly', 'Path=/api/v1/auth', 'SameSite=Lax'];
// Persistent for the Session lifetime (7 days), otherwise the browser drops it on restart.
const persistent = [...host, 'Max-Age=604800'];
const sorted = (items: string[]) => [...items].sort((a, b) => a.localeCompare(b));
const login = async () => {
const registration = await request(app.getHttpServer())
.post('/api/v1/auth/register')
@@ -150,7 +153,7 @@ describe('Authentication (e2e)', () => {
}
};
it('issues, rotates and clears it HttpOnly, Lax, path-scoped, host-only and Secure in production', async () => {
it('issues and rotates it persistent (Max-Age) HttpOnly, Lax, path-scoped, host-only and Secure in production, and clears it', async () => {
await inEnvironment('production', async () => {
const issued = await login();
const rotated = await request(app.getHttpServer())
@@ -162,16 +165,20 @@ describe('Authentication (e2e)', () => {
.set('Cookie', rotated.headers['set-cookie'][0])
.expect(204);
// Exact set: any added Domain/Max-Age or a dropped flag fails.
for (const cookie of [issued, rotated.headers['set-cookie'][0], cleared.headers['set-cookie'][0]]) {
expect(attributes(cookie)).toEqual([...host, 'Secure'].sort((a, b) => a.localeCompare(b)));
// Exact set: any added Domain, changed Max-Age or dropped flag fails.
for (const cookie of [issued, rotated.headers['set-cookie'][0]]) {
expect(attributes(cookie)).toEqual(sorted([...persistent, 'Secure']));
}
// Clearing must not carry Max-Age; Express expires it with a past Expires.
const clearedCookie = cleared.headers['set-cookie'][0];
expect(attributes(clearedCookie)).toEqual(sorted([...host, 'Secure']));
expect(clearedCookie).toContain('Expires=Thu, 01 Jan 1970');
});
});
it('drops only Secure in development and keeps every other attribute', async () => {
await inEnvironment('development', async () => {
expect(attributes(await login())).toEqual(host);
expect(attributes(await login())).toEqual(sorted(persistent));
});
});
});