This commit is contained in:
2025-07-15 11:23:20 +09:00
parent 368c52029a
commit 8fd4066d42
27 changed files with 5091 additions and 1847 deletions

View File

@@ -6,7 +6,8 @@
"": {
"name": "functions",
"dependencies": {
"firebase-admin": "^13.3.0",
"@sendgrid/mail": "^8.1.5",
"firebase-admin": "^13.4.0",
"firebase-functions": "^6.3.2"
},
"devDependencies": {
@@ -1995,6 +1996,44 @@
"dev": true,
"license": "MIT"
},
"node_modules/@sendgrid/client": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.5.tgz",
"integrity": "sha512-Jqt8aAuGIpWGa15ZorTWI46q9gbaIdQFA21HIPQQl60rCjzAko75l3D1z7EyjFrNr4MfQ0StusivWh8Rjh10Cg==",
"license": "MIT",
"dependencies": {
"@sendgrid/helpers": "^8.0.0",
"axios": "^1.8.2"
},
"engines": {
"node": ">=12.*"
}
},
"node_modules/@sendgrid/helpers": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
"integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/@sendgrid/mail": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.5.tgz",
"integrity": "sha512-W+YuMnkVs4+HA/bgfto4VHKcPKLc7NiZ50/NH2pzO6UHCCFuq8/GNB98YJlLEr/ESDyzAaDr7lVE7hoBwFTT3Q==",
"license": "MIT",
"dependencies": {
"@sendgrid/client": "^8.1.5",
"@sendgrid/helpers": "^8.0.0"
},
"engines": {
"node": ">=12.*"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -2869,8 +2908,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
@@ -2888,6 +2926,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -3394,7 +3459,6 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"optional": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -3604,9 +3668,7 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3652,7 +3714,6 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.4.0"
}
@@ -3912,7 +3973,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -4654,9 +4714,9 @@
}
},
"node_modules/firebase-admin": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.3.0.tgz",
"integrity": "sha512-MFxv86Aw2rjM/TpKwU86jN7YUFfN1jy6mREYZTLL1aW1rCpZFi4c70b9U12J9Xa4RbJkiXpWBAwth9IVSqR91A==",
"version": "13.4.0",
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.4.0.tgz",
"integrity": "sha512-Y8DcyKK+4pl4B93ooiy1G8qvdyRMkcNFfBSh+8rbVcw4cW8dgG0VXCCTp5NUwub8sn9vSPsOwpb9tE2OuFmcfQ==",
"license": "Apache-2.0",
"dependencies": {
"@fastify/busboy": "^3.0.0",
@@ -4743,6 +4803,26 @@
"license": "ISC",
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -5249,7 +5329,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -7819,6 +7898,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@@ -15,7 +15,8 @@
},
"main": "lib/index.js",
"dependencies": {
"firebase-admin": "^13.3.0",
"@sendgrid/mail": "^8.1.5",
"firebase-admin": "^13.4.0",
"firebase-functions": "^6.3.2"
},
"devDependencies": {

View File

@@ -4,16 +4,19 @@ import { initializeApp, getApps } from 'firebase-admin/app';
import * as cookie from 'cookie';
import { logger } from 'firebase-functions/v2';
import { cookieExpiration, corsMiddlewareHandler } from '../config';
// ✅ Admin SDK init
if (!getApps().length) {
initializeApp(); // ✅ auto uses service account in Firebase Functions
initializeApp();
}
export const createSession = onRequest(
{ region: 'asia-northeast3' },
async (req, res) => {
return corsMiddlewareHandler(req, res, async () => {
logger.info('countBoards: CORSenabled function called');
// Fix this logger message: it should be about createSession, not countBoards
logger.info('createSession: CORS-enabled function called');
if (req.method !== 'POST') {
res.status(405).send('Method not allowed');
return;
@@ -38,7 +41,7 @@ export const createSession = onRequest(
httpOnly: true,
secure: true,
maxAge: expiresIn / 1000,
sameSite: 'lax' as const,
sameSite: 'none' as const,
path: '/',
};
@@ -48,6 +51,10 @@ export const createSession = onRequest(
);
res.status(200).json({ success: true });
} catch (error) {
logger.error(
'createSession: Invalid token or session cookie creation failed',
error,
); // Added more specific logging
res.status(401).send('Invalid token');
}
});

View File

@@ -1,11 +1,11 @@
// In your verifySession.ts (the API endpoint Cloud Function)
import { onRequest } from 'firebase-functions/v2/https';
import * as cookie from 'cookie';
import { getAuth } from 'firebase-admin/auth';
import { initializeApp, getApps } from 'firebase-admin/app';
import { corsMiddlewareHandler } from '../config';
import { logger } from 'firebase-functions/v2';
import { logger } from 'firebase-functions/v2'; // Ensure logger is imported
// ✅ Ensure Admin SDK is initialized
if (!getApps().length) {
initializeApp();
}
@@ -14,17 +14,31 @@ export const verifySession = onRequest(
{ region: 'asia-northeast3' },
async (req, res) => {
return corsMiddlewareHandler(req, res, async () => {
logger.info('verifySession: Function invoked');
logger.info('verifySession: Function invoked'); // This log is already there
// --- ADD THESE LOGS ---
logger.info(
'verifySession (endpoint): Raw headers.cookie:',
req.headers.cookie,
);
const cookies = cookie.parse(req.headers.cookie || '');
logger.info('verifySession (endpoint): Parsed cookies object:', cookies);
const session = cookies.__session;
logger.info(
'verifySession (endpoint): __session cookie value:',
session ? 'Found' : 'Not Found',
);
// --- END ADDED LOGS ---
if (req.method !== 'GET') {
res.status(405).send('Method Not Allowed');
return;
}
const cookies = cookie.parse(req.headers.cookie || '');
const session = cookies.__session;
if (!session) {
logger.warn(
'verifySession (endpoint): No __session cookie found, returning 401.',
); // Specific warn
res.status(401).send({ message: 'No session cookie' });
return;
}
@@ -32,14 +46,22 @@ export const verifySession = onRequest(
try {
const decoded = await getAuth().verifySessionCookie(session, true);
const role = decoded.role || 0;
logger.info(
'verifySession (endpoint): Session verified successfully for UID:',
decoded.uid,
); // Log success
res.status(200).json({
uid: decoded.uid,
email: decoded.email,
role,
});
} catch (err) {
logger.warn('verifySession: Session invalid or expired', err);
} catch (err: unknown) {
// Use any for err to access message
logger.warn(
'verifySession (endpoint): Session invalid or expired, returning 401:',
err instanceof Error ? err.message : String(err),
); // Specific warn
res.status(401).send({ message: 'Invalid session' });
}
});

View File

@@ -0,0 +1,73 @@
import { onRequest } from 'firebase-functions/v2/https';
import { logger } from 'firebase-functions/v2';
import { defineSecret } from 'firebase-functions/params';
import { initializeApp, getApps } from 'firebase-admin/app';
import sgMail from '@sendgrid/mail';
import type { BoardAccessMode } from '../types/boardItem';
import { corsMiddlewareHandler, SendEmail } from '../config';
/* ─── Boiler-plate init ───────────────────────────────────────── */
if (!getApps().length) initializeApp();
const SENDGRID_API_KEY = defineSecret('SENDGRID_API_KEY');
let isSendGridInitialized = false;
/* ─── Cloud Function entry ────────────────────────────────────── */
export const boardMail = onRequest(
{
region: 'asia-northeast3',
secrets: [SENDGRID_API_KEY], // ❷ declare dependency
},
async (req, res) => {
return corsMiddlewareHandler(req, res, async () => {
/* one-time SendGrid init per cold start */
if (!isSendGridInitialized) {
const apiKey = process.env.SENDGRID_API_KEY;
if (!apiKey) {
logger.error('SendGrid API key is undefined. Check secret setup.');
throw new Error('Missing SendGrid API key');
}
sgMail.setApiKey(apiKey);
isSendGridInitialized = true;
}
/* 1. Method guard --------------------------------------------------- */
if (req.method !== 'POST') {
res.status(405).send({ message: 'Method Not Allowed' });
return;
}
/* 2. Extract params ------------------------------------------------- */
const { access = 'public', subject, html, action = '' } = req.body ?? {};
if (!['public', 'private', 'admin'].includes(access)) {
logger.warn(`Invalid access mode: ${access}`);
res.status(400).send({ message: 'Bad access mode' });
return;
}
const effectiveAccess = access as BoardAccessMode; // ❹ always public/param
logger.info('boardMail: effectiveAccess', effectiveAccess);
if (!subject || !html) {
res.status(400).send({ message: 'Missing subject or html' });
return;
}
try {
await sgMail.send({
to: SendEmail.to,
from: SendEmail.from,
subject,
html,
});
logger.info(
`[boardMail] Email sent successfully via SendGrid [${action}]`,
);
res.status(200).send({ message: 'Email sent' });
} catch (err) {
logger.error('[boardMail] Error sending email', err as Error);
res.status(500).send({ message: 'Internal Server Error' });
}
});
},
);

View File

@@ -26,32 +26,22 @@ export const fetchBoards = onRequest(
return corsMiddlewareHandler(request, response, async () => {
logger.info('fetchBoards: CORSenabled function called');
/* 1.Method guard ------------------------------------------------------------------ */
/* 1.Method guard */
if (request.method !== 'GET') {
response.status(405).send({ message: 'Method Not Allowed' });
return;
}
/* 2.Auth (optional) ---------------------------------------------------------------- */
let userRole = 0;
let userId: string | null = null;
const authUser = await verifySessionFromRequest(request);
if (authUser) {
userId = authUser.uid;
userRole = authUser.role;
}
/* 3.Read & validate query params --------------------------------------------------- */
/* 2.Read & validate query params EARLY */
const {
collection = '',
sortOrder = 'desc',
itemsPerPage = '10',
access = 'public',
access = 'public', // Default to 'public' if not provided
pageToken,
} = request.query as Record<string, string>;
// ❶collection allowlist
// ❶ collection allow-list
if (
!ALLOWED_COLLECTIONS.has(
collection as unknown as typeof ALLOWED_COLLECTIONS extends Set<
@@ -65,25 +55,45 @@ export const fetchBoards = onRequest(
return;
}
// ❷access must be one of three strings
// ❷ access must be one of three strings. Validate BEFORE auth for basic correctness.
if (!['public', 'private', 'admin'].includes(access)) {
response.status(400).send({ message: 'Bad access mode' });
return;
}
/* 3. Authentication and Authorization Check */
let userRole = 0;
let userId: string | null = null;
let authUser = null; // Initialize authUser to null
try {
authUser = await verifySessionFromRequest(request);
} catch (err) {
logger.warn('fetchBoards: Session verification failed', err);
}
if (authUser) {
userId = authUser.uid;
userRole = authUser.role;
}
// If user is NOT logged in (userId is null) AND they are requesting 'private' or 'admin' access
if (!userId && (access === 'private' || access === 'admin')) {
response
.status(401)
.send({ message: 'Unauthorized: Login required for this resource.' });
return; // Function stops here.
}
// ❸numeric arguments
const limit = Math.max(1, Math.min(Number(itemsPerPage) || 10, 100));
const order = sortOrder === 'asc' ? 'asc' : 'desc';
/* 4.Build query with access control ----------------------------------------------- */
const accessParam = (access ?? 'public') as BoardAccessMode;
const effectiveAccess = userId ? accessParam : 'public'; // force public if anonymous
let queryRef: Query<DocumentData>;
try {
queryRef = buildQueryWithAccessControl(
db.collection(collection),
effectiveAccess,
access as BoardAccessMode, // Use the original 'access'
userId,
userRole,
);
@@ -141,7 +151,7 @@ export const fetchBoards = onRequest(
)
: null;
if (effectiveAccess === 'public')
if (access === 'public')
response.set('Cache-Control', 'public,max-age=60');
response.status(200).send({ items, nextPageToken });

View File

@@ -0,0 +1,112 @@
import { onRequest } from 'firebase-functions/v2/https';
import { logger } from 'firebase-functions/v2';
import { getFirestore } from 'firebase-admin/firestore';
import { initializeApp, getApps } from 'firebase-admin/app';
import type { BoardItem, BoardAccessMode } from '../types/boardItem';
import { corsMiddlewareHandler, ALLOWED_COLLECTIONS } from '../config';
import {
isValidBoardItem,
verifySessionFromRequest,
buildQueryWithAccessControl,
} from '../utils';
if (!getApps().length) {
initializeApp();
}
const db = getFirestore();
export const fetchSingleItem = onRequest(
{ region: 'asia-northeast3' },
async (request, response) => {
return corsMiddlewareHandler(request, response, async () => {
logger.info('fetchSingleItem: CORS-enabled function called');
/* 1.Method guard ------------------------------------------------------------------ */
if (request.method !== 'GET') {
response.status(405).send({ message: 'Method Not Allowed' });
return;
}
// 2. Auth (optional)
let userRole = 0;
let userId: string | null = null;
const authUser = await verifySessionFromRequest(request);
if (authUser) {
userId = authUser.uid;
userRole = authUser.role;
}
// 3. Query params
const {
collection = '',
docId = '',
access = 'public',
} = request.query as Record<string, string>;
if (!collection || !docId) {
response.status(400).send({ message: 'Missing parameters' });
return;
}
// ❶collection allowlist
if (
!ALLOWED_COLLECTIONS.has(
collection as unknown as typeof ALLOWED_COLLECTIONS extends Set<
infer U
>
? U
: never,
)
) {
response.status(400).send({ message: 'Invalid collection' });
return;
}
if (!['public', 'private', 'admin'].includes(access)) {
response.status(400).send({ message: 'Bad access mode' });
return;
}
const accessParam = (access ?? 'public') as BoardAccessMode;
const effectiveAccess = userId ? accessParam : 'public'; // force public if anonymous
try {
// Build query with access control
const query = buildQueryWithAccessControl(
db.collection(collection),
effectiveAccess,
userId,
userRole,
).where('__name__', '==', docId);
const snap = await query.limit(1).get();
if (snap.empty) {
response
.status(404)
.send({ message: 'Item not found or access denied' });
return;
}
const docSnap = snap.docs[0];
const data = docSnap.data() as BoardItem;
if (!isValidBoardItem(data)) {
response.status(500).send({ message: 'Invalid item data' });
return;
}
if (access === 'public') {
response.set('Cache-Control', 'public,max-age=60');
}
response.status(200).send({ ...data, docId: docSnap.id });
return;
} catch (err) {
logger.error('fetchSingleItem: Firestore error', err);
response.status(500).send({ message: 'Internal Server Error' });
}
});
},
);

View File

@@ -1,2 +1,4 @@
export * from './countBoard';
export * from './fetchBoard';
export * from './fetchSingleItem';
export * from './boardMail';

View File

@@ -82,6 +82,7 @@ const MAX_FILE_SIZE_MB = 20;
const MAX_TOTAL_FILES = 10;
export const ALLOWED_COLLECTIONS = new Set([
'notices',
'wadizes' /* … */,
'projects' /* … */,
] as const);
const ALLOWED_IMAGE_TYPES = [
@@ -123,6 +124,12 @@ const ALLOWED_FILE_TYPES = [
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
export const SendEmail = {
to: 'bobu1104@naver.com',
from: 'movemama@manos.kr',
subject: '[보부웹앱] - 새로운 신청',
};
const isImageFile = (file: File): boolean =>
ALLOWED_IMAGE_TYPES.includes(file.type);
const isVideoFile = (file: File): boolean =>

View File

@@ -111,6 +111,23 @@ export type BoardItem = {
thumbnail?: ImageItem; //Leave it for previous data structure
};
// Board : Types
export interface WadizBoard extends BoardItem {
name: string;
paymentId: string;
email: string;
phone: string;
address: string;
emergencyPhone: string;
scheduleStart?: string | Date;
scheduleEnd?: string | Date;
attending: 'yes' | 'no';
altName?: string;
altPhone?: string;
companions?: string;
healthNotes?: string;
remarks?: string;
}
export interface ProjectBoard extends BoardItem {
subtitle: string;
displayDate?: string;

View File

@@ -1,33 +1,60 @@
// Your verifyHelper.ts (or wherever this function is)
import { getAuth } from 'firebase-admin/auth';
import type { Request } from 'express';
import * as cookie from 'cookie';
import { logger } from 'firebase-functions/v2'; // <--- Make sure you import logger here
export type DecodedAuthInfo = {
uid: string;
role: number;
};
/**
* Verifies the session cookie from the request and returns the decoded auth info.
* Throws an error if invalid or missing.
* @param {Request} req - The Express request object containing the session cookie
* @param {Response} [res] - Optional Express response object for sending error responses
*/
export async function verifySessionFromRequest(
req: Request,
): Promise<DecodedAuthInfo | null> {
// --- ADD THESE LOGS ---
logger.info('verifySessionFromRequest: STARTING verification');
logger.info(
'verifySessionFromRequest: Raw headers.cookie:',
req.headers.cookie,
); // Log the raw cookie header
// --- END ADDED LOGS ---
const cookies = cookie.parse(req.headers.cookie || '');
// --- ADD THESE LOGS ---
logger.info('verifySessionFromRequest: Parsed cookies object:', cookies); // Log the parsed cookies object
// --- END ADDED LOGS ---
const session = cookies.__session;
// --- ADD THESE LOGS ---
logger.info(
'verifySessionFromRequest: __session cookie value:',
session ? 'Found (length: ' + session.length + ')' : 'Not Found',
);
// --- END ADDED LOGS ---
if (!session) {
logger.warn('verifySessionFromRequest: No __session cookie found.'); // Specific log for missing cookie
return null;
}
try {
const decoded = await getAuth().verifySessionCookie(session, true);
const role = (decoded.role || 0) as number;
logger.info(
'verifySessionFromRequest: Session cookie decoded successfully for UID:',
decoded.uid,
); // Log success
return { uid: decoded.uid, role };
} catch (err) {
console.error('[Auth] Session cookie verification failed:', err);
} catch (err: unknown) {
// Use 'any' type for err to easily access its properties
logger.error(
'[Auth] Session cookie verification failed:',
err instanceof Error ? err.message : String(err),
); // Log the actual error message
return null;
}
}