Import/export conversions run asynchronously against an internal pandoc-server
sidecar with limits and graceful failure (ADR 0009). This is the plumbing;
the import (#63) and export (#65) features enqueue jobs onto it.
Sidecar & config:
- pandoc/core:3.6 in HTTP server mode added to the Compose stack, internal
network only, with a wget healthcheck on /version; the api depends on it
healthy and reaches it via the new PANDOC_URL env (default http://pandoc:3030).
- readyz gains a warning-level `converter` check: an unreachable sidecar
degrades import/export but never flips the instance to unready (new `warn`
status on ReadinessCheck).
Conversion flow (apps/api/src/import-export/):
- ConversionJob table (per-request work queue, distinct from the name-keyed
maintenance Job table): owner, formats, input/result bytes, status, attempts,
lockedAt. Migration + owner cascade.
- PandocConverter (abstract) + PandocServerConverter: POST / with
{text,from,to,standalone}; binary input formats (docx/odt/…) are base64-encoded
in `text`; 60 s AbortController timeout; input/output size caps. Failures map
to distinct localized codes — converter_unavailable / converter_timeout
(retryable) and conversion_failed (final).
- ConversionWorker: claims one job at a time with `FOR UPDATE SKIP LOCKED`
(safe against overlapping sweeps and a second process), recovers a stale
RUNNING lock, retries transient failures up to 3 attempts then fails. A 2 s
sweep plus wake-on-enqueue means a queued job survives an API restart.
- ConversionJobService.enqueue (size-limited) + owner-scoped GET /jobs/:id
(poll) and GET /jobs/:id/result (stream the output); a foreign/unknown id is
404. ConversionJobView in @dorfteich/shared.
Tests:
- conversion-job.e2e.db.test.ts (fake converter injected via a new createTestApp
override hook): enqueue→convert→poll→result; foreign/unknown job 404; a
persisted PENDING job picked up by a fresh app's worker (restart survival);
sidecar-down fails after 3 retries while the API stays healthy.
- pandoc.converter.test.ts: success, non-200→conversion_failed, refused→
converter_unavailable, and a delay-injecting server→converter_timeout.
- Verified locally against a real pandoc/core:3.6 container: markdown→html,
markdown→docx (valid PK/OOXML bytes), and a docx→markdown round-trip.
Local: typecheck, lint, i18n:check, build all green; api 193 tests
(9 new), shared 121, web 50.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
AuthModule implements the M1 core as one coherent unit:
Signup (#13): POST signup/verify-email/resend-verification with shared
Zod validation (field-level error details), double opt-in via hashed
single-use tokens (24h, superseding reissue), registration_mode
enforcement, and per-IP rate limits.
Sessions (#14): opaque 32-byte cookie tokens stored as SHA-256 row
ids, sliding 30-day expiry (refresh at most hourly), global AuthGuard
with @Public() opt-out attaching the user to every request, CSRF
origin check on mutating requests, per-account login backoff (5/15min,
reset on success), generic 401 for wrong-vs-unknown credentials,
logout with immediate invalidation, GET /auth/me.
Reset (#15): forgot-password without account enumeration, one-hour
single-use tokens, reset destroys all existing sessions.
A 14-case supertest e2e suite drives every flow against the test
database, reading verification/reset links from the mail outbox.
Closes#13Closes#14Closes#15
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/api gains Prisma (instance_settings as the first model) with the
initial migration applied automatically at startup via prisma migrate
deploy, a lazy-connecting PrismaService, and GET /api/v1/readyz
reporting named checks (database reachable, migrations applied) with
200/503. DATABASE_URL joins the validated environment schema;
MIGRATE_ON_START=false skips deploys for tests and tooling. An
idempotent seed script and a Compose dev overlay with PostgreSQL
(host port 5434 — 5433 is taken locally) complete the loop.
Closes#3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/api boots a NestJS application with: Zod-validated environment
configuration (schema in @dorfteich/shared, fails fast listing every
invalid variable), structured pino request logging via nestjs-pino
(pretty in development, JSON otherwise, auth headers redacted), a
global exception filter producing the uniform ApiErrorBody shape, and
GET /api/v1/healthz. Vitest runs Nest through SWC for decorator
metadata; supertest covers healthz and the 404 error shape.
Closes#2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>