Compare commits

..
4 Commits
Author SHA1 Message Date
radoslaw.smolinski 9359f26788 1.0.20 2026-08-27 12:45:47 +02:00
radoslaw.smolinski 5630765215 R2 storage from env + filename normalization 2026-08-27 12:45:47 +02:00
radoslaw.smolinski ed4a78b109 1.0.19 2026-08-27 12:31:09 +02:00
radoslaw.smolinski 9acb22e2f9 R2 storage from env + filename normalization 2026-08-27 12:31:09 +02:00
28 changed files with 963 additions and 69 deletions
+4
View File
@@ -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 {};
+4 -5
View File
@@ -1,7 +1,6 @@
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';
/**
* Builds the SiteIntegrations global.
@@ -14,6 +13,10 @@ import { turnstileFields } from './fields/turnstile.js';
* 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 } = {}) {
return {
slug: 'site-integrations',
@@ -42,10 +45,6 @@ import { turnstileFields } from './fields/turnstile.js';
fields: smtpFields,
label: 'SMTP'
},
{
fields: storageFields,
label: 'Storage'
},
...additionalFields?.length ? [
{
fields: additionalFields,
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sources":["../../../src/globals/SiteIntegrations/index.ts"],"sourcesContent":["import type { Field, GlobalConfig } from 'payload'\n\nimport { isAdmin } from '../../modules/access/index.js'\nimport { analyticsFields } from './fields/analytics.js'\nimport { smtpFields } from './fields/smtp.js'\nimport { storageFields } from './fields/storage.js'\nimport { turnstileFields } from './fields/turnstile.js'\n\ntype BuildSiteIntegrationsArgs = {\n /** Extra fields injected by the client project */\n additionalFields?: Field[]\n}\n\n/**\n * Builds the SiteIntegrations global.\n *\n * Holds third-party service credentials. Access is enforced at the global\n * level — the whole global requires an authenticated user — so secrets stay\n * out of anonymous API responses while remaining editable in the admin panel\n * and readable via the server-side Local API. (Field-level read:false was\n * avoided because it also hides fields from the admin UI, making them\n * impossible to enter.)\n *\n * Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).\n */\nexport function buildSiteIntegrations({\n additionalFields,\n}: BuildSiteIntegrationsArgs = {}): GlobalConfig {\n return {\n slug: 'site-integrations',\n access: {\n // Admin-only — secrets live here. Anonymous and non-admin users get\n // nothing through the API; admins read/edit in the panel and via Local API.\n read: ({ req: { user } }) => isAdmin(user),\n update: ({ req: { user } }) => isAdmin(user),\n },\n admin: {\n group: 'Settings',\n },\n fields: [\n {\n type: 'tabs',\n tabs: [\n { fields: analyticsFields, label: 'Analytics' },\n { fields: turnstileFields, label: 'Turnstile' },\n { fields: smtpFields, label: 'SMTP' },\n { fields: storageFields, label: 'Storage' },\n ...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []),\n ],\n },\n ],\n label: 'Site Integrations',\n }\n}\n"],"names":["isAdmin","analyticsFields","smtpFields","storageFields","turnstileFields","buildSiteIntegrations","additionalFields","slug","access","read","req","user","update","admin","group","fields","type","tabs","label","length"],"mappings":"AAEA,SAASA,OAAO,QAAQ,gCAA+B;AACvD,SAASC,eAAe,QAAQ,wBAAuB;AACvD,SAASC,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,aAAa,QAAQ,sBAAqB;AACnD,SAASC,eAAe,QAAQ,wBAAuB;AAOvD;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,sBAAsB,EACpCC,gBAAgB,EACU,GAAG,CAAC,CAAC;IAC/B,OAAO;QACLC,MAAM;QACNC,QAAQ;YACN,oEAAoE;YACpE,4EAA4E;YAC5EC,MAAM,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKX,QAAQW;YACrCC,QAAQ,CAAC,EAAEF,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKX,QAAQW;QACzC;QACAE,OAAO;YACLC,OAAO;QACT;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;oBACJ;wBAAEF,QAAQd;wBAAiBiB,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQX;wBAAiBc,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQb;wBAAYgB,OAAO;oBAAO;oBACpC;wBAAEH,QAAQZ;wBAAee,OAAO;oBAAU;uBACtCZ,kBAAkBa,SAAS;wBAAC;4BAAEJ,QAAQT;4BAAkBY,OAAO;wBAAS;qBAAE,GAAG,EAAE;iBACpF;YACH;SACD;QACDA,OAAO;IACT;AACF"}
{"version":3,"sources":["../../../src/globals/SiteIntegrations/index.ts"],"sourcesContent":["import type { Field, GlobalConfig } from 'payload'\n\nimport { isAdmin } from '../../modules/access/index.js'\nimport { analyticsFields } from './fields/analytics.js'\nimport { smtpFields } from './fields/smtp.js'\nimport { turnstileFields } from './fields/turnstile.js'\n\ntype BuildSiteIntegrationsArgs = {\n /** Extra fields injected by the client project */\n additionalFields?: Field[]\n}\n\n/**\n * Builds the SiteIntegrations global.\n *\n * Holds third-party service credentials. Access is enforced at the global\n * level — the whole global requires an authenticated user — so secrets stay\n * out of anonymous API responses while remaining editable in the admin panel\n * and readable via the server-side Local API. (Field-level read:false was\n * avoided because it also hides fields from the admin UI, making them\n * impossible to enter.)\n *\n * Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).\n *\n * Note: R2 storage credentials are NOT here — storage is infrastructure and\n * binds at boot, so its config lives in .env (R2_BUCKET, R2_ENDPOINT, ...),\n * consumed by buildR2Storage. See docs/storage.md.\n */\nexport function buildSiteIntegrations({\n additionalFields,\n}: BuildSiteIntegrationsArgs = {}): GlobalConfig {\n return {\n slug: 'site-integrations',\n access: {\n // Admin-only — secrets live here. Anonymous and non-admin users get\n // nothing through the API; admins read/edit in the panel and via Local API.\n read: ({ req: { user } }) => isAdmin(user),\n update: ({ req: { user } }) => isAdmin(user),\n },\n admin: {\n group: 'Settings',\n },\n fields: [\n {\n type: 'tabs',\n tabs: [\n { fields: analyticsFields, label: 'Analytics' },\n { fields: turnstileFields, label: 'Turnstile' },\n { fields: smtpFields, label: 'SMTP' },\n ...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []),\n ],\n },\n ],\n label: 'Site Integrations',\n }\n}\n"],"names":["isAdmin","analyticsFields","smtpFields","turnstileFields","buildSiteIntegrations","additionalFields","slug","access","read","req","user","update","admin","group","fields","type","tabs","label","length"],"mappings":"AAEA,SAASA,OAAO,QAAQ,gCAA+B;AACvD,SAASC,eAAe,QAAQ,wBAAuB;AACvD,SAASC,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,eAAe,QAAQ,wBAAuB;AAOvD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,sBAAsB,EACpCC,gBAAgB,EACU,GAAG,CAAC,CAAC;IAC/B,OAAO;QACLC,MAAM;QACNC,QAAQ;YACN,oEAAoE;YACpE,4EAA4E;YAC5EC,MAAM,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKV,QAAQU;YACrCC,QAAQ,CAAC,EAAEF,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKV,QAAQU;QACzC;QACAE,OAAO;YACLC,OAAO;QACT;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;oBACJ;wBAAEF,QAAQb;wBAAiBgB,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQX;wBAAiBc,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQZ;wBAAYe,OAAO;oBAAO;uBAChCZ,kBAAkBa,SAAS;wBAAC;4BAAEJ,QAAQT;4BAAkBY,OAAO;wBAAS;qBAAE,GAAG,EAAE;iBACpF;YACH;SACD;QACDA,OAAO;IACT;AACF"}
+4 -2
View File
@@ -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';
+5 -1
View File
@@ -12,13 +12,17 @@ export { buildFormsPlugin } from './modules/forms/formsPluginConfig.js';
export { createContentHelpers } from './modules/frontend/index.js';
export { buildLocalizedPath, getDefaultLocale, getLocaleCodes, getLocaleDefinition, getLocalizedSlugs, isValidLocale, LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale, switchLocalePath } 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 { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js';
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG } from './modules/payload/index.js';
export { buildSecurityHeaders } from './modules/security/index.js';
export { buildHreflangAlternates, buildMetadata, composeTitle } 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';
// Storage — Cloudflare R2 media offload, configured from .env.
export { buildR2Storage } from './modules/storage/index.js';
export { ipalKit } from './plugin.js';
//# sourceMappingURL=index.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js';
+3
View File
@@ -0,0 +1,3 @@
export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/media/index.ts"],"sourcesContent":["export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js'\n"],"names":["normalizeFilename","normalizeFilenameHook"],"mappings":"AAAA,SAASA,iBAAiB,EAAEC,qBAAqB,QAAQ,yBAAwB"}
+25
View File
@@ -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;
+57
View File
@@ -0,0 +1,57 @@
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) {
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 = ({ req, operation })=>{
if (operation !== 'create' && operation !== 'update') return;
const file = req.file;
if (file?.name) {
file.name = normalizeFilename(file.name);
}
};
//# sourceMappingURL=normalizeFilename.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/media/normalizeFilename.ts"],"sourcesContent":["import type { CollectionBeforeOperationHook } from 'payload'\nimport slugify from 'slugify'\n\n/**\n * Normalizes a filename: slugifies the NAME part (diacritics, spaces, case)\n * while preserving the extension. Keeps uploaded media URLs clean and portable.\n *\n * \"Zdjęcie jeden nad morzem.jpg\" → \"zdjecie-jeden-nad-morzem.jpg\"\n * \"Faktura #12 (2024).PDF\" → \"faktura-12-2024.pdf\"\n * \"already-clean.webp\" → \"already-clean.webp\"\n *\n * Why not toSlug(): toSlug uses strict:true, which would strip the dot and\n * merge name+extension. Here we split on the LAST dot, slug the stem, lowercase\n * the extension, and rejoin.\n */\nexport function normalizeFilename(filename: string): string {\n const lastDot = filename.lastIndexOf('.')\n\n // No extension (or leading-dot dotfile) → slug the whole thing.\n if (lastDot <= 0) {\n return slugify(filename, { lower: true, strict: true, trim: true })\n }\n\n const stem = filename.slice(0, lastDot)\n const ext = filename.slice(lastDot + 1).toLowerCase()\n\n const cleanStem = slugify(stem, { lower: true, strict: true, trim: true })\n const cleanExt = slugify(ext, { lower: true, strict: true, trim: true })\n\n // Stem could slug to empty (e.g. filename was all symbols) — fall back so we\n // never produce a nameless file.\n const safeStem = cleanStem || 'plik'\n\n return cleanExt ? `${safeStem}.${cleanExt}` : safeStem\n}\n\n/**\n * beforeOperation hook for an upload collection (e.g. Media). Rewrites the\n * incoming file's name to its normalized form before Payload stores it, so both\n * the stored file and its DB filename are clean. Works with local disk and with\n * cloud storage adapters (R2/S3) — it runs before the storage layer.\n *\n * Wire into your Media collection:\n * import { normalizeFilenameHook } from '@intecion/ipal-kit'\n * hooks: { beforeOperation: [normalizeFilenameHook] }\n */\nexport const normalizeFilenameHook: CollectionBeforeOperationHook = ({ req, operation }) => {\n if (operation !== 'create' && operation !== 'update') return\n const file = req.file\n if (file?.name) {\n file.name = normalizeFilename(file.name)\n }\n}\n"],"names":["slugify","normalizeFilename","filename","lastDot","lastIndexOf","lower","strict","trim","stem","slice","ext","toLowerCase","cleanStem","cleanExt","safeStem","normalizeFilenameHook","req","operation","file","name"],"mappings":"AACA,OAAOA,aAAa,UAAS;AAE7B;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,kBAAkBC,QAAgB;IAChD,MAAMC,UAAUD,SAASE,WAAW,CAAC;IAErC,gEAAgE;IAChE,IAAID,WAAW,GAAG;QAChB,OAAOH,QAAQE,UAAU;YAAEG,OAAO;YAAMC,QAAQ;YAAMC,MAAM;QAAK;IACnE;IAEA,MAAMC,OAAON,SAASO,KAAK,CAAC,GAAGN;IAC/B,MAAMO,MAAMR,SAASO,KAAK,CAACN,UAAU,GAAGQ,WAAW;IAEnD,MAAMC,YAAYZ,QAAQQ,MAAM;QAAEH,OAAO;QAAMC,QAAQ;QAAMC,MAAM;IAAK;IACxE,MAAMM,WAAWb,QAAQU,KAAK;QAAEL,OAAO;QAAMC,QAAQ;QAAMC,MAAM;IAAK;IAEtE,6EAA6E;IAC7E,iCAAiC;IACjC,MAAMO,WAAWF,aAAa;IAE9B,OAAOC,WAAW,GAAGC,SAAS,CAAC,EAAED,UAAU,GAAGC;AAChD;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMC,wBAAuD,CAAC,EAAEC,GAAG,EAAEC,SAAS,EAAE;IACrF,IAAIA,cAAc,YAAYA,cAAc,UAAU;IACtD,MAAMC,OAAOF,IAAIE,IAAI;IACrB,IAAIA,MAAMC,MAAM;QACdD,KAAKC,IAAI,GAAGlB,kBAAkBiB,KAAKC,IAAI;IACzC;AACF,EAAC"}
+18
View File
@@ -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;
+57
View File
@@ -0,0 +1,57 @@
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 = [
'media'
])=>{
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;
};
}
// s3Storage wants Record<string, true> (the literal true, per collection),
// not Record<string, boolean>. Object.fromEntries widens true → boolean, so
// build the map with an explicitly-typed accumulator to keep the literal.
const collectionsConfig = {};
for (const slug of collections){
collectionsConfig[slug] = true;
}
return s3Storage({
bucket,
collections: collectionsConfig,
config: {
credentials: {
accessKeyId,
secretAccessKey
},
endpoint,
region: 'auto',
// R2 requires path-style addressing for S3 compatibility.
forcePathStyle: true
}
});
};
//# sourceMappingURL=buildR2Storage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/storage/buildR2Storage.ts"],"sourcesContent":["import type { Plugin } from 'payload'\n\nimport { s3Storage } from '@payloadcms/storage-s3'\n\n/**\n * Cloudflare R2 media storage — configured from environment variables (agency\n * infrastructure, not per-project panel data). R2 is S3-compatible, so we use\n * @payloadcms/storage-s3 pointed at the R2 endpoint.\n *\n * Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at\n * boot, and its credentials are agency-owned — so it lives in .env, not the\n * panel. See docs/storage.md for the required variables.\n *\n * Returns the storage plugin when all R2 vars are present; otherwise returns a\n * no-op passthrough so the project falls back to Payload's default local disk\n * storage (useful in dev without R2). This mirrors how mailAdapter degrades\n * gracefully when a transport isn't configured.\n *\n * @param collections - slugs of upload collections to offload to R2 (e.g. ['media'])\n */\nexport const buildR2Storage = (collections: string[] = ['media']): Plugin => {\n const bucket = process.env.R2_BUCKET\n const endpoint = process.env.R2_ENDPOINT\n const accessKeyId = process.env.R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY\n\n // Any missing → skip R2, fall back to local disk. Warn so it's not silent.\n if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) {\n return (config) => {\n // Only warn when SOME vars are set (partial config = likely a mistake).\n if (bucket || endpoint || accessKeyId || secretAccessKey) {\n console.warn(\n '[ipal] R2 storage: incomplete env (need R2_BUCKET, R2_ENDPOINT, ' +\n 'R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY). Falling back to local disk.',\n )\n }\n return config\n }\n }\n\n // s3Storage wants Record<string, true> (the literal true, per collection),\n // not Record<string, boolean>. Object.fromEntries widens true → boolean, so\n // build the map with an explicitly-typed accumulator to keep the literal.\n const collectionsConfig: Record<string, true> = {}\n for (const slug of collections) {\n collectionsConfig[slug] = true\n }\n\n return s3Storage({\n bucket,\n collections: collectionsConfig,\n config: {\n credentials: { accessKeyId, secretAccessKey },\n endpoint,\n region: 'auto', // R2 uses 'auto'\n // R2 requires path-style addressing for S3 compatibility.\n forcePathStyle: true,\n },\n })\n}\n"],"names":["s3Storage","buildR2Storage","collections","bucket","process","env","R2_BUCKET","endpoint","R2_ENDPOINT","accessKeyId","R2_ACCESS_KEY_ID","secretAccessKey","R2_SECRET_ACCESS_KEY","config","console","warn","collectionsConfig","slug","credentials","region","forcePathStyle"],"mappings":"AAEA,SAASA,SAAS,QAAQ,yBAAwB;AAElD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,iBAAiB,CAACC,cAAwB;IAAC;CAAQ;IAC9D,MAAMC,SAASC,QAAQC,GAAG,CAACC,SAAS;IACpC,MAAMC,WAAWH,QAAQC,GAAG,CAACG,WAAW;IACxC,MAAMC,cAAcL,QAAQC,GAAG,CAACK,gBAAgB;IAChD,MAAMC,kBAAkBP,QAAQC,GAAG,CAACO,oBAAoB;IAExD,2EAA2E;IAC3E,IAAI,CAACT,UAAU,CAACI,YAAY,CAACE,eAAe,CAACE,iBAAiB;QAC5D,OAAO,CAACE;YACN,wEAAwE;YACxE,IAAIV,UAAUI,YAAYE,eAAeE,iBAAiB;gBACxDG,QAAQC,IAAI,CACV,qEACE;YAEN;YACA,OAAOF;QACT;IACF;IAEA,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,MAAMG,oBAA0C,CAAC;IACjD,KAAK,MAAMC,QAAQf,YAAa;QAC9Bc,iBAAiB,CAACC,KAAK,GAAG;IAC5B;IAEA,OAAOjB,UAAU;QACfG;QACAD,aAAac;QACbH,QAAQ;YACNK,aAAa;gBAAET;gBAAaE;YAAgB;YAC5CJ;YACAY,QAAQ;YACR,0DAA0D;YAC1DC,gBAAgB;QAClB;IACF;AACF,EAAC"}
+1
View File
@@ -0,0 +1 @@
export { buildR2Storage } from './buildR2Storage.js';
+3
View File
@@ -0,0 +1,3 @@
export { buildR2Storage } from './buildR2Storage.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/storage/index.ts"],"sourcesContent":["export { buildR2Storage } from './buildR2Storage.js'\n"],"names":["buildR2Storage"],"mappings":"AAAA,SAASA,cAAc,QAAQ,sBAAqB"}
+131
View File
@@ -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://<ACCOUNT_ID>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=<access-key-id>
R2_SECRET_ACCESS_KEY=<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.
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@intecion/ipal-kit",
"version": "1.0.18",
"version": "1.0.20",
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
"license": "MIT",
"repository": {
@@ -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",
+509
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@payloadcms/storage-s3':
specifier: ^3.88.0
version: 3.88.0(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
lucide-react:
specifier: ^0.400.0
version: 0.400.0([email protected])
@@ -133,6 +136,88 @@ packages:
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
engines: {node: '>= 16'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-8+UH1uBWwjbRu0ugTBhJ/cu3rL/vzf1tm9/3+xZGf1uybIO3HTF2xLJJaTKkRPvzxz8/n/w+YrwoHGZ2Q/a2Zg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-4dxJX2pueKHtFyxdtT/IH385NpAWjbNjpbbIN6L9YK58dbae6ldyiLSom1EV5nPoOxKtWLXiqch+yo3NjXfleQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@aws-sdk/client-s3': ^3.1119.0
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-yEqb0ispm9uXZ7nk5stErMB7yn43j3XCAaHM15MsdWaCk7RvEmGjYdbyHZcH86JpUg7mk1v1m2xwzLj2o+3uCg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==}
engines: {node: '>=20.0.0'}
'@aws/[email protected]':
resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
engines: {node: '>=18.0.0'}
'@babel/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
resolution: {integrity: sha512-+35JRD4y7ImnIJuPSBhECHGGml0A+xju9BT/zW+lRUycDvvA0cqZNP+t95OVoZrxIB6V75Vg8GKPvAaRAmgOEw==}
engines: {node: ^18.20.2 || >=20.9.0}
peerDependencies:
payload: 3.88.0
'@payloadcms/[email protected]':
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/[email protected]':
resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==}
engines: {node: '>=18.0.0'}
'@standard-schema/[email protected]':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -2563,6 +2685,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==}
[email protected]:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
[email protected]:
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'}
[email protected]:
resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==}
[email protected]:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -2866,6 +2994,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==}
engines: {node: '>=12.20'}
@@ -3363,10 +3495,18 @@ packages:
[email protected]:
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
[email protected]:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
[email protected]:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==}
engines: {node: '>=0.10.0'}
[email protected]:
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'}
[email protected]:
resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==}
[email protected]:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
@@ -3470,6 +3613,10 @@ packages:
resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==}
engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==}
engines: {node: '>= 8'}
[email protected]:
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 [email protected]
[email protected]:
resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -3672,6 +3827,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
[email protected]:
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
@@ -3725,6 +3884,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
[email protected]:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
[email protected]:
resolution: {integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==}
@@ -3894,6 +4056,10 @@ packages:
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
engines: {node: '>=16'}
@@ -4098,6 +4264,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
[email protected]:
resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
[email protected]:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -4430,6 +4599,10 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -4682,6 +4855,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
[email protected]:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
[email protected]:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@@ -4721,6 +4898,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
[email protected]:
resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==}
engines: {node: '>=0.10.0'}
[email protected]:
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'}
[email protected]:
resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
[email protected]:
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'}
[email protected]:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
[email protected]:
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected](@aws-sdk/[email protected])':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
dependencies:
'@aws-sdk/types': 3.974.5
'@smithy/signature-v4': 5.7.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
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/[email protected]':
dependencies:
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws/[email protected]': {}
'@babel/[email protected]':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -7020,6 +7392,21 @@ snapshots:
- supports-color
- typescript
'@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@payloadcms/ui': 3.88.0(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
find-node-modules: 2.1.3
payload: 3.88.0([email protected])([email protected])
range-parser: 1.2.1
react: 19.2.6
react-dom: 19.2.6([email protected])
transitivePeerDependencies:
- '@types/react'
- monaco-editor
- next
- supports-color
- typescript
'@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@payloadcms/translations': 3.88.0
@@ -7094,6 +7481,22 @@ snapshots:
- utf-8-validate
- yjs
'@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@aws-sdk/client-s3': 3.1119.0
'@aws-sdk/lib-storage': 3.1119.0(@aws-sdk/[email protected])
'@aws-sdk/s3-request-presigner': 3.1119.0
'@payloadcms/plugin-cloud-storage': 3.88.0(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
payload: 3.88.0([email protected])([email protected])
transitivePeerDependencies:
- '@types/react'
- monaco-editor
- next
- react
- react-dom
- supports-color
- typescript
'@payloadcms/[email protected]':
dependencies:
date-fns: 4.1.0
@@ -7220,6 +7623,39 @@ snapshots:
'@sindresorhus/[email protected]': {}
'@smithy/[email protected]':
dependencies:
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
tslib: 2.8.1
'@standard-schema/[email protected]': {}
'@swc-node/[email protected](@swc/[email protected])(@swc/[email protected])':
@@ -8043,6 +8479,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
balanced-match: 1.0.2
@@ -8078,6 +8516,11 @@ snapshots:
dependencies:
'@types/node': 22.19.9
[email protected]:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
[email protected]:
dependencies:
base64-js: 1.5.1
@@ -8330,6 +8773,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -9098,6 +9543,8 @@ snapshots:
transitivePeerDependencies:
- bare-abort-controller
[email protected]: {}
[email protected]:
dependencies:
cross-spawn: 7.0.6
@@ -9110,6 +9557,10 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
[email protected]:
dependencies:
homedir-polyfill: 1.0.3
[email protected]: {}
[email protected]:
@@ -9204,6 +9655,11 @@ snapshots:
make-dir: 3.1.0
pkg-dir: 4.2.0
[email protected]:
dependencies:
findup-sync: 4.0.0
merge: 2.1.1
[email protected]: {}
[email protected]:
@@ -9220,6 +9676,13 @@ snapshots:
dependencies:
semver-regex: 4.0.5
[email protected]:
dependencies:
detect-file: 1.0.0
is-glob: 4.0.3
micromatch: 4.0.8
resolve-dir: 1.0.1
[email protected]:
dependencies:
flatted: 3.4.2
@@ -9330,6 +9793,20 @@ snapshots:
once: 1.4.0
path-is-absolute: 1.0.1
[email protected]:
dependencies:
global-prefix: 1.0.2
is-windows: 1.0.2
resolve-dir: 1.0.1
[email protected]:
dependencies:
expand-tilde: 2.0.2
homedir-polyfill: 1.0.3
ini: 1.3.8
is-windows: 1.0.2
which: 1.3.1
[email protected]: {}
[email protected]: {}
@@ -9425,6 +9902,10 @@ snapshots:
dependencies:
react-is: 16.13.1
[email protected]:
dependencies:
parse-passwd: 1.0.0
[email protected]: {}
[email protected]: {}
@@ -9467,6 +9948,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
kind-of: 6.0.3
@@ -9630,6 +10113,8 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
[email protected]: {}
[email protected]:
dependencies:
is-inside-container: 1.0.0
@@ -9855,6 +10340,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
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
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -10610,6 +11099,12 @@ snapshots:
string_decoder: 1.1.1
util-deprecate: 1.0.2
[email protected]:
dependencies:
inherits: 2.0.4
string_decoder: 1.1.1
util-deprecate: 1.0.2
[email protected]:
dependencies:
picomatch: 2.3.2
@@ -10653,6 +11148,11 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
expand-tilde: 2.0.2
global-modules: 1.0.0
[email protected]: {}
[email protected]: {}
@@ -10979,6 +11479,11 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
[email protected]:
dependencies:
inherits: 2.0.4
readable-stream: 3.6.2
[email protected]: {}
[email protected]:
@@ -11479,6 +11984,10 @@ snapshots:
gopd: 1.2.0
has-tostringtag: 1.0.2
[email protected]:
dependencies:
isexe: 2.0.0
[email protected]:
dependencies:
isexe: 2.0.0
@@ -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',
},
},
},
]
+4 -2
View File
@@ -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' }] : []),
],
},
+14 -12
View File
@@ -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'
+1
View File
@@ -0,0 +1 @@
export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js'
+53
View File
@@ -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)
}
}
+60
View File
@@ -0,0 +1,60 @@
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
}
}
// s3Storage wants Record<string, true> (the literal true, per collection),
// not Record<string, boolean>. Object.fromEntries widens true → boolean, so
// build the map with an explicitly-typed accumulator to keep the literal.
const collectionsConfig: Record<string, true> = {}
for (const slug of collections) {
collectionsConfig[slug] = true
}
return s3Storage({
bucket,
collections: collectionsConfig,
config: {
credentials: { accessKeyId, secretAccessKey },
endpoint,
region: 'auto', // R2 uses 'auto'
// R2 requires path-style addressing for S3 compatibility.
forcePathStyle: true,
},
})
}
+1
View File
@@ -0,0 +1 @@
export { buildR2Storage } from './buildR2Storage.js'