From 9acb22e2f95778455c0f22004218920a5c7634c8 Mon Sep 17 00:00:00 2001 From: rasm-its Date: Thu, 27 Aug 2026 12:31:09 +0200 Subject: [PATCH] R2 storage from env + filename normalization --- dist/globals/SiteIntegrations/index.d.ts | 4 + dist/index.d.ts | 6 +- dist/modules/media/index.d.ts | 1 + dist/modules/media/normalizeFilename.d.ts | 25 + dist/modules/storage/buildR2Storage.d.ts | 18 + dist/modules/storage/index.d.ts | 1 + docs/storage.md | 131 +++++ package.json | 1 + pnpm-lock.yaml | 509 ++++++++++++++++++ .../SiteIntegrations/fields/storage.ts | 44 -- src/globals/SiteIntegrations/index.ts | 6 +- src/index.ts | 26 +- src/modules/media/index.ts | 1 + src/modules/media/normalizeFilename.ts | 53 ++ src/modules/storage/buildR2Storage.ts | 53 ++ src/modules/storage/index.ts | 1 + 16 files changed, 820 insertions(+), 60 deletions(-) create mode 100644 dist/modules/media/index.d.ts create mode 100644 dist/modules/media/normalizeFilename.d.ts create mode 100644 dist/modules/storage/buildR2Storage.d.ts create mode 100644 dist/modules/storage/index.d.ts create mode 100644 docs/storage.md delete mode 100644 src/globals/SiteIntegrations/fields/storage.ts create mode 100644 src/modules/media/index.ts create mode 100644 src/modules/media/normalizeFilename.ts create mode 100644 src/modules/storage/buildR2Storage.ts create mode 100644 src/modules/storage/index.ts diff --git a/dist/globals/SiteIntegrations/index.d.ts b/dist/globals/SiteIntegrations/index.d.ts index 52e35d7..7b757fe 100644 --- a/dist/globals/SiteIntegrations/index.d.ts +++ b/dist/globals/SiteIntegrations/index.d.ts @@ -14,6 +14,10 @@ type BuildSiteIntegrationsArgs = { * impossible to enter.) * * Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId). + * + * Note: R2 storage credentials are NOT here — storage is infrastructure and + * binds at boot, so its config lives in .env (R2_BUCKET, R2_ENDPOINT, ...), + * consumed by buildR2Storage. See docs/storage.md. */ export declare function buildSiteIntegrations({ additionalFields, }?: BuildSiteIntegrationsArgs): GlobalConfig; export {}; diff --git a/dist/index.d.ts b/dist/index.d.ts index e240888..2eb5335 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -20,6 +20,9 @@ export type { I18nConfig, LocaleDefinition, LocalizedSlugs } from './modules/i18 export { buildLocalizedPath, getDefaultLocale, getLocaleCodes, getLocaleDefinition, getLocalizedSlugs, isValidLocale, LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale, switchLocalePath, } from './modules/i18n/index.js'; export type { LocaleMiddlewareResult } from './modules/i18n/index.js'; export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js'; +export { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'; +export { getNotificationTexts, NOTIFICATION_FALLBACK, resolveFormMessage, } from './modules/notifications/index.js'; +export type { FormNotificationTexts, NotificationsData, NotificationTexts, } from './modules/notifications/index.js'; export type { PagesOption, SystemPageRole } from './modules/pages/index.js'; export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js'; export type { GlobalQueryOptions } from './modules/payload/index.js'; @@ -31,7 +34,6 @@ export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/ export type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js'; export { buildAutoFillMetaHook, buildRobots, buildSitemapEntries, createMetadataGenerator, createPageMetadata, injectAutoFillMeta, } from './modules/seo/index.js'; export { buildSlugField, toSlug } from './modules/slug/index.js'; -export { NOTIFICATION_FALLBACK, getNotificationTexts, resolveFormMessage, } from './modules/notifications/index.js'; -export type { FormNotificationTexts, NotificationsData, NotificationTexts, } from './modules/notifications/index.js'; +export { buildR2Storage } from './modules/storage/index.js'; export { ipalKit } from './plugin.js'; export type { IpalOptions } from './types.js'; diff --git a/dist/modules/media/index.d.ts b/dist/modules/media/index.d.ts new file mode 100644 index 0000000..4f41aff --- /dev/null +++ b/dist/modules/media/index.d.ts @@ -0,0 +1 @@ +export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js'; diff --git a/dist/modules/media/normalizeFilename.d.ts b/dist/modules/media/normalizeFilename.d.ts new file mode 100644 index 0000000..086c381 --- /dev/null +++ b/dist/modules/media/normalizeFilename.d.ts @@ -0,0 +1,25 @@ +import type { CollectionBeforeOperationHook } from 'payload'; +/** + * Normalizes a filename: slugifies the NAME part (diacritics, spaces, case) + * while preserving the extension. Keeps uploaded media URLs clean and portable. + * + * "Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg" + * "Faktura #12 (2024).PDF" → "faktura-12-2024.pdf" + * "already-clean.webp" → "already-clean.webp" + * + * Why not toSlug(): toSlug uses strict:true, which would strip the dot and + * merge name+extension. Here we split on the LAST dot, slug the stem, lowercase + * the extension, and rejoin. + */ +export declare function normalizeFilename(filename: string): string; +/** + * beforeOperation hook for an upload collection (e.g. Media). Rewrites the + * incoming file's name to its normalized form before Payload stores it, so both + * the stored file and its DB filename are clean. Works with local disk and with + * cloud storage adapters (R2/S3) — it runs before the storage layer. + * + * Wire into your Media collection: + * import { normalizeFilenameHook } from '@intecion/ipal-kit' + * hooks: { beforeOperation: [normalizeFilenameHook] } + */ +export declare const normalizeFilenameHook: CollectionBeforeOperationHook; diff --git a/dist/modules/storage/buildR2Storage.d.ts b/dist/modules/storage/buildR2Storage.d.ts new file mode 100644 index 0000000..85f3812 --- /dev/null +++ b/dist/modules/storage/buildR2Storage.d.ts @@ -0,0 +1,18 @@ +import type { Plugin } from 'payload'; +/** + * Cloudflare R2 media storage — configured from environment variables (agency + * infrastructure, not per-project panel data). R2 is S3-compatible, so we use + * @payloadcms/storage-s3 pointed at the R2 endpoint. + * + * Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at + * boot, and its credentials are agency-owned — so it lives in .env, not the + * panel. See docs/storage.md for the required variables. + * + * Returns the storage plugin when all R2 vars are present; otherwise returns a + * no-op passthrough so the project falls back to Payload's default local disk + * storage (useful in dev without R2). This mirrors how mailAdapter degrades + * gracefully when a transport isn't configured. + * + * @param collections - slugs of upload collections to offload to R2 (e.g. ['media']) + */ +export declare const buildR2Storage: (collections?: string[]) => Plugin; diff --git a/dist/modules/storage/index.d.ts b/dist/modules/storage/index.d.ts new file mode 100644 index 0000000..2ec10de --- /dev/null +++ b/dist/modules/storage/index.d.ts @@ -0,0 +1 @@ +export { buildR2Storage } from './buildR2Storage.js'; diff --git a/docs/storage.md b/docs/storage.md new file mode 100644 index 0000000..a606a8d --- /dev/null +++ b/docs/storage.md @@ -0,0 +1,131 @@ +# storage — media na Cloudflare R2 + +Offload mediów (obrazy, pliki) do Cloudflare R2 zamiast lokalnego dysku. R2 jest +S3-kompatybilny; plugin dostarcza `buildR2Storage`, który czyta dane z `.env` +i konfiguruje adapter. + +> **Storage to infrastruktura, nie treść.** Dane R2 (klucze, bucket) idą do +> `.env` — jak DATABASE_URI, PAYLOAD_SECRET, GRAPH_*. NIE do panelu (to sekrety +> agencyjne, wiążą się przy starcie, nie zmienia ich redaktor). + +## Zależność + +```bash +pnpm add @payloadcms/storage-s3 +``` + +## Zmienne .env + +Patrz [R2-ENV-przyklad](../R2-ENV-przyklad.md) po pełną instrukcję skąd wziąć wartości. + +```bash +R2_BUCKET=nazwa-bucketa +R2_ENDPOINT=https://.r2.cloudflarestorage.com +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= +``` + +## Wpięcie (payload.config.ts) + +```ts +import { buildR2Storage } from '@intecion/ipal-kit' + +export default buildConfig({ + // ... + plugins: [ + ipalKit({ /* ... */ }), + buildR2Storage(['media']), // slugi kolekcji upload do offloadu + ], +}) +``` + +`buildR2Storage` przyjmuje listę kolekcji upload (domyślnie `['media']`). Jeśli +masz więcej kolekcji plików: `buildR2Storage(['media', 'documents'])`. + +## Zachowanie (fallback) + +- **Wszystkie 4 zmienne** → media w R2. +- **Brak zmiennych** → fallback na lokalny dysk (dev działa bez R2, zero konfiguracji). +- **Część zmiennych** → ostrzeżenie w logu + fallback (częściowa konfiguracja = + pewnie pomyłka). + +To wzorzec „degrade gracefully" — jak mailAdapter, który wraca do SMTP, gdy brak +Graph. Projekt działa niezależnie od tego, czy R2 jest skonfigurowany. + +## Publiczny dostęp (WAŻNE) + +R2 domyślnie prywatny. Upload zadziała, ale obrazy się NIE wyświetlą (403), dopóki +nie skonfigurujesz publicznego odczytu: + +1. Cloudflare → R2 → bucket → Settings → **Public access** → podłącz custom domain +2. Albo serwuj przez Cloudflare CDN / własną domenę + +Bez tego media wgrają się do R2, ale front nie pokaże obrazów. Konfiguracja domeny +jest po stronie Cloudflare, nie kodu. + +## Migracja istniejących mediów + +Jeśli projekt miał media lokalnie i przełączasz na R2 — nowe uploady idą do R2, +ale STARE zostają na dysku (i znikną przy redeployu bez wolumenu). Przed +przełączeniem na produkcji przenieś istniejące pliki do bucketa (np. `rclone` +albo ręcznie przez R2 dashboard), inaczej stare obrazy znikną. + +## Weryfikacja + +```bash +# po wpięciu i ustawieniu .env: +pnpm dev +# wgraj obraz w panelu (Media) → sprawdź w Cloudflare R2, czy plik się pojawił +``` + +## Dev na lokalnym I na R2 (seedowanie podczas developmentu) + +Fallback (brak zmiennych → lokalny dysk) oznacza, że **dev działa w obu trybach**: + +- **Dev bez R2 w .env** → media na lokalnym dysku. Szybki start, zero konfiguracji. +- **Dev z R2 w .env** → media w R2 już podczas developmentu. Przydatne, gdy + seedujesz treść w devie i chcesz, żeby od razu lądowała w buckecie (np. wspólny + bucket dev, albo test realnego flow przed produkcją). + +Przełączasz trybem po prostu obecnością zmiennych R2 w `.env`. Ten sam kod, +`buildR2Storage` sam wykrywa. Nie musisz nic zmieniać w configu między trybami. + +> Jeśli seedujesz w devie do R2 — pamiętaj, że to realny bucket. Używaj osobnego +> bucketa dev (nie produkcyjnego), żeby nie mieszać danych testowych z realnymi. + +## Normalizacja nazw plików (automatyczna) + +Plik `normalizeFilenameHook` czyści nazwy wgrywanych plików — slugifikuje nazwę, +zachowuje rozszerzenie: + +``` +"Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg" +"Faktura #12 (2024).PDF" → "faktura-12-2024.pdf" +``` + +Wpięcie w kolekcję Media (projekt): + +```ts +import { normalizeFilenameHook } from '@intecion/ipal-kit' + +export const Media: CollectionConfig = { + slug: 'media', + upload: { staticDir: 'media' /* ... */ }, + hooks: { + beforeOperation: [normalizeFilenameHook], // czyści nazwę przed zapisem + }, + fields: [ /* alt itd. */ ], +} +``` + +Działa z lokalnym dyskiem i z R2/S3 (hook biegnie PRZED warstwą storage, więc +czysta nazwa trafia i do bazy, i do bucketa). Dlaczego to ważne: + +- **URL-e mediów są czyste** — `/media/zdjecie-nad-morzem.jpg`, nie + `/media/Zdjęcie%20jeden%20nad%20morzem.jpg` (spacje/diakrytyki w URL = problemy). +- **Przenośność** — nazwa bez polskich znaków/spacji działa wszędzie (CDN, S3, systemy plików). +- **Bez kolizji kodowania** — spacje i `#`, `()` w nazwach plików potrafią psuć + ścieżki i cache. + +Sama funkcja `normalizeFilename(name)` też jest wyeksportowana, gdybyś potrzebował +jej poza hookiem. \ No newline at end of file diff --git a/package.json b/package.json index 65e6359..bee0b98 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "test:int": "vitest" }, "dependencies": { + "@payloadcms/storage-s3": "^3.88.0", "lucide-react": "^0.400.0", "nodemailer": "^8.0.1", "server-only": "^0.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c2dd3e..7078ba7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@payloadcms/storage-s3': + specifier: ^3.88.0 + version: 3.88.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.88.0(graphql@16.14.2)(typescript@5.7.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.7.3) lucide-react: specifier: ^0.400.0 version: 0.400.0(react@19.2.6) @@ -133,6 +136,88 @@ packages: resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} + '@aws-sdk/checksums@3.1000.29': + resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1119.0': + resolution: {integrity: sha512-8+UH1uBWwjbRu0ugTBhJ/cu3rL/vzf1tm9/3+xZGf1uybIO3HTF2xLJJaTKkRPvzxz8/n/w+YrwoHGZ2Q/a2Zg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.81': + resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/lib-storage@3.1119.0': + resolution: {integrity: sha512-4dxJX2pueKHtFyxdtT/IH385NpAWjbNjpbbIN6L9YK58dbae6ldyiLSom1EV5nPoOxKtWLXiqch+yo3NjXfleQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@aws-sdk/client-s3': ^3.1119.0 + + '@aws-sdk/middleware-sdk-s3@3.972.75': + resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.1119.0': + resolution: {integrity: sha512-yEqb0ispm9uXZ7nk5stErMB7yn43j3XCAaHM15MsdWaCk7RvEmGjYdbyHZcH86JpUg7mk1v1m2xwzLj2o+3uCg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -1646,6 +1731,13 @@ packages: next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.6 <17.0.0' payload: 3.88.0 + '@payloadcms/plugin-cloud-storage@3.88.0': + resolution: {integrity: sha512-wIcRVedtBZIQMu7dpSE1TOJKJd0HuVSUNPrV2KxIbtqQFm+ly34NCJYY0m1+K0YS6my8BKdcB4jj2exg8454Ig==} + peerDependencies: + payload: 3.88.0 + react: ^19.0.1 || ^19.1.2 || ^19.2.1 + react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1 + '@payloadcms/plugin-form-builder@3.88.0': resolution: {integrity: sha512-C915jEHADVN9xW7fSq/bYIZig1zjfQo+t4wrgiqjOCmB81oMgyqLRB3RlL2e6y4PzBUtgss2VS40vn/tVD/61Q==} peerDependencies: @@ -1671,6 +1763,12 @@ packages: react: ^19.0.1 || ^19.1.2 || ^19.2.1 react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1 + '@payloadcms/storage-s3@3.88.0': + resolution: {integrity: sha512-+35JRD4y7ImnIJuPSBhECHGGml0A+xju9BT/zW+lRUycDvvA0cqZNP+t95OVoZrxIB6V75Vg8GKPvAaRAmgOEw==} + engines: {node: ^18.20.2 || >=20.9.0} + peerDependencies: + payload: 3.88.0 + '@payloadcms/translations@3.88.0': resolution: {integrity: sha512-AZVQjjW5pKxO+K9ODeh6yw6/EfXK1hxGPw55KzzDC5srO3hjNskgyWtkauKvbz+S0+xFspANbRc4OD/Rg/74WA==} @@ -1839,6 +1937,30 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.11.3': + resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} + engines: {node: '>=18.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2563,6 +2685,9 @@ packages: body-scroll-lock@4.0.0-beta.0: resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} @@ -2596,6 +2721,9 @@ packages: resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} engines: {node: '>=4.0'} + buffer@5.6.0: + resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -2866,6 +2994,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + detect-indent@7.0.2: resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} engines: {node: '>=12.20'} @@ -3363,10 +3495,18 @@ packages: events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -3455,6 +3595,9 @@ packages: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} + find-node-modules@2.1.3: + resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -3470,6 +3613,10 @@ packages: resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==} engines: {node: '>=12'} + findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -3577,6 +3724,14 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -3672,6 +3827,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -3725,6 +3884,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inspect-with-kind@1.0.5: resolution: {integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==} @@ -3894,6 +4056,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -4098,6 +4264,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + merge@2.1.1: + resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -4430,6 +4599,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4682,6 +4855,10 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -4721,6 +4898,10 @@ packages: resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4966,6 +5147,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5393,6 +5577,10 @@ packages: resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5490,6 +5678,190 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.3.0 + '@aws-sdk/checksums@3.1000.29': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1119.0': + dependencies: + '@aws-sdk/checksums': 3.1000.29 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/middleware-sdk-s3': 3.972.75 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.9': + dependencies: + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.77': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.81': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.14': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.76': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/lib-storage@3.1119.0(@aws-sdk/client-s3@3.1119.0)': + dependencies: + '@aws-sdk/client-s3': 3.1119.0 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.75': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.44': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.1119.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.46': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1116.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.5': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.40': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -7020,6 +7392,21 @@ snapshots: - supports-color - typescript + '@payloadcms/plugin-cloud-storage@3.88.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.88.0(graphql@16.14.2)(typescript@5.7.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.7.3)': + dependencies: + '@payloadcms/ui': 3.88.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.88.0(graphql@16.14.2)(typescript@5.7.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.7.3) + find-node-modules: 2.1.3 + payload: 3.88.0(graphql@16.14.2)(typescript@5.7.3) + range-parser: 1.2.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + - monaco-editor + - next + - supports-color + - typescript + '@payloadcms/plugin-form-builder@3.88.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.88.0(graphql@16.14.2)(typescript@5.7.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.7.3)': dependencies: '@payloadcms/translations': 3.88.0 @@ -7094,6 +7481,22 @@ snapshots: - utf-8-validate - yjs + '@payloadcms/storage-s3@3.88.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.88.0(graphql@16.14.2)(typescript@5.7.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.7.3)': + dependencies: + '@aws-sdk/client-s3': 3.1119.0 + '@aws-sdk/lib-storage': 3.1119.0(@aws-sdk/client-s3@3.1119.0) + '@aws-sdk/s3-request-presigner': 3.1119.0 + '@payloadcms/plugin-cloud-storage': 3.88.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.88.0(graphql@16.14.2)(typescript@5.7.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.7.3) + payload: 3.88.0(graphql@16.14.2)(typescript@5.7.3) + transitivePeerDependencies: + - '@types/react' + - monaco-editor + - next + - react + - react-dom + - supports-color + - typescript + '@payloadcms/translations@3.88.0': dependencies: date-fns: 4.1.0 @@ -7220,6 +7623,39 @@ snapshots: '@sindresorhus/is@5.6.0': {} + '@smithy/core@3.33.3': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} '@swc-node/core@1.14.1(@swc/core@1.15.43)(@swc/types@0.1.27)': @@ -8043,6 +8479,8 @@ snapshots: body-scroll-lock@4.0.0-beta.0: {} + bowser@2.14.1: {} + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 @@ -8078,6 +8516,11 @@ snapshots: dependencies: '@types/node': 22.19.9 + buffer@5.6.0: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -8330,6 +8773,8 @@ snapshots: dequal@2.0.3: {} + detect-file@1.0.0: {} + detect-indent@7.0.2: {} detect-libc@2.0.2: {} @@ -9098,6 +9543,8 @@ snapshots: transitivePeerDependencies: - bare-abort-controller + events@3.3.0: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -9110,6 +9557,10 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + expect-type@1.4.0: {} ext-list@2.2.2: @@ -9204,6 +9655,11 @@ snapshots: make-dir: 3.1.0 pkg-dir: 4.2.0 + find-node-modules@2.1.3: + dependencies: + findup-sync: 4.0.0 + merge: 2.1.1 + find-root@1.1.0: {} find-up@4.1.0: @@ -9220,6 +9676,13 @@ snapshots: dependencies: semver-regex: 4.0.5 + findup-sync@4.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -9330,6 +9793,20 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + globals@14.0.0: {} globals@16.0.0: {} @@ -9425,6 +9902,10 @@ snapshots: dependencies: react-is: 16.13.1 + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + http-cache-semantics@4.2.0: {} http-status@2.1.0: {} @@ -9467,6 +9948,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + inspect-with-kind@1.0.5: dependencies: kind-of: 6.0.3 @@ -9630,6 +10113,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-windows@1.0.2: {} + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -9855,6 +10340,8 @@ snapshots: merge2@1.4.1: {} + merge@2.1.1: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -10320,6 +10807,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-passwd@1.0.0: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -10610,6 +11099,12 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.2 @@ -10653,6 +11148,11 @@ snapshots: resolve-alpn@1.2.1: {} + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -10979,6 +11479,11 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + streamsearch@1.1.0: {} streamx@2.28.0: @@ -11479,6 +11984,10 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/globals/SiteIntegrations/fields/storage.ts b/src/globals/SiteIntegrations/fields/storage.ts deleted file mode 100644 index 07b4f70..0000000 --- a/src/globals/SiteIntegrations/fields/storage.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Field } from 'payload' - -/** - * Cloudflare R2 storage credentials. - * Reserved for future use — media offloading to R2. - * - * Protected at the global level (SiteIntegrations requires an authenticated - * user), so the access keys stay editable in the admin panel while remaining - * inaccessible to anonymous API requests. - */ -export const storageFields: Field[] = [ - { - name: 'r2Bucket', - type: 'text', - admin: { - description: 'R2 bucket name.', - }, - }, - { - name: 'r2Endpoint', - type: 'text', - admin: { - description: 'R2 S3-compatible endpoint URL.', - }, - }, - { - name: 'r2AccessKeyId', - type: 'text', - admin: { - description: 'R2 access key ID.', - }, - }, - { - name: 'r2SecretAccessKey', - type: 'text', - admin: { - description: 'R2 secret access key.', - // Masked in the UI (••••) — stored plaintext, readable for R2 auth. - components: { - Field: '@intecion/ipal-kit/client#MaskedField', - }, - }, - }, -] diff --git a/src/globals/SiteIntegrations/index.ts b/src/globals/SiteIntegrations/index.ts index 2cc75de..cd886f0 100644 --- a/src/globals/SiteIntegrations/index.ts +++ b/src/globals/SiteIntegrations/index.ts @@ -3,7 +3,6 @@ import type { Field, GlobalConfig } from 'payload' import { isAdmin } from '../../modules/access/index.js' import { analyticsFields } from './fields/analytics.js' import { smtpFields } from './fields/smtp.js' -import { storageFields } from './fields/storage.js' import { turnstileFields } from './fields/turnstile.js' type BuildSiteIntegrationsArgs = { @@ -22,6 +21,10 @@ type BuildSiteIntegrationsArgs = { * impossible to enter.) * * Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId). + * + * Note: R2 storage credentials are NOT here — storage is infrastructure and + * binds at boot, so its config lives in .env (R2_BUCKET, R2_ENDPOINT, ...), + * consumed by buildR2Storage. See docs/storage.md. */ export function buildSiteIntegrations({ additionalFields, @@ -44,7 +47,6 @@ export function buildSiteIntegrations({ { fields: analyticsFields, label: 'Analytics' }, { fields: turnstileFields, label: 'Turnstile' }, { fields: smtpFields, label: 'SMTP' }, - { fields: storageFields, label: 'Storage' }, ...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []), ], }, diff --git a/src/index.ts b/src/index.ts index 0e1dea5..ecfdcc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,6 +75,18 @@ export { } from './modules/i18n/index.js' export type { LocaleMiddlewareResult } from './modules/i18n/index.js' export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js' +// Media — filename normalization hook for upload collections (Media). +export { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js' +export { + getNotificationTexts, + NOTIFICATION_FALLBACK, + resolveFormMessage, +} from './modules/notifications/index.js' +export type { + FormNotificationTexts, + NotificationsData, + NotificationTexts, +} from './modules/notifications/index.js' export type { PagesOption, SystemPageRole } from './modules/pages/index.js' export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js' export type { GlobalQueryOptions } from './modules/payload/index.js' @@ -99,18 +111,8 @@ export { injectAutoFillMeta, } from './modules/seo/index.js' export { buildSlugField, toSlug } from './modules/slug/index.js' -export { - NOTIFICATION_FALLBACK, - getNotificationTexts, - resolveFormMessage, -} from './modules/notifications/index.js' -export type { - FormNotificationTexts, - NotificationsData, - NotificationTexts, -} from './modules/notifications/index.js' +// Storage — Cloudflare R2 media offload, configured from .env. +export { buildR2Storage } from './modules/storage/index.js' export { ipalKit } from './plugin.js' export type { IpalOptions } from './types.js' - - diff --git a/src/modules/media/index.ts b/src/modules/media/index.ts new file mode 100644 index 0000000..1b32c1c --- /dev/null +++ b/src/modules/media/index.ts @@ -0,0 +1 @@ +export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js' diff --git a/src/modules/media/normalizeFilename.ts b/src/modules/media/normalizeFilename.ts new file mode 100644 index 0000000..c56cd43 --- /dev/null +++ b/src/modules/media/normalizeFilename.ts @@ -0,0 +1,53 @@ +import type { CollectionBeforeOperationHook } from 'payload' +import slugify from 'slugify' + +/** + * Normalizes a filename: slugifies the NAME part (diacritics, spaces, case) + * while preserving the extension. Keeps uploaded media URLs clean and portable. + * + * "Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg" + * "Faktura #12 (2024).PDF" → "faktura-12-2024.pdf" + * "already-clean.webp" → "already-clean.webp" + * + * Why not toSlug(): toSlug uses strict:true, which would strip the dot and + * merge name+extension. Here we split on the LAST dot, slug the stem, lowercase + * the extension, and rejoin. + */ +export function normalizeFilename(filename: string): string { + const lastDot = filename.lastIndexOf('.') + + // No extension (or leading-dot dotfile) → slug the whole thing. + if (lastDot <= 0) { + return slugify(filename, { lower: true, strict: true, trim: true }) + } + + const stem = filename.slice(0, lastDot) + const ext = filename.slice(lastDot + 1).toLowerCase() + + const cleanStem = slugify(stem, { lower: true, strict: true, trim: true }) + const cleanExt = slugify(ext, { lower: true, strict: true, trim: true }) + + // Stem could slug to empty (e.g. filename was all symbols) — fall back so we + // never produce a nameless file. + const safeStem = cleanStem || 'plik' + + return cleanExt ? `${safeStem}.${cleanExt}` : safeStem +} + +/** + * beforeOperation hook for an upload collection (e.g. Media). Rewrites the + * incoming file's name to its normalized form before Payload stores it, so both + * the stored file and its DB filename are clean. Works with local disk and with + * cloud storage adapters (R2/S3) — it runs before the storage layer. + * + * Wire into your Media collection: + * import { normalizeFilenameHook } from '@intecion/ipal-kit' + * hooks: { beforeOperation: [normalizeFilenameHook] } + */ +export const normalizeFilenameHook: CollectionBeforeOperationHook = ({ req, operation }) => { + if (operation !== 'create' && operation !== 'update') return + const file = req.file + if (file?.name) { + file.name = normalizeFilename(file.name) + } +} diff --git a/src/modules/storage/buildR2Storage.ts b/src/modules/storage/buildR2Storage.ts new file mode 100644 index 0000000..5159392 --- /dev/null +++ b/src/modules/storage/buildR2Storage.ts @@ -0,0 +1,53 @@ +import type { Plugin } from 'payload' +import { s3Storage } from '@payloadcms/storage-s3' + +/** + * Cloudflare R2 media storage — configured from environment variables (agency + * infrastructure, not per-project panel data). R2 is S3-compatible, so we use + * @payloadcms/storage-s3 pointed at the R2 endpoint. + * + * Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at + * boot, and its credentials are agency-owned — so it lives in .env, not the + * panel. See docs/storage.md for the required variables. + * + * Returns the storage plugin when all R2 vars are present; otherwise returns a + * no-op passthrough so the project falls back to Payload's default local disk + * storage (useful in dev without R2). This mirrors how mailAdapter degrades + * gracefully when a transport isn't configured. + * + * @param collections - slugs of upload collections to offload to R2 (e.g. ['media']) + */ +export const buildR2Storage = (collections: string[] = ['media']): Plugin => { + const bucket = process.env.R2_BUCKET + const endpoint = process.env.R2_ENDPOINT + const accessKeyId = process.env.R2_ACCESS_KEY_ID + const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY + + // Any missing → skip R2, fall back to local disk. Warn so it's not silent. + if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) { + return (config) => { + // Only warn when SOME vars are set (partial config = likely a mistake). + if (bucket || endpoint || accessKeyId || secretAccessKey) { + console.warn( + '[ipal] R2 storage: incomplete env (need R2_BUCKET, R2_ENDPOINT, ' + + 'R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY). Falling back to local disk.', + ) + } + return config + } + } + + const collectionsConfig = Object.fromEntries(collections.map((slug) => [slug, true])) + + return s3Storage({ + collections: collectionsConfig, + bucket, + config: { + endpoint, + region: 'auto', // R2 uses 'auto' + credentials: { accessKeyId, secretAccessKey }, + // R2 requires path-style addressing for S3 compatibility. + forcePathStyle: true, + }, + }) +} diff --git a/src/modules/storage/index.ts b/src/modules/storage/index.ts new file mode 100644 index 0000000..899d36f --- /dev/null +++ b/src/modules/storage/index.ts @@ -0,0 +1 @@ +export { buildR2Storage } from './buildR2Storage.js'