Files
suaveplan/openspec/archive/2026-05-19-add-api-plugin-file-upload/proposal.md
2026-05-27 23:29:53 +01:00

6.8 KiB

Add @suaveplan/api-plugin-file-upload

Package: @suaveplan/api-plugin-file-upload Status: PROPOSED Type: new-package

  • Design: ./design.md
  • Spec delta: ./specs/api-plugin-file-upload/spec.md

Why

@suaveplan/api apps repeatedly hand-roll multipart and chunked upload routes — every consumer rebuilds size limits, MIME allow-listing, hash verification, session bookkeeping, and per-user isolation from scratch and gets at least one of them wrong. A single composable plugin that bolts on top of @suaveplan/api-plugin-auth removes that surface from every downstream service and gives the same OTel + error-code observability every other plugin already provides.

The upstream enterprise/api-plugin-file-upload.md audit calls out concrete gaps in the legacy implementation: no HTTP routes shipped, as any event casts, in-memory state with no persistence, deterministic-ID concerns, no SHA-256 hashing. Genesis ships routes from day one, decouples the storage and session-store contracts, and wires the same auth/principal pipeline every sibling plugin uses.

What changes

  • ADDED: createFileUploadPlugin(options): PluginDescriptor<FileUploadPluginConfig> — Zod-validated factory mounting 6 routes through definePluginRoutes.
  • ADDED: 6 Hono routes — POST /uploads, POST /uploads/initiate, PUT /uploads/{uploadId}/chunks/{chunkIndex}, POST /uploads/{uploadId}/complete, DELETE /uploads/{uploadId}, GET /uploads/{uploadId} — every route requireAuth()-gated.
  • ADDED: UploadStorage interface + MemoryUploadStorage (test-only) + FileSystemUploadStorage (production) implementations.
  • ADDED: UploadSessionStore interface + MemoryUploadSessionStore implementation.
  • ADDED: fileUploadService typed slot in ServiceRegistry exposing initiate/appendChunk/complete/abort/get/delete.
  • ADDED: 11 UPLOAD_* error codes (INVALID_INPUT, NOT_FOUND, FORBIDDEN, MIME_REJECTED, TOO_LARGE, CHUNK_OUT_OF_ORDER, HASH_MISMATCH, SESSION_EXPIRED, STORAGE_FAILURE, INCOMPLETE, SERVICE_NOT_AVAILABLE).
  • ADDED: 4 OTel counters (upload.singleshot_total{outcome}, upload.chunks_total{outcome}, upload.completes_total{outcome}, upload.bytes_total) + 1 histogram (upload.complete_duration_ms).
  • ADDED: MIME sniff cross-check via @suaveplan/mime; optional image dimension cap stub via @suaveplan/exif.
  • ADDED: Cross-user isolation — foreign uploadId returns 404 (anti-enumeration) at every route.

Out of scope

  • Persistent (SurrealDB) session/upload storage — MemoryUploadSessionStore + FileSystemUploadStorage ship; a Surreal-backed UploadSessionStore is a follow-up because api-plugin-database integration is deferred to its own change.
  • Virus-scan hooks (ClamAV / VirusTotal) — ScanHook shape is defined but no concrete adapters; the hook slot is extension-ready.
  • TUS 1.0 wire-protocol compatibility — the chunked protocol is genesis-native; a TUS adapter is a follow-up change.
  • Image transform pipeline (compress-image, PDF) — deferred until those packages ship downstream.

Source mapping

This change derives from ../suaveplan/enterprise/api-plugin-file-upload.md.

Requirement Source lines Notes
Plugin factory exports createFileUploadPlugin + service slot L17-L19, L42-L43 enhanced — routes shipped from day one
6 HTTP routes mounted under config.basePath L42-L43, L94 verbatim — adds GET /uploads/{id} for resume status
UploadStorage adapter interface + MemoryUploadStorage + FileSystemUploadStorage L25, L52, L97 verbatim — drops S3 (deferred until storage-s3 lands fully)
Pluggable UploadSessionStore (in-memory default) L24, L36, L46 verbatim — replaces in-process Map with injectable contract
SHA-256 hash of assembled content on complete L48 verbatim
MIME validation + sniff cross-check L64 verbatim — uses @suaveplan/mime
Optional image/* dimension cap L64 enhanced — uses @suaveplan/exif
Stable error-code registry L93 enhanced — 11 codes via defineCodes
OTel counters + histogram L52 enhanced — names normalized to genesis convention
AuthN required on every route via requireAuth() L49 verbatim
Cross-user isolation (404 on foreign uploadId) L98 (verified persistence ≈ isolation) enhanced — anti-enumeration explicit
Resume capability — GET /uploads/{id} returns received chunk indices L42-L43 (chunked protocol) enhanced — explicit resume contract
Session cleanup on TTL L96 verbatim
Zod schemas at boundary Cross-cutting §3 / §6 verbatim
100% test coverage with provider: istanbul L90 verbatim
Sibling .md per impl .ts L91-L92 verbatim

Requirements with no source line

Requirement Justification
fileUploadService typed ServiceRegistry slot Genesis-wide pattern: every plugin registers a typed service so siblings get inference (mirrors authService).
expectTypeOf assertions on public surface Genesis monorepo policy — every public type is pinned.
bun scripts/openspec-conformance.ts exit 0 Genesis-wide gate.

Verification approach

  • src/plugin/plugin.test.ts — factory validation, descriptor shape, lifecycle hooks.
  • src/error-codes/error-codes.test.ts — registry shape + every code thrown from production path.
  • src/error-codes/error-codes.spec-conformance.test.ts — every UPLOAD_* code wired in production.
  • src/otel/instruments.test.ts — counters + histogram emit on every route via real MeterProvider.
  • src/storage/file-system.test.ts + src/storage/memory.test.ts — adapter contracts.
  • src/session-store/memory.test.ts — session-store contract.
  • src/assembler/assembler.test.ts — chunk assembly + SHA-256 verify + resume.
  • src/validators/mime.test.ts + src/validators/size.test.ts — allowlist/denylist + sniff cross-check + size cap.
  • src/routes/build-routes.test.ts — 6 routes built with RouteBuilder.
  • src/services/file-upload-service.test.ts + .spec-conformance.test.ts — service contract + registry-slot typing.
  • src/middleware/context-augmentation.ts (no test, types-only).
  • tests/integration/full-flow.test.ts — real PluginRegistry + real api + real api-plugin-auth + 6-route round-trip (single-shot, chunked, resume, abort, isolation).
  • tests/property.test.ts — fast-check 200-run property test on InitiateUploadBodySchema.
  • tests/dep-graph.test.ts — package layout invariants.

Migration impact

None — additive only. No existing genesis package depends on @suaveplan/api-plugin-file-upload.

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/api-plugin-file-upload to enumerate. No dependents exist on landing — this is the first change introducing the package.

No dependents (verified by bunx turbo ls --filter=...@suaveplan/api-plugin-file-upload).