nowo-tech / storage-kit-bundle
S3-compatible object storage for Symfony: per-scope buckets with encrypted credentials, image uploads for Markdown editors, reference-aware purge, verified bucket moves and an SSRF-guarded endpoint test.
Package info
github.com/nowo-tech/StorageKitBundle
Type:symfony-bundle
pkg:composer/nowo-tech/storage-kit-bundle
Fund package maintenance!
Requires
- php: >=8.2 <8.6
- ext-gd: *
- async-aws/core: ^1.27
- async-aws/s3: ^2.8 || ^3.0
- doctrine/dbal: ^3.8 || ^4.0
- doctrine/doctrine-bundle: ^2.13 || ^3.0
- doctrine/orm: ^3.3
- doctrine/persistence: ^3.1 || ^4.0
- league/flysystem: ^3.29
- league/flysystem-async-aws-s3: ^3.29
- nowo-tech/doctrine-encrypt-bundle: ^2.10
- nowo-tech/form-kit-bundle: ^2.17
- nowo-tech/instance-settings-bundle: ^1.3.1
- nowo-tech/outbound-url-guard-bundle: ^1.3
- nowo-tech/ui-kit-bundle: ^2.23
- psr/log: ^2.0 || ^3.0
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/event-dispatcher: ^7.4 || ^8.0
- symfony/event-dispatcher-contracts: ^3.5
- symfony/form: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-client: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/lock: ^7.4 || ^8.0
- symfony/polyfill-intl-idn: ^1.31
- symfony/rate-limiter: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/security-core: ^7.4 || ^8.0
- symfony/security-csrf: ^7.4 || ^8.0
- symfony/service-contracts: ^3.5
- symfony/translation-contracts: ^3.5
- symfony/twig-bundle: ^7.4 || ^8.0
- symfony/uid: ^7.4 || ^8.0
- symfony/validator: ^7.4 || ^8.0
- twig/extra-bundle: ^3.12
- twig/string-extra: ^3.12
- twig/twig: ^3.12
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- igor-php/igor-php: ^0.10.1
- league/flysystem-memory: ^3.29
- nowo-tech/http-hardening-bundle: ^1.8
- nowo-tech/phpstan-frankenphp: ^1.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.5
- rector/rector: ^2.0
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/css-selector: ^7.4 || ^8.0
- symfony/dom-crawler: ^7.4 || ^8.0
- symfony/messenger: ^7.4 || ^8.0
- symfony/mime: ^7.4 || ^8.0
- symfony/phpunit-bridge: ^7.4 || ^8.0
- symfony/process: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
- symfony/translation: ^7.4 || ^8.0
- symfony/yaml: ^7.4 || ^8.0
Suggests
- nowo-tech/http-hardening-bundle: Adds the bucket origin of the redirect-delivered profiles to the CSP img-src (CspSourceProviderInterface adapter)
- nowo-tech/milkdown-editor-bundle: Markdown editor whose upload hook posts to the StorageKit upload endpoint (see docs/USAGE.md)
- symfony/flex: For automatic recipe installation
- symfony/messenger: Runs bucket moves in the background (otherwise run nowo:storage-kit:sync)
- symfony/security-bundle: Required for the admin UI unless security.allow_unauthenticated is true (dev / demo only)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 20:49:40 UTC
README
⭐ Found this useful? Install from Packagist · Give it a star on GitHub so more developers can find it.
Storage Kit Bundle — S3-compatible object storage for the images users paste into Markdown texts. The instance
bucket is a storage section of Instance Settings (Amazon
S3 or any S3-compatible service — R2, B2, Wasabi, Scaleway, RustFS, MinIO —, secret encrypted at rest, Test
connection) or an ops-managed configuration profile; organisations or tenants can bring their own bucket.
Uploads are content-sniffed, size- and pixel-capped, re-encoded and stored under random keys in private buckets;
your file contexts decide who writes and who reads, downloads are streamed with a sandbox CSP (or redirected to a
short-lived pre-signed URL for configuration profiles), texts keep storage-file:<uuid> URIs, orphans are purged
after a retention, and a whole owner can be moved to another bucket with a SHA-256-verified, resumable copy and a
switch. Every bucket typed into a page goes through an SSRF guard. Tested on Symfony 7.4 and 8.1 · PHP 8.2+
(Symfony 8.x requires PHP 8.4+).
This bundle is FrankenPHP worker mode friendly.
Bucket connections, resolved profiles and per-viewer read decisions are request-scoped memos reset on kernel.reset
(and on a storage section change), so a bucket saved or switched in one request is used by the next one in every
worker. See docs/FRANKENPHP-WORKER-AUDIT.md.
Screenshots
Features
- S3-compatible buckets over Flysystem + async-aws — providers
aws_s3(endpoint derived from the region) ands3_compatible(endpoint, public endpoint, path style); objects always private; no local-disk provider. - Instance bucket in Instance Settings — the
storagesection (provider, endpoint, public endpoint, region, bucket, prefix, access key, encrypted write-only secret, path style), provider-dependent fields withvisible-when, empty provider = uploads off, notices, Test connection (write, read, sign, public-read warning, delete) and Usage and move. For Amazon S3, the access key and secret can also come from the AWS console's access-key CSV (Import from AWS CSV, never stored). Credentials live in the database, encrypted; ops-managed infrastructure buckets can instead be pinned in the configuration (profiles+instance_profile, e.g. a DSN from an env var — ops-only). - Scope buckets —
scopes.enabledwith yourScopeResolverInterfaceandScopeAccessCheckerInterface: each organisation / tenant manages its own bucket on its Storage page (404 for everyone else); a turned-off scope bucket turns its uploads off, never falls back to someone else's. - Image uploads for Markdown editors —
POST /storage/upload/{type}/{id}: CSRF header, upload roles, per-user rate limit, PNG / JPEG / GIF / WebP by content (SVG and HTML refused),max_bytes,max_pixelsbefore decoding, memory check, re-encoding (EXIF / GPS stripped), preview and thumbnail variants, random server-side keys, optional plan quota (UploadLimitInterface); answers the Markdown snippet. Twig helpers for MilkdownEditor and any other editor. - Host-decided access —
FileContextInterfacesays who may write in a context (and which scope / owner pays) and who may read it, batched per context type. - Downloads — 404 for missing and forbidden alike; streamed with
nosniff, a sandbox CSP andattachment, or302to a short-lived pre-signed URL (configuration profiles withdelivery: redirect, origin added to the CSP through HttpHardening);nowo_storage_images()resolves a text's images for your renderer. - References and purge —
FileReferenceSourceInterface+ a Doctrine flush listener track which text points at which file (same context only);nowo:storage-kit:purgedeletes orphans afterpurge.retention_daysin keyset batches. - Verified bucket moves — Move page (or
nowo:storage-kit:sync): SHA-256-verified, idempotent, resumable copy in Messenger slices; switch only after a complete copy with no upload since; the old bucket is retired, never emptied. - SSRF guard — private / loopback / link-local / metadata / IPv4-mapped endpoints refused, DNS resolved with a
deadline, connect-time filter without redirects,
endpoints.allowed_hostsfor internal services,allow_privatein debug only. - No secret leaks — credentials encrypted at rest (Instance Settings secret, DoctrineEncrypt), write-only fields,
provider messages scrubbed and kept out of pages, audit events (
StorageAuditEvent) with field names only; GDPR export / erasure hooks. - Admin UI on the UiKit page contract (FormKit profile
storage_kit, opt-in route loader, 7 locales) and fail-closed wiring (no DoctrineEncrypt, Instance Settings, CSRF or SecurityBundle when needed → no compile).
Installation
composer require nowo-tech/storage-kit-bundle
With Symfony Flex the recipe adds config/packages/nowo_storage_kit.yaml and config/routes/nowo_storage_kit.yaml.
Without Flex, register NowoDoctrineEncryptBundle, NowoUiKitBundle, NowoFormKitBundle,
NowoInstanceSettingsBundle and NowoStorageKitBundle — see docs/INSTALLATION.md. Then:
# config/routes/nowo_storage_kit.yaml (opt-in routes) nowo_storage_kit: resource: . type: nowo_storage_kit # config/routes/nowo_instance_settings.yaml (the "storage" section page) nowo_instance_settings: resource: . type: nowo_instance_settings
Register the UiKit visible-when Stimulus peer (provider-dependent fields):
import VisibleWhenController from '…/ui-kit-bundle/src/Resources/assets/stimulus-peers/visible_when_controller'; app.register('visible-when', VisibleWhenController);
Create the tables with a migration (php bin/console doctrine:migrations:diff) and protect the prefixes:
# config/packages/security.yaml security: access_control: - { path: ^/admin/storage, roles: ROLE_ADMIN } - { path: ^/admin/settings, roles: ROLE_ADMIN }
Quick start
- Open Settings → Storage (
/admin/settings/sections/storage), choose a provider, fill in the bucket and press Test connection. - Tell the kit who may write and read, per kind of text:
use Nowo\StorageKitBundle\Context\FileContextInterface; use Nowo\StorageKitBundle\Context\FileContextTarget; final class IssueContext implements FileContextInterface // autoconfigured { public function type(): string { return 'issue'; } public function readable(?object $viewer, array $ids): array { /* ids the viewer may read, one query */ } public function existing(array $ids): array { /* ids that still exist */ } public function writableTarget(object $user, string $id): ?FileContextTarget { return $this->canComment($user, $id) ? new FileContextTarget() : null; // null scope = instance bucket } }
- Give the editor the upload endpoint and render the images:
{{ form_row(form.body, nowo_storage_milkdown_upload('issue', issue.id)) }}
{% set images = nowo_storage_images(comment.body) %} {# uuid => {src, original, width, height, alt} #}
- Implement a
FileReferenceSourceInterfacefor the texts and schedulephp bin/console nowo:storage-kit:purgedaily.
Requirements
- PHP 8.2+ (
>=8.2 <8.6,ext-gd; Symfony 8.x requires PHP 8.4+) - Symfony 7.4, 8.0 or 8.1 (FrameworkBundle with CSRF, Form, Validator, RateLimiter, HttpClient, Lock, TwigBundle; SecurityBundle for the admin pages)
- Doctrine ORM 3.3+;
nowo-tech/doctrine-encrypt-bundle^2.10 (registered);nowo-tech/instance-settings-bundle^1.3.1;nowo-tech/ui-kit-bundle^2.23;nowo-tech/form-kit-bundle^2.17;nowo-tech/outbound-url-guard-bundle^1.3;async-aws/s3^2.8 || ^3.0;league/flysystem^3.29;twig/extra-bundle - Optional:
symfony/messenger(background moves),nowo-tech/http-hardening-bundle(CSP adapter),nowo-tech/milkdown-editor-bundle(Markdown editor)
Configuration
Every key, its default and its validation are documented in docs/CONFIGURATION.md, including
the storage section fields per provider. Safe defaults: instance bucket in Instance Settings, private endpoints
refused, 10 MiB / 24 MP uploads, 30 uploads per user and minute, attachment downloads, 7-day orphan retention, pages
under /admin/storage for ROLE_ADMIN, uploads for ROLE_USER.
Usage
The instance bucket and providers, file contexts, uploading from an editor, rendering, downloads and CSP, references and
the purge, scope buckets, quotas, moving to another bucket, adopting an existing instance bucket
(nowo:storage-kit:adopt-instance-bucket), personal data and audit events: docs/USAGE.md.
Demo
demo/symfony8— Symfony 8.1 (FrankenPHP PHP 8.5, worker mode) with MySQL and RustFS (two private buckets); app onhttp://localhost:8213(usersadmin/demo,alice/demo). Notes with image uploads, the Storage and Move pages, a verified move to the archive bucket.
Runtime mode is FRANKENPHP_MODE (worker default, or classic) — see docs/DEMO-FRANKENPHP.md.
make -C demo up-symfony8 # http://localhost:8213 make -C demo/symfony8 setup # encryption key, schema, buckets, demo bucket profile make test-e2e # Playwright e2e against the running demo make demo-screenshots # refresh docs/images/demo/*.webp make demo-smoke # boot, HTTP 200, down (REQ-TEST-011)
Development
make up
make install
make test
make cs-check
make phpstan
make igor
make test-coverage-100
make release-check
Documentation
- Installation
- Configuration
- Usage
- Contributing
- Code of Conduct
- Changelog
- Upgrading
- Release
- Security
- Engram
- Spec-driven development
- GitHub Spec Kit
Additional documentation
- Templates (UiKit page contract)
- Architecture
- Roadmap
- PSR evaluation (REQ-CS-007)
- Demo (FrankenPHP)
- FrankenPHP worker audit (kernel not reset)
- GitHub Actions CI requirements
Tests and coverage
- Tests: PHPUnit (PHP) — unit tests (S3 settings and DSN parsing, SSRF guard, image processing, contexts, references,
profiles, configuration) plus kernel integration tests (upload / download end to end on an in-memory bucket, the
storageInstance Settings section, Storage and Move pages with SecurityBundle and CSRF, references and purge, sync and switch, opt-in routes, invalid configuration) and the UiKit page contract test; Playwright e2e and axe (WCAG 2.1 AA) checks on the demo - PHP: 100% (2990/2990 statements, 3437/3437 elements, 290 tests; gate enforced in CI by
composer test-coverage-100, no exclusions) - TS/JS: N/A
- Python: N/A
License and author
MIT · Nowo.tech · Héctor Franco Aceituno




