Files
2026-05-27 23:29:53 +01:00

94 lines
6.8 KiB
Markdown

# 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`).