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 throughdefinePluginRoutes. - 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 routerequireAuth()-gated. - ADDED:
UploadStorageinterface +MemoryUploadStorage(test-only) +FileSystemUploadStorage(production) implementations. - ADDED:
UploadSessionStoreinterface +MemoryUploadSessionStoreimplementation. - ADDED:
fileUploadServicetyped slot inServiceRegistryexposinginitiate/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
uploadIdreturns 404 (anti-enumeration) at every route.
Out of scope
- Persistent (SurrealDB) session/upload storage —
MemoryUploadSessionStore+FileSystemUploadStorageship; a Surreal-backedUploadSessionStoreis a follow-up becauseapi-plugin-databaseintegration is deferred to its own change. - Virus-scan hooks (ClamAV / VirusTotal) —
ScanHookshape 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— everyUPLOAD_*code wired in production.src/otel/instruments.test.ts— counters + histogram emit on every route via realMeterProvider.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 withRouteBuilder.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— realPluginRegistry+ realapi+ realapi-plugin-auth+ 6-route round-trip (single-shot, chunked, resume, abort, isolation).tests/property.test.ts— fast-check 200-run property test onInitiateUploadBodySchema.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).