dorfteich/apps/api/src/app.module.ts
Claude Fable 5 05a979bac3
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m25s
CI / Build container images (pull_request) Successful in 2m58s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
CD / Build and push images (push) Has been cancelled
#222: read-access trail for classified pages
Instrument every full-content read channel for pages with
classification = vs_nfd (ADR 0023, variant A): SPA state fetch and read
rendering, public JSON content, no-JS shell, expanded embeds, public API
GET (incl. the MCP read_page path and write echoes), attachment download
under the #212 effective classification, all export shapes (markdown,
pond ZIP, account data export, queued docx/odt/pdf at enqueue), and
collab-token issuance as the api-side proxy for the WS join.

Events land in the new read_events table (no FKs — evidence survives
page purges and hard user deletions) with actor, session key
(session:/token:/job:/anon), page, pond, channel and the classification
at read time. Recording failures are NOT swallowed: a failed write
aborts the read (hard failure, the deliberate contrast to AuditService —
decision recorded in ADR 0023 and security.md §Logging, together with
the recorded residuals: content fragments and feeds).

One e2e test per channel proves both the event and its absence for
unclassified pages, plus the hard-failure semantics.

Refs #222.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 12:12:55 +02:00

117 lines
4.6 KiB
TypeScript

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { LoggerModule } from 'nestjs-pino';
import { AdminModule } from './admin/admin.module';
import { AuditModule } from './audit/audit.module';
import { AuthModule } from './auth/auth.module';
import { BackupModule } from './backup/backup.module';
import { ApiExceptionFilter } from './common/api-exception.filter';
import { maskTokenParam } from './common/mask-token-param';
import { SecurityHeadersMiddleware } from './common/security-headers.middleware';
import { CommentsModule } from './comments/comments.module';
import { CompactionModule } from './compaction/compaction.module';
import { AppConfig } from './config/app-config.service';
import { ConfigModule } from './config/config.module';
import { FilesModule } from './files/files.module';
import { GrantsModule } from './grants/grants.module';
import { HealthModule } from './health/health.module';
import { HomeModule } from './home/home.module';
import { ImportExportModule } from './import-export/import-export.module';
import { LabelsModule } from './labels/labels.module';
import { LegalModule } from './legal/legal.module';
import { LinksModule } from './links/links.module';
import { MailModule } from './mail/mail.module';
import { McpModule } from './mcp/mcp.module';
import { MembersModule } from './members/members.module';
import { PagesModule } from './pages/pages.module';
import { PermissionsModule } from './permissions/permissions.module';
import { PluginsModule } from './plugins/plugins.module';
import { PondsModule } from './ponds/ponds.module';
import { PrismaModule } from './prisma/prisma.module';
import { PublicApiModule } from './public-api/public-api.module';
import { PublicModule } from './public/public.module';
import { RateLimitModule } from './rate-limit/rate-limit.module';
import { ReadTrailModule } from './read-trail/read-trail.module';
import { SearchModule } from './search/search.module';
import { SettingsModule } from './settings/settings.module';
import { SetupModule } from './setup/setup.module';
import { TrashModule } from './trash/trash.module';
import { UsersModule } from './users/users.module';
import { NotificationsModule } from './notifications/notifications.module';
import { WatchesModule } from './watches/watches.module';
import { FavoritesModule } from './favorites/favorites.module';
import { VersionsModule } from './versions/versions.module';
@Module({
imports: [
ConfigModule,
PrismaModule,
AuditModule,
ReadTrailModule,
RateLimitModule,
MailModule,
SettingsModule,
// Before SetupModule and AuthModule: global guards run in registration
// order, and the maintenance gate (in-app restore, issue #103) must
// answer before anything touches the mid-restore database.
BackupModule,
// Before AuthModule: the setup gate must win over AuthGuard's 401 while
// setup is pending.
SetupModule,
UsersModule,
PermissionsModule,
PondsModule,
PagesModule,
CommentsModule,
WatchesModule,
FavoritesModule,
NotificationsModule,
FilesModule,
TrashModule,
CompactionModule,
VersionsModule,
LabelsModule,
LegalModule,
HomeModule,
LinksModule,
SearchModule,
GrantsModule,
MembersModule,
PublicModule,
PublicApiModule,
McpModule,
ImportExportModule,
PluginsModule,
AuthModule,
AdminModule,
LoggerModule.forRootAsync({
inject: [AppConfig],
useFactory: (config: AppConfig) => ({
pinoHttp: {
level: config.env.LOG_LEVEL,
// Human-readable logs in local development, JSON everywhere else.
transport: config.env.NODE_ENV === 'development' ? { target: 'pino-pretty' } : undefined,
autoLogging: config.env.NODE_ENV !== 'test',
// Request bodies are never logged (operations.md logging rules).
redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], remove: true },
// Feed tokens travel as `?token=` (issue #191) — mask them so the
// request log never stores the credential.
serializers: {
req: (req: { url?: string }) => ({ ...req, url: maskTokenParam(req.url) }),
},
},
}),
}),
HealthModule,
],
providers: [{ provide: APP_FILTER, useClass: ApiExceptionFilter }],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
// Module-level (not main.ts) so createTestApp boots the identical
// security-header/CORS middleware — see security-headers.middleware.ts.
consumer.apply(SecurityHeadersMiddleware).forRoutes('{*path}');
}
}