906733b5c8
Blocking (review id 2514): - [security] Forbid symlinks in vaults. ensureServable now sets core.symlinks=false in each vault's local git config (a pushed symlink is checked out as a plain file, never a real link), and the engine cycle wraps every read/write/mkdir in an lstat/realpath guard (new path-guard.ts) that refuses a path that is — or traverses — a symlink, or whose realpath escapes the vault root. Prevents a writer from publishing /etc/passwd or the server .env, or writing outside the vault. Adds unit tests (path-guard.test.ts) + a read-guard integration test (cycle.test.ts) + real lstat/realpath in the roundtrip integration test. - [simplification] Delete dead lib/diff.ts + test/diff.test.ts and drop the now-unused @fellow/prosemirror-recreate-transform dependency. - [documentation] Add a CHANGELOG [Unreleased] → Added entry for git-sync. Warnings: - [test-coverage] Cover the CREATE-branch conflict-markers guard (a new .md with markers and no gitmost_id is recorded as a create failure, never created). Suggestions: - [stability] Bound each `git config` in ensureServable with a timeout. - [authz] Trigger endpoint resolves spaceId workspace-scoped and 404s a foreign space before any vault directory is created. - [stability] Attribute git-initiated moves to the service account (lastUpdatedById), via an optional actor param on PageService.movePage. - [documentation] Document the per-space autoMergeConflicts toggle in AGENTS.md. - [test-coverage] Cover the unterminated `:::` callout fence fallback. - [simplification] Move test-only roundtrip-helpers.ts out of src/ into test/. Architecture: - Move the Yjs/ProseMirror merge primitives (yjs-body-merge, three-way-merge, lcs + specs) into collaboration/merge/, breaking the collaboration → integrations/git-sync dependency cycle this PR introduced. - Port the schema-surface drift gate to packages/mcp (the mcp schema mirror had none); pins 52 entries. Deferred (with rationale in the review thread): the incremental-pull perf warning (correctness-neutral; needs a high-water-mark design + its own tests on the data-loss-critical path) and the redis-sync rolling-deploy mixed-version edge (the deficient behavior is in already-released old-instance code; the new code is correct on both sides; impact is a transient rollout-window artifact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
ForbiddenException,
|
|
HttpCode,
|
|
HttpStatus,
|
|
NotFoundException,
|
|
Post,
|
|
Get,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
|
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
|
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
|
import WorkspaceAbilityFactory from '../../core/casl/abilities/workspace-ability.factory';
|
|
import {
|
|
WorkspaceCaslAction,
|
|
WorkspaceCaslSubject,
|
|
} from '../../core/casl/interfaces/workspace-ability.type';
|
|
import { EnvironmentService } from '../environment/environment.service';
|
|
import { IsUUID } from 'class-validator';
|
|
import {
|
|
GitSyncOrchestrator,
|
|
GitSyncRunStatus,
|
|
} from './services/git-sync.orchestrator';
|
|
|
|
/** Body for the manual one-shot trigger. */
|
|
class TriggerGitSyncDto {
|
|
// The global ValidationPipe runs with whitelist:true, which STRIPS any field
|
|
// lacking a validation decorator — without this @IsUUID the spaceId would be
|
|
// dropped and arrive as undefined.
|
|
@IsUUID()
|
|
spaceId: string;
|
|
}
|
|
|
|
/**
|
|
* Ops/testing endpoints for the git-sync control plane. Admin-guarded
|
|
* (workspace Manage/Settings, mirroring WorkspaceController) so only workspace
|
|
* admins can force a cycle. Mounted under the global `/api` prefix:
|
|
* - POST /api/git-sync/trigger { spaceId } — run one cycle now (await result),
|
|
* - GET /api/git-sync/status — report whether sync is enabled + config.
|
|
*/
|
|
@UseGuards(JwtAuthGuard)
|
|
@Controller('git-sync')
|
|
export class GitSyncController {
|
|
constructor(
|
|
private readonly orchestrator: GitSyncOrchestrator,
|
|
private readonly environmentService: EnvironmentService,
|
|
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
|
private readonly spaceRepo: SpaceRepo,
|
|
) {}
|
|
|
|
/** Throw unless the caller is a workspace admin (Manage Settings). */
|
|
private assertAdmin(user: User, workspace: Workspace): void {
|
|
const ability = this.workspaceAbility.createForUser(user, workspace);
|
|
if (
|
|
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings)
|
|
) {
|
|
throw new ForbiddenException();
|
|
}
|
|
}
|
|
|
|
@HttpCode(HttpStatus.OK)
|
|
@Post('trigger')
|
|
async trigger(
|
|
@Body() dto: TriggerGitSyncDto,
|
|
@AuthUser() user: User,
|
|
@AuthWorkspace() workspace: Workspace,
|
|
): Promise<GitSyncRunStatus> {
|
|
this.assertAdmin(user, workspace);
|
|
// Verify the client-supplied spaceId BELONGS to this workspace before doing
|
|
// any work (review): without this, `runOnce` -> `buildSettings` reads the
|
|
// raw `spaces` row and creates an empty per-space vault directory for a
|
|
// foreign/non-existent space before the content read finally 404s. Resolve
|
|
// it workspace-scoped and 404 early.
|
|
const space = await this.spaceRepo.findById(dto.spaceId, workspace.id);
|
|
if (!space) {
|
|
throw new NotFoundException('Space not found');
|
|
}
|
|
// Use the workspace from the request context (never client-supplied).
|
|
return this.orchestrator.runOnce(dto.spaceId, workspace.id);
|
|
}
|
|
|
|
@HttpCode(HttpStatus.OK)
|
|
@Get('status')
|
|
async status(
|
|
@AuthUser() user: User,
|
|
@AuthWorkspace() workspace: Workspace,
|
|
): Promise<{
|
|
enabled: boolean;
|
|
dataDir: string;
|
|
pollIntervalMs: number;
|
|
debounceMs: number;
|
|
serviceUserConfigured: boolean;
|
|
}> {
|
|
this.assertAdmin(user, workspace);
|
|
return {
|
|
enabled: this.environmentService.isGitSyncEnabled(),
|
|
dataDir: this.environmentService.getGitSyncDataDir(),
|
|
pollIntervalMs: this.environmentService.getGitSyncPollIntervalMs(),
|
|
debounceMs: this.environmentService.getGitSyncDebounceMs(),
|
|
serviceUserConfigured: Boolean(
|
|
this.environmentService.getGitSyncServiceUserId(),
|
|
),
|
|
};
|
|
}
|
|
}
|