Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
# `complete-chunked.ts`
## Purpose
Internal back-end implementing the `complete` operation of `FileUploadService`. Extracted from the main service file so each module stays under the suaveplan 300-line budget. The pipeline is: assemble every chunk in `[0, totalChunks)` order → verify SHA-256 against the declared expected hash (or an explicit override) → write the assembled bytes to the canonical record key → save the `UploadRecord` → garbage-collect per-chunk storage entries → clear the in-flight session.
## Features
- Single-pass assembly via the `assembler` helper so chunk ordering and missing-chunk detection live in one place.
- Override-on-complete lets clients adjust the declared hash if they recomputed mid-flight without rebuilding the entire session.
- Emits both the outcome counter (`upload.completes_total{outcome}`) and the duration histogram (`upload.complete_duration_ms{outcome}`) on every code path so dashboards can correlate failure rate with latency.
- Cleanup runs even on success (chunk-side storage entries are deleted) so the storage adapter never accumulates orphaned chunk keys.
## Usage: Basic
```ts
import { runComplete } from "@suaveplan/api-plugin-file-upload";
const record = await runComplete(session, undefined, {
storage,
sessionStore,
clock: () => Date.now(),
});
```
## Usage: Advanced
```ts
import { runComplete, FileUploadError } from "@suaveplan/api-plugin-file-upload";
try {
await runComplete(session, clientRecomputedHash, opts);
} catch (err) {
if (err instanceof FileUploadError) {
switch (err.uploadCode) {
case "UPLOAD.INCOMPLETE":
return c.json({ retry: true, reason: "missing-chunks" }, 409);
case "UPLOAD.HASH_MISMATCH":
return c.json({ retry: true, reason: "hash-mismatch" }, 422);
}
}
throw err;
}
```
## API Reference
- `RunCompleteOptions` — `{ storage, sessionStore, clock }`.
- `runComplete(session, overrideSha256?, opts): Promise<UploadRecord>` — see Purpose for pipeline order; throws `UPLOAD.INCOMPLETE`, `UPLOAD.HASH_MISMATCH`, or `UPLOAD.STORAGE_FAILURE`.
## Implementation Notes
- The chunk deletion loop runs sequentially. A parallel `Promise.all` is tempting but most storage adapters (`FileSystemUploadStorage` in particular) prefer FIFO syscalls to avoid file-descriptor exhaustion on very large uploads.
- The histogram is recorded INSIDE the `try` so even early failures (e.g. an assemble that throws on the second chunk) still contribute to the observed-failure-latency picture.

View File

@@ -0,0 +1,57 @@
# `file-upload-helpers.ts`
## Purpose
Internal helpers shared by `file-upload-service.ts`: the storage-key naming convention (`CHUNK_KEY`, `RECORD_KEY`) and the `assertOwnership` cross-principal guard. Extracted into its own module so the main service file stays under the per-file line budget, and so the ownership rule is testable in isolation.
## Features
- `CHUNK_KEY` / `RECORD_KEY` centralise the storage-key format so future migrations can change the prefix in one place.
- `assertOwnership` enforces the anti-enumeration rule: foreign uploadIds raise `UPLOAD.NOT_FOUND` (not `FORBIDDEN`).
- `PrincipalScope` is the minimal projection the service needs from the auth principal — `principalId` + optional `tenantId`.
## Usage: Basic
```ts
import {
assertOwnership,
CHUNK_KEY,
} from "@suaveplan/api-plugin-file-upload";
const key = CHUNK_KEY("u1", 3);
assertOwnership(
{ principalId: "alice" },
{ principalId: "alice" },
"u1"
);
```
## Usage: Advanced
```ts
import { assertOwnership, FileUploadError } from "@suaveplan/api-plugin-file-upload";
try {
assertOwnership(
{ principalId: "alice" },
{ principalId: "bob" },
"u1"
);
} catch (err) {
if (err instanceof FileUploadError && err.uploadCode === "UPLOAD.NOT_FOUND") {
// 404 surfaced — anti-enumeration enforced
}
}
```
## API Reference
- `CHUNK_KEY(uploadId: string, idx: number): string` — returns `session-${uploadId}-chunk-${idx}`.
- `RECORD_KEY(uploadId: string): string` — returns `record-${uploadId}`.
- `PrincipalScope` — `{ principalId: string; tenantId?: string }`.
- `assertOwnership(owner, record, uploadId): void` — throws `UPLOAD.NOT_FOUND` (404) on mismatch.
## Implementation Notes
- The "missing tenant on both sides" case is normalised via `?? undefined` so undefined and absent compare equal. Two records with the same `principalId` but mismatched tenant IDs are still considered foreign (multi-tenant isolation is enforced at the same boundary as user isolation).
- The 404-vs-403 choice is deliberate: a 403 leaks the existence of the upload to a different principal. Tests pin this behaviour.

View File

@@ -0,0 +1,68 @@
# `file-upload-service.ts`
## Purpose
Canonical `FileUploadService` factory consumed by the plugin's route handlers and registered as the typed `fileUploadService` slot in the host's `ServiceRegistry`. It is the single integration point between the HTTP surface and the storage/session/validation primitives shipped in this package — siblings that need programmatic upload access call `ctx.getService("fileUploadService")` instead of replaying multipart parsing or chunk bookkeeping.
## Features
- 7-method surface: `initiate`, `appendChunk`, `complete`, `abort`, `get`, `singleShot`, `cleanupExpired`.
- Pluggable `idGenerator` + `clock` so deterministic tests are first-class (defaults to `randomUUID` + `Date.now`).
- Cross-principal isolation built into every lookup via `assertOwnership` — foreign IDs raise `UPLOAD.NOT_FOUND` (404, anti-enumeration).
- TTL enforcement on every chunked-session interaction — expired sessions reject with `UPLOAD.SESSION_EXPIRED` (410).
- Idempotent chunk uploads — re-uploading an already-received chunk is a no-op (and emits the `duplicate` counter outcome).
- `cleanupExpired(at)` returns the number of sessions reclaimed so the plugin's interval scheduler can log throughput.
## Usage: Basic
```ts
import { createFileUploadService, MemoryUploadSessionStore, MemoryUploadStorage } from "@suaveplan/api-plugin-file-upload";
const svc = createFileUploadService({
config: resolvedConfig,
storage: new MemoryUploadStorage(),
sessionStore: new MemoryUploadSessionStore(),
});
const record = await svc.singleShot({
principalId: "alice",
fileName: "hello.txt",
bytes: new TextEncoder().encode("hi"),
declaredMimeType: "text/plain",
});
```
## Usage: Advanced
```ts
import { createFileUploadService, FileSystemUploadStorage } from "@suaveplan/api-plugin-file-upload";
const svc = createFileUploadService({
config,
storage: new FileSystemUploadStorage({ rootDir: "/var/lib/uploads" }),
sessionStore: new MemoryUploadSessionStore(),
idGenerator: () => crypto.randomUUID(),
clock: () => Date.now(),
});
const session = await svc.initiate({ principalId, fileName, totalSize, totalSha256, mimeType: "application/octet-stream" });
for (let i = 0; i < session.totalChunks; i++) {
await svc.appendChunk(session.uploadId, i, sliceFor(i), { principalId });
}
const record = await svc.complete(session.uploadId, { principalId });
```
## API Reference
- `FileUploadService` — interface with `initiate`/`appendChunk`/`complete`/`abort`/`get`/`singleShot`/`cleanupExpired`.
- `InitiateInput` — `{ principalId, tenantId?, fileName, totalSize, totalSha256, mimeType, chunkSize? }`.
- `SingleShotInput` — re-exported from `./single-shot`.
- `PrincipalScope` — re-exported from `./file-upload-helpers`.
- `CreateFileUploadServiceOptions` — `{ config, storage, sessionStore, idGenerator?, clock? }`.
- `createFileUploadService(opts): FileUploadService`.
## Implementation Notes
- The complete and single-shot paths delegate to `complete-chunked.ts` and `single-shot.ts` respectively so each file stays under the 300-line suaveplan budget.
- `ensureSession` is a single funnel that performs lookup + ownership check + TTL check. Every method that touches an in-flight session goes through it.
- `notFound` is a `never`-returning helper so TypeScript narrows the caller's binding without an explicit `if` after the throw.
- `get` returns `UploadSession | UploadRecord` — callers use a `state` discriminator (computed by the route handler) to fork rendering.
- `abort` is idempotent: missing sessions and missing records BOTH return cleanly (after the ownership check on a record, if one exists).

View File

@@ -0,0 +1,43 @@
# `registry-augmentation.ts`
## Purpose
Augments the cross-package `ServiceRegistry` interface declared in `@suaveplan/api-plugin/service-registry` so consumers of `@suaveplan/api-plugin-file-upload` see `ctx.getService("fileUploadService")` resolve to `FileUploadService | undefined` without an explicit cast. The file is import-for-side-effects only: it adds the new slot AND re-declares the host-side slots (`app`, `openAPIRegistry`, `logger`, `eventBus`) so plugin unit tests work in isolation without depending on the full `@suaveplan/api` augmentation chain.
## Features
- Single `declare module` block — TypeScript merges with the parent interface; no runtime code.
- Re-declares the host slots so the plugin's `onInit(ctx)` resolves `app` / `openAPIRegistry` with full typing even outside the `@suaveplan/api` boot path.
- Side-effect import from `src/index.ts` ensures every consumer of the package gets the typing without an explicit subpath import.
## Usage: Basic
```ts
import "@suaveplan/api-plugin-file-upload"; // side-effect: augments ServiceRegistry
// In a sibling plugin's onInit
const upload = ctx.getService("fileUploadService"); // typed
```
## Usage: Advanced
```ts
import "@suaveplan/api-plugin-file-upload/services/file-upload-service";
// Import the augmentation directly when consumers want to defer the
// full root-barrel side-effect (e.g. tree-shaking-sensitive bundles).
```
## API Reference
The module exports no runtime values. It augments `ServiceRegistry` with:
- `fileUploadService: FileUploadService`
- `app: OpenAPIHono` (cumulative)
- `openAPIRegistry: OpenAPIRegistry` (cumulative)
- `logger: LoggerLike` (cumulative)
- `eventBus: EventBusLike` (cumulative)
## Implementation Notes
- The cumulative re-declaration is deliberate. Different plugin packages augment overlapping slots; TypeScript merges those declarations across the dependency graph so any one plugin sees the union.
- The augmentation is `.ts`-only; the build emits an empty `.js` (or trivially a no-op import) because there is no runtime side-effect beyond the type-system contribution.

View File

@@ -0,0 +1,49 @@
# `single-shot.ts`
## Purpose
Internal back-end implementing the `singleShot` operation of `FileUploadService`. Extracted from the main service file to keep each implementation file under the suaveplan 300-line budget while still keeping the responsibility cohesive: size limit → MIME validation (with sniff cross-check) → storage persistence → record save → OTel emission.
## Features
- Encapsulates the full single-shot validation pipeline so the public `FileUploadService.singleShot` is a single function call.
- Reads the first 4 KiB into the MIME sniffer so spoofed Content-Types are rejected without buffering the whole payload twice.
- Counts outcomes (`success` / `rejected` / `error`) onto `upload.singleshot_total` and bytes onto `upload.bytes_total` so dashboards reflect every code path.
- Generic in the id generator and clock so `createFileUploadService` can plug in its own deterministic values for tests.
## Usage: Basic
```ts
import { runSingleShot } from "@suaveplan/api-plugin-file-upload";
const record = await runSingleShot(
{ principalId: "alice", fileName: "hello.txt", bytes, declaredMimeType: "text/plain" },
{ config, storage, sessionStore, idGenerator: () => "u1", clock: () => Date.now() }
);
```
## Usage: Advanced
```ts
import { runSingleShot, FileUploadError } from "@suaveplan/api-plugin-file-upload";
try {
await runSingleShot(input, opts);
} catch (err) {
if (err instanceof FileUploadError && err.uploadCode === "UPLOAD.MIME_REJECTED") {
// spoof or denied — render UI-friendly 415 page
}
throw err;
}
```
## API Reference
- `SingleShotInput` — `{ principalId, tenantId?, fileName, bytes, declaredMimeType }`.
- `RunSingleShotOptions` — `{ config, storage, sessionStore, idGenerator, clock }`.
- `runSingleShot(input, opts): Promise<UploadRecord>` — see Purpose for pipeline order; throws `UPLOAD.TOO_LARGE`, `UPLOAD.MIME_REJECTED`, or `UPLOAD.STORAGE_FAILURE` depending on which guard fires.
## Implementation Notes
- The sniff sample is capped at 4 KiB because every magic-bytes signature the `@suaveplan/mime` registry ships fits well inside that window; sniffing more wastes CPU on tiny files and offers no additional confidence.
- Outcomes distinguish `rejected` (validation-side failure, `FileUploadError`) from `error` (storage/store-side failure, any other Error) so on-call rotations can alert on `error` without being paged for legitimate user rejections.