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 WORKDIR /app
FROM base AS dependencies FROM base AS dependencies
+1 -1
View File
@@ -37,7 +37,7 @@ export default defineConfig(
linterOptions: { reportUnusedDisableDirectives: 'error' }, linterOptions: { reportUnusedDisableDirectives: 'error' },
settings: { settings: {
'import-x/resolver-next': [createTypeScriptImportResolver()], '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: { rules: {
'curly': ['error', 'multi-line'], 'curly': ['error', 'multi-line'],
+1 -1
View File
@@ -75,7 +75,7 @@
"typescript-eslint": "8.70.1" "typescript-eslint": "8.70.1"
}, },
"engines": { "engines": {
"node": "v24.21.0" "node": "v24.22.1"
} }
}, },
"node_modules/@angular-devkit/schematics": { "node_modules/@angular-devkit/schematics": {
+1 -1
View File
@@ -6,7 +6,7 @@
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"engines": { "engines": {
"node": "v24.21.0" "node": "v24.22.1"
}, },
"scripts": { "scripts": {
"build": "prisma generate && nest build", "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 EXPECTED_ROTATED_SESSIONS = 1;
const PASSWORD_HASH_ROUNDS = 12; 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 ACCESS_TTL = '15m';
const REFRESH_TTL = '7d'; const REFRESH_TTL = '7d';
@@ -22,7 +22,7 @@ import {
import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator'; import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator';
import { HttpErrorCode } from '../../../common/error-code'; 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 { AccessPayload, AuthenticationResult } from '../application/auth.types';
import { LoginDto, RegisterDto } from './auth.dto'; import { LoginDto, RegisterDto } from './auth.dto';
import { JwtAuthGuard } from './jwt-auth.guard'; import { JwtAuthGuard } from './jwt-auth.guard';
@@ -134,6 +134,7 @@ export class AuthController {
sameSite: 'lax' as const, sameSite: 'lax' as const,
secure: process.env['NODE_ENV'] !== 'development', secure: process.env['NODE_ENV'] !== 'development',
path: COOKIE_PATH, path: COOKIE_PATH,
maxAge: REFRESH_TTL_MS,
}; };
} }
} }
+12 -5
View File
@@ -131,6 +131,9 @@ describe('Authentication (e2e)', () => {
.filter((attribute) => !attribute.startsWith('Expires=')) .filter((attribute) => !attribute.startsWith('Expires='))
.sort((a, b) => a.localeCompare(b)); .sort((a, b) => a.localeCompare(b));
const host = ['HttpOnly', 'Path=/api/v1/auth', 'SameSite=Lax']; 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 login = async () => {
const registration = await request(app.getHttpServer()) const registration = await request(app.getHttpServer())
.post('/api/v1/auth/register') .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 () => { await inEnvironment('production', async () => {
const issued = await login(); const issued = await login();
const rotated = await request(app.getHttpServer()) const rotated = await request(app.getHttpServer())
@@ -162,16 +165,20 @@ describe('Authentication (e2e)', () => {
.set('Cookie', rotated.headers['set-cookie'][0]) .set('Cookie', rotated.headers['set-cookie'][0])
.expect(204); .expect(204);
// Exact set: any added Domain/Max-Age or a dropped flag fails. // Exact set: any added Domain, changed Max-Age or dropped flag fails.
for (const cookie of [issued, rotated.headers['set-cookie'][0], cleared.headers['set-cookie'][0]]) { for (const cookie of [issued, rotated.headers['set-cookie'][0]]) {
expect(attributes(cookie)).toEqual([...host, 'Secure'].sort((a, b) => a.localeCompare(b))); 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 () => { it('drops only Secure in development and keeps every other attribute', async () => {
await inEnvironment('development', async () => { await inEnvironment('development', async () => {
expect(attributes(await login())).toEqual(host); expect(attributes(await login())).toEqual(sorted(persistent));
}); });
}); });
}); });