mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
refactor(server): extract addon enablement into the addons domain
This commit is contained in:
@@ -5,7 +5,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
||||
import { User } from '../types';
|
||||
import { verifyMcpToken, verifyJwtToken } from '../services/authService';
|
||||
import { getUserByAccessToken } from '../services/oauthService';
|
||||
import { isAddonEnabled } from '../services/adminService';
|
||||
import { isAddonEnabled } from '../nest/addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../addons';
|
||||
import { registerResources } from './resources';
|
||||
import { registerTools } from './tools';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { listTrips, getTrip, getTripOwner, listMembers } from '../services/tripS
|
||||
import { listPlaces } from '../services/placeService';
|
||||
import { listBucketList, listVisitedCountries, getStats as getAtlasStats, listManuallyVisitedRegions } from '../services/atlasService';
|
||||
import { getNotifications } from '../services/inAppNotifications';
|
||||
import { isAddonEnabled } from '../services/adminService';
|
||||
import { isAddonEnabled } from '../nest/addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../addons';
|
||||
import { canAccessJourney, getJourneyFull, listEntries, listJourneys } from '../services/journeyService';
|
||||
import { canRead, canReadTrips } from './scopes';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getStats as getAtlasStats, listManuallyVisitedRegions,
|
||||
markRegionVisited, unmarkRegionVisited, getCountryPlaces, updateBucketItem,
|
||||
} from '../../services/atlasService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../../nest/addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import {
|
||||
TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import {
|
||||
createOrUpdateJourneyShareLink, deleteJourneyShareLink, getJourneyShareLink,
|
||||
} from '../../services/journeyShareService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../../nest/addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import {
|
||||
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||
import { canAccessTrip } from '../../db/database';
|
||||
import { getTripSummary } from '../../services/tripService';
|
||||
import { listItems as listPackingItems } from '../../nest/packing/packing.bridge';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../../nest/addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
|
||||
export function registerMcpPrompts(server: McpServer, _userId: number, isStaticToken = false): void {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import {
|
||||
createOrUpdateShareLink, getShareLink, deleteShareLink,
|
||||
} from '../../nest/share/share.bridge';
|
||||
import { isAddonEnabled, getCollabFeatures } from '../../services/adminService';
|
||||
import { isAddonEnabled, getCollabFeatures } from '../../nest/addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { countMessages, listPolls } from '../../nest/collab/collab.bridge';
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { db } from '../../db/database';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import { AddonsService } from './addons.service';
|
||||
|
||||
/**
|
||||
* Non-Nest entry point for the addons domain — for code running OUTSIDE the
|
||||
* Nest container (the legacy MCP registrars/resources, systemNotices, the
|
||||
* pre-init platform.routes Express router, the module-level `when:` gate
|
||||
* closures in the DI-discovered *.mcp.ts files, and the legacy
|
||||
* oauthService/airtrailSync/thumbnailService modules). Exports the legacy
|
||||
* services/adminService names 1:1 so repointing a consumer is an
|
||||
* import-path-only diff. Inside the container, inject AddonsService instead.
|
||||
* Delete this file when the last legacy consumer migrates.
|
||||
*
|
||||
* Only the enablement READS are bridged — the paired writers
|
||||
* (updateBagTracking/updateCollabFeatures) have in-container consumers only.
|
||||
* The reads are uncached per-call queries on purpose: admin toggles must stay
|
||||
* immediately visible without invalidation wiring.
|
||||
*
|
||||
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
|
||||
* the shared better-sqlite3 singleton.
|
||||
*/
|
||||
const addons = new AddonsService(new DatabaseService(db));
|
||||
|
||||
export function isAddonEnabled(addonId: string): boolean {
|
||||
return addons.isAddonEnabled(addonId);
|
||||
}
|
||||
|
||||
export function getBagTracking(): { enabled: boolean } {
|
||||
return addons.getBagTracking();
|
||||
}
|
||||
|
||||
export function getCollabFeatures(): { chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean } {
|
||||
return addons.getCollabFeatures();
|
||||
}
|
||||
@@ -6,9 +6,16 @@ import { AddonsService } from './addons.service';
|
||||
* GET /api/addons — enabled add-ons + photo providers (was an inline handler in
|
||||
* server/src/app.ts). The addon sub-features (atlas, vacay) keep their own
|
||||
* modules; this only serves the EXACT /api/addons listing.
|
||||
*
|
||||
* Exports AddonsService — the owner of addon/feature-flag enablement reads
|
||||
* (isAddonEnabled + bag-tracking/collab-features flags, extracted from
|
||||
* adminService per finding `admin-1`) — for the in-container consumers.
|
||||
* Deliberately NOT @Global (permissions precedent): e2e TestingModules resolve
|
||||
* it transitively through each consumer's explicit import.
|
||||
*/
|
||||
@Module({
|
||||
controllers: [AddonsController],
|
||||
providers: [AddonsService],
|
||||
exports: [AddonsService],
|
||||
})
|
||||
export class AddonsModule {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import type { Addon } from '../../types';
|
||||
import { getBagTracking, getCollabFeatures } from '../../services/adminService';
|
||||
import { getPhotoProviderConfig } from '../../services/memories/helpersService';
|
||||
|
||||
/**
|
||||
@@ -9,6 +8,14 @@ import { getPhotoProviderConfig } from '../../services/memories/helpersService';
|
||||
* inline `GET /api/addons` handler performed (server/src/app.ts). The SQL,
|
||||
* ordering, boolean coercions and the merged photo-provider entries are
|
||||
* reproduced 1:1 so the body is byte-identical for the client.
|
||||
*
|
||||
* Also the single owner of addon/feature-flag enablement state (moved verbatim
|
||||
* from services/adminService.ts, finding `admin-1`): isAddonEnabled plus the
|
||||
* bag-tracking and collab-features flags. The boolean polarities differ on
|
||||
* purpose — bag tracking is opt-in (`=== 'true'`, default OFF), the collab
|
||||
* sub-features are opt-out (`!== 'false'`, default ON). Reads are uncached
|
||||
* per-call queries so admin toggles stay immediately visible; out-of-container
|
||||
* consumers go through addons.bridge.ts.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonsService {
|
||||
@@ -18,6 +25,63 @@ export class AddonsService {
|
||||
return this.dbs.connection;
|
||||
}
|
||||
|
||||
isAddonEnabled(addonId: string): boolean {
|
||||
const addon = this.db.prepare('SELECT enabled FROM addons WHERE id = ?').get(addonId) as
|
||||
| { enabled: number }
|
||||
| undefined;
|
||||
return !!addon?.enabled;
|
||||
}
|
||||
|
||||
getBagTracking() {
|
||||
const row = this.db.prepare("SELECT value FROM app_settings WHERE key = 'bag_tracking_enabled'").get() as
|
||||
| { value: string }
|
||||
| undefined;
|
||||
return { enabled: row?.value === 'true' };
|
||||
}
|
||||
|
||||
updateBagTracking(enabled: boolean) {
|
||||
this.db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('bag_tracking_enabled', ?)").run(
|
||||
enabled ? 'true' : 'false',
|
||||
);
|
||||
return { enabled: !!enabled };
|
||||
}
|
||||
|
||||
getCollabFeatures() {
|
||||
const rows = this.db
|
||||
.prepare(
|
||||
"SELECT key, value FROM app_settings WHERE key IN ('collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled')",
|
||||
)
|
||||
.all() as { key: string; value: string }[];
|
||||
const map: Record<string, string> = {};
|
||||
for (const r of rows) map[r.key] = r.value;
|
||||
return {
|
||||
chat: map['collab_chat_enabled'] !== 'false',
|
||||
notes: map['collab_notes_enabled'] !== 'false',
|
||||
polls: map['collab_polls_enabled'] !== 'false',
|
||||
whatsnext: map['collab_whatsnext_enabled'] !== 'false',
|
||||
};
|
||||
}
|
||||
|
||||
updateCollabFeatures(features: { chat?: boolean; notes?: boolean; polls?: boolean; whatsnext?: boolean }) {
|
||||
const mapping: Record<string, string> = {
|
||||
chat: 'collab_chat_enabled',
|
||||
notes: 'collab_notes_enabled',
|
||||
polls: 'collab_polls_enabled',
|
||||
whatsnext: 'collab_whatsnext_enabled',
|
||||
};
|
||||
const before = this.getCollabFeatures();
|
||||
const stmt = this.db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)');
|
||||
for (const [feat, key] of Object.entries(mapping)) {
|
||||
if (features[feat] !== undefined) stmt.run(key, features[feat] ? 'true' : 'false');
|
||||
}
|
||||
const after = this.getCollabFeatures();
|
||||
// Collab flags gate MCP tool/resource registration, so callers must know
|
||||
// whether anything actually flipped — a no-op save must not tear down every
|
||||
// live MCP session (#1414).
|
||||
const changed = (Object.keys(after) as Array<keyof typeof after>).some((k) => after[k] !== before[k]);
|
||||
return { features: after, changed };
|
||||
}
|
||||
|
||||
list() {
|
||||
const addons = this.db
|
||||
.prepare('SELECT id, name, type, icon, enabled FROM addons WHERE enabled = 1 ORDER BY sort_order')
|
||||
@@ -58,8 +122,8 @@ export class AddonsService {
|
||||
}
|
||||
|
||||
return {
|
||||
collabFeatures: getCollabFeatures(),
|
||||
bagTracking: getBagTracking().enabled,
|
||||
collabFeatures: this.getCollabFeatures(),
|
||||
bagTracking: this.getBagTracking().enabled,
|
||||
addons: [
|
||||
...addons.map((a) => ({ ...a, enabled: !!a.enabled })),
|
||||
...providers.map((p) => ({
|
||||
|
||||
@@ -4,9 +4,10 @@ import { AdminService } from './admin.service';
|
||||
import { PluginsModule } from '../plugins/plugins.module';
|
||||
import { SettingsModule } from '../settings/settings.module';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
|
||||
@Module({
|
||||
imports: [PluginsModule, SettingsModule, AuditModule],
|
||||
imports: [PluginsModule, SettingsModule, AuditModule, AddonsModule],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as svc from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { SettingsService } from '../settings/settings.service';
|
||||
import { invalidateMcpSessions } from '../../mcp';
|
||||
import { getPreferencesMatrix, setAdminPreferences } from '../../services/notificationPreferencesService';
|
||||
@@ -13,7 +14,10 @@ import { adminResetPasskeys } from '../../services/passkeyService';
|
||||
*/
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
constructor(private readonly settings: SettingsService) {}
|
||||
constructor(
|
||||
private readonly settings: SettingsService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
// Users
|
||||
listUsers() { return svc.listUsers(); }
|
||||
@@ -41,16 +45,16 @@ export class AdminService {
|
||||
deleteInvite(id: string) { return svc.deleteInvite(id); }
|
||||
|
||||
// Feature toggles
|
||||
getBagTracking() { return svc.getBagTracking(); }
|
||||
updateBagTracking(enabled: unknown) { return svc.updateBagTracking(enabled as boolean); }
|
||||
getBagTracking() { return this.addons.getBagTracking(); }
|
||||
updateBagTracking(enabled: unknown) { return this.addons.updateBagTracking(enabled as boolean); }
|
||||
getPlacesPhotos() { return svc.getPlacesPhotos(); }
|
||||
updatePlacesPhotos(enabled: boolean) { return svc.updatePlacesPhotos(enabled); }
|
||||
getPlacesAutocomplete() { return svc.getPlacesAutocomplete(); }
|
||||
updatePlacesAutocomplete(enabled: boolean) { return svc.updatePlacesAutocomplete(enabled); }
|
||||
getPlacesDetails() { return svc.getPlacesDetails(); }
|
||||
updatePlacesDetails(enabled: boolean) { return svc.updatePlacesDetails(enabled); }
|
||||
getCollabFeatures() { return svc.getCollabFeatures(); }
|
||||
updateCollabFeatures(body: unknown) { return svc.updateCollabFeatures(body as Parameters<typeof svc.updateCollabFeatures>[0]); }
|
||||
getCollabFeatures() { return this.addons.getCollabFeatures(); }
|
||||
updateCollabFeatures(body: unknown) { return this.addons.updateCollabFeatures(body as Parameters<AddonsService['updateCollabFeatures']>[0]); }
|
||||
|
||||
// Packing templates
|
||||
listPackingTemplates() { return svc.listPackingTemplates(); }
|
||||
|
||||
@@ -8,9 +8,10 @@ import { LlmParseModule } from '../llm-parse/llm-parse.module';
|
||||
import { ReservationsModule } from '../reservations/reservations.module';
|
||||
import { PermissionsModule } from '../permissions/permissions.module';
|
||||
import { BudgetModule } from '../budget/budget.module';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
|
||||
@Module({
|
||||
imports: [LlmParseModule, ReservationsModule, PermissionsModule, BudgetModule],
|
||||
imports: [LlmParseModule, ReservationsModule, PermissionsModule, BudgetModule, AddonsModule],
|
||||
controllers: [BookingImportController, FeaturesController],
|
||||
providers: [BookingImportService, KitineraryExtractorService, ImportJobsService],
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { verifyTripAccess } from '../../services/tripAccess';
|
||||
import { ReservationsService } from '../reservations/reservations.service';
|
||||
import { createPlace } from '../../services/placeService';
|
||||
import { BudgetService } from '../budget/budget.service';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { searchNominatim } from '../../services/mapsService';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
@@ -26,6 +26,7 @@ export class BookingImportService {
|
||||
private readonly reservations: ReservationsService,
|
||||
private readonly permissions: PermissionsService,
|
||||
private readonly budget: BudgetService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
private get db() {
|
||||
@@ -235,7 +236,7 @@ export class BookingImportService {
|
||||
|
||||
// Turn an extracted price into a real linked cost (Costs addon), so the
|
||||
// booking shows up as an expense — not just a price in metadata.
|
||||
if (isAddonEnabled(ADDON_IDS.BUDGET)) {
|
||||
if (this.addons.isAddonEnabled(ADDON_IDS.BUDGET)) {
|
||||
const meta =
|
||||
reservationData.metadata && typeof reservationData.metadata === 'object'
|
||||
? (reservationData.metadata as Record<string, unknown>)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { KitineraryExtractorService } from './kitinerary-extractor.service';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
|
||||
/** Exposes server feature flags consumed by the frontend to show/hide optional UI. */
|
||||
@Controller('api/health')
|
||||
export class FeaturesController {
|
||||
constructor(private readonly extractor: KitineraryExtractorService) {}
|
||||
constructor(
|
||||
private readonly extractor: KitineraryExtractorService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
@Get('features')
|
||||
features() {
|
||||
@@ -14,7 +17,7 @@ export class FeaturesController {
|
||||
bookingImport: this.extractor.isAvailable(),
|
||||
// Addon-level flag (per-user config availability is reported per-file in
|
||||
// the preview response). Drives whether the client shows AI affordances.
|
||||
aiParsing: isAddonEnabled(ADDON_IDS.LLM_PARSING),
|
||||
aiParsing: this.addons.isAddonEnabled(ADDON_IDS.LLM_PARSING),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@trek/nest-mcp';
|
||||
import { z } from 'zod';
|
||||
import { isDemoUser } from '../../services/authService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { safeBroadcast, noAccess, hasTripPermission, permissionDenied } from '../../mcp/tools/_shared';
|
||||
import { getTripOwner, listMembers } from '../../services/tripService';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@trek/nest-mcp';
|
||||
import { z } from 'zod';
|
||||
import { isDemoUser } from '../../services/authService';
|
||||
import { isAddonEnabled, getCollabFeatures } from '../../services/adminService';
|
||||
import { isAddonEnabled, getCollabFeatures } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { safeBroadcast, noAccess, hasTripPermission, permissionDenied } from '../../mcp/tools/_shared';
|
||||
import { CollabService } from './collab.service';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CanActivate, HttpException, Injectable } from '@nestjs/common';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
|
||||
/**
|
||||
@@ -9,8 +9,10 @@ import { ADDON_IDS } from '../../addons';
|
||||
*/
|
||||
@Injectable()
|
||||
export class CollectionsAddonGuard implements CanActivate {
|
||||
constructor(private readonly addons: AddonsService) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (!isAddonEnabled(ADDON_IDS.COLLECTIONS)) {
|
||||
if (!this.addons.isAddonEnabled(ADDON_IDS.COLLECTIONS)) {
|
||||
throw new HttpException({ error: 'Collections addon is not enabled' }, 404);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { CollectionsController } from './collections.controller';
|
||||
import { CollectionsService } from './collections.service';
|
||||
import { CollectionsAddonGuard } from './collections-addon.guard';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
|
||||
@Module({
|
||||
imports: [AddonsModule],
|
||||
controllers: [CollectionsController],
|
||||
providers: [CollectionsService, CollectionsAddonGuard],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CanActivate, HttpException, Injectable } from '@nestjs/common';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
|
||||
/**
|
||||
@@ -10,8 +10,10 @@ import { ADDON_IDS } from '../../addons';
|
||||
*/
|
||||
@Injectable()
|
||||
export class AirtrailAddonGuard implements CanActivate {
|
||||
constructor(private readonly addons: AddonsService) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (!isAddonEnabled(ADDON_IDS.AIRTRAIL)) {
|
||||
if (!this.addons.isAddonEnabled(ADDON_IDS.AIRTRAIL)) {
|
||||
throw new HttpException({ error: 'AirTrail addon is not enabled' }, 404);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
|
||||
import { AirtrailController } from './airtrail.controller';
|
||||
import { AirtrailImportController } from './airtrail-import.controller';
|
||||
import { PermissionsModule } from '../permissions/permissions.module';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
import { AirtrailAddonGuard } from './airtrail-addon.guard';
|
||||
|
||||
/**
|
||||
* AirTrail integration domain. The connection lives under
|
||||
@@ -10,7 +12,8 @@ import { PermissionsModule } from '../permissions/permissions.module';
|
||||
* services/airtrail/* (plain functions over better-sqlite3).
|
||||
*/
|
||||
@Module({
|
||||
imports: [PermissionsModule],
|
||||
imports: [PermissionsModule, AddonsModule],
|
||||
controllers: [AirtrailController, AirtrailImportController],
|
||||
providers: [AirtrailAddonGuard],
|
||||
})
|
||||
export class AirtrailModule {}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { JourneyController } from './journey.controller';
|
||||
import { JourneyPublicController } from './journey-public.controller';
|
||||
import { JourneyService } from './journey.service';
|
||||
import { JourneyAddonGuard } from './journey-addon.guard';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
|
||||
@Module({
|
||||
imports: [AddonsModule],
|
||||
controllers: [JourneyController, JourneyPublicController],
|
||||
providers: [JourneyService, JourneyAddonGuard],
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as svc from '../../services/journeyService';
|
||||
import * as share from '../../services/journeyShareService';
|
||||
import { uploadToImmich, streamImmichAsset } from '../../services/memories/immichService';
|
||||
import { streamPhoto } from '../../services/memories/photoResolverService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import type { Response } from 'express';
|
||||
|
||||
@@ -16,14 +16,17 @@ import type { Response } from 'express';
|
||||
*/
|
||||
@Injectable()
|
||||
export class JourneyService {
|
||||
constructor(private readonly dbs: DatabaseService) {}
|
||||
constructor(
|
||||
private readonly dbs: DatabaseService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
private get db() {
|
||||
return this.dbs.connection;
|
||||
}
|
||||
|
||||
journeyAddonEnabled(): boolean {
|
||||
return isAddonEnabled(ADDON_IDS.JOURNEY);
|
||||
return this.addons.isAddonEnabled(ADDON_IDS.JOURNEY);
|
||||
}
|
||||
|
||||
// Journeys
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { decryptLlmApiKey, LLM_PROVIDERS, type LlmProvider, type ResolvedLlmConfig } from '../../services/llmConfig';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import { SettingsService } from '../settings/settings.service';
|
||||
@@ -19,6 +19,7 @@ export class LlmConfigResolver {
|
||||
constructor(
|
||||
private readonly settings: SettingsService,
|
||||
private readonly dbService: DatabaseService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,7 @@ export class LlmConfigResolver {
|
||||
* else null. This is the single place the API key is decrypted.
|
||||
*/
|
||||
resolve(userId: number): ResolvedLlmConfig | null {
|
||||
if (!isAddonEnabled(ADDON_IDS.LLM_PARSING)) return null;
|
||||
if (!this.addons.isAddonEnabled(ADDON_IDS.LLM_PARSING)) return null;
|
||||
return this.readInstanceConfig() ?? this.readUserConfig(userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import { LlmLocalService } from './llm-local.service';
|
||||
import { LlmLocalController } from './llm-local.controller';
|
||||
import { LlmConfigResolver } from './llm-config.resolver';
|
||||
import { SettingsModule } from '../settings/settings.module';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
|
||||
/**
|
||||
* Provides the LLM booking-import fallback; imported by BookingImportModule.
|
||||
* Exports LlmConfigResolver for the plugin host wiring (PluginHostDepsFactory).
|
||||
*/
|
||||
@Module({
|
||||
imports: [SettingsModule],
|
||||
imports: [SettingsModule, AddonsModule],
|
||||
controllers: [LlmLocalController],
|
||||
providers: [LlmParseService, LlmLocalService, LlmConfigResolver],
|
||||
exports: [LlmParseService, LlmConfigResolver],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { OauthApiController } from './oauth-api.controller';
|
||||
import { OauthService } from './oauth.service';
|
||||
import { RateLimitService } from '../auth/rate-limit.service';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
|
||||
/**
|
||||
* OAuth 2.1 server (MCP). Public token/userinfo/revoke endpoints + the SPA's
|
||||
@@ -12,7 +13,7 @@ import { AuditModule } from '../audit/audit.module';
|
||||
* strangler lists /oauth/token, /oauth/userinfo, /oauth/revoke explicitly.
|
||||
*/
|
||||
@Module({
|
||||
imports: [AuditModule],
|
||||
imports: [AuditModule, AddonsModule],
|
||||
controllers: [OauthPublicController, OauthApiController],
|
||||
providers: [OauthService, RateLimitService],
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as oauth from '../../services/oauthService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { getMcpSafeUrl } from '../../app-config';
|
||||
|
||||
@@ -11,7 +11,9 @@ import { getMcpSafeUrl } from '../../app-config';
|
||||
*/
|
||||
@Injectable()
|
||||
export class OauthService {
|
||||
mcpEnabled(): boolean { return isAddonEnabled(ADDON_IDS.MCP); }
|
||||
constructor(private readonly addons: AddonsService) {}
|
||||
|
||||
mcpEnabled(): boolean { return this.addons.isAddonEnabled(ADDON_IDS.MCP); }
|
||||
mcpSafeUrl(): string { return getMcpSafeUrl(); }
|
||||
|
||||
consumeAuthCode(code: string) { return oauth.consumeAuthCode(code); }
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
} from '@trek/nest-mcp';
|
||||
import { z } from 'zod';
|
||||
import { isDemoUser } from '../../services/authService';
|
||||
import { isAddonEnabled, deletePackingTemplate } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { deletePackingTemplate } from '../../services/adminService';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { safeBroadcast, noAccess, hasTripPermission, permissionDenied, isAdminUser, adminRequired } from '../../mcp/tools/_shared';
|
||||
import { PackingService } from './packing.service';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { verifyJwtAndLoadUser } from '../../middleware/auth';
|
||||
import { db } from '../../db/database';
|
||||
import { mcpHandler } from '../../mcp';
|
||||
import { trekOAuthProvider, trekClientsStore } from '../../mcp/oauthProvider';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { ALL_SCOPES } from '../../mcp/scopes';
|
||||
import { mcpAuthMetadataRouter } from '@modelcontextprotocol/sdk/server/auth/router';
|
||||
|
||||
@@ -15,7 +15,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { PermissionsService } from '../../permissions/permissions.service';
|
||||
import { listTrips, updateTrip, createTrip, removeMember as removeTripMemberSvc, NotFoundError, ValidationError } from '../../../services/tripService';
|
||||
import { createPlace, updatePlace, deletePlace } from '../../../services/placeService';
|
||||
import { isAddonEnabled } from '../../../services/adminService';
|
||||
import { AddonsService } from '../../addons/addons.service';
|
||||
import { isDemoEmail } from '../../../services/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 } from '../../../services/journeyService';
|
||||
@@ -40,12 +40,6 @@ import { PluginRpcHost, ForbiddenResource, BadParams } from './rpc-host';
|
||||
import { appendAudit } from './plugin-audit';
|
||||
import { getPluginDataDb, budgetFor } from './plugin-host-state';
|
||||
|
||||
// A subsystem read is refused when its addon is off — parity with the app, where a
|
||||
// disabled addon means there is simply nothing to read (same shape as the Costs gate).
|
||||
function requireAddon(addonId: string, noun: string): void {
|
||||
if (!isAddonEnabled(addonId)) throw new ForbiddenResource(`the ${noun} addon is disabled`);
|
||||
}
|
||||
|
||||
// collectionsService self-gates per-collection role by THROWING status-tagged errors
|
||||
// (assertAccess 404, assertCanEdit 403, validation 400). Map them onto the RPC error
|
||||
// classes so a plugin sees RESOURCE_FORBIDDEN / BAD_PARAMS instead of HOST_ERROR.
|
||||
@@ -156,6 +150,7 @@ export class PluginHostDepsFactory {
|
||||
private readonly days: DaysService,
|
||||
private readonly permissions: PermissionsService,
|
||||
private readonly exchangeRates: ExchangeRatesService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -179,6 +174,11 @@ export class PluginHostDepsFactory {
|
||||
}
|
||||
|
||||
create(id: string, granted: ReadonlySet<string>, router: PluginCallRouter): PluginRpcHost {
|
||||
// A subsystem read is refused when its addon is off — parity with the app, where a
|
||||
// disabled addon means there is simply nothing to read (same shape as the Costs gate).
|
||||
const requireAddon = (addonId: string, noun: string): void => {
|
||||
if (!this.addons.isAddonEnabled(addonId)) throw new ForbiddenResource(`the ${noun} addon is disabled`);
|
||||
};
|
||||
return new PluginRpcHost(id, granted, {
|
||||
// Resolve the data handle lazily on every access rather than capturing it once.
|
||||
// disable()/uninstall() drop the entry from `running` BEFORE awaiting the kill grace
|
||||
@@ -210,7 +210,7 @@ export class PluginHostDepsFactory {
|
||||
broadcastToUser: (userId, payload) => broadcastToUser(userId, { type: `plugin:${id}`, ...payload }),
|
||||
audit: (entry) => appendAudit(this.db.connection, entry),
|
||||
// --- Costs (budget items) ---
|
||||
budgetAddonEnabled: () => isAddonEnabled(ADDON_IDS.BUDGET),
|
||||
budgetAddonEnabled: () => this.addons.isAddonEnabled(ADDON_IDS.BUDGET),
|
||||
// Same gate as a REST/MCP budget mutation: the acting user must have trip
|
||||
// access AND the 'budget_edit' permission for their global role.
|
||||
canEditCosts: (tripId, userId) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import { canAccessJourney } from '../../services/journeyService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { pluginsEnabled } from './kill-switch';
|
||||
@@ -69,6 +69,7 @@ export class JournalEntryRowsController {
|
||||
constructor(
|
||||
private readonly runtime: PluginRuntimeService,
|
||||
private readonly dbs: DatabaseService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
@Get(':entryId')
|
||||
@@ -76,7 +77,7 @@ export class JournalEntryRowsController {
|
||||
@Param('entryId') entryIdRaw: string,
|
||||
@Req() req: Request & { user?: { id: number } },
|
||||
): Promise<{ providers: ProviderResult[] }> {
|
||||
if (!pluginsEnabled() || !isAddonEnabled(ADDON_IDS.JOURNEY)) return { providers: [] };
|
||||
if (!pluginsEnabled() || !this.addons.isAddonEnabled(ADDON_IDS.JOURNEY)) return { providers: [] };
|
||||
const entryId = Number(entryIdRaw);
|
||||
const userId = req.user?.id;
|
||||
if (!Number.isFinite(entryId) || userId == null) return { providers: [] };
|
||||
|
||||
@@ -27,7 +27,7 @@ import { assertHostCompatible, PluginRegistryService, RegistryError } from './re
|
||||
import { hostSatisfies, hostVersion } from './install/host-compat';
|
||||
import { keyFingerprint } from './signature-status';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import type { PluginDependency } from './install/manifest';
|
||||
import type { VersionMismatch, PluginDepRow } from './dependencies';
|
||||
import { parseDependencies, disabledRequiredAddons, resolveDependencyState, enableOrder, findDependentsTransitive, DependencyCycleError } from './dependencies';
|
||||
@@ -166,6 +166,7 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(
|
||||
private readonly dbs: DatabaseService,
|
||||
private readonly audit: AuditService,
|
||||
private readonly addons: AddonsService,
|
||||
private readonly registry?: PluginRegistryService,
|
||||
private readonly hostDeps?: PluginHostDepsFactory,
|
||||
) {}
|
||||
@@ -514,7 +515,7 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
const deps = parseDependencies(row.dependencies);
|
||||
const disabledAddons = disabledRequiredAddons(deps, isAddonEnabled);
|
||||
const disabledAddons = disabledRequiredAddons(deps, (id) => this.addons.isAddonEnabled(id));
|
||||
if (disabledAddons.length) {
|
||||
throw new PluginDependencyError(`plugin ${id} requires disabled addon(s): ${disabledAddons.join(', ')}`, 'ADDON_DISABLED', {
|
||||
addons: disabledAddons,
|
||||
|
||||
@@ -38,6 +38,7 @@ import { CollabModule } from '../collab/collab.module';
|
||||
import { VacayModule } from '../vacay/vacay.module';
|
||||
import { PermissionsModule } from '../permissions/permissions.module';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
|
||||
/**
|
||||
* Plugin system (#plugins). M0 read side + M2 isolated runtime + M3 frontend:
|
||||
@@ -49,7 +50,7 @@ import { AuditModule } from '../audit/audit.module';
|
||||
@Module({
|
||||
// The DI-native domain services the plugin host wiring injects
|
||||
// (PluginHostDepsFactory); DatabaseModule is @Global, so not listed.
|
||||
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule, AssignmentsModule, LlmParseModule, FilesModule, CollabModule, VacayModule, PermissionsModule, AuditModule],
|
||||
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule, AssignmentsModule, LlmParseModule, FilesModule, CollabModule, VacayModule, PermissionsModule, AuditModule, AddonsModule],
|
||||
controllers: [PluginsController, PluginsFeedController, PluginsProxyController, PluginFrameController, PlaceDetailsController, TripWarningsController, ViewContributionsController, TripCardContributionsController, PluginPhotosController, PluginCalendarController, MapMarkersController, MapLayersController, PluginRoutesController, DayScheduleController, PdfSectionsController, AtlasLayersController, JournalEntryRowsController, PluginUserSettingsController, PluginOAuthController, PluginActivityController],
|
||||
providers: [PluginsService, PluginRuntimeService, PluginRegistryService, PluginOAuthService, PluginHostDepsFactory],
|
||||
// Exported so the admin addon-toggle handler can cascade-disable plugins whose
|
||||
|
||||
@@ -7,7 +7,7 @@ import { maybe_encrypt_api_key, decrypt_api_key } from '../../services/apiKeyCry
|
||||
import { readAudit } from './host/plugin-audit';
|
||||
import { keyFingerprint } from './signature-status';
|
||||
import { pluginBudgetUsage } from './host/plugin-host-state';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { parseDependencies, disabledRequiredAddons, resolveDependencyState, type PluginDepRow, type PluginDependencies, type VersionMismatch } from './dependencies';
|
||||
import { hostSatisfies, hostVersion } from './install/host-compat';
|
||||
import type { PluginDependency } from './install/manifest';
|
||||
@@ -91,7 +91,10 @@ export interface PluginListItem {
|
||||
|
||||
@Injectable()
|
||||
export class PluginsService {
|
||||
constructor(private readonly dbs: DatabaseService) {}
|
||||
constructor(
|
||||
private readonly dbs: DatabaseService,
|
||||
private readonly addons: AddonsService,
|
||||
) {}
|
||||
|
||||
private get db() {
|
||||
return this.dbs.connection;
|
||||
@@ -121,7 +124,7 @@ export class PluginsService {
|
||||
);
|
||||
const plugins: PluginListItem[] = rows.map((r) => {
|
||||
const deps = parseDependencies(r.dependencies);
|
||||
const disabledAddons = disabledRequiredAddons(deps, isAddonEnabled);
|
||||
const disabledAddons = disabledRequiredAddons(deps, (id) => this.addons.isAddonEnabled(id));
|
||||
const state = resolveDependencyState(deps, installed);
|
||||
// Mirrors the order of assertActivatable's gate, so the card explains the same
|
||||
// blocker the activate call would hit rather than a second, lesser one.
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@trek/nest-mcp';
|
||||
import { z } from 'zod';
|
||||
import { isDemoUser } from '../../services/authService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { safeBroadcast, noAccess, hasTripPermission, permissionDenied } from '../../mcp/tools/_shared';
|
||||
import { TodoService } from './todo.service';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@trek/nest-mcp';
|
||||
import { z } from 'zod';
|
||||
import { isDemoUser, getCurrentUser } from '../../services/authService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { VacayService } from './vacay.service';
|
||||
|
||||
|
||||
@@ -565,22 +565,6 @@ export function deleteInvite(id: string) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── Bag Tracking ───────────────────────────────────────────────────────────
|
||||
|
||||
export function getBagTracking() {
|
||||
const row = db.prepare("SELECT value FROM app_settings WHERE key = 'bag_tracking_enabled'").get() as
|
||||
| { value: string }
|
||||
| undefined;
|
||||
return { enabled: row?.value === 'true' };
|
||||
}
|
||||
|
||||
export function updateBagTracking(enabled: boolean) {
|
||||
db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('bag_tracking_enabled', ?)").run(
|
||||
enabled ? 'true' : 'false',
|
||||
);
|
||||
return { enabled: !!enabled };
|
||||
}
|
||||
|
||||
// ── Places Photos ─────────────────────────────────────────────────────────
|
||||
|
||||
export function getPlacesPhotos() {
|
||||
@@ -629,56 +613,6 @@ export function updatePlacesDetails(enabled: boolean) {
|
||||
return { enabled: !!enabled };
|
||||
}
|
||||
|
||||
// ── Collab Features ───────────────────────────────────────────────────────
|
||||
|
||||
const COLLAB_FEATURE_KEYS = [
|
||||
'collab_chat_enabled',
|
||||
'collab_notes_enabled',
|
||||
'collab_polls_enabled',
|
||||
'collab_whatsnext_enabled',
|
||||
] as const;
|
||||
|
||||
export function getCollabFeatures() {
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT key, value FROM app_settings WHERE key IN ('collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled')",
|
||||
)
|
||||
.all() as { key: string; value: string }[];
|
||||
const map: Record<string, string> = {};
|
||||
for (const r of rows) map[r.key] = r.value;
|
||||
return {
|
||||
chat: map['collab_chat_enabled'] !== 'false',
|
||||
notes: map['collab_notes_enabled'] !== 'false',
|
||||
polls: map['collab_polls_enabled'] !== 'false',
|
||||
whatsnext: map['collab_whatsnext_enabled'] !== 'false',
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCollabFeatures(features: {
|
||||
chat?: boolean;
|
||||
notes?: boolean;
|
||||
polls?: boolean;
|
||||
whatsnext?: boolean;
|
||||
}) {
|
||||
const mapping: Record<string, string> = {
|
||||
chat: 'collab_chat_enabled',
|
||||
notes: 'collab_notes_enabled',
|
||||
polls: 'collab_polls_enabled',
|
||||
whatsnext: 'collab_whatsnext_enabled',
|
||||
};
|
||||
const before = getCollabFeatures();
|
||||
const stmt = db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)');
|
||||
for (const [feat, key] of Object.entries(mapping)) {
|
||||
if (features[feat] !== undefined) stmt.run(key, features[feat] ? 'true' : 'false');
|
||||
}
|
||||
const after = getCollabFeatures();
|
||||
// Collab flags gate MCP tool/resource registration, so callers must know
|
||||
// whether anything actually flipped — a no-op save must not tear down every
|
||||
// live MCP session (#1414).
|
||||
const changed = (Object.keys(after) as Array<keyof typeof after>).some((k) => after[k] !== before[k]);
|
||||
return { features: after, changed };
|
||||
}
|
||||
|
||||
// ── Packing Templates ──────────────────────────────────────────────────────
|
||||
|
||||
export function listPackingTemplates() {
|
||||
@@ -804,11 +738,10 @@ export function deleteTemplateItem(itemId: string) {
|
||||
}
|
||||
|
||||
// ── Addons ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export function isAddonEnabled(addonId: string): boolean {
|
||||
const addon = db.prepare('SELECT enabled FROM addons WHERE id = ?').get(addonId) as { enabled: number } | undefined;
|
||||
return !!addon?.enabled;
|
||||
}
|
||||
// isAddonEnabled and the bag-tracking/collab-features flags moved to
|
||||
// nest/addons/addons.service.ts (bridge: nest/addons/addons.bridge.ts) —
|
||||
// finding `admin-1`. listAddons/updateAddon + photo-provider CRUD stay here
|
||||
// until the Wave-5 admin migration.
|
||||
|
||||
export function listAddons() {
|
||||
const addons = db.prepare('SELECT * FROM addons ORDER BY sort_order, id').all() as Addon[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { db } from '../../db/database';
|
||||
import { broadcast } from '../../websocket';
|
||||
import { isAddonEnabled } from '../adminService';
|
||||
import { isAddonEnabled } from '../../nest/addons/addons.bridge';
|
||||
import { logError, logInfo } from '../../nest/audit/audit-log.logger';
|
||||
import { getReservation, getReservationWithJoins, updateReservation } from '../../nest/reservations/reservations.bridge';
|
||||
import {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Jimp } from 'jimp'
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import crypto from 'crypto'
|
||||
import { isAddonEnabled } from '../adminService'
|
||||
import { isAddonEnabled } from '../../nest/addons/addons.bridge'
|
||||
import { ADDON_IDS } from '../../addons'
|
||||
|
||||
const THUMB_MAX = 800
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto, { randomBytes, createHash, randomUUID } from 'crypto';
|
||||
import { db } from '../db/database';
|
||||
import { isAddonEnabled } from './adminService';
|
||||
import { isAddonEnabled } from '../nest/addons/addons.bridge';
|
||||
import { validateScopes } from '../mcp/scopes';
|
||||
import { ADDON_IDS } from '../addons';
|
||||
import { User } from '../types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import semver from 'semver';
|
||||
import { isAddonEnabled } from '../services/adminService.js';
|
||||
import { isAddonEnabled } from '../nest/addons/addons.bridge.js';
|
||||
import type { NoticeCondition, SystemNotice } from './types.js';
|
||||
|
||||
interface ConditionContext {
|
||||
|
||||
Reference in New Issue
Block a user