@factory/storage
Storage layer for AI Software Factory (ARCHITECTURE.md §8): the object key
layout, magic-byte MIME detection, AWS SigV4 presigner, a thin S3/MinIO
client over an injectable fetch, and pure thumbnail/preview planning plus a
real dependency-free PNG encoder.
Source-first:
main/typespoint atsrc/index.ts— no build step. Zero runtime dependencies: globalfetch+node:crypto+node:zlibonly. The only declared dependency is@factory/shared(workspace).
Public API
| Module | Exports | Purpose |
|---|---|---|
@factory/storage (root) |
everything below | single import surface |
@factory/storage/keys |
buildUploadKey, buildThumbnailKey, buildPreviewKey, parseKey, keyForUpload, sanitizeFileName, isWithinBucketPrefix |
object key layout — PURE |
@factory/storage/mime |
detectMimeType, MIME_SIGNATURES, EXTENSION_MIME, isAllowedUpload, isImageMime/isVideoMime/isAudioMime/isDocumentMime |
magic-byte detection + upload policy — PURE |
@factory/storage/presign |
signS3Request, presignPut, presignGet, sha256Hex, hmacSha256, canonicalQuery, uriEncode |
AWS SigV4 (deterministic given now) — PURE |
@factory/storage/client |
S3Client, parseListBucketXml |
thin S3 adapter over fetchLike (the only I/O) |
@factory/storage/thumbnails |
planThumbnails, generatePngThumbnail, encodePng, parsePngHeader, parseJpegDimensions, computeMetadata, crc32 |
artifact planning + PNG path + metadata extraction — PURE |
| (types) | FileMeta, StorageError, S3Config, FetchLike |
contracts shared with the API/worker |
Key layout (the contract)
workspaces/<workspaceId>/files/<fileId>/v<version>/<sanitizedName>
…/v1/a.png__thumb_sm | __thumb_md | __thumb_lg | __thumb_orig
…/v1/a.pdf__preview_pdf | __preview_audio | __preview_video
Deterministic (no clock), workspace-scoped prefix = deletion boundary.
parseKey is the strict inverse — throws on anything malformed.
Usage — the upload pipeline
import { buildUploadKey, presignPut, S3Client, isAllowedUpload, computeMetadata, planThumbnails } from "@factory/storage";
// 1. CreateUpload → presign (API)
const key = buildUploadKey({ workspaceId, fileId, version: 1, name: dto.name });
const put = presignPut({ config, key, contentType: dto.mimeType, expiresSec: 900, now: new Date() });
// client PUTs with put.headers → 2. File record status="uploading"
// 3. Worker: object lands → extract + verify (pure, over bytes)
const obj = await client.getObject(key);
const meta = computeMetadata(dto.mimeType, obj.body, dto.name); // bytes win over the claim
if (!isAllowedUpload(meta.mimeType, { size: obj.size, filename: dto.name }).allowed) reject();
// 4. Thumbnail planning (pure) → 5. worker generates PNGs via generatePngThumbnail
const plan = planThumbnails(meta.mimeType, { key, width: meta.width, height: meta.height });
// 6. File record status="ready" → file.uploaded event → search index + permissions
// Downloads: presignGet({ config, key, expiresSec: 900, now }) — private buckets only.
Roadmap phases that build on this
- Phase 8 (Files): the API
FileModulecallspresignPut/S3Clientand persistsFileMeta; the ClamAV + metadata worker callscomputeMetadata,planThumbnails,generatePngThumbnail(pixel work for webp/avif/heic/pdf/ media posters is the worker's, not this package's). - Phase 9 (Sharing/Permissions):
parseKey/isWithinBucketPrefixscope workspace deletions and signed-URL validation;presignGetserves short-lived download URLs.
Decisions
- SigV4 header signing, not query-string presigning: one signer
(
signS3Request) for everything;presignPut/Getreturn the exact headers the caller must send, withX-Amz-Expiresincluded in the signed canonical query. Deterministic given an injectednow— unit-tested against an independent HMAC-chain recomputation. - The PNG encoder is real, not a stub: IHDR/IDAT (zlib
deflateSync)/ IEND with correct CRC32 on every chunk + nearest-neighbor downscale. Fully round-trip tested (magic, IHDR dims, inflated scanlines, per-chunk CRC). No image libraries anywhere in the package. - Planning vs generation:
planThumbnailsis the pure contract (which artifacts, which keys, target widths, never upscale); actual pixel work for non-PNG inputs needs codecs the package deliberately does not ship. - Zip refinement is heuristic-by-design: docx/xlsx/pptx/epub are detected
by scanning the first local file header names; the result carries
isZipContainer+refinementso callers can weight confidence. StorageErroris a thinApiErrorsubclass (code: "STORAGE_ERROR", default 502) adding onlyretryable(404/4xx → false, 5xx/timeout → true).