refactor(server): move the stateless helpers into nest/common (#1823)

Eight files in services/ hold no state and touch no database: avatarUrl,
conflictResult, demo, passwordPolicy, timezoneService, cookie, and the two
crypto helpers. They are imported from 65 places, 62 of them already inside
src/nest, so having them live in the legacy layer was pure inertia.

They stay plain functions rather than becoming providers. Three importers sit
outside the container and would have no way to reach an injectable one:
db/migrations.ts runs before the app is built, and middleware/auth.ts and
middleware/mfaPolicy.ts run beside it. Wrapping these in DI would buy ceremony
and nothing else.

mfaCrypto and apiKeyCrypto move together into common/crypto — they read the
same ENCRYPTION_KEY, and the directory says so.

services/ drops from 44 files to 36, 9,870 lines to 8,493.

One trap worth recording: src/app-config and src/nest/app-config both exist.
cookie.ts imported '../app-config' from services/, which resolved to the former;
after the move the same string silently pointed at the latter, which has no
readEnv. It is now explicit about which one it wants.

Nothing else changes — no signatures, no behaviour, no tests rewritten beyond
their import paths.
This commit is contained in:
Maurice
2026-08-07 15:14:14 +02:00
committed by GitHub
parent 3dfbb3fd02
commit 6dda05f672
84 changed files with 103 additions and 103 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import { readEnv } from '../app-config';
import { encrypt_api_key } from '../services/apiKeyCrypto';
import { encrypt_api_key } from '../nest/common/crypto/apiKeyCrypto';
import Database from 'better-sqlite3';
import fs from 'fs';
+1 -1
View File
@@ -5,7 +5,7 @@ import { db } from '../db/database';
import { JWT_SECRET } from '../config';
import { AuthRequest, OptionalAuthRequest, User } from '../types';
import { applyIdempotency } from './idempotency';
import { isDemoEmail } from '../services/demo';
import { isDemoEmail } from '../nest/common/demo';
export function extractToken(req: Request): string | null {
// Prefer httpOnly cookie; fall back to Authorization: Bearer (MCP, API clients)
+1 -1
View File
@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from 'express';
import { readEnv } from '../app-config';
import { db } from '../db/database';
import { extractToken, verifyJwtAndLoadUser } from './auth';
import { DEMO_EMAILS } from '../services/demo';
import { DEMO_EMAILS } from '../nest/common/demo';
/** Paths that never require MFA (public or pre-auth). */
export function isPublicApiPath(method: string, pathNoQuery: string): boolean {
+3 -3
View File
@@ -15,11 +15,11 @@ import { revokeUserSessions, revokeUserSessionsForClient } from '../../mcp/sessi
import { invalidateMcpSessions } from '../../mcp';
import { emitUserDeleted } from '../../plugin-user-lifecycle';
import type { User, Addon } from '../../types';
import { maybe_encrypt_api_key, decrypt_api_key } from '../../services/apiKeyCrypto';
import { avatarUrl } from '../../services/avatarUrl';
import { maybe_encrypt_api_key, decrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { avatarUrl } from '../common/avatarUrl';
import { prepareLlmAddonConfigForWrite, maskLlmAddonConfig } from '../../services/llmConfig';
import { getPhotoProviderConfig } from '../../services/memories/helpersService';
import { validatePassword } from '../../services/passwordPolicy';
import { validatePassword } from '../common/passwordPolicy';
import { deleteUserCompletely } from '../../services/userCleanupService';
import { getPreferencesMatrix, setAdminPreferences } from '../../services/notificationPreferencesService';
import { DatabaseService } from '../database/database.service';
@@ -6,7 +6,7 @@ import { RateLimitService } from '../common/rate-limit.service';
import { OptionalJwtGuard } from './optional-jwt.guard';
import { getClientIp } from '../audit/client-ip';
import { AuditService } from '../audit/audit.service';
import { willDropSecureCookie } from '../../services/cookie';
import { willDropSecureCookie } from '../common/cookie';
import type { User } from '../../types';
const WINDOW = 15 * 60 * 1000;
+1 -1
View File
@@ -35,7 +35,7 @@ const auth = new AuthService(
);
export { stripUserForClient } from './auth.helpers';
export { avatarUrl } from '../../services/avatarUrl';
export { avatarUrl } from '../common/avatarUrl';
export function isDemoUser(userId: number): boolean {
return auth.isDemoUser(userId);
+1 -1
View File
@@ -39,7 +39,7 @@ import { JwtAuthGuard } from './jwt-auth.guard';
import { CurrentUser } from './current-user.decorator';
import { getClientIp } from '../audit/client-ip';
import { AuditService } from '../audit/audit.service';
import { isDemoEmail } from '../../services/demo';
import { isDemoEmail } from '../common/demo';
import type { User } from '../../types';
const WINDOW = 15 * 60 * 1000;
+1 -1
View File
@@ -3,7 +3,7 @@ import crypto from 'crypto';
import path from 'path';
import fs from 'fs';
import { User } from '../../types';
import { decrypt_api_key } from '../../services/apiKeyCrypto';
import { decrypt_api_key } from '../common/crypto/apiKeyCrypto';
/**
* Pure half of the auth domain (no DB, no injected deps) — the DB half lives
+6 -6
View File
@@ -13,9 +13,9 @@ import { DatabaseService } from '../database/database.service';
import { PermissionsService } from '../permissions/permissions.service';
import { AtlasService } from '../atlas/atlas.service';
import { getCountryFromCoords } from '../atlas/atlas-geo';
import { validatePassword } from '../../services/passwordPolicy';
import { encryptMfaSecret, decryptMfaSecret } from '../../services/mfaCrypto';
import { decrypt_api_key, maybe_encrypt_api_key, encrypt_api_key } from '../../services/apiKeyCrypto';
import { validatePassword } from '../common/passwordPolicy';
import { encryptMfaSecret, decryptMfaSecret } from '../common/crypto/mfaCrypto';
import { decrypt_api_key, maybe_encrypt_api_key, encrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { createEphemeralToken } from '../../services/ephemeralTokens';
// Import from sessionManager directly, NOT the ../../mcp barrel: the barrel pulls
// the whole tools fan-out (and via the domain bridges, the Nest services) into
@@ -27,11 +27,11 @@ import { emitUserDeleted } from '../../plugin-user-lifecycle';
import { getFlightDistanceKm } from '../../services/distanceService';
import { verifyJwtAndLoadUser } from '../../middleware/auth';
import { User } from '../../types';
import { DEMO_EMAIL_PRIMARY, isDemoEmail } from '../../services/demo';
import { avatarUrl } from '../../services/avatarUrl';
import { DEMO_EMAIL_PRIMARY, isDemoEmail } from '../common/demo';
import { avatarUrl } from '../common/avatarUrl';
import { joinTripAsMember } from '../../services/tripMembership';
import { isPasskeyConfigured } from '../../services/webauthnConfig';
import { setAuthCookie, clearAuthCookie } from '../../services/cookie';
import { setAuthCookie, clearAuthCookie } from '../common/cookie';
import { sendPasswordResetEmail } from '../../services/notifications';
import { getAppUrl } from '../../app-config';
import {
+1 -1
View File
@@ -4,7 +4,7 @@ import { RateLimitService } from '../common/rate-limit.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { PasskeyEnabledGuard } from './passkey-enabled.guard';
import { CurrentUser } from './current-user.decorator';
import { setAuthCookie } from '../../services/cookie';
import { setAuthCookie } from '../common/cookie';
import { getClientIp } from '../audit/client-ip';
import { AuditService } from '../audit/audit.service';
import { PasskeyService } from './passkey.service';
+1 -1
View File
@@ -8,7 +8,7 @@ import {
type AuthenticatorTransportFuture,
} from '@simplewebauthn/server';
import { resolveWebauthnConfig } from '../../services/webauthnConfig';
import { avatarUrl } from '../../services/avatarUrl';
import { avatarUrl } from '../common/avatarUrl';
import { stripUserForClient } from './auth.helpers';
import { AuthService } from './auth.service';
import { DatabaseService } from '../database/database.service';
+1 -1
View File
@@ -4,7 +4,7 @@ import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import { RealtimeService } from '../realtime/realtime.service';
import { PermissionsService } from '../permissions/permissions.service';
import { verifyTripAccess } from '../../services/tripAccess';
import { avatarUrl } from '../../services/avatarUrl';
import { avatarUrl } from '../common/avatarUrl';
import type { User, BudgetItem, BudgetItemMember, BudgetItemPayer } from '../../types';
import { ExchangeRatesService } from './exchange-rates.service';
+1 -1
View File
@@ -6,7 +6,7 @@ import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import { RealtimeService } from '../realtime/realtime.service';
import { PermissionsService } from '../permissions/permissions.service';
import { verifyTripAccess } from '../../services/tripAccess';
import { avatarUrl } from '../../services/avatarUrl';
import { avatarUrl } from '../common/avatarUrl';
import { checkSsrf, createPinnedDispatcher } from '../../utils/ssrfGuard';
import type { CollabNote, CollabPoll, CollabMessage, TripFile, User } from '../../types';
@@ -29,7 +29,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { PLACE_IMAGE_UPLOAD } from '../common/place-image-upload';
import { placeImageUrl } from '../../services/placeImage';
import { isDemoEmail } from '../../services/demo';
import { isDemoEmail } from '../common/demo';
import {
CollectionCreateDto,
CollectionUpdateDto,
@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import { readEnv } from '../app-config';
import { SESSION_DURATION_MS, SESSION_DURATION_REMEMBER_MS } from '../config';
import { readEnv } from '../../app-config';
import { SESSION_DURATION_MS, SESSION_DURATION_REMEMBER_MS } from '../../config';
const COOKIE_NAME = 'trek_session';
@@ -1,5 +1,5 @@
import * as crypto from 'node:crypto';
import { ENCRYPTION_KEY } from '../config';
import { ENCRYPTION_KEY } from '../../../config';
const ENCRYPTED_PREFIX = 'enc:v1:';
@@ -1,5 +1,5 @@
import crypto from 'crypto';
import { ENCRYPTION_KEY } from '../config';
import { ENCRYPTION_KEY } from '../../../config';
function getKey(): Buffer {
return crypto.createHash('sha256').update(`${ENCRYPTION_KEY}:mfa:v1`).digest();
+1 -1
View File
@@ -28,7 +28,7 @@ import { CurrentUser } from '../auth/current-user.decorator';
import { MAX_FILE_SIZE, MAX_VIDEO_SIZE, BLOCKED_EXTENSIONS, filesDir, isVideoExtension } from './files.constants';
import { getAllowedExtensions } from './files.bridge';
import { FileUploadDto, FileUpdateDto, FileLinkDto } from './files.dto';
import { isDemoEmail } from '../../services/demo';
import { isDemoEmail } from '../common/demo';
const UPLOAD = {
storage: diskStorage({
+1 -1
View File
@@ -5,7 +5,7 @@ import type { Request } from 'express';
import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import { RealtimeService } from '../realtime/realtime.service';
import { PermissionsService } from '../permissions/permissions.service';
import { avatarUrl } from '../../services/avatarUrl';
import { avatarUrl } from '../common/avatarUrl';
import { consumeEphemeralToken } from '../../services/ephemeralTokens';
import { verifyJwtAndLoadUser } from '../../middleware/auth';
import type { User, TripFile } from '../../types';
+1 -1
View File
@@ -9,7 +9,7 @@ import type {
} from '@trek/shared';
import { readEnv, getAppUrl } from '../../app-config';
import { safeFetchFollow, SsrfBlockedError } from '../../utils/ssrfGuard';
import { decrypt_api_key } from '../../services/apiKeyCrypto';
import { decrypt_api_key } from '../common/crypto/apiKeyCrypto';
// ── Photo cache (disk-backed) ────────────────────────────────────────────────
import * as placePhotoCache from '../../services/placePhotoCache';
import { DatabaseService } from '../database/database.service';
@@ -25,7 +25,7 @@ import {
} from '../../services/notificationPreferencesService';
import { getChannel, listChannels, type ChannelMessage, type ExternalChannel } from '../../services/notifications/channelRegistry';
import { getAction } from '../../services/inAppNotificationActions';
import { avatarUrl } from '../../services/avatarUrl';
import { avatarUrl } from '../common/avatarUrl';
import { DatabaseService } from '../database/database.service';
import { RealtimeService } from '../realtime/realtime.service';
+1 -1
View File
@@ -2,7 +2,7 @@ import { Controller, Get, Query, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { readEnv } from '../../app-config';
import { OidcService, OIDC_STATE_TTL_MS } from './oidc.service';
import { cookieOptions } from '../../services/cookie';
import { cookieOptions } from '../common/cookie';
const OIDC_STATE_COOKIE = 'trek_oidc_state';
+2 -2
View File
@@ -8,9 +8,9 @@ import type { Request, Response } from 'express';
import { readEnv, getAppUrl } from '../../app-config';
import { JWT_SECRET, SESSION_DURATION_SECONDS } from '../../config';
import { User } from '../../types';
import { decrypt_api_key } from '../../services/apiKeyCrypto';
import { decrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { joinTripAsMember } from '../../services/tripMembership';
import { setAuthCookie } from '../../services/cookie';
import { setAuthCookie } from '../common/cookie';
import { AuthService } from '../auth/auth.service';
import { DatabaseService } from '../database/database.service';
@@ -14,7 +14,7 @@ import {
import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import type { User } from '../../types';
import { PackingService } from './packing.service';
import { isUpdateConflict } from '../../services/conflictResult';
import { isUpdateConflict } from '../common/conflictResult';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import {
+2 -2
View File
@@ -3,8 +3,8 @@ import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import { RealtimeService } from '../realtime/realtime.service';
import { PermissionsService } from '../permissions/permissions.service';
import { verifyTripAccess } from '../../services/tripAccess';
import { avatarUrl } from '../../services/avatarUrl';
import type { UpdateConflict } from '../../services/conflictResult';
import { avatarUrl } from '../common/avatarUrl';
import type { UpdateConflict } from '../common/conflictResult';
import type { User } from '../../types';
import { DatabaseService } from '../database/database.service';
+2 -2
View File
@@ -20,12 +20,12 @@ import { hexColorSchema } from '@trek/shared';
import { readEnv } from '../../app-config';
import type { User } from '../../types';
import { PlacesService } from './places.service';
import { isUpdateConflict } from '../../services/conflictResult';
import { isUpdateConflict } from '../common/conflictResult';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { PLACE_IMAGE_UPLOAD } from '../common/place-image-upload';
import { placeImageUrl } from '../../services/placeImage';
import { isDemoEmail } from '../../services/demo';
import { isDemoEmail } from '../common/demo';
import {
PlaceBulkDeleteDto,
PlaceBulkUpdateDto,
+1 -1
View File
@@ -19,7 +19,7 @@ import {
resolveCategoryIdForFolder,
} from '../../services/kmlImport';
import { searchUnsplashPhotos, getUnsplashKey } from '../../services/unsplashService';
import { type UpdateConflict, isUpdateConflict } from '../../services/conflictResult';
import { type UpdateConflict, isUpdateConflict } from '../common/conflictResult';
import { reclaimPlaceImage } from '../../services/placeImage';
import { onPlaceCreated, onPlaceUpdated, onPlaceDeleted } from '../../services/journeyService';
import {
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import { readEnv } from '../../../app-config';
import { RealtimeService } from '../../realtime/realtime.service';
import { isUpdateConflict } from '../../../services/conflictResult';
import { isUpdateConflict } from '../../common/conflictResult';
import { getWeather } from '../../../services/weatherService';
import { BLOCKED_EXTENSIONS, filesDir } from '../../files/files.constants';
import { joinTripAsMember } from '../../../services/tripMembership';
@@ -17,7 +17,7 @@ import { PermissionsService } from '../../permissions/permissions.service';
import { TripsService, NotFoundError, ValidationError } from '../../trips/trips.service';
import { PlacesService, type PlaceCreateInput, type PlaceUpdateInput } from '../../places/places.service';
import { AddonsService } from '../../addons/addons.service';
import { isDemoEmail } from '../../../services/demo';
import { isDemoEmail } from '../../common/demo';
import { ADDON_IDS } from '../../../addons';
import { listJourneys, listEntries as listJournalEntriesSvc, createEntry as createJournalEntrySvc, updateEntry as updateJournalEntrySvc, deleteEntry as deleteJournalEntrySvc, createJourney as createJourneySvc, deleteJourney as deleteJourneySvc, onPlaceCreated, onPlaceUpdated, onPlaceDeleted } from '../../../services/journeyService';
import { AtlasService } from '../../atlas/atlas.service';
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import crypto from 'node:crypto';
import { DatabaseService } from '../database/database.service';
import { encrypt_api_key, decrypt_api_key } from '../../services/apiKeyCrypto';
import { encrypt_api_key, decrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { getAppUrl } from '../../app-config';
import { isPrivateIp } from './install/safe-fetch';
import { safeFetchLlm } from '../../utils/ssrfGuard';
@@ -9,7 +9,7 @@ import { readUserSettingsDecrypted, hasRequiredUserSettings } from './plugins.se
import { PLUGIN_CHANNEL_EVENTS } from './install/manifest';
import { stripEmoji } from './text-sanitize';
import { applyStagedPluginTrees, setStagedRestoreApplier } from './plugin-backup';
import { decrypt_api_key } from '../../services/apiKeyCrypto';
import { decrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { PluginSupervisor, type PluginRouteInfo } from './supervisor/plugin-supervisor';
import fs from 'node:fs';
import path from 'node:path';
+1 -1
View File
@@ -3,7 +3,7 @@ import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { pluginsEnabled } from './kill-switch';
import { devLinkEnabled } from './dev-link';
import { maybe_encrypt_api_key, decrypt_api_key } from '../../services/apiKeyCrypto';
import { maybe_encrypt_api_key, decrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { readAudit } from './host/plugin-audit';
import { keyFingerprint } from './signature-status';
import { pluginBudgetUsage } from './host/plugin-host-state';
@@ -4,7 +4,7 @@ import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import { RealtimeService } from '../realtime/realtime.service';
import { PermissionsService } from '../permissions/permissions.service';
import { verifyTripAccess } from '../../services/tripAccess';
import { avatarUrl } from '../../services/avatarUrl';
import { avatarUrl } from '../common/avatarUrl';
import type { Reservation, User } from '../../types';
import { BudgetService } from '../budget/budget.service';
import { typeToCostCategory } from '@trek/shared';
+1 -1
View File
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import { decrypt_api_key, maybe_encrypt_api_key } from '../../services/apiKeyCrypto';
import { decrypt_api_key, maybe_encrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { MASKED_SETTING_VALUE, normalizeAppearance } from '@trek/shared';
const ENCRYPTED_SETTING_KEYS = new Set(['webhook_url', 'ntfy_token', 'mapbox_access_token', 'llm_api_key']);
@@ -9,7 +9,7 @@
*/
import { haversineKm } from '../../services/distanceService';
import type { EndpointInput } from '../reservations/reservations.service';
import { localParts, resolveTimeZone } from '../../services/timezoneService';
import { localParts, resolveTimeZone } from '../common/timezoneService';
import { deriveTransitStats, type TransitItinerary, type TransitLeg } from './transit.helpers';
import { z } from 'zod';
+1 -1
View File
@@ -30,7 +30,7 @@ import { CurrentUser } from '../auth/current-user.decorator';
import { getClientIp } from '../audit/client-ip';
import { logInfo } from '../audit/audit-log.logger';
import { AuditService } from '../audit/audit.service';
import { isDemoEmail } from '../../services/demo';
import { isDemoEmail } from '../common/demo';
import { NotFoundError, ValidationError } from './trips.service';
import { TripCreateDto, TripUpdateDto, TripCopyDto, TripAddMemberDto, TripTransferOwnershipDto, TripCreateGuestDto, TripRenameGuestDto } from './trips.dto';
import { saveUnsplashCover, isUnsplashCoverUrl } from '../../services/unsplashService';
+2 -2
View File
@@ -7,10 +7,10 @@ import type { TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
import { RealtimeService } from '../realtime/realtime.service';
import { PermissionsService } from '../permissions/permissions.service';
import type { Trip, User } from '../../types';
import { avatarUrl } from '../../services/avatarUrl';
import { avatarUrl } from '../common/avatarUrl';
import { erasePluginUserData } from '../../services/userCleanupService';
import { emitUserDeleted } from '../../plugin-user-lifecycle';
import { resolveTimeZone } from '../../services/timezoneService';
import { resolveTimeZone } from '../common/timezoneService';
import { PlacesService } from '../places/places.service';
import { DaysService, addDays } from '../days/days.service';
import { PackingService } from '../packing/packing.service';
@@ -1,4 +1,4 @@
import { localParts } from '../timezoneService';
import { localParts } from '../../nest/common/timezoneService';
import type { AirtrailAirport, AirtrailFlightRaw, AirtrailNamedCode } from './airtrailClient';
import type { AirtrailFlight } from '@trek/shared';
@@ -1,6 +1,6 @@
import type { AirtrailFlight } from '@trek/shared';
import { db } from '../../db/database';
import { maybe_encrypt_api_key, decrypt_api_key } from '../apiKeyCrypto';
import { maybe_encrypt_api_key, decrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
import { checkSsrf } from '../../utils/ssrfGuard';
import { writeAudit } from '../../nest/audit/audit.bridge';
import { AirtrailAuthError, AirtrailCreds, AirtrailRequestError, listFlights } from './airtrailClient';
+1 -1
View File
@@ -1,5 +1,5 @@
import { db, canAccessTrip } from '../db/database';
import { avatarUrl } from './avatarUrl';
import { avatarUrl } from '../nest/common/avatarUrl';
import type { Journey, JourneyEntry, JourneyPhoto, JourneyContributor } from '../types';
import { broadcastToUser } from '../websocket';
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import { maybe_encrypt_api_key, decrypt_api_key } from './apiKeyCrypto';
import { maybe_encrypt_api_key, decrypt_api_key } from '../nest/common/crypto/apiKeyCrypto';
/**
* Shared types + helpers for the `llm_parsing` addon configuration.
@@ -3,7 +3,7 @@ import { Readable } from 'node:stream';
import { Response } from 'express';
import { canAccessTrip, db } from "../../db/database";
import { safeFetch, SsrfBlockedError, type SafeFetchOptions } from '../../utils/ssrfGuard';
import { decrypt_api_key } from '../apiKeyCrypto';
import { decrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
// helpers for handling return types
@@ -1,6 +1,6 @@
import { Response } from 'express';
import { db } from '../../db/database';
import { maybe_encrypt_api_key, decrypt_api_key } from '../apiKeyCrypto';
import { maybe_encrypt_api_key, decrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
import { checkSsrf, safeFetch } from '../../utils/ssrfGuard';
import { writeAudit } from '../../nest/audit/audit.bridge';
import { addTripPhotos} from './unifiedService';
@@ -7,7 +7,7 @@ import { streamImmichAsset, fetchImmichThumbnailBytes, getAssetInfo as getImmich
import { streamSynologyAsset, fetchSynologyThumbnailBytes, getSynologyAssetInfo } from './synologyService';
import type { ServiceResult, AssetInfo } from './helpersService';
import { fail, success } from './helpersService';
import { encrypt_api_key, decrypt_api_key } from '../apiKeyCrypto';
import { encrypt_api_key, decrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
import * as photoCache from './trekPhotoCache';
import { ensureLocalThumbnail } from './thumbnailService';
@@ -1,7 +1,7 @@
import { Response } from 'express';
import { db } from '../../db/database';
import { decrypt_api_key, encrypt_api_key, maybe_encrypt_api_key } from '../apiKeyCrypto';
import { decrypt_api_key, encrypt_api_key, maybe_encrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
import { safeFetch, SsrfBlockedError, checkSsrf } from '../../utils/ssrfGuard';
import { addTripPhotos } from './unifiedService';
import {
@@ -9,7 +9,7 @@ import {
Selection,
} from './helpersService';
import { getOrCreateTrekPhoto, deleteTrekPhotoIfOrphan } from './photoResolverService';
import { encrypt_api_key } from '../apiKeyCrypto';
import { encrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
function _providers(): Array<{id: string; enabled: boolean}> {
+1 -1
View File
@@ -1,7 +1,7 @@
import { getAppUrl, readEnv } from '../app-config';
import { db } from '../db/database';
import { checkSsrf, createPinnedDispatcher } from '../utils/ssrfGuard';
import { decrypt_api_key } from './apiKeyCrypto';
import { decrypt_api_key } from '../nest/common/crypto/apiKeyCrypto';
import { logInfo, logDebug, logError } from '../nest/audit/audit-log.logger';
// ── Types ──────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import { readEnv } from '../app-config';
import { safeFetch } from '../utils/ssrfGuard';
import { db } from '../db/database';
import { decrypt_api_key } from './apiKeyCrypto';
import { decrypt_api_key } from '../nest/common/crypto/apiKeyCrypto';
interface UnsplashSearchResponse {
results?: {
+1 -1
View File
@@ -45,7 +45,7 @@ vi.mock('../../src/db/database', () => ({
db, canAccessTrip, isOwner: vi.fn(() => true), getPlaceWithTags: vi.fn(), closeDb: () => {}, reinitialize: () => {},
}));
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
vi.mock('../../src/services/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
vi.mock('../../src/nest/common/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
+1 -1
View File
@@ -108,7 +108,7 @@ vi.mock('../../src/nest/notifications/notifications.bridge', () => ({ send: vi.f
// The audit domain is DI-native now: writeAudit runs for real against the temp
// db's audit_log table; only the file logger is silenced.
vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
vi.mock('../../src/services/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
vi.mock('../../src/nest/common/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
+2 -2
View File
@@ -6,8 +6,8 @@
import Database from 'better-sqlite3';
import bcrypt from 'bcryptjs';
import { encryptMfaSecret } from '../../src/services/mfaCrypto';
import { encrypt_api_key } from '../../src/services/apiKeyCrypto';
import { encryptMfaSecret } from '../../src/nest/common/crypto/mfaCrypto';
import { encrypt_api_key } from '../../src/nest/common/crypto/apiKeyCrypto';
let _userSeq = 0;
let _tripSeq = 0;
@@ -704,7 +704,7 @@ describe('Synology auth checks', () => {
// ── Album sync ────────────────────────────────────────────────────────────────
import { addAlbumLink } from '../helpers/factories';
import { encrypt_api_key } from '../../src/services/apiKeyCrypto';
import { encrypt_api_key } from '../../src/nest/common/crypto/apiKeyCrypto';
describe('Synology syncSynologyAlbumLink', () => {
it('SYNO-050 — POST sync happy path: trip owner with album link saves photos to DB', async () => {
@@ -1187,7 +1187,7 @@ describe('Synology SSRF blocked error handling', () => {
// ── Passphrase persistence fixes ─────────────────────────────────────────────
import { getOrCreateTrekPhoto, deleteTrekPhotoIfOrphan } from '../../src/services/memories/photoResolverService';
import { decrypt_api_key } from '../../src/services/apiKeyCrypto';
import { decrypt_api_key } from '../../src/nest/common/crypto/apiKeyCrypto';
describe('trek_photos passphrase healing (SYNO-090)', () => {
it('SYNO-090 — getOrCreateTrekPhoto overwrites an existing bad passphrase when a new one is supplied', () => {
+1 -1
View File
@@ -35,7 +35,7 @@ vi.mock('../../../src/config', () => ({
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
updateJwtSecret: () => {},
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
encrypt_api_key: (v: string) => v,
decrypt_api_key: (v: string) => v,
maybe_encrypt_api_key: (v: string) => v,
+2 -2
View File
@@ -3,7 +3,7 @@ import { HttpException } from '@nestjs/common';
import type { Request } from 'express';
vi.mock('../../../src/middleware/auth', () => ({ extractToken: vi.fn(), verifyJwtAndLoadUser: vi.fn() }));
vi.mock('../../../src/services/cookie', () => ({ setAuthCookie: vi.fn() }));
vi.mock('../../../src/nest/common/cookie', () => ({ setAuthCookie: vi.fn() }));
vi.mock('../../../src/nest/audit/client-ip', () => ({ getClientIp: vi.fn(() => '1.2.3.4') }));
vi.mock('../../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
@@ -35,7 +35,7 @@ import { CurrentUser } from '../../../src/nest/auth/current-user.decorator';
import { extractToken, verifyJwtAndLoadUser } from '../../../src/middleware/auth';
import type { AuthService } from '../../../src/nest/auth/auth.service';
import type { PasskeyService } from '../../../src/nest/auth/passkey.service';
import { setAuthCookie } from '../../../src/services/cookie';
import { setAuthCookie } from '../../../src/nest/common/cookie';
import type { AuditService } from '../../../src/nest/audit/audit.service';
import type { User } from '../../../src/types';
@@ -4,14 +4,14 @@ import type { Request, Response } from 'express';
vi.mock('../../../src/nest/audit/client-ip', () => ({ getClientIp: vi.fn(() => '1.2.3.4') }));
vi.mock('../../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
vi.mock('../../../src/services/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
vi.mock('../../../src/nest/common/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
import { AuthPublicController } from '../../../src/nest/auth/auth-public.controller';
import { AuthController } from '../../../src/nest/auth/auth.controller';
import { RateLimitService } from '../../../src/nest/common/rate-limit.service';
import type { AuthService } from '../../../src/nest/auth/auth.service';
import type { AuditService } from '../../../src/nest/audit/audit.service';
import { isDemoEmail } from '../../../src/services/demo';
import { isDemoEmail } from '../../../src/nest/common/demo';
import type { User } from '../../../src/types';
const user = { id: 1, username: 'u', role: 'user', email: 'u@example.test' } as User;
+2 -2
View File
@@ -10,7 +10,7 @@
import { describe, it, expect, vi } from 'vitest';
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'test-secret', ENCRYPTION_KEY: '0'.repeat(64) }));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: vi.fn((v) => v),
maybe_encrypt_api_key: vi.fn((v) => v),
encrypt_api_key: vi.fn((v) => v),
@@ -25,7 +25,7 @@ import {
generateBackupCodes,
parseBackupCodeHashes,
} from '../../../src/nest/auth/auth.helpers';
import { avatarUrl } from '../../../src/services/avatarUrl';
import { avatarUrl } from '../../../src/nest/common/avatarUrl';
import type { User } from '../../../src/types';
// ── utcSuffix ────────────────────────────────────────────────────────────────
+2 -2
View File
@@ -43,11 +43,11 @@ vi.mock('../../../src/config', () => ({
SESSION_DURATION_REMEMBER_SECONDS: 2592000,
updateJwtSecret: () => {},
}));
vi.mock('../../../src/services/mfaCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/mfaCrypto', () => ({
encryptMfaSecret: vi.fn((s) => `enc:${s}`),
decryptMfaSecret: vi.fn((s: string) => s.replace('enc:', '')),
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: vi.fn((v) => v),
maybe_encrypt_api_key: vi.fn((v) => v),
mask_stored_api_key: vi.fn((v: string | null | undefined) => (v ? '••••••••' : null)),
@@ -57,7 +57,7 @@ import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip } from '../../helpers/factories';
import { avatarUrl } from '../../../src/services/avatarUrl';
import { avatarUrl } from '../../../src/nest/common/avatarUrl';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { CollabService } from '../../../src/nest/collab/collab.service';
@@ -5,14 +5,14 @@ import os from 'os';
import path from 'path';
import fs from 'fs';
vi.mock('../../../src/services/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
vi.mock('../../../src/nest/common/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
import { FilesController } from '../../../src/nest/files/files.controller';
import { FilesDownloadController } from '../../../src/nest/files/files-download.controller';
import { PhotosController } from '../../../src/nest/photos/photos.controller';
import type { FilesService } from '../../../src/nest/files/files.service';
import type { PhotosService } from '../../../src/nest/photos/photos.service';
import { isDemoEmail } from '../../../src/services/demo';
import { isDemoEmail } from '../../../src/nest/common/demo';
import type { User } from '../../../src/types';
const user = { id: 1, username: 'u', role: 'user', email: 'u@example.test' } as User;
+1 -1
View File
@@ -79,7 +79,7 @@ vi.mock('../../../src/utils/ssrfGuard', () => {
};
});
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: (v: string | null) => v,
}));
@@ -31,7 +31,7 @@ vi.mock('../../../src/config', () => ({
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
updateJwtSecret: () => {},
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: (v: string | null) => v,
maybe_encrypt_api_key: (v: string) => v,
encrypt_api_key: (v: string) => v,
+4 -4
View File
@@ -51,11 +51,11 @@ vi.mock('../../../src/config', () => ({
// as auth.service.test.ts) — services/tripMembership is deliberately NOT
// mocked: OIDC-SVC-045 asserts the real trip_members row a trip-bound invite
// creates.
vi.mock('../../../src/services/mfaCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/mfaCrypto', () => ({
encryptMfaSecret: vi.fn((s) => `enc:${s}`),
decryptMfaSecret: vi.fn((s: string) => s.replace('enc:', '')),
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: vi.fn((v) => v),
maybe_encrypt_api_key: vi.fn((v) => v),
mask_stored_api_key: vi.fn((v: string | null | undefined) => (v ? '••••••••' : null)),
@@ -78,8 +78,8 @@ vi.mock('../../../src/app-config', async (importOriginal) => {
});
const { setAuthCookieMock } = vi.hoisted(() => ({ setAuthCookieMock: vi.fn() }));
vi.mock('../../../src/services/cookie', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../src/services/cookie')>();
vi.mock('../../../src/nest/common/cookie', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../src/nest/common/cookie')>();
return { ...actual, setAuthCookie: setAuthCookieMock };
});
@@ -43,11 +43,11 @@ vi.mock('../../../src/config', () => ({
// Construction insurance for the injected AuthService's import graph (same set
// as oidc.service.test.ts / auth.service.test.ts).
vi.mock('../../../src/services/mfaCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/mfaCrypto', () => ({
encryptMfaSecret: vi.fn((s) => `enc:${s}`),
decryptMfaSecret: vi.fn((s: string) => s.replace('enc:', '')),
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: vi.fn((v) => v),
maybe_encrypt_api_key: vi.fn((v) => v),
mask_stored_api_key: vi.fn((v: string | null | undefined) => (v ? '••••••••' : null)),
@@ -35,7 +35,7 @@ vi.mock('../../../src/config', () => ({
}));
// Passthrough crypto — value comes back unchanged for most tests
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
maybe_encrypt_api_key: (v: string) => v,
decrypt_api_key: (v: string) => v,
}));
@@ -5,7 +5,7 @@ import type { Request } from 'express';
vi.mock('../../../src/nest/audit/client-ip', () => ({ getClientIp: vi.fn(() => '1.2.3.4') }));
vi.mock('../../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
const { isDemoEmail } = vi.hoisted(() => ({ isDemoEmail: vi.fn(() => false) }));
vi.mock('../../../src/services/demo', () => ({ isDemoEmail }));
vi.mock('../../../src/nest/common/demo', () => ({ isDemoEmail }));
// Mock the Unsplash cover internalisation so the controller test never hits the
// network; isUnsplashCoverUrl keeps its real (host-based) logic.
vi.mock('../../../src/services/unsplashService', () => ({
@@ -148,7 +148,7 @@ const packingStub = {
deleteBag: vi.fn((_tid: number | string, bagId: string) => Number(bagId) !== 404),
setBagMembers: vi.fn((_tid: number | string, bagId: string, userIds: number[]) => (Number(bagId) === 404 ? null : userIds.map((u) => ({ user_id: u })))),
} as unknown as PackingService;
vi.mock('../../../src/services/conflictResult', () => ({ isUpdateConflict: (r: unknown) => !!(r as { conflict?: boolean })?.conflict }));
vi.mock('../../../src/nest/common/conflictResult', () => ({ isUpdateConflict: (r: unknown) => !!(r as { conflict?: boolean })?.conflict }));
vi.mock('../../../src/services/weatherService', () => ({ getWeather: vi.fn(async (lat: string, lng: string) => ({ lat, lng, temp: 20 })) }));
const categoriesStub = { list: vi.fn(() => [{ id: 1, name: 'Food' }]) } as unknown as CategoriesService;
const tagsStub = {
@@ -6,7 +6,7 @@
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
encrypt_api_key: (v: unknown) => (typeof v === 'string' ? `enc:${v}` : v),
decrypt_api_key: (v: unknown) => (typeof v === 'string' && v.startsWith('enc:') ? v.slice(4) : v),
}));
@@ -8,7 +8,7 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Reversible crypto stub so we can assert encrypt-at-rest without a real key env.
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
maybe_encrypt_api_key: (v: unknown) => (typeof v === 'string' ? `enc:${v}` : v),
decrypt_api_key: (v: unknown) => (typeof v === 'string' && v.startsWith('enc:') ? v.slice(4) : v),
}));
@@ -7,7 +7,7 @@ vi.mock('../../../src/config', () => ({
updateJwtSecret: () => {},
}));
import { encrypt_api_key, decrypt_api_key, maybe_encrypt_api_key } from '../../../src/services/apiKeyCrypto';
import { encrypt_api_key, decrypt_api_key, maybe_encrypt_api_key } from '../../../src/nest/common/crypto/apiKeyCrypto';
describe('apiKeyCrypto', () => {
const PLAINTEXT_KEY = 'my-secret-api-key-12345';
+1 -1
View File
@@ -4,7 +4,7 @@
* an OIDC `picture` claim (#1399); the helper resolves both to a renderable src.
*/
import { describe, it, expect } from 'vitest';
import { avatarUrl } from '../../../src/services/avatarUrl';
import { avatarUrl } from '../../../src/nest/common/avatarUrl';
describe('avatarUrl', () => {
it('AVATAR-URL-001: prefixes an uploaded file name with the avatars path', () => {
@@ -16,7 +16,7 @@ const { testDb, dbMock } = vi.hoisted(() => {
});
vi.mock('../../../src/db/database', () => dbMock);
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'x'.repeat(40), ENCRYPTION_KEY: 'a'.repeat(64), updateJwtSecret: () => {} }));
vi.mock('../../../src/services/apiKeyCrypto', () => ({ decrypt_api_key: (v: string) => v, maybe_encrypt_api_key: (v: string) => v, encrypt_api_key: (v: string) => v }));
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({ decrypt_api_key: (v: string) => v, maybe_encrypt_api_key: (v: string) => v, encrypt_api_key: (v: string) => v }));
const { sendMailMock } = vi.hoisted(() => ({ sendMailMock: vi.fn().mockResolvedValue({ accepted: ['a@b.c'] }) }));
vi.mock('nodemailer', () => ({ default: { createTransport: vi.fn(() => ({ sendMail: sendMailMock, verify: vi.fn() })) } }));
vi.stubGlobal('fetch', vi.fn());
@@ -38,7 +38,7 @@ import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip } from '../../helpers/factories';
import { isUpdateConflict } from '../../../src/services/conflictResult';
import { isUpdateConflict } from '../../../src/nest/common/conflictResult';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { PackingService } from '../../../src/nest/packing/packing.service';
import { PlacesService } from '../../../src/nest/places/places.service';
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { cookieOptions } from '../../../src/services/cookie';
import { cookieOptions } from '../../../src/nest/common/cookie';
import { SESSION_DURATION_MS, SESSION_DURATION_REMEMBER_MS } from '../../../src/config';
describe('cookieOptions', () => {
+1 -1
View File
@@ -7,7 +7,7 @@ vi.mock('../../../src/config', () => ({
updateJwtSecret: () => {},
}));
import { encryptMfaSecret, decryptMfaSecret } from '../../../src/services/mfaCrypto';
import { encryptMfaSecret, decryptMfaSecret } from '../../../src/nest/common/crypto/mfaCrypto';
describe('mfaCrypto', () => {
const TOTP_SECRET = 'JBSWY3DPEHPK3PXP'; // typical base32 TOTP secret
@@ -26,7 +26,7 @@ vi.mock('../../../src/config', () => ({
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
updateJwtSecret: () => {},
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: (v: string | null) => v,
maybe_encrypt_api_key: (v: string) => v,
encrypt_api_key: (v: string) => v,
@@ -3,7 +3,7 @@ import { describe, it, expect, vi, afterEach, afterAll, beforeEach } from 'vites
vi.mock('../../../src/db/database', () => ({
db: { prepare: () => ({ get: vi.fn(() => undefined), all: vi.fn(() => []) }) },
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
decrypt_api_key: vi.fn((v) => v),
maybe_encrypt_api_key: vi.fn((v) => v),
}));
@@ -29,7 +29,7 @@ vi.mock('../../../src/config', () => ({
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
updateJwtSecret: () => {},
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
encrypt_api_key: (v: string) => v,
decrypt_api_key: (v: string) => v,
maybe_encrypt_api_key: (v: string) => v,
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { validatePassword } from '../../../src/services/passwordPolicy';
import { validatePassword } from '../../../src/nest/common/passwordPolicy';
describe('validatePassword', () => {
// AUTH-006 — Registration with weak password
@@ -1,4 +1,4 @@
import { localParts, resolveTimeZone } from '../../../src/services/timezoneService';
import { localParts, resolveTimeZone } from '../../../src/nest/common/timezoneService';
import { describe, expect, it } from 'vitest';
@@ -11,7 +11,7 @@ vi.mock('../../../src/utils/ssrfGuard', () => ({ safeFetch }));
vi.mock('../../../src/db/database', () => ({
db: { prepare: () => ({ get: mockDbGet, all: vi.fn(() => []), run: vi.fn() }) },
}));
vi.mock('../../../src/services/apiKeyCrypto', () => ({ decrypt_api_key: (v: string | null) => v }));
vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({ decrypt_api_key: (v: string | null) => v }));
import { searchUnsplashPhotos, getUnsplashKey, saveUnsplashCover, isUnsplashCoverUrl } from '../../../src/services/unsplashService';