Skip to content

Sketch: provider-agnostic StorageService + S3/MinIO (Azure Blob replacement)#3921

Draft
TaprootFreak wants to merge 7 commits into
developfrom
feature/storage-service-sketch
Draft

Sketch: provider-agnostic StorageService + S3/MinIO (Azure Blob replacement)#3921
TaprootFreak wants to merge 7 commits into
developfrom
feature/storage-service-sketch

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

⚠️ NICHT eigenständig mergen — dieser PR IST der Cutover-Schritt

Dieser PR schaltet die dfx-api von Azure Blob auf MinIO um und entfernt den Azure-Storage-Code.
Bei develop = main erreicht er damit PRD. Die Migration läuft Infra/Daten zuerst — dieser
PR ist der letzte Schritt:

  1. DFXServer/server [DEV-893] BTC Fee #463 (MinIO-only) → main → MinIO auf dfxprd (leer, zero impact)
  2. PRD-Secrets + Buckets provisionieren
  3. rclone Azure→MinIO (Daten kopieren; alte App läuft weiter auf Azure)
  4. DANN dieser PR + server [DEV-759] Round statistic values to 2 decimals #466 (dfx-api→MinIO-Wiring) → main, im Cutover-Fenster, + DB-URL-Rewrite

Wird dieser PR vor Schritt 1–3 nach PRD released: dfx-api startet ohne fertiges MinIO nicht
bzw. bestehende Dokument-Reads brechen (Prod-/Compliance-Incident).

Phase 1–3 brauchen diesen PR nicht. Vollständiges Runbook:
DFXServer/serverinfrastructure/migrations/minio-azure-cutover-runbook.md.

→ Bleibt Draft bis zum Cutover-Fenster (dann Ready + Merge als allerletzter Schritt, zusammen mit #466).


Was

Design-Sketch (WIP, Draft) für die Ablösung von Azure Blob Storage durch einen S3-kompatiblen Object-Store (MinIO).

Führt eine provider-agnostische StorageService-Abstraktion ein und implementiert sie gegen S3. Der dfx-api ist der erste Konsument; die Abstraktion ist generisch und liesse sich auch von anderen Diensten nutzen.

Änderungen

  • storage/storage.service.ts — abstrakte StorageService mit exakt der bisherigen 6-Methoden-Oberfläche (listBlobs, getBlob, uploadBlob, copyBlobs, blobUrl, blobName). blobUrl-Form bleibt identisch → blobName reversibel, persistierte URLs konsistent.
  • storage/s3-storage.service.tsS3StorageService (MinIO via @aws-sdk/client-s3, forcePathStyle). Object Lock (Compliance-Mode) wird auf konfigurierte Compliance-Buckets angewandt — definierte, verlängerbare (nie verkürzbare) Retention.
  • storage/mock-storage.service.ts — In-Memory-Mock für lokale Entwicklung (aus dem alten Mock-Mode portiert).
  • storage/storage.factory.tscreateStorageService(container) (LOC → Mock, sonst → S3).
  • 3 Aufrufstellen umgestellt (new AzureStorageService(...)createStorageService(...)): KYC, Support, fiat-output.
  • config.tss3-Block (Secrets via env, per-Env-Endpoint/URL via compose, invariante Settings als Code-Konstanten).
  • @aws-sdk/client-s3 als Dependency.

WORM ist bewusst nicht Teil der Methoden-Signaturen — der Service wendet Retention bucket-getrieben an.

Bewusst (noch) nicht in diesem Sketch

  • AzureStorageService bleibt vorerst (read-only Fallback für den Daten-Cutover); wird danach entfernt, ebenso @azure/storage-blob und der azure-Config-Block.
  • Daten-Migration (Bulk-Copy + 2-Spalten-URL-Rewrite support_message.fileUrl / kyc_log.pdfUrl) ist ein separater Schritt.
  • EP2-Container sind dynamisch — falls WORM nötig, per Pattern statt Bucket-Liste matchen.
  • listBlobs liefert keine contentType/Metadaten (S3-Listing tut das nicht; HeadObject nur falls ein Aufrufer es braucht — aktuell keiner).

Hinweis

Sketch/WIP zur Design-Review, nicht merge-fertig. Dep wurde via package-lock --package-lock-only ergänzt (kein lokales node_modules); die CI validiert den Build.

Sketch for replacing Azure Blob Storage with an S3-compatible object store:

- StorageService abstraction (same 6-method surface as the previous service)
- S3StorageService (MinIO via @aws-sdk/client-s3), with Object Lock
  (Compliance mode) applied to configured compliance buckets
- MockStorageService for local development
- createStorageService() factory; the three call sites (KYC, support,
  fiat-output) switch from 'new AzureStorageService(...)' to the factory
- s3 config block (secrets via env, per-env endpoint/url via compose,
  invariant settings as code constants)

WIP/draft: Azure service kept for cutover fallback; dep installed via
package-lock only (no local node_modules), CI validates the build.
- Drop per-object Object Lock from S3StorageService; WORM is enforced
  server-side via the bucket's default retention (Compliance mode,
  provisioned at setup) — robust against a client that forgets it
- s3 config block reduced to connection settings only
- Document S3StorageService as a protocol client (on-prem MinIO, not AWS
  cloud); abstraction stays provider-agnostic
- Fix Prettier formatting (mock storage line wrap) that failed the format check
- Keep Blob/BlobContent/BlobMetaData required (structurally identical to the
  previous types) so downstream DTOs compile unchanged
- Point the remaining type imports (kyc-file dto/mapper, support-issue
  controller/service) at storage.service instead of the deprecated azure module
- S3 listBlobs fetches per-object metadata via HeadObject (S3 listings carry
  no content-type, unlike the Azure listing it replaces)
…anup)

- Restore LOC dummy-file fallback in MockStorageService.getBlob (parity with
  the old mock; LOC document reads return bytes again)
- Fail-fast S3 config validation (endpoint/region/keys/publicUrl required;
  publicUrl must end with '/') instead of an opaque SDK error at first use
- URL-encode CopyObject CopySource (copyBlobs no longer corrupts keys with
  spaces/special chars)
- Move blobUrl/blobName/encodeKey to the StorageService base (dedup); base
  now owns the container
- Delete the now-unused AzureStorageService, drop @azure/storage-blob and the
  azure storage config block (appInsights kept)
- Soften WORM doc to 'enforced via bucket default retention, provisioned in infra'
- Document S3 metadata lowercasing / created==updated; remove unused logger
- Add storage.service.spec.ts for the blobUrl/blobName round-trip invariant
- Document the factory-over-DI rationale (per-container, runtime EP2 container)
- Guard empty GetObject body in getBlob (clear error instead of NPE)
- copyBlobs uses a key-only listing (no per-object HeadObject fan-out)
- blobName throws a clear error for URLs outside the container
- Move spec into __tests__/ per repo convention; add container-mismatch case
- Restore original 'pg' position in package.json (npm had reordered it)
- Document that eager KYC/support providers make S3 config a boot-time fail-fast
- storage.service.spec.ts now sets up Config via TestUtil.provideConfig in
  beforeAll, so blobUrl no longer throws on an undefined Config singleton;
  also assert the full public-URL prefix (trailing-slash contract)
- Shorten WORM doc comment (drop the dangling infra-RFC reference)
- Fix stale 'azure-storage' wording in the local KYC seed script
… (Säule 2) (#3934)

* Anchoring core: Merkle tree + OpenTimestamps wrapper (Säule 2, stage 1)

- merkle.ts: sha256, buildMerkleRoot, merkleInclusionProof, verifyMerkleProof
  (SHA-256, parent=sha256(left||right), odd-node duplicated, 0=throw/1=leaf)
- opentimestamps.service.ts: stamp/upgrade/verify wrapper over the public calendars
- merkle.spec.ts: 19 unit tests (roots, per-leaf inclusion proofs, tamper detection)
- add opentimestamps dependency

* Anchoring: DB entities + ArchiveService (Säule 2, stage 2)

- ArchiveBatch / ArchiveFile entities (IEntity base, BaseRepository pattern),
  migration for both tables + unique(bucket,name) + batch FK
- ArchiveService: recordHash (idempotent), anchorPending (Merkle root over
  pending hashes -> batch -> OTS stamp), upgradeBatches (Bitcoin attestation),
  verifyDocument (hash match + Merkle inclusion proof + OTS verify)
- ArchiveModule; archive.service.spec.ts (round-trip + tamper, real Merkle, mocked OTS)

* Anchoring: scheduler, upload-path hash capture, WORM provisioning (Säule 2, stage 3)

- Capture SHA-256 on compliance uploads: kyc-document (kyc) + fiat-output (EP2)
  call archiveService.recordHash after uploadBlob; support bucket untouched
- ArchiveScheduler (@DfxCron + process guard): daily anchorPending, hourly
  upgradeBatches; ARCHIVE_ANCHOR/ARCHIVE_UPGRADE process entries
- scripts/storage/provision-bucket.ts: create bucket with Object Lock +
  versioning + COMPLIANCE default retention (idempotent); note for dynamic EP2
- wire ArchiveModule into kyc + fiat-output modules; scheduler spec

* Anchoring: fix opentimestamps transitive uuid resolution

The opentimestamps lib pulls request->request-promise->uuid via the legacy
'uuid/v4' subpath, which the repo-wide uuid@9 override breaks
(ERR_PACKAGE_PATH_NOT_EXPORTED). Scope a uuid@3.4.0 override under 'request'
only (nested override) so its calendar HTTP works, without touching the app's
uuid@9. Surfaced by the m5me integration test.

* Anchoring: address review (anchored-hash guard, best-effort capture, upgrade persistence)

- recordHash: never overwrite the hash of an already-anchored file; identical
  -> no-op, differing -> hard error+log (deterministic blob names could
  otherwise silently rewrite an anchored leaf and break verifyDocument)
- KYC + EP2: recordHash is best-effort (try/catch + log), never rolls back the
  completed upload; EP2 sets reportCreated before anchoring to avoid a WORM
  re-PUT retry trap
- upgradeBatches: persist upgraded .ots bytes whenever they change, even while
  still pending; confirm only on a real Bitcoin attestation
- verify result exposes confirmed vs pending; verifyDocument loads batch via
  relations (removed dead branch); tests extended

* Anchoring/storage: strong unit test coverage for new modules

- s3-storage.service.spec (aws-sdk-client-mock): config validation, listBlobs
  pagination + per-key HeadObject, getBlob empty-body guard, uploadBlob,
  copyBlobs CopySource encoding -> 100%
- mock-storage.service.spec: roundtrip, prefix filter, dummy-file fallbacks -> 100%
- storage.factory.spec: LOC->Mock, otherwise->S3 -> 100%
- opentimestamps.service.spec (lib mocked): stamp/upgrade/verify, confirmed vs
  pending -> 100%
- archive.service.spec: closed upgrade-no-change + verifyDocument confirmed branches
- kyc-document + fiat-output wiring specs: recordHash called with sha256, best-effort
  (upload not rolled back; reportCreated set before anchoring)
- add aws-sdk-client-mock devDependency

* Anchoring/storage tests: harden against silent regressions

- archive.service.spec fakeStore now honors relations:['batch'] (batch only
  hydrated when requested) + explicit assertion that recordHash/verifyDocument
  request the relation -> removing it in prod now fails the suite
- fiat-output spec asserts invocationCallOrder uploadBlob < reportCreated <
  recordHash (WORM re-PUT deadlock guard)
- fiat-output spec covers the routeId -> sell.id fallback
Mutation-checked: each guard fails when the corresponding prod behavior is removed

* Anchoring: clear ESLint warnings (unused import + stale eslint-disable)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant