Add an ephemeral, process-local blob store so the in-app agent (and the
embedded MCP) can hand a large page document and its images to an external
consumer WITHOUT routing the bytes through the model context or Docmost auth.
- SandboxStore (@Injectable singleton): Map<uuid,{buf,mime,sha256,expiresAt}>
in RAM only. put() picks a per-blob cap by mime (image vs doc), enforces a
total-bytes RAM guard with oldest-first eviction, and stamps a TTL; get()
lazily expires. sha256 computed at put() doubles as the strong ETag. An
unref'd sweep interval clears expired entries and is cleared on destroy.
- GET /api/sb/:uuid anonymous controller: serves raw bytes with Content-Type,
Content-Length and ETag=sha256; 404 on missing/expired/non-UUID (anti-
traversal), 304 on a matching If-None-Match. No tokens, no 401 — the
capability is the unguessable UUID + short TTL + TLS. Auth-exempt the same
way as /api/files/public (no JwtAuthGuard) plus an /api/sb entry in main.ts's
workspace-resolution preHandler so a remote consumer with no workspace host
is not rejected.
- stash_page tool in both layers (MCP resource_link + in-app {uri,size,sha256,
images}). client.stashPage serializes the get_page_json shape, mirrors every
INTERNAL file/image src (type-agnostic, covers drawio/excalidraw/video/file)
into the sandbox under Docmost auth and rewrites src to the sandbox URL;
external http(s) srcs are left untouched; dedup by src; a failed image fetch
is counted, never aborts the doc.
- SANDBOX_PUBLIC_URL / SANDBOX_TTL_MS / SANDBOX_MAX_BYTES /
SANDBOX_MAX_IMAGE_BYTES / SANDBOX_MAX_TOTAL_BYTES wired through the
environment service + validation + .env.example.
- SandboxModule (@Global) provides the shared store to the controller,
McpService and AiChatToolsService (same instance for put and get).
Tests: SandboxStore (round-trip, sha256, TTL lazy + sweep, caps, eviction),
SandboxController (200+ETag+CT+CL, 404 missing/expired/non-UUID, 304), and a
mock-HTTP stashPage test (mirror+rewrite internal, keep external, dedup, failed
image counted, returns only a link). Interoperates with the vvzvlad/habr-mcp
consumer's anonymous-GET + sha256-ETag + resource_link contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.7 KiB
TypeScript
108 lines
3.7 KiB
TypeScript
import { Module } from '@nestjs/common';
|
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
import { AppController } from './app.controller';
|
|
import { AppService } from './app.service';
|
|
import { EnvironmentService } from './integrations/environment/environment.service';
|
|
import { AuditActorInterceptor } from './common/interceptors/audit-actor.interceptor';
|
|
import { CoreModule } from './core/core.module';
|
|
import { EnvironmentModule } from './integrations/environment/environment.module';
|
|
import { CollaborationModule } from './collaboration/collaboration.module';
|
|
import { WsModule } from './ws/ws.module';
|
|
import { DatabaseModule } from '@docmost/db/database.module';
|
|
import { StorageModule } from './integrations/storage/storage.module';
|
|
import { MailModule } from './integrations/mail/mail.module';
|
|
import { QueueModule } from './integrations/queue/queue.module';
|
|
import { StaticModule } from './integrations/static/static.module';
|
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
|
import { HealthModule } from './integrations/health/health.module';
|
|
import { ExportModule } from './integrations/export/export.module';
|
|
import { ImportModule } from './integrations/import/import.module';
|
|
import { SecurityModule } from './integrations/security/security.module';
|
|
import { TelemetryModule } from './integrations/telemetry/telemetry.module';
|
|
import { RedisModule } from '@nestjs-labs/nestjs-ioredis';
|
|
import { RedisConfigService } from './integrations/redis/redis-config.service';
|
|
import { CacheModule } from '@nestjs/cache-manager';
|
|
import KeyvRedis from '@keyv/redis';
|
|
import { LoggerModule } from './common/logger/logger.module';
|
|
import { ClsModule } from 'nestjs-cls';
|
|
import { NoopAuditModule } from './integrations/audit/audit.module';
|
|
import { ThrottleModule } from './integrations/throttle/throttle.module';
|
|
import { McpModule } from './integrations/mcp/mcp.module';
|
|
import { SandboxModule } from './integrations/sandbox/sandbox.module';
|
|
import { AiModule } from './integrations/ai/ai.module';
|
|
import { AiChatModule } from './core/ai-chat/ai-chat.module';
|
|
|
|
const enterpriseModules = [];
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
if (require('./ee/ee.module')?.EeModule) {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
enterpriseModules.push(require('./ee/ee.module')?.EeModule);
|
|
}
|
|
} catch (err) {
|
|
if (process.env.CLOUD === 'true') {
|
|
console.warn('Failed to load enterprise modules. Exiting program.\n', err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
@Module({
|
|
imports: [
|
|
ClsModule.forRoot({
|
|
global: true,
|
|
middleware: { mount: true },
|
|
}),
|
|
LoggerModule,
|
|
NoopAuditModule,
|
|
CoreModule,
|
|
DatabaseModule,
|
|
EnvironmentModule,
|
|
RedisModule.forRootAsync({
|
|
useClass: RedisConfigService,
|
|
}),
|
|
CacheModule.registerAsync({
|
|
isGlobal: true,
|
|
useFactory: async (environmentService: EnvironmentService) => {
|
|
const redisUrl = environmentService.getRedisUrl();
|
|
|
|
return {
|
|
ttl: 5 * 1000,
|
|
stores: [new KeyvRedis(redisUrl)],
|
|
};
|
|
},
|
|
inject: [EnvironmentService],
|
|
}),
|
|
CollaborationModule,
|
|
WsModule,
|
|
QueueModule,
|
|
StaticModule,
|
|
HealthModule,
|
|
ImportModule,
|
|
ExportModule,
|
|
StorageModule.forRootAsync({
|
|
imports: [EnvironmentModule],
|
|
}),
|
|
MailModule.forRootAsync({
|
|
imports: [EnvironmentModule],
|
|
}),
|
|
EventEmitterModule.forRoot(),
|
|
SecurityModule,
|
|
TelemetryModule,
|
|
ThrottleModule,
|
|
McpModule,
|
|
SandboxModule,
|
|
AiModule,
|
|
AiChatModule,
|
|
...enterpriseModules,
|
|
],
|
|
controllers: [AppController],
|
|
providers: [
|
|
AppService,
|
|
{
|
|
provide: APP_INTERCEPTOR,
|
|
useClass: AuditActorInterceptor,
|
|
},
|
|
],
|
|
})
|
|
export class AppModule {}
|