@trieb.work/payload-plugin-backup-mongodb 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -50
- package/dist/components/BackupDashboard/BackupListCollapsible.client.js.map +1 -1
- package/dist/components/BackupDashboard/BackupSettingsModal.client.js.map +1 -1
- package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.js.map +1 -1
- package/dist/components/BackupDashboard/RestoreBackupDialog.client.js.map +1 -1
- package/dist/components/BackupDashboard/TaskActionButton.client.js.map +1 -1
- package/dist/components/BackupDashboard/index.js.map +1 -1
- package/dist/core/archive.js.map +1 -1
- package/dist/core/backup.js.map +1 -1
- package/dist/core/backupBlobIO.js.map +1 -1
- package/dist/core/backupSettings.js.map +1 -1
- package/dist/core/blobTokenValidate.js.map +1 -1
- package/dist/core/restore.js.map +1 -1
- package/dist/core/restorePreview.js.map +1 -1
- package/dist/core/vercelBackupCron.js.map +1 -1
- package/dist/endpoints/paths/admin-manual.js.map +1 -1
- package/dist/endpoints/paths/admin-restore.js.map +1 -1
- package/dist/endpoints/paths/admin-settings.js.map +1 -1
- package/dist/endpoints/shared.js.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/utils/blobName.js.map +1 -1
- package/dist/utils/dashboardRoleAccess.js.map +1 -1
- package/dist/utils/dialogBackdrop.js.map +1 -1
- package/dist/utils/hostname.js.map +1 -1
- package/dist/utils/maskBlobToken.js.map +1 -1
- package/package.json +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/endpoints/paths/admin-manual.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport { after } from 'next/server'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { createBackup } from '../../core/backup'\nimport {\n getResolvedCronBackupSettings,\n resolveBackupBlobAccess,\n resolveBackupBlobToken,\n} from '../../core/backupSettings'\nimport { completeBackupTask, createBackupTask, failBackupTask } from '../../core/taskProgress'\nimport { sanitizeBackupLabel } from '../../utils/index'\nimport { jsonError, readRequestJson, requireBackupAdmin } from '../shared'\n\nexport function createAdminManualEndpoint(options: BackupPluginOptions): Endpoint {\n return {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const settings = await getResolvedCronBackupSettings(payload)\n const blobToken = resolveBackupBlobToken(settings)\n const blobAccess = resolveBackupBlobAccess(settings)\n if (!blobToken) {\n return jsonError('Service unavailable', 503)\n }\n\n const body = (await readRequestJson(req)) as Record<string, unknown>\n const clientSkipRaw = body?.skipCollections\n const clientSkip
|
|
1
|
+
{"version":3,"sources":["../../../src/endpoints/paths/admin-manual.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport { after } from 'next/server'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { createBackup } from '../../core/backup'\nimport {\n getResolvedCronBackupSettings,\n resolveBackupBlobAccess,\n resolveBackupBlobToken,\n} from '../../core/backupSettings'\nimport { completeBackupTask, createBackupTask, failBackupTask } from '../../core/taskProgress'\nimport { sanitizeBackupLabel } from '../../utils/index'\nimport { jsonError, readRequestJson, requireBackupAdmin } from '../shared'\n\nexport function createAdminManualEndpoint(options: BackupPluginOptions): Endpoint {\n return {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const settings = await getResolvedCronBackupSettings(payload)\n const blobToken = resolveBackupBlobToken(settings)\n const blobAccess = resolveBackupBlobAccess(settings)\n if (!blobToken) {\n return jsonError('Service unavailable', 503)\n }\n\n const body = (await readRequestJson(req)) as Record<string, unknown>\n const clientSkipRaw = body?.skipCollections\n const clientSkip =\n Array.isArray(clientSkipRaw) ?\n clientSkipRaw.filter(\n (x: unknown): x is string => typeof x === 'string' && x.length > 0 && x.length < 512,\n )\n : []\n const wantsBlobMedia = body?.includeMedia === true\n const includeMedia = wantsBlobMedia && !clientSkip.includes('media')\n const label = sanitizeBackupLabel(body?.label)\n\n const { pollSecret, taskId } = await createBackupTask(payload, 'backup', 'Backup queued')\n\n payload.logger.info(\n { hasLabel: Boolean(label), includeMedia, skipCount: clientSkip.length, taskId },\n '[backup-endpoint] Manual backup queued',\n )\n\n after(\n createBackup(payload, {\n blobAccess,\n blobToken,\n cron: false,\n includeMedia,\n label: label || undefined,\n skipCollections: clientSkip,\n taskId,\n })\n .then(() => completeBackupTask(payload, taskId, 'Backup completed'))\n .catch(async (error) => {\n await failBackupTask(payload, taskId, error)\n payload.logger.error({ err: error, taskId }, '[backup-endpoint] Manual backup failed')\n }),\n )\n\n return Response.json({ pollSecret, taskId }, { status: 202 })\n },\n method: 'post',\n path: '/backup-mongodb/admin/manual',\n }\n}\n"],"names":["after","createBackup","getResolvedCronBackupSettings","resolveBackupBlobAccess","resolveBackupBlobToken","completeBackupTask","createBackupTask","failBackupTask","sanitizeBackupLabel","jsonError","readRequestJson","requireBackupAdmin","createAdminManualEndpoint","options","handler","req","auth","Response","payload","settings","blobToken","blobAccess","body","clientSkipRaw","skipCollections","clientSkip","Array","isArray","filter","x","length","wantsBlobMedia","includeMedia","includes","label","pollSecret","taskId","logger","info","hasLabel","Boolean","skipCount","cron","undefined","then","catch","error","err","json","status","method","path"],"mappings":"AAEA,SAASA,KAAK,QAAQ,cAAa;AAInC,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SACEC,6BAA6B,EAC7BC,uBAAuB,EACvBC,sBAAsB,QACjB,4BAA2B;AAClC,SAASC,kBAAkB,EAAEC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAyB;AAC9F,SAASC,mBAAmB,QAAQ,oBAAmB;AACvD,SAASC,SAAS,EAAEC,eAAe,EAAEC,kBAAkB,QAAQ,YAAW;AAE1E,OAAO,SAASC,0BAA0BC,OAA4B;IACpE,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAO,MAAML,mBAAmBI,KAAKF;YAC3C,IAAIG,gBAAgBC,UAAU;gBAC5B,OAAOD;YACT;YAEA,MAAM,EAAEE,OAAO,EAAE,GAAGH;YACpB,MAAMI,WAAW,MAAMjB,8BAA8BgB;YACrD,MAAME,YAAYhB,uBAAuBe;YACzC,MAAME,aAAalB,wBAAwBgB;YAC3C,IAAI,CAACC,WAAW;gBACd,OAAOX,UAAU,uBAAuB;YAC1C;YAEA,MAAMa,OAAQ,MAAMZ,gBAAgBK;YACpC,MAAMQ,gBAAgBD,MAAME;YAC5B,MAAMC,aACJC,MAAMC,OAAO,CAACJ,iBACZA,cAAcK,MAAM,CAClB,CAACC,IAA4B,OAAOA,MAAM,YAAYA,EAAEC,MAAM,GAAG,KAAKD,EAAEC,MAAM,GAAG,OAEnF,EAAE;YACN,MAAMC,iBAAiBT,MAAMU,iBAAiB;YAC9C,MAAMA,eAAeD,kBAAkB,CAACN,WAAWQ,QAAQ,CAAC;YAC5D,MAAMC,QAAQ1B,oBAAoBc,MAAMY;YAExC,MAAM,EAAEC,UAAU,EAAEC,MAAM,EAAE,GAAG,MAAM9B,iBAAiBY,SAAS,UAAU;YAEzEA,QAAQmB,MAAM,CAACC,IAAI,CACjB;gBAAEC,UAAUC,QAAQN;gBAAQF;gBAAcS,WAAWhB,WAAWK,MAAM;gBAAEM;YAAO,GAC/E;YAGFpC,MACEC,aAAaiB,SAAS;gBACpBG;gBACAD;gBACAsB,MAAM;gBACNV;gBACAE,OAAOA,SAASS;gBAChBnB,iBAAiBC;gBACjBW;YACF,GACGQ,IAAI,CAAC,IAAMvC,mBAAmBa,SAASkB,QAAQ,qBAC/CS,KAAK,CAAC,OAAOC;gBACZ,MAAMvC,eAAeW,SAASkB,QAAQU;gBACtC5B,QAAQmB,MAAM,CAACS,KAAK,CAAC;oBAAEC,KAAKD;oBAAOV;gBAAO,GAAG;YAC/C;YAGJ,OAAOnB,SAAS+B,IAAI,CAAC;gBAAEb;gBAAYC;YAAO,GAAG;gBAAEa,QAAQ;YAAI;QAC7D;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/endpoints/paths/admin-restore.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport { after } from 'next/server'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport {\n getResolvedCronBackupSettings,\n resolveBackupArchiveRead,\n resolveBackupBlobAccess,\n resolveBackupBlobToken,\n} from '../../core/backupSettings'\nimport { restoreBackup } from '../../core/restore'\nimport { completeBackupTask, createBackupTask, failBackupTask } from '../../core/taskProgress'\nimport { jsonError, readRequestJson, requireBackupAdmin } from '../shared'\n\nexport function createAdminRestoreEndpoint(options: BackupPluginOptions): Endpoint {\n return {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const settings = await getResolvedCronBackupSettings(payload)\n const blobToken = resolveBackupBlobToken(settings)\n const blobAccess = resolveBackupBlobAccess(settings)\n if (!blobToken) {\n return jsonError('Service unavailable', 503)\n }\n\n const body = (await readRequestJson(req)) as Record<string, unknown>\n const pathname = body?.pathname\n const url = body?.url\n const clientSkipRaw = body?.skipCollections\n const clientSkip
|
|
1
|
+
{"version":3,"sources":["../../../src/endpoints/paths/admin-restore.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport { after } from 'next/server'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport {\n getResolvedCronBackupSettings,\n resolveBackupArchiveRead,\n resolveBackupBlobAccess,\n resolveBackupBlobToken,\n} from '../../core/backupSettings'\nimport { restoreBackup } from '../../core/restore'\nimport { completeBackupTask, createBackupTask, failBackupTask } from '../../core/taskProgress'\nimport { jsonError, readRequestJson, requireBackupAdmin } from '../shared'\n\nexport function createAdminRestoreEndpoint(options: BackupPluginOptions): Endpoint {\n return {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const settings = await getResolvedCronBackupSettings(payload)\n const blobToken = resolveBackupBlobToken(settings)\n const blobAccess = resolveBackupBlobAccess(settings)\n if (!blobToken) {\n return jsonError('Service unavailable', 503)\n }\n\n const body = (await readRequestJson(req)) as Record<string, unknown>\n const pathname = body?.pathname\n const url = body?.url\n const clientSkipRaw = body?.skipCollections\n const clientSkip =\n Array.isArray(clientSkipRaw) ?\n clientSkipRaw.filter(\n (x: unknown): x is string => typeof x === 'string' && x.length > 0 && x.length < 512,\n )\n : []\n\n if (!url || typeof url !== 'string') {\n return jsonError('Missing url', 400)\n }\n try {\n new URL(url)\n } catch {\n return jsonError('Invalid url', 400)\n }\n\n const backupRead = resolveBackupArchiveRead(settings, pathname)\n if (blobAccess === 'private' && !backupRead) {\n return jsonError('Missing pathname (required for dedicated backup blob store)', 400)\n }\n\n const { pollSecret, taskId } = await createBackupTask(payload, 'restore', 'Restore queued')\n\n payload.logger.info({ taskId, url }, '[backup-endpoint] Restore queued')\n\n const collectionBlacklist = [...new Set(['backup-tasks', ...clientSkip])]\n const restoreArchiveMedia = body?.restoreArchiveMedia !== false\n\n after(\n restoreBackup(payload, url, collectionBlacklist, false, taskId, {\n backupRead: backupRead ?? undefined,\n blobAccess,\n blobToken,\n restoreArchiveMedia,\n })\n .then(() => completeBackupTask(payload, taskId, 'Restore completed'))\n .catch(async (error) => {\n await failBackupTask(payload, taskId, error)\n payload.logger.error({ err: error, taskId, url }, '[backup-endpoint] Restore failed')\n }),\n )\n\n return Response.json({ pollSecret, taskId }, { status: 202 })\n },\n method: 'post',\n path: '/backup-mongodb/admin/restore',\n }\n}\n"],"names":["after","getResolvedCronBackupSettings","resolveBackupArchiveRead","resolveBackupBlobAccess","resolveBackupBlobToken","restoreBackup","completeBackupTask","createBackupTask","failBackupTask","jsonError","readRequestJson","requireBackupAdmin","createAdminRestoreEndpoint","options","handler","req","auth","Response","payload","settings","blobToken","blobAccess","body","pathname","url","clientSkipRaw","skipCollections","clientSkip","Array","isArray","filter","x","length","URL","backupRead","pollSecret","taskId","logger","info","collectionBlacklist","Set","restoreArchiveMedia","undefined","then","catch","error","err","json","status","method","path"],"mappings":"AAEA,SAASA,KAAK,QAAQ,cAAa;AAInC,SACEC,6BAA6B,EAC7BC,wBAAwB,EACxBC,uBAAuB,EACvBC,sBAAsB,QACjB,4BAA2B;AAClC,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,kBAAkB,EAAEC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAyB;AAC9F,SAASC,SAAS,EAAEC,eAAe,EAAEC,kBAAkB,QAAQ,YAAW;AAE1E,OAAO,SAASC,2BAA2BC,OAA4B;IACrE,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAO,MAAML,mBAAmBI,KAAKF;YAC3C,IAAIG,gBAAgBC,UAAU;gBAC5B,OAAOD;YACT;YAEA,MAAM,EAAEE,OAAO,EAAE,GAAGH;YACpB,MAAMI,WAAW,MAAMlB,8BAA8BiB;YACrD,MAAME,YAAYhB,uBAAuBe;YACzC,MAAME,aAAalB,wBAAwBgB;YAC3C,IAAI,CAACC,WAAW;gBACd,OAAOX,UAAU,uBAAuB;YAC1C;YAEA,MAAMa,OAAQ,MAAMZ,gBAAgBK;YACpC,MAAMQ,WAAWD,MAAMC;YACvB,MAAMC,MAAMF,MAAME;YAClB,MAAMC,gBAAgBH,MAAMI;YAC5B,MAAMC,aACJC,MAAMC,OAAO,CAACJ,iBACZA,cAAcK,MAAM,CAClB,CAACC,IAA4B,OAAOA,MAAM,YAAYA,EAAEC,MAAM,GAAG,KAAKD,EAAEC,MAAM,GAAG,OAEnF,EAAE;YAEN,IAAI,CAACR,OAAO,OAAOA,QAAQ,UAAU;gBACnC,OAAOf,UAAU,eAAe;YAClC;YACA,IAAI;gBACF,IAAIwB,IAAIT;YACV,EAAE,OAAM;gBACN,OAAOf,UAAU,eAAe;YAClC;YAEA,MAAMyB,aAAahC,yBAAyBiB,UAAUI;YACtD,IAAIF,eAAe,aAAa,CAACa,YAAY;gBAC3C,OAAOzB,UAAU,+DAA+D;YAClF;YAEA,MAAM,EAAE0B,UAAU,EAAEC,MAAM,EAAE,GAAG,MAAM7B,iBAAiBW,SAAS,WAAW;YAE1EA,QAAQmB,MAAM,CAACC,IAAI,CAAC;gBAAEF;gBAAQZ;YAAI,GAAG;YAErC,MAAMe,sBAAsB;mBAAI,IAAIC,IAAI;oBAAC;uBAAmBb;iBAAW;aAAE;YACzE,MAAMc,sBAAsBnB,MAAMmB,wBAAwB;YAE1DzC,MACEK,cAAca,SAASM,KAAKe,qBAAqB,OAAOH,QAAQ;gBAC9DF,YAAYA,cAAcQ;gBAC1BrB;gBACAD;gBACAqB;YACF,GACGE,IAAI,CAAC,IAAMrC,mBAAmBY,SAASkB,QAAQ,sBAC/CQ,KAAK,CAAC,OAAOC;gBACZ,MAAMrC,eAAeU,SAASkB,QAAQS;gBACtC3B,QAAQmB,MAAM,CAACQ,KAAK,CAAC;oBAAEC,KAAKD;oBAAOT;oBAAQZ;gBAAI,GAAG;YACpD;YAGJ,OAAOP,SAAS8B,IAAI,CAAC;gBAAEZ;gBAAYC;YAAO,GAAG;gBAAEY,QAAQ;YAAI;QAC7D;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/endpoints/paths/admin-settings.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport { list } from '@vercel/blob'\nimport { after } from 'next/server'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { transferBackupBlobsToToken } from '../../core/backupBlobTransfer'\nimport {\n BACKUP_SETTINGS_SLUG,\n getResolvedCronBackupSettings,\n normalizeSkipMongoCollections,\n resolveBackupBlobAccess,\n resolveBackupBlobToken,\n toPayloadSkipRows,\n} from '../../core/backupSettings'\nimport { validateBackupBlobToken } from '../../core/blobTokenValidate'\nimport {\n completeBackupTask,\n createBackupTask,\n failBackupTask,\n updateBackupTask,\n} from '../../core/taskProgress'\nimport { describeCronSchedule, readVercelBackupCronFromRepo } from '../../core/vercelBackupCron'\nimport {\n maskBlobReadWriteToken,\n shouldPreserveBackupBlobTokenField,\n} from '../../utils/maskBlobToken'\nimport { readRequestJson, requireBackupAdmin } from '../shared'\n\nfunction clampBackupsToKeep(n: unknown): number {\n if (typeof n !== 'number' || !Number.isFinite(n)) {\n return 10\n }\n return Math.min(365, Math.max(1, Math.floor(n)))\n}\n\nfunction buildSettingsJson(\n stored: Awaited<ReturnType<typeof getResolvedCronBackupSettings>>,\n options: BackupPluginOptions,\n vercelCron: ReturnType<typeof readVercelBackupCronFromRepo>,\n humanDescription: null | string,\n transfer:\n | {\n deferred: boolean\n failed: number\n performed: boolean\n pollSecret?: string\n skipped: number\n taskId?: string\n total: number\n transferred: number\n }\n | undefined,\n) {\n const t = transfer ?? {\n deferred: false,\n failed: 0,\n performed: false,\n skipped: 0,\n total: 0,\n transferred: 0,\n }\n return {\n id: stored.id,\n backupBlobAccess: stored.backupBlobAccess,\n backupBlobAccessEffective: resolveBackupBlobAccess(stored),\n backupBlobTokenMasked: maskBlobReadWriteToken(stored.backupBlobReadWriteToken),\n backupsToKeep: stored.backupsToKeep,\n cron: vercelCron\n ? {\n configFileRelative: vercelCron.configFileRelative,\n humanDescription,\n path: vercelCron.path,\n schedule: vercelCron.schedule,\n }\n : null,\n effectiveBackupsToKeep:\n typeof options.backupsToKeep === 'number' ? options.backupsToKeep : stored.backupsToKeep,\n hasBackupBlobReadWriteToken: stored.backupBlobReadWriteToken.trim().length > 0,\n includeMediaForCron: stored.includeMediaForCron,\n pluginBackupsToKeepOverride: typeof options.backupsToKeep === 'number',\n skipMongoCollections: stored.skipMongoCollections,\n transfer: t,\n }\n}\n\nexport function createAdminSettingsEndpoints(options: BackupPluginOptions): Endpoint[] {\n return [\n {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const stored = await getResolvedCronBackupSettings(payload)\n const vercelCron = readVercelBackupCronFromRepo()\n const humanDescription =\n vercelCron?.schedule != null ? describeCronSchedule(vercelCron.schedule) : null\n\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, undefined),\n )\n },\n method: 'get',\n path: '/backup-mongodb/admin/settings',\n },\n {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const body = (await readRequestJson(req)) as Record<string, unknown>\n const backupsToKeep = clampBackupsToKeep(body?.backupsToKeep)\n const skipMongoCollections = normalizeSkipMongoCollections(body?.skipMongoCollections)\n const includeMediaForCron = body?.includeMediaForCron === true\n const transferBackupBlobs =\n typeof body?.transferBackupBlobs === 'boolean' ? body.transferBackupBlobs : false\n const deleteBackupBlobsFromSource =\n typeof body?.deleteBackupBlobsFromSource === 'boolean'\n ? body.deleteBackupBlobsFromSource\n : false\n const requestedRaw =\n typeof body?.backupBlobReadWriteToken === 'string' ? body.backupBlobReadWriteToken : ''\n const requestedTrimmed = requestedRaw.trim()\n\n let stored = await getResolvedCronBackupSettings(payload)\n const previousAccess = resolveBackupBlobAccess(stored)\n const previousBackupBlobToken = stored.backupBlobReadWriteToken.trim()\n const hadStoredToken = previousBackupBlobToken.length > 0\n const preserveTokenField = shouldPreserveBackupBlobTokenField(\n requestedTrimmed,\n hadStoredToken,\n )\n const tokenForDb = preserveTokenField ? previousBackupBlobToken : requestedTrimmed\n\n // Access detection: re-probe when the token actually changes; preserve existing otherwise;\n // clear when the override is removed (falls back to heuristic on the default env token).\n let backupBlobAccessForDb: 'private' | 'public' | null = stored.backupBlobAccess\n if (!preserveTokenField) {\n if (tokenForDb.length === 0) {\n backupBlobAccessForDb = null\n } else {\n const validation = await validateBackupBlobToken(tokenForDb)\n if (!validation.ok) {\n return Response.json(\n { error: `Blob token rejected: ${validation.error ?? 'unknown error'}` },\n { status: 422 },\n )\n }\n backupBlobAccessForDb = validation.access ?? null\n }\n }\n\n if (!stored.id) {\n await payload.create({\n collection: BACKUP_SETTINGS_SLUG,\n data: {\n backupBlobAccess: backupBlobAccessForDb,\n backupBlobReadWriteToken: tokenForDb,\n backupsToKeep,\n includeMediaForCron,\n skipMongoCollections: toPayloadSkipRows(skipMongoCollections),\n },\n overrideAccess: true,\n })\n } else {\n await payload.update({\n id: stored.id,\n collection: BACKUP_SETTINGS_SLUG,\n data: {\n backupBlobAccess: backupBlobAccessForDb,\n backupBlobReadWriteToken: tokenForDb,\n backupsToKeep,\n includeMediaForCron,\n skipMongoCollections: toPayloadSkipRows(skipMongoCollections),\n },\n overrideAccess: true,\n })\n }\n\n stored = await getResolvedCronBackupSettings(payload)\n if (!stored.id) {\n return Response.json({ error: 'Could not persist backup settings' }, { status: 500 })\n }\n\n const vercelCron = readVercelBackupCronFromRepo()\n const humanDescription =\n vercelCron?.schedule != null ? describeCronSchedule(vercelCron.schedule) : null\n\n const newBlobToken = stored.backupBlobReadWriteToken.trim()\n // Read blobs from the *previous* store when rotating tokens; otherwise first-time setup\n // reads from BLOB_READ_WRITE_TOKEN (default Vercel store).\n const sourceTokenForTransfer =\n previousBackupBlobToken.length > 0\n ? previousBackupBlobToken\n : (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n\n const shouldTransferToNewBlobToken =\n newBlobToken.length > 0 &&\n sourceTokenForTransfer.length > 0 &&\n newBlobToken !== sourceTokenForTransfer\n\n if (!transferBackupBlobs || !shouldTransferToNewBlobToken) {\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, {\n deferred: false,\n failed: 0,\n performed: false,\n skipped: 0,\n total: 0,\n transferred: 0,\n }),\n )\n }\n\n const targetToken = resolveBackupBlobToken(stored)\n const targetAccess = resolveBackupBlobAccess(stored)\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/',\n token: sourceTokenForTransfer,\n })\n const total = blobs.length\n\n if (total === 0) {\n const summary = await transferBackupBlobsToToken(\n payload,\n sourceTokenForTransfer,\n targetToken,\n {\n deleteFromSource: deleteBackupBlobsFromSource,\n sourceAccessHint: previousAccess,\n targetAccess,\n },\n )\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, {\n deferred: false,\n failed: summary.failed,\n performed: true,\n skipped: summary.skipped,\n total: summary.total,\n transferred: summary.transferred,\n }),\n )\n }\n\n const { pollSecret, taskId } = await createBackupTask(\n payload,\n 'blobTransfer',\n JSON.stringify({ failed: 0, pathname: '', total, transferred: 0 }),\n )\n\n after(\n (async () => {\n try {\n await updateBackupTask(payload, taskId, {\n message: JSON.stringify({ failed: 0, pathname: '', total, transferred: 0 }),\n status: 'running',\n })\n const summary = await transferBackupBlobsToToken(\n payload,\n sourceTokenForTransfer,\n targetToken,\n {\n deleteFromSource: deleteBackupBlobsFromSource,\n sourceAccessHint: previousAccess,\n targetAccess,\n taskId,\n },\n )\n await completeBackupTask(\n payload,\n taskId,\n JSON.stringify({\n failed: summary.failed,\n pathname: '',\n phase: 'done',\n total: summary.total,\n transferred: summary.transferred,\n }),\n )\n } catch (e) {\n await failBackupTask(payload, taskId, e)\n payload.logger.error(\n { err: e, taskId },\n '[backup-settings] Blob transfer task failed',\n )\n }\n })(),\n )\n\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, {\n deferred: true,\n failed: 0,\n performed: true,\n pollSecret,\n skipped: 0,\n taskId,\n total,\n transferred: 0,\n }),\n )\n },\n method: 'patch',\n path: '/backup-mongodb/admin/settings',\n },\n ]\n}\n"],"names":["list","after","transferBackupBlobsToToken","BACKUP_SETTINGS_SLUG","getResolvedCronBackupSettings","normalizeSkipMongoCollections","resolveBackupBlobAccess","resolveBackupBlobToken","toPayloadSkipRows","validateBackupBlobToken","completeBackupTask","createBackupTask","failBackupTask","updateBackupTask","describeCronSchedule","readVercelBackupCronFromRepo","maskBlobReadWriteToken","shouldPreserveBackupBlobTokenField","readRequestJson","requireBackupAdmin","clampBackupsToKeep","n","Number","isFinite","Math","min","max","floor","buildSettingsJson","stored","options","vercelCron","humanDescription","transfer","t","deferred","failed","performed","skipped","total","transferred","id","backupBlobAccess","backupBlobAccessEffective","backupBlobTokenMasked","backupBlobReadWriteToken","backupsToKeep","cron","configFileRelative","path","schedule","effectiveBackupsToKeep","hasBackupBlobReadWriteToken","trim","length","includeMediaForCron","pluginBackupsToKeepOverride","skipMongoCollections","createAdminSettingsEndpoints","handler","req","auth","Response","payload","json","undefined","method","body","transferBackupBlobs","deleteBackupBlobsFromSource","requestedRaw","requestedTrimmed","previousAccess","previousBackupBlobToken","hadStoredToken","preserveTokenField","tokenForDb","backupBlobAccessForDb","validation","ok","error","status","access","create","collection","data","overrideAccess","update","newBlobToken","sourceTokenForTransfer","process","env","BLOB_READ_WRITE_TOKEN","shouldTransferToNewBlobToken","targetToken","targetAccess","blobs","limit","prefix","token","summary","deleteFromSource","sourceAccessHint","pollSecret","taskId","JSON","stringify","pathname","message","phase","e","logger","err"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/endpoints/paths/admin-settings.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport { list } from '@vercel/blob'\nimport { after } from 'next/server'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { transferBackupBlobsToToken } from '../../core/backupBlobTransfer'\nimport {\n BACKUP_SETTINGS_SLUG,\n getResolvedCronBackupSettings,\n normalizeSkipMongoCollections,\n resolveBackupBlobAccess,\n resolveBackupBlobToken,\n toPayloadSkipRows,\n} from '../../core/backupSettings'\nimport { validateBackupBlobToken } from '../../core/blobTokenValidate'\nimport {\n completeBackupTask,\n createBackupTask,\n failBackupTask,\n updateBackupTask,\n} from '../../core/taskProgress'\nimport { describeCronSchedule, readVercelBackupCronFromRepo } from '../../core/vercelBackupCron'\nimport {\n maskBlobReadWriteToken,\n shouldPreserveBackupBlobTokenField,\n} from '../../utils/maskBlobToken'\nimport { readRequestJson, requireBackupAdmin } from '../shared'\n\nfunction clampBackupsToKeep(n: unknown): number {\n if (typeof n !== 'number' || !Number.isFinite(n)) {\n return 10\n }\n return Math.min(365, Math.max(1, Math.floor(n)))\n}\n\nfunction buildSettingsJson(\n stored: Awaited<ReturnType<typeof getResolvedCronBackupSettings>>,\n options: BackupPluginOptions,\n vercelCron: ReturnType<typeof readVercelBackupCronFromRepo>,\n humanDescription: null | string,\n transfer:\n | {\n deferred: boolean\n failed: number\n performed: boolean\n pollSecret?: string\n skipped: number\n taskId?: string\n total: number\n transferred: number\n }\n | undefined,\n) {\n const t = transfer ?? {\n deferred: false,\n failed: 0,\n performed: false,\n skipped: 0,\n total: 0,\n transferred: 0,\n }\n return {\n id: stored.id,\n backupBlobAccess: stored.backupBlobAccess,\n backupBlobAccessEffective: resolveBackupBlobAccess(stored),\n backupBlobTokenMasked: maskBlobReadWriteToken(stored.backupBlobReadWriteToken),\n backupsToKeep: stored.backupsToKeep,\n cron:\n vercelCron ?\n {\n configFileRelative: vercelCron.configFileRelative,\n humanDescription,\n path: vercelCron.path,\n schedule: vercelCron.schedule,\n }\n : null,\n effectiveBackupsToKeep:\n typeof options.backupsToKeep === 'number' ? options.backupsToKeep : stored.backupsToKeep,\n hasBackupBlobReadWriteToken: stored.backupBlobReadWriteToken.trim().length > 0,\n includeMediaForCron: stored.includeMediaForCron,\n pluginBackupsToKeepOverride: typeof options.backupsToKeep === 'number',\n skipMongoCollections: stored.skipMongoCollections,\n transfer: t,\n }\n}\n\nexport function createAdminSettingsEndpoints(options: BackupPluginOptions): Endpoint[] {\n return [\n {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const stored = await getResolvedCronBackupSettings(payload)\n const vercelCron = readVercelBackupCronFromRepo()\n const humanDescription =\n vercelCron?.schedule != null ? describeCronSchedule(vercelCron.schedule) : null\n\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, undefined),\n )\n },\n method: 'get',\n path: '/backup-mongodb/admin/settings',\n },\n {\n handler: async (req) => {\n const auth = await requireBackupAdmin(req, options)\n if (auth instanceof Response) {\n return auth\n }\n\n const { payload } = req\n const body = (await readRequestJson(req)) as Record<string, unknown>\n const backupsToKeep = clampBackupsToKeep(body?.backupsToKeep)\n const skipMongoCollections = normalizeSkipMongoCollections(body?.skipMongoCollections)\n const includeMediaForCron = body?.includeMediaForCron === true\n const transferBackupBlobs =\n typeof body?.transferBackupBlobs === 'boolean' ? body.transferBackupBlobs : false\n const deleteBackupBlobsFromSource =\n typeof body?.deleteBackupBlobsFromSource === 'boolean' ?\n body.deleteBackupBlobsFromSource\n : false\n const requestedRaw =\n typeof body?.backupBlobReadWriteToken === 'string' ? body.backupBlobReadWriteToken : ''\n const requestedTrimmed = requestedRaw.trim()\n\n let stored = await getResolvedCronBackupSettings(payload)\n const previousAccess = resolveBackupBlobAccess(stored)\n const previousBackupBlobToken = stored.backupBlobReadWriteToken.trim()\n const hadStoredToken = previousBackupBlobToken.length > 0\n const preserveTokenField = shouldPreserveBackupBlobTokenField(\n requestedTrimmed,\n hadStoredToken,\n )\n const tokenForDb = preserveTokenField ? previousBackupBlobToken : requestedTrimmed\n\n // Access detection: re-probe when the token actually changes; preserve existing otherwise;\n // clear when the override is removed (falls back to heuristic on the default env token).\n let backupBlobAccessForDb: 'private' | 'public' | null = stored.backupBlobAccess\n if (!preserveTokenField) {\n if (tokenForDb.length === 0) {\n backupBlobAccessForDb = null\n } else {\n const validation = await validateBackupBlobToken(tokenForDb)\n if (!validation.ok) {\n return Response.json(\n { error: `Blob token rejected: ${validation.error ?? 'unknown error'}` },\n { status: 422 },\n )\n }\n backupBlobAccessForDb = validation.access ?? null\n }\n }\n\n if (!stored.id) {\n await payload.create({\n collection: BACKUP_SETTINGS_SLUG,\n data: {\n backupBlobAccess: backupBlobAccessForDb,\n backupBlobReadWriteToken: tokenForDb,\n backupsToKeep,\n includeMediaForCron,\n skipMongoCollections: toPayloadSkipRows(skipMongoCollections),\n },\n overrideAccess: true,\n })\n } else {\n await payload.update({\n id: stored.id,\n collection: BACKUP_SETTINGS_SLUG,\n data: {\n backupBlobAccess: backupBlobAccessForDb,\n backupBlobReadWriteToken: tokenForDb,\n backupsToKeep,\n includeMediaForCron,\n skipMongoCollections: toPayloadSkipRows(skipMongoCollections),\n },\n overrideAccess: true,\n })\n }\n\n stored = await getResolvedCronBackupSettings(payload)\n if (!stored.id) {\n return Response.json({ error: 'Could not persist backup settings' }, { status: 500 })\n }\n\n const vercelCron = readVercelBackupCronFromRepo()\n const humanDescription =\n vercelCron?.schedule != null ? describeCronSchedule(vercelCron.schedule) : null\n\n const newBlobToken = stored.backupBlobReadWriteToken.trim()\n // Read blobs from the *previous* store when rotating tokens; otherwise first-time setup\n // reads from BLOB_READ_WRITE_TOKEN (default Vercel store).\n const sourceTokenForTransfer =\n previousBackupBlobToken.length > 0 ?\n previousBackupBlobToken\n : (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n\n const shouldTransferToNewBlobToken =\n newBlobToken.length > 0 &&\n sourceTokenForTransfer.length > 0 &&\n newBlobToken !== sourceTokenForTransfer\n\n if (!transferBackupBlobs || !shouldTransferToNewBlobToken) {\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, {\n deferred: false,\n failed: 0,\n performed: false,\n skipped: 0,\n total: 0,\n transferred: 0,\n }),\n )\n }\n\n const targetToken = resolveBackupBlobToken(stored)\n const targetAccess = resolveBackupBlobAccess(stored)\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/',\n token: sourceTokenForTransfer,\n })\n const total = blobs.length\n\n if (total === 0) {\n const summary = await transferBackupBlobsToToken(\n payload,\n sourceTokenForTransfer,\n targetToken,\n {\n deleteFromSource: deleteBackupBlobsFromSource,\n sourceAccessHint: previousAccess,\n targetAccess,\n },\n )\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, {\n deferred: false,\n failed: summary.failed,\n performed: true,\n skipped: summary.skipped,\n total: summary.total,\n transferred: summary.transferred,\n }),\n )\n }\n\n const { pollSecret, taskId } = await createBackupTask(\n payload,\n 'blobTransfer',\n JSON.stringify({ failed: 0, pathname: '', total, transferred: 0 }),\n )\n\n after(\n (async () => {\n try {\n await updateBackupTask(payload, taskId, {\n message: JSON.stringify({ failed: 0, pathname: '', total, transferred: 0 }),\n status: 'running',\n })\n const summary = await transferBackupBlobsToToken(\n payload,\n sourceTokenForTransfer,\n targetToken,\n {\n deleteFromSource: deleteBackupBlobsFromSource,\n sourceAccessHint: previousAccess,\n targetAccess,\n taskId,\n },\n )\n await completeBackupTask(\n payload,\n taskId,\n JSON.stringify({\n failed: summary.failed,\n pathname: '',\n phase: 'done',\n total: summary.total,\n transferred: summary.transferred,\n }),\n )\n } catch (e) {\n await failBackupTask(payload, taskId, e)\n payload.logger.error(\n { err: e, taskId },\n '[backup-settings] Blob transfer task failed',\n )\n }\n })(),\n )\n\n return Response.json(\n buildSettingsJson(stored, options, vercelCron, humanDescription, {\n deferred: true,\n failed: 0,\n performed: true,\n pollSecret,\n skipped: 0,\n taskId,\n total,\n transferred: 0,\n }),\n )\n },\n method: 'patch',\n path: '/backup-mongodb/admin/settings',\n },\n ]\n}\n"],"names":["list","after","transferBackupBlobsToToken","BACKUP_SETTINGS_SLUG","getResolvedCronBackupSettings","normalizeSkipMongoCollections","resolveBackupBlobAccess","resolveBackupBlobToken","toPayloadSkipRows","validateBackupBlobToken","completeBackupTask","createBackupTask","failBackupTask","updateBackupTask","describeCronSchedule","readVercelBackupCronFromRepo","maskBlobReadWriteToken","shouldPreserveBackupBlobTokenField","readRequestJson","requireBackupAdmin","clampBackupsToKeep","n","Number","isFinite","Math","min","max","floor","buildSettingsJson","stored","options","vercelCron","humanDescription","transfer","t","deferred","failed","performed","skipped","total","transferred","id","backupBlobAccess","backupBlobAccessEffective","backupBlobTokenMasked","backupBlobReadWriteToken","backupsToKeep","cron","configFileRelative","path","schedule","effectiveBackupsToKeep","hasBackupBlobReadWriteToken","trim","length","includeMediaForCron","pluginBackupsToKeepOverride","skipMongoCollections","createAdminSettingsEndpoints","handler","req","auth","Response","payload","json","undefined","method","body","transferBackupBlobs","deleteBackupBlobsFromSource","requestedRaw","requestedTrimmed","previousAccess","previousBackupBlobToken","hadStoredToken","preserveTokenField","tokenForDb","backupBlobAccessForDb","validation","ok","error","status","access","create","collection","data","overrideAccess","update","newBlobToken","sourceTokenForTransfer","process","env","BLOB_READ_WRITE_TOKEN","shouldTransferToNewBlobToken","targetToken","targetAccess","blobs","limit","prefix","token","summary","deleteFromSource","sourceAccessHint","pollSecret","taskId","JSON","stringify","pathname","message","phase","e","logger","err"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/shared.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport type { BackupPluginOptions } from '../types'\n\nimport { isUserAllowedByEnvRoles } from '../utils/dashboardRoleAccess'\n\nexport async function readRequestJson(req: PayloadRequest): Promise<unknown> {\n return (req as unknown as Request).json() as Promise<unknown>\n}\n\n/**\n * Canonical JSON error response for admin-facing endpoints. The dashboard clients\n * always parse responses as JSON, so plain-text bodies would crash `await res.json()`\n * with `Unexpected token ... is not valid JSON`.\n */\nexport function jsonError(message: string, status: number): Response {\n return Response.json({ error: message }, { status })\n}\n\nexport function requireBlobEnv(): null | Response {\n if (!process.env.BLOB_READ_WRITE_TOKEN) {\n return jsonError('Service unavailable', 503)\n }\n return null\n}\n\nexport function requireCronBearer(req: PayloadRequest): null | Response {\n // Security gate for cron/external backup routes: only a matching CRON_SECRET bearer may pass.\n const authHeader = req.headers.get('authorization')\n if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {\n return new Response('Unauthorized', { status: 401 })\n }\n return null\n}\n\nexport async function getAuthorizedBackupAdmin(\n req: PayloadRequest,\n options: BackupPluginOptions,\n): Promise<null | Record<string, unknown>> {\n const fromRequest = (req as { user?: null | Record<string, unknown> } & PayloadRequest).user\n const authUser
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/shared.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport type { BackupPluginOptions } from '../types'\n\nimport { isUserAllowedByEnvRoles } from '../utils/dashboardRoleAccess'\n\nexport async function readRequestJson(req: PayloadRequest): Promise<unknown> {\n return (req as unknown as Request).json() as Promise<unknown>\n}\n\n/**\n * Canonical JSON error response for admin-facing endpoints. The dashboard clients\n * always parse responses as JSON, so plain-text bodies would crash `await res.json()`\n * with `Unexpected token ... is not valid JSON`.\n */\nexport function jsonError(message: string, status: number): Response {\n return Response.json({ error: message }, { status })\n}\n\nexport function requireBlobEnv(): null | Response {\n if (!process.env.BLOB_READ_WRITE_TOKEN) {\n return jsonError('Service unavailable', 503)\n }\n return null\n}\n\nexport function requireCronBearer(req: PayloadRequest): null | Response {\n // Security gate for cron/external backup routes: only a matching CRON_SECRET bearer may pass.\n const authHeader = req.headers.get('authorization')\n if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {\n return new Response('Unauthorized', { status: 401 })\n }\n return null\n}\n\nexport async function getAuthorizedBackupAdmin(\n req: PayloadRequest,\n options: BackupPluginOptions,\n): Promise<null | Record<string, unknown>> {\n const fromRequest = (req as { user?: null | Record<string, unknown> } & PayloadRequest).user\n const authUser =\n fromRequest ? fromRequest : (\n ((await req.payload.auth({ headers: req.headers }))?.user as\n | null\n | Record<string, unknown>\n | undefined)\n )\n const user = authUser\n if (!user) {\n return null\n }\n if (options.access) {\n return options.access(user) ? user : null\n }\n // Default access check stays in sync with the dashboard visibility rule:\n // driven by `PAYLOAD_BACKUP_ALLOWED_ROLES` with backwards-compatible defaults\n // (admin role required when roles exist, or allow when the project has no roles field).\n return isUserAllowedByEnvRoles(user) ? user : null\n}\n\nexport async function requireBackupAdmin(\n req: PayloadRequest,\n options: BackupPluginOptions,\n): Promise<Record<string, unknown> | Response> {\n // Security gate for admin routes: request must resolve to an authorized backup admin user.\n const user = await getAuthorizedBackupAdmin(req, options)\n if (!user) {\n return jsonError('Unauthorized', 401)\n }\n return user\n}\n"],"names":["isUserAllowedByEnvRoles","readRequestJson","req","json","jsonError","message","status","Response","error","requireBlobEnv","process","env","BLOB_READ_WRITE_TOKEN","requireCronBearer","authHeader","headers","get","CRON_SECRET","getAuthorizedBackupAdmin","options","fromRequest","user","authUser","payload","auth","access","requireBackupAdmin"],"mappings":"AAIA,SAASA,uBAAuB,QAAQ,+BAA8B;AAEtE,OAAO,eAAeC,gBAAgBC,GAAmB;IACvD,OAAO,AAACA,IAA2BC,IAAI;AACzC;AAEA;;;;CAIC,GACD,OAAO,SAASC,UAAUC,OAAe,EAAEC,MAAc;IACvD,OAAOC,SAASJ,IAAI,CAAC;QAAEK,OAAOH;IAAQ,GAAG;QAAEC;IAAO;AACpD;AAEA,OAAO,SAASG;IACd,IAAI,CAACC,QAAQC,GAAG,CAACC,qBAAqB,EAAE;QACtC,OAAOR,UAAU,uBAAuB;IAC1C;IACA,OAAO;AACT;AAEA,OAAO,SAASS,kBAAkBX,GAAmB;IACnD,8FAA8F;IAC9F,MAAMY,aAAaZ,IAAIa,OAAO,CAACC,GAAG,CAAC;IACnC,IAAIF,eAAe,CAAC,OAAO,EAAEJ,QAAQC,GAAG,CAACM,WAAW,EAAE,EAAE;QACtD,OAAO,IAAIV,SAAS,gBAAgB;YAAED,QAAQ;QAAI;IACpD;IACA,OAAO;AACT;AAEA,OAAO,eAAeY,yBACpBhB,GAAmB,EACnBiB,OAA4B;IAE5B,MAAMC,cAAc,AAAClB,IAAmEmB,IAAI;IAC5F,MAAMC,WACJF,cAAcA,cACV,CAAA,MAAMlB,IAAIqB,OAAO,CAACC,IAAI,CAAC;QAAET,SAASb,IAAIa,OAAO;IAAC,EAAC,GAAIM;IAKzD,MAAMA,OAAOC;IACb,IAAI,CAACD,MAAM;QACT,OAAO;IACT;IACA,IAAIF,QAAQM,MAAM,EAAE;QAClB,OAAON,QAAQM,MAAM,CAACJ,QAAQA,OAAO;IACvC;IACA,yEAAyE;IACzE,8EAA8E;IAC9E,wFAAwF;IACxF,OAAOrB,wBAAwBqB,QAAQA,OAAO;AAChD;AAEA,OAAO,eAAeK,mBACpBxB,GAAmB,EACnBiB,OAA4B;IAE5B,2FAA2F;IAC3F,MAAME,OAAO,MAAMH,yBAAyBhB,KAAKiB;IACjD,IAAI,CAACE,MAAM;QACT,OAAOjB,UAAU,gBAAgB;IACnC;IACA,OAAOiB;AACT"}
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Plugin } from 'payload'\n\nimport type { BackupPluginOptions } from './types'\n\nimport { BackupSettingsCollection } from './collections/BackupSettings'\nimport { BackupTasksCollection } from './collections/BackupTasks'\nimport { BACKUP_SETTINGS_SLUG } from './core/backupSettings'\nimport { createBackupMongodbEndpoints } from './endpoints/index'\n\nexport const backupMongodbPlugin = (options: BackupPluginOptions = {}): Plugin => {\n return (incomingConfig: Config): Config => {\n if (options.enabled === false) {\n return incomingConfig\n }\n\n const existingAfterDashboard = incomingConfig.admin?.components?.afterDashboard || []\n const existingCollections = incomingConfig.collections || []\n const existingEndpoints = incomingConfig.endpoints ?? []\n const backupEndpoints = createBackupMongodbEndpoints(options)\n\n return {\n ...incomingConfig,\n admin: {\n ...incomingConfig.admin,\n components: {\n ...incomingConfig.admin?.components,\n afterDashboard: [\n ...existingAfterDashboard,\n '@trieb.work/payload-plugin-backup-mongodb/rsc#BackupDashboard',\n ],\n },\n },\n collections: [...existingCollections, BackupTasksCollection, BackupSettingsCollection],\n endpoints: [...existingEndpoints, ...backupEndpoints],\n onInit: async (payload) => {\n await incomingConfig.onInit?.(payload)\n try {\n const existing = await payload.count({\n collection: BACKUP_SETTINGS_SLUG,\n overrideAccess: true,\n })\n if (existing.totalDocs === 0) {\n await payload.create({\n collection: BACKUP_SETTINGS_SLUG,\n data: {\n backupBlobAccess: null,\n backupBlobReadWriteToken: '',\n backupsToKeep: Number(process.env.BACKUPS_TO_KEEP) || 10,\n includeMediaForCron: true,\n skipMongoCollections: [],\n },\n overrideAccess: true,\n })\n payload.logger.info('[backup-plugin] Created default backup-settings document')\n }\n } catch (err) {\n payload.logger.warn({ err }, '[backup-plugin] Could not ensure backup-settings document')\n }\n try {\n if (payload.db.name === 'mongoose') {\n const mongooseDb = payload.db as {\n connection?: {\n db?: {\n collection: (name: string) => {\n createIndex: (\n keys: Record<string, number>,\n options?: { background?: boolean; expireAfterSeconds?: number },\n ) => Promise<string>\n }\n }\n }\n }\n const db = mongooseDb.connection?.db\n if (db) {\n await db.collection('backup-tasks').createIndex(
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Plugin } from 'payload'\n\nimport type { BackupPluginOptions } from './types'\n\nimport { BackupSettingsCollection } from './collections/BackupSettings'\nimport { BackupTasksCollection } from './collections/BackupTasks'\nimport { BACKUP_SETTINGS_SLUG } from './core/backupSettings'\nimport { createBackupMongodbEndpoints } from './endpoints/index'\n\nexport const backupMongodbPlugin = (options: BackupPluginOptions = {}): Plugin => {\n return (incomingConfig: Config): Config => {\n if (options.enabled === false) {\n return incomingConfig\n }\n\n const existingAfterDashboard = incomingConfig.admin?.components?.afterDashboard || []\n const existingCollections = incomingConfig.collections || []\n const existingEndpoints = incomingConfig.endpoints ?? []\n const backupEndpoints = createBackupMongodbEndpoints(options)\n\n return {\n ...incomingConfig,\n admin: {\n ...incomingConfig.admin,\n components: {\n ...incomingConfig.admin?.components,\n afterDashboard: [\n ...existingAfterDashboard,\n '@trieb.work/payload-plugin-backup-mongodb/rsc#BackupDashboard',\n ],\n },\n },\n collections: [...existingCollections, BackupTasksCollection, BackupSettingsCollection],\n endpoints: [...existingEndpoints, ...backupEndpoints],\n onInit: async (payload) => {\n await incomingConfig.onInit?.(payload)\n try {\n const existing = await payload.count({\n collection: BACKUP_SETTINGS_SLUG,\n overrideAccess: true,\n })\n if (existing.totalDocs === 0) {\n await payload.create({\n collection: BACKUP_SETTINGS_SLUG,\n data: {\n backupBlobAccess: null,\n backupBlobReadWriteToken: '',\n backupsToKeep: Number(process.env.BACKUPS_TO_KEEP) || 10,\n includeMediaForCron: true,\n skipMongoCollections: [],\n },\n overrideAccess: true,\n })\n payload.logger.info('[backup-plugin] Created default backup-settings document')\n }\n } catch (err) {\n payload.logger.warn({ err }, '[backup-plugin] Could not ensure backup-settings document')\n }\n try {\n if (payload.db.name === 'mongoose') {\n const mongooseDb = payload.db as {\n connection?: {\n db?: {\n collection: (name: string) => {\n createIndex: (\n keys: Record<string, number>,\n options?: { background?: boolean; expireAfterSeconds?: number },\n ) => Promise<string>\n }\n }\n }\n }\n const db = mongooseDb.connection?.db\n if (db) {\n await db\n .collection('backup-tasks')\n .createIndex({ updatedAt: 1 }, { background: true, expireAfterSeconds: 1800 })\n }\n }\n } catch {\n payload.logger.warn('[backup-plugin] Could not create TTL index on backup-tasks')\n }\n },\n }\n }\n}\n"],"names":["BackupSettingsCollection","BackupTasksCollection","BACKUP_SETTINGS_SLUG","createBackupMongodbEndpoints","backupMongodbPlugin","options","incomingConfig","enabled","existingAfterDashboard","admin","components","afterDashboard","existingCollections","collections","existingEndpoints","endpoints","backupEndpoints","onInit","payload","existing","count","collection","overrideAccess","totalDocs","create","data","backupBlobAccess","backupBlobReadWriteToken","backupsToKeep","Number","process","env","BACKUPS_TO_KEEP","includeMediaForCron","skipMongoCollections","logger","info","err","warn","db","name","mongooseDb","connection","createIndex","updatedAt","background","expireAfterSeconds"],"mappings":"AAIA,SAASA,wBAAwB,QAAQ,+BAA8B;AACvE,SAASC,qBAAqB,QAAQ,4BAA2B;AACjE,SAASC,oBAAoB,QAAQ,wBAAuB;AAC5D,SAASC,4BAA4B,QAAQ,oBAAmB;AAEhE,OAAO,MAAMC,sBAAsB,CAACC,UAA+B,CAAC,CAAC;IACnE,OAAO,CAACC;QACN,IAAID,QAAQE,OAAO,KAAK,OAAO;YAC7B,OAAOD;QACT;QAEA,MAAME,yBAAyBF,eAAeG,KAAK,EAAEC,YAAYC,kBAAkB,EAAE;QACrF,MAAMC,sBAAsBN,eAAeO,WAAW,IAAI,EAAE;QAC5D,MAAMC,oBAAoBR,eAAeS,SAAS,IAAI,EAAE;QACxD,MAAMC,kBAAkBb,6BAA6BE;QAErD,OAAO;YACL,GAAGC,cAAc;YACjBG,OAAO;gBACL,GAAGH,eAAeG,KAAK;gBACvBC,YAAY;oBACV,GAAGJ,eAAeG,KAAK,EAAEC,UAAU;oBACnCC,gBAAgB;2BACXH;wBACH;qBACD;gBACH;YACF;YACAK,aAAa;mBAAID;gBAAqBX;gBAAuBD;aAAyB;YACtFe,WAAW;mBAAID;mBAAsBE;aAAgB;YACrDC,QAAQ,OAAOC;gBACb,MAAMZ,eAAeW,MAAM,GAAGC;gBAC9B,IAAI;oBACF,MAAMC,WAAW,MAAMD,QAAQE,KAAK,CAAC;wBACnCC,YAAYnB;wBACZoB,gBAAgB;oBAClB;oBACA,IAAIH,SAASI,SAAS,KAAK,GAAG;wBAC5B,MAAML,QAAQM,MAAM,CAAC;4BACnBH,YAAYnB;4BACZuB,MAAM;gCACJC,kBAAkB;gCAClBC,0BAA0B;gCAC1BC,eAAeC,OAAOC,QAAQC,GAAG,CAACC,eAAe,KAAK;gCACtDC,qBAAqB;gCACrBC,sBAAsB,EAAE;4BAC1B;4BACAZ,gBAAgB;wBAClB;wBACAJ,QAAQiB,MAAM,CAACC,IAAI,CAAC;oBACtB;gBACF,EAAE,OAAOC,KAAK;oBACZnB,QAAQiB,MAAM,CAACG,IAAI,CAAC;wBAAED;oBAAI,GAAG;gBAC/B;gBACA,IAAI;oBACF,IAAInB,QAAQqB,EAAE,CAACC,IAAI,KAAK,YAAY;wBAClC,MAAMC,aAAavB,QAAQqB,EAAE;wBAY7B,MAAMA,KAAKE,WAAWC,UAAU,EAAEH;wBAClC,IAAIA,IAAI;4BACN,MAAMA,GACHlB,UAAU,CAAC,gBACXsB,WAAW,CAAC;gCAAEC,WAAW;4BAAE,GAAG;gCAAEC,YAAY;gCAAMC,oBAAoB;4BAAK;wBAChF;oBACF;gBACF,EAAE,OAAM;oBACN5B,QAAQiB,MAAM,CAACG,IAAI,CAAC;gBACtB;YACF;QACF;IACF;AACF,EAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/blobName.ts"],"sourcesContent":["const SEPARATOR = '---'\n\n/**\n * Last segment is `{collectionCount}-{timestampMs}` (new) or legacy `{timestampMs}` only.\n * A hyphenated tail that does not pass {@link isPlausibleNewFormatTail} is treated as legacy\n * (whole `last` string, no collection count) so older blobs are never mis-parsed.\n */\nconst NEW_NAME_TAIL = /^(\\d+)-(\\d{10,})$/\n\nconst NEW_TAIL_MAX_COLLECTIONS = 100_000\n\n/** Max length for a user-supplied manual backup label (after sanitization). */\nexport const BACKUP_LABEL_MAX_LENGTH = 64\n\nfunction isPlausibleNewFormatTail(collectionCount: number, timestampMs: number): boolean {\n return (\n Number.isFinite(collectionCount) &&\n Number.isFinite(timestampMs) &&\n collectionCount >= 0 &&\n collectionCount <= NEW_TAIL_MAX_COLLECTIONS &&\n timestampMs > 0 &&\n timestampMs < 1e15\n )\n}\n\n/**\n * Normalize a user-supplied manual backup label so it is safe to embed as a\n * blob-name segment. Trims, collapses whitespace, removes characters that would\n * confuse the separator, and enforces {@link BACKUP_LABEL_MAX_LENGTH}.\n * Returns an empty string when nothing usable remains.\n */\nexport function sanitizeBackupLabel(raw: unknown): string {\n if (typeof raw !== 'string') {return ''}\n const collapsed = raw.replace(/\\s+/g, ' ').trim()\n if (!collapsed) {return ''}\n // The blob-name separator is three hyphens; collapse any hyphen run in the\n // label to a single hyphen so round-trip parsing stays unambiguous.\n const deHyphen = collapsed.replace(/-{2,}/g, '-')\n return deHyphen.slice(0, BACKUP_LABEL_MAX_LENGTH).trim()\n}\n\nexport interface TransformBlobNameResult {\n collectionCount?: number\n date: string\n dbName: string\n fileType: 'json' | 'na' | 'tar.gz'\n hostname: string\n label?: string\n type: string\n}\n\nexport function transformBlobName(blobName: string): TransformBlobNameResult {\n const fileType: 'json' | 'na' | 'tar.gz'
|
|
1
|
+
{"version":3,"sources":["../../src/utils/blobName.ts"],"sourcesContent":["const SEPARATOR = '---'\n\n/**\n * Last segment is `{collectionCount}-{timestampMs}` (new) or legacy `{timestampMs}` only.\n * A hyphenated tail that does not pass {@link isPlausibleNewFormatTail} is treated as legacy\n * (whole `last` string, no collection count) so older blobs are never mis-parsed.\n */\nconst NEW_NAME_TAIL = /^(\\d+)-(\\d{10,})$/\n\nconst NEW_TAIL_MAX_COLLECTIONS = 100_000\n\n/** Max length for a user-supplied manual backup label (after sanitization). */\nexport const BACKUP_LABEL_MAX_LENGTH = 64\n\nfunction isPlausibleNewFormatTail(collectionCount: number, timestampMs: number): boolean {\n return (\n Number.isFinite(collectionCount) &&\n Number.isFinite(timestampMs) &&\n collectionCount >= 0 &&\n collectionCount <= NEW_TAIL_MAX_COLLECTIONS &&\n timestampMs > 0 &&\n timestampMs < 1e15\n )\n}\n\n/**\n * Normalize a user-supplied manual backup label so it is safe to embed as a\n * blob-name segment. Trims, collapses whitespace, removes characters that would\n * confuse the separator, and enforces {@link BACKUP_LABEL_MAX_LENGTH}.\n * Returns an empty string when nothing usable remains.\n */\nexport function sanitizeBackupLabel(raw: unknown): string {\n if (typeof raw !== 'string') {\n return ''\n }\n const collapsed = raw.replace(/\\s+/g, ' ').trim()\n if (!collapsed) {\n return ''\n }\n // The blob-name separator is three hyphens; collapse any hyphen run in the\n // label to a single hyphen so round-trip parsing stays unambiguous.\n const deHyphen = collapsed.replace(/-{2,}/g, '-')\n return deHyphen.slice(0, BACKUP_LABEL_MAX_LENGTH).trim()\n}\n\nexport interface TransformBlobNameResult {\n collectionCount?: number\n date: string\n dbName: string\n fileType: 'json' | 'na' | 'tar.gz'\n hostname: string\n label?: string\n type: string\n}\n\nexport function transformBlobName(blobName: string): TransformBlobNameResult {\n const fileType: 'json' | 'na' | 'tar.gz' =\n blobName.endsWith('json') ? 'json'\n : blobName.endsWith('tar.gz') ? 'tar.gz'\n : 'na'\n const [type = '', dbName = '', hostname = '', last = '', labelEncoded = ''] = blobName\n .replace(/\\.(?:json|tar\\.gz)$/, '')\n .replace(/^backups\\//, '')\n .split(SEPARATOR)\n\n let label: string | undefined\n if (labelEncoded) {\n try {\n const decoded = decodeURIComponent(labelEncoded)\n if (decoded) {\n label = decoded\n }\n } catch {\n label = labelEncoded\n }\n }\n\n const m = NEW_NAME_TAIL.exec(last)\n if (m) {\n const collectionCount = Number(m[1])\n const timestampMs = Number(m[2])\n if (isPlausibleNewFormatTail(collectionCount, timestampMs)) {\n return {\n type,\n collectionCount,\n date: m[2],\n dbName: decodeURIComponent(dbName),\n fileType,\n hostname: decodeURIComponent(hostname),\n label,\n }\n }\n }\n\n return {\n type,\n date: last,\n dbName: decodeURIComponent(dbName),\n fileType,\n hostname: decodeURIComponent(hostname),\n label,\n }\n}\n\n/**\n * Backup blob pathname segment (without `backups/` prefix or extension).\n * New format: `{collectionCount}-{timestampMs}` encodes included collection count + backup time.\n * When a non-empty `label` is supplied it is appended as a separate URL-encoded segment\n * (`---{encodedLabel}`) so older parsers that only read the first 4 segments still work.\n */\nexport function createBlobName(\n type: string,\n dbName: string,\n hostname: string,\n collectionCount: number,\n timestampMs: number,\n fileType: 'json' | 'tar.gz',\n label?: string,\n): string {\n const tail = `${Math.max(0, Math.floor(collectionCount))}-${Math.floor(timestampMs)}`\n const sanitizedLabel = sanitizeBackupLabel(label)\n const labelSegment = sanitizedLabel ? `${SEPARATOR}${encodeURIComponent(sanitizedLabel)}` : ''\n return `${type}${SEPARATOR}${encodeURIComponent(dbName)}${SEPARATOR}${encodeURIComponent(hostname)}${SEPARATOR}${tail}${labelSegment}.${fileType}`\n}\n\n/**\n * Prefer timestamp encoded in filename (stable across blob store migration); else blob upload time.\n * Legacy tails are digits-only epoch values; if {@link TransformBlobNameResult.collectionCount}\n * is absent, values below 1e12 are treated as **Unix seconds** (common older convention), else ms.\n */\nexport function getBackupSortTimeMs(parsed: TransformBlobNameResult, uploadedAt: Date): number {\n if (!parsed.date || !/^\\d+$/.test(parsed.date)) {\n return uploadedAt.getTime()\n }\n const n = Number(parsed.date)\n if (!Number.isFinite(n)) {\n return uploadedAt.getTime()\n }\n const isLegacyTimestampOnly = parsed.collectionCount === undefined\n if (isLegacyTimestampOnly && n < 1e12) {\n return n * 1000\n }\n return n\n}\n"],"names":["SEPARATOR","NEW_NAME_TAIL","NEW_TAIL_MAX_COLLECTIONS","BACKUP_LABEL_MAX_LENGTH","isPlausibleNewFormatTail","collectionCount","timestampMs","Number","isFinite","sanitizeBackupLabel","raw","collapsed","replace","trim","deHyphen","slice","transformBlobName","blobName","fileType","endsWith","type","dbName","hostname","last","labelEncoded","split","label","decoded","decodeURIComponent","m","exec","date","createBlobName","tail","Math","max","floor","sanitizedLabel","labelSegment","encodeURIComponent","getBackupSortTimeMs","parsed","uploadedAt","test","getTime","n","isLegacyTimestampOnly","undefined"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/dashboardRoleAccess.ts"],"sourcesContent":["/**\n * Resolves which users may see/use the backup dashboard when no custom `access`\n * option is passed to the plugin. Driven by the `PAYLOAD_BACKUP_ALLOWED_ROLES`\n * environment variable.\n *\n * Syntax:\n * PAYLOAD_BACKUP_ALLOWED_ROLES=admin,editor // any user whose roles[].slug matches\n * PAYLOAD_BACKUP_ALLOWED_ROLES=* // any authenticated user\n * (unset / empty) // backwards-compatible default:\n * // - roles present -> require `admin`\n * // - no roles field -> allow\n */\n\nexport const BACKUP_ALLOWED_ROLES_ENV = 'PAYLOAD_BACKUP_ALLOWED_ROLES'\n\n/** Parses the env var value into a normalized list of role slugs (lower-cased, trimmed). */\nexport function parseAllowedRolesEnv(raw: string | undefined): string[] {\n if (!raw) {return []}\n return raw\n .split(',')\n .map((s) => s.trim().toLowerCase())\n .filter((s) => s.length > 0)\n}\n\nfunction extractRoleSlugs(user: Record<string, unknown>): null | string[] {\n const roles = user.roles as Array<{ slug?: unknown } | string> | undefined\n if (!Array.isArray(roles)) {return null}\n const slugs: string[] = []\n for (const entry of roles) {\n if (typeof entry === 'string') {\n slugs.push(entry.toLowerCase())\n } else if (entry && typeof entry === 'object' && typeof entry.slug === 'string') {\n slugs.push(entry.slug.toLowerCase())\n }\n }\n return slugs\n}\n\n/**\n * Returns true when the authenticated user is allowed to view the backup dashboard based\n * on the `PAYLOAD_BACKUP_ALLOWED_ROLES` env var. Unauthenticated users always return false.\n *\n * Exported for unit testing and for advanced consumers that want to reuse the same check\n * (e.g. wrap it inside their own `access` option).\n */\nexport function isUserAllowedByEnvRoles(\n user: null | Record<string, unknown>,\n envValue: string | undefined = process.env[BACKUP_ALLOWED_ROLES_ENV],\n): boolean {\n if (!user) {return false}\n const allowed = parseAllowedRolesEnv(envValue)\n const userRoles = extractRoleSlugs(user)\n\n // Env var not set -> backwards-compatible default.\n if (allowed.length === 0) {\n if (userRoles === null || userRoles.length === 0) {return true}\n return userRoles.includes('admin')\n }\n\n // Wildcard -> any authenticated user.\n if (allowed.includes('*')) {return true}\n\n // Explicit allow-list requires the user to actually expose roles that match.\n if (!userRoles || userRoles.length === 0) {return false}\n return userRoles.some((slug) => allowed.includes(slug))\n}\n"],"names":["BACKUP_ALLOWED_ROLES_ENV","parseAllowedRolesEnv","raw","split","map","s","trim","toLowerCase","filter","length","extractRoleSlugs","user","roles","Array","isArray","slugs","entry","push","slug","isUserAllowedByEnvRoles","envValue","process","env","allowed","userRoles","includes","some"],"mappings":"AAAA;;;;;;;;;;;CAWC,GAED,OAAO,MAAMA,2BAA2B,+BAA8B;AAEtE,0FAA0F,GAC1F,OAAO,SAASC,qBAAqBC,GAAuB;IAC1D,IAAI,CAACA,KAAK;
|
|
1
|
+
{"version":3,"sources":["../../src/utils/dashboardRoleAccess.ts"],"sourcesContent":["/**\n * Resolves which users may see/use the backup dashboard when no custom `access`\n * option is passed to the plugin. Driven by the `PAYLOAD_BACKUP_ALLOWED_ROLES`\n * environment variable.\n *\n * Syntax:\n * PAYLOAD_BACKUP_ALLOWED_ROLES=admin,editor // any user whose roles[].slug matches\n * PAYLOAD_BACKUP_ALLOWED_ROLES=* // any authenticated user\n * (unset / empty) // backwards-compatible default:\n * // - roles present -> require `admin`\n * // - no roles field -> allow\n */\n\nexport const BACKUP_ALLOWED_ROLES_ENV = 'PAYLOAD_BACKUP_ALLOWED_ROLES'\n\n/** Parses the env var value into a normalized list of role slugs (lower-cased, trimmed). */\nexport function parseAllowedRolesEnv(raw: string | undefined): string[] {\n if (!raw) {\n return []\n }\n return raw\n .split(',')\n .map((s) => s.trim().toLowerCase())\n .filter((s) => s.length > 0)\n}\n\nfunction extractRoleSlugs(user: Record<string, unknown>): null | string[] {\n const roles = user.roles as Array<{ slug?: unknown } | string> | undefined\n if (!Array.isArray(roles)) {\n return null\n }\n const slugs: string[] = []\n for (const entry of roles) {\n if (typeof entry === 'string') {\n slugs.push(entry.toLowerCase())\n } else if (entry && typeof entry === 'object' && typeof entry.slug === 'string') {\n slugs.push(entry.slug.toLowerCase())\n }\n }\n return slugs\n}\n\n/**\n * Returns true when the authenticated user is allowed to view the backup dashboard based\n * on the `PAYLOAD_BACKUP_ALLOWED_ROLES` env var. Unauthenticated users always return false.\n *\n * Exported for unit testing and for advanced consumers that want to reuse the same check\n * (e.g. wrap it inside their own `access` option).\n */\nexport function isUserAllowedByEnvRoles(\n user: null | Record<string, unknown>,\n envValue: string | undefined = process.env[BACKUP_ALLOWED_ROLES_ENV],\n): boolean {\n if (!user) {\n return false\n }\n const allowed = parseAllowedRolesEnv(envValue)\n const userRoles = extractRoleSlugs(user)\n\n // Env var not set -> backwards-compatible default.\n if (allowed.length === 0) {\n if (userRoles === null || userRoles.length === 0) {\n return true\n }\n return userRoles.includes('admin')\n }\n\n // Wildcard -> any authenticated user.\n if (allowed.includes('*')) {\n return true\n }\n\n // Explicit allow-list requires the user to actually expose roles that match.\n if (!userRoles || userRoles.length === 0) {\n return false\n }\n return userRoles.some((slug) => allowed.includes(slug))\n}\n"],"names":["BACKUP_ALLOWED_ROLES_ENV","parseAllowedRolesEnv","raw","split","map","s","trim","toLowerCase","filter","length","extractRoleSlugs","user","roles","Array","isArray","slugs","entry","push","slug","isUserAllowedByEnvRoles","envValue","process","env","allowed","userRoles","includes","some"],"mappings":"AAAA;;;;;;;;;;;CAWC,GAED,OAAO,MAAMA,2BAA2B,+BAA8B;AAEtE,0FAA0F,GAC1F,OAAO,SAASC,qBAAqBC,GAAuB;IAC1D,IAAI,CAACA,KAAK;QACR,OAAO,EAAE;IACX;IACA,OAAOA,IACJC,KAAK,CAAC,KACNC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,GAAGC,WAAW,IAC/BC,MAAM,CAAC,CAACH,IAAMA,EAAEI,MAAM,GAAG;AAC9B;AAEA,SAASC,iBAAiBC,IAA6B;IACrD,MAAMC,QAAQD,KAAKC,KAAK;IACxB,IAAI,CAACC,MAAMC,OAAO,CAACF,QAAQ;QACzB,OAAO;IACT;IACA,MAAMG,QAAkB,EAAE;IAC1B,KAAK,MAAMC,SAASJ,MAAO;QACzB,IAAI,OAAOI,UAAU,UAAU;YAC7BD,MAAME,IAAI,CAACD,MAAMT,WAAW;QAC9B,OAAO,IAAIS,SAAS,OAAOA,UAAU,YAAY,OAAOA,MAAME,IAAI,KAAK,UAAU;YAC/EH,MAAME,IAAI,CAACD,MAAME,IAAI,CAACX,WAAW;QACnC;IACF;IACA,OAAOQ;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASI,wBACdR,IAAoC,EACpCS,WAA+BC,QAAQC,GAAG,CAACtB,yBAAyB;IAEpE,IAAI,CAACW,MAAM;QACT,OAAO;IACT;IACA,MAAMY,UAAUtB,qBAAqBmB;IACrC,MAAMI,YAAYd,iBAAiBC;IAEnC,mDAAmD;IACnD,IAAIY,QAAQd,MAAM,KAAK,GAAG;QACxB,IAAIe,cAAc,QAAQA,UAAUf,MAAM,KAAK,GAAG;YAChD,OAAO;QACT;QACA,OAAOe,UAAUC,QAAQ,CAAC;IAC5B;IAEA,sCAAsC;IACtC,IAAIF,QAAQE,QAAQ,CAAC,MAAM;QACzB,OAAO;IACT;IAEA,6EAA6E;IAC7E,IAAI,CAACD,aAAaA,UAAUf,MAAM,KAAK,GAAG;QACxC,OAAO;IACT;IACA,OAAOe,UAAUE,IAAI,CAAC,CAACR,OAASK,QAAQE,QAAQ,CAACP;AACnD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/dialogBackdrop.ts"],"sourcesContent":["import type { MouseEvent, PointerEvent, RefObject } from 'react'\n\n/** Mouse or pointer event with viewport coordinates (native {@code <dialog>} backdrop close). */\nexport type DialogBackdropPointer =\n | Pick<MouseEvent<HTMLDialogElement>, 'clientX' | 'clientY'>\n | Pick<PointerEvent<HTMLDialogElement>, 'clientX' | 'clientY'>\n\n/**\n * Closes a native {@code <dialog>} when the pointer event is outside the dialog panel\n * (on the dimmed backdrop). Modal dialogs only expose the panel in {@code getBoundingClientRect()},\n * not the full viewport overlay.\n */\nexport function closeNativeDialogOnBackdropPointer(\n event: DialogBackdropPointer,\n dialogRef: RefObject<HTMLDialogElement | null>,\n): void {\n const el = dialogRef.current\n if (!el) {return}\n const r = el.getBoundingClientRect()\n const { clientX: x, clientY: y } = event\n if (x < r.left || x > r.right || y < r.top || y > r.bottom) {\n el.close()\n }\n}\n"],"names":["closeNativeDialogOnBackdropPointer","event","dialogRef","el","current","r","getBoundingClientRect","clientX","x","clientY","y","left","right","top","bottom","close"],"mappings":"AAOA;;;;CAIC,GACD,OAAO,SAASA,mCACdC,KAA4B,EAC5BC,SAA8C;IAE9C,MAAMC,KAAKD,UAAUE,OAAO;IAC5B,IAAI,CAACD,IAAI;
|
|
1
|
+
{"version":3,"sources":["../../src/utils/dialogBackdrop.ts"],"sourcesContent":["import type { MouseEvent, PointerEvent, RefObject } from 'react'\n\n/** Mouse or pointer event with viewport coordinates (native {@code <dialog>} backdrop close). */\nexport type DialogBackdropPointer =\n | Pick<MouseEvent<HTMLDialogElement>, 'clientX' | 'clientY'>\n | Pick<PointerEvent<HTMLDialogElement>, 'clientX' | 'clientY'>\n\n/**\n * Closes a native {@code <dialog>} when the pointer event is outside the dialog panel\n * (on the dimmed backdrop). Modal dialogs only expose the panel in {@code getBoundingClientRect()},\n * not the full viewport overlay.\n */\nexport function closeNativeDialogOnBackdropPointer(\n event: DialogBackdropPointer,\n dialogRef: RefObject<HTMLDialogElement | null>,\n): void {\n const el = dialogRef.current\n if (!el) {\n return\n }\n const r = el.getBoundingClientRect()\n const { clientX: x, clientY: y } = event\n if (x < r.left || x > r.right || y < r.top || y > r.bottom) {\n el.close()\n }\n}\n"],"names":["closeNativeDialogOnBackdropPointer","event","dialogRef","el","current","r","getBoundingClientRect","clientX","x","clientY","y","left","right","top","bottom","close"],"mappings":"AAOA;;;;CAIC,GACD,OAAO,SAASA,mCACdC,KAA4B,EAC5BC,SAA8C;IAE9C,MAAMC,KAAKD,UAAUE,OAAO;IAC5B,IAAI,CAACD,IAAI;QACP;IACF;IACA,MAAME,IAAIF,GAAGG,qBAAqB;IAClC,MAAM,EAAEC,SAASC,CAAC,EAAEC,SAASC,CAAC,EAAE,GAAGT;IACnC,IAAIO,IAAIH,EAAEM,IAAI,IAAIH,IAAIH,EAAEO,KAAK,IAAIF,IAAIL,EAAEQ,GAAG,IAAIH,IAAIL,EAAES,MAAM,EAAE;QAC1DX,GAAGY,KAAK;IACV;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/hostname.ts"],"sourcesContent":["export function getCurrentDbName(): string {\n try {\n const { hostname, pathname } = new URL(process.env.MONGODB_URI!)\n return hostname + pathname\n } catch {\n return 'none'\n }\n}\n\nexport function getCurrentHostname(): string {\n try {\n return process.env.NEXT_PUBLIC_SERVER_URL
|
|
1
|
+
{"version":3,"sources":["../../src/utils/hostname.ts"],"sourcesContent":["export function getCurrentDbName(): string {\n try {\n const { hostname, pathname } = new URL(process.env.MONGODB_URI!)\n return hostname + pathname\n } catch {\n return 'none'\n }\n}\n\nexport function getCurrentHostname(): string {\n try {\n return process.env.NEXT_PUBLIC_SERVER_URL ?\n new URL(process.env.NEXT_PUBLIC_SERVER_URL).hostname\n : (process.env.VERCEL_URL ?? 'none')\n } catch {\n return process.env.VERCEL_URL! || 'none'\n }\n}\n"],"names":["getCurrentDbName","hostname","pathname","URL","process","env","MONGODB_URI","getCurrentHostname","NEXT_PUBLIC_SERVER_URL","VERCEL_URL"],"mappings":"AAAA,OAAO,SAASA;IACd,IAAI;QACF,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAG,IAAIC,IAAIC,QAAQC,GAAG,CAACC,WAAW;QAC9D,OAAOL,WAAWC;IACpB,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA,OAAO,SAASK;IACd,IAAI;QACF,OAAOH,QAAQC,GAAG,CAACG,sBAAsB,GACrC,IAAIL,IAAIC,QAAQC,GAAG,CAACG,sBAAsB,EAAEP,QAAQ,GACnDG,QAAQC,GAAG,CAACI,UAAU,IAAI;IACjC,EAAE,OAAM;QACN,OAAOL,QAAQC,GAAG,CAACI,UAAU,IAAK;IACpC;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/maskBlobToken.ts"],"sourcesContent":["/**\n * Mask a Vercel Blob read/write token for display in the admin UI (never send the full secret).\n * Shape: prefix + fixed-length asterisk run + suffix (length of real token does not change star count).\n * Example: vercel_blob_rw_yfn + 23×* + bsM7\n */\nexport function maskBlobReadWriteToken(raw: string): string {\n const t = raw.trim()\n if (t.length === 0) {return ''}\n\n /** Fixed number of asterisks between prefix and suffix (UI stays compact for any token length). */\n const middleRun = 32\n\n const prefixLen = Math.min(18, Math.max(8, Math.floor(t.length * 0.42)))\n const suffixLen = Math.min(4, Math.max(3, Math.floor(t.length * 0.12)))\n\n if (t.length <= prefixLen + suffixLen + 6) {\n const headLen = Math.min(6, t.length)\n const head = t.slice(0, headLen)\n const tailLen = Math.min(3, Math.max(0, t.length - headLen))\n const tail = tailLen > 0 ? t.slice(-tailLen) : ''\n const middle = '*'.repeat(middleRun)\n return tail ? `${head}${middle}${tail}` : `${head}${middle}`\n }\n\n const prefix = t.slice(0, prefixLen)\n const suffix = t.slice(-suffixLen)\n return `${prefix}${'*'.repeat(middleRun)}${suffix}`\n}\n\n/**\n * When a token is already stored, treat the client value as \"do not change the stored token\" if\n * they left the masked placeholder or cleared the field while editing other settings.\n */\nexport function shouldPreserveBackupBlobTokenField(\n clientValue: string,\n hadStoredToken: boolean,\n): boolean {\n if (!hadStoredToken) {return false}\n const v = clientValue.trim()\n if (v.length === 0) {return true}\n if (/\\*{2,}/.test(v)) {return true}\n return false\n}\n"],"names":["maskBlobReadWriteToken","raw","t","trim","length","middleRun","prefixLen","Math","min","max","floor","suffixLen","headLen","head","slice","tailLen","tail","middle","repeat","prefix","suffix","shouldPreserveBackupBlobTokenField","clientValue","hadStoredToken","v","test"],"mappings":"AAAA;;;;CAIC,GACD,OAAO,SAASA,uBAAuBC,GAAW;IAChD,MAAMC,IAAID,IAAIE,IAAI;IAClB,IAAID,EAAEE,MAAM,KAAK,GAAG;
|
|
1
|
+
{"version":3,"sources":["../../src/utils/maskBlobToken.ts"],"sourcesContent":["/**\n * Mask a Vercel Blob read/write token for display in the admin UI (never send the full secret).\n * Shape: prefix + fixed-length asterisk run + suffix (length of real token does not change star count).\n * Example: vercel_blob_rw_yfn + 23×* + bsM7\n */\nexport function maskBlobReadWriteToken(raw: string): string {\n const t = raw.trim()\n if (t.length === 0) {\n return ''\n }\n\n /** Fixed number of asterisks between prefix and suffix (UI stays compact for any token length). */\n const middleRun = 32\n\n const prefixLen = Math.min(18, Math.max(8, Math.floor(t.length * 0.42)))\n const suffixLen = Math.min(4, Math.max(3, Math.floor(t.length * 0.12)))\n\n if (t.length <= prefixLen + suffixLen + 6) {\n const headLen = Math.min(6, t.length)\n const head = t.slice(0, headLen)\n const tailLen = Math.min(3, Math.max(0, t.length - headLen))\n const tail = tailLen > 0 ? t.slice(-tailLen) : ''\n const middle = '*'.repeat(middleRun)\n return tail ? `${head}${middle}${tail}` : `${head}${middle}`\n }\n\n const prefix = t.slice(0, prefixLen)\n const suffix = t.slice(-suffixLen)\n return `${prefix}${'*'.repeat(middleRun)}${suffix}`\n}\n\n/**\n * When a token is already stored, treat the client value as \"do not change the stored token\" if\n * they left the masked placeholder or cleared the field while editing other settings.\n */\nexport function shouldPreserveBackupBlobTokenField(\n clientValue: string,\n hadStoredToken: boolean,\n): boolean {\n if (!hadStoredToken) {\n return false\n }\n const v = clientValue.trim()\n if (v.length === 0) {\n return true\n }\n if (/\\*{2,}/.test(v)) {\n return true\n }\n return false\n}\n"],"names":["maskBlobReadWriteToken","raw","t","trim","length","middleRun","prefixLen","Math","min","max","floor","suffixLen","headLen","head","slice","tailLen","tail","middle","repeat","prefix","suffix","shouldPreserveBackupBlobTokenField","clientValue","hadStoredToken","v","test"],"mappings":"AAAA;;;;CAIC,GACD,OAAO,SAASA,uBAAuBC,GAAW;IAChD,MAAMC,IAAID,IAAIE,IAAI;IAClB,IAAID,EAAEE,MAAM,KAAK,GAAG;QAClB,OAAO;IACT;IAEA,iGAAiG,GACjG,MAAMC,YAAY;IAElB,MAAMC,YAAYC,KAAKC,GAAG,CAAC,IAAID,KAAKE,GAAG,CAAC,GAAGF,KAAKG,KAAK,CAACR,EAAEE,MAAM,GAAG;IACjE,MAAMO,YAAYJ,KAAKC,GAAG,CAAC,GAAGD,KAAKE,GAAG,CAAC,GAAGF,KAAKG,KAAK,CAACR,EAAEE,MAAM,GAAG;IAEhE,IAAIF,EAAEE,MAAM,IAAIE,YAAYK,YAAY,GAAG;QACzC,MAAMC,UAAUL,KAAKC,GAAG,CAAC,GAAGN,EAAEE,MAAM;QACpC,MAAMS,OAAOX,EAAEY,KAAK,CAAC,GAAGF;QACxB,MAAMG,UAAUR,KAAKC,GAAG,CAAC,GAAGD,KAAKE,GAAG,CAAC,GAAGP,EAAEE,MAAM,GAAGQ;QACnD,MAAMI,OAAOD,UAAU,IAAIb,EAAEY,KAAK,CAAC,CAACC,WAAW;QAC/C,MAAME,SAAS,IAAIC,MAAM,CAACb;QAC1B,OAAOW,OAAO,GAAGH,OAAOI,SAASD,MAAM,GAAG,GAAGH,OAAOI,QAAQ;IAC9D;IAEA,MAAME,SAASjB,EAAEY,KAAK,CAAC,GAAGR;IAC1B,MAAMc,SAASlB,EAAEY,KAAK,CAAC,CAACH;IACxB,OAAO,GAAGQ,SAAS,IAAID,MAAM,CAACb,aAAae,QAAQ;AACrD;AAEA;;;CAGC,GACD,OAAO,SAASC,mCACdC,WAAmB,EACnBC,cAAuB;IAEvB,IAAI,CAACA,gBAAgB;QACnB,OAAO;IACT;IACA,MAAMC,IAAIF,YAAYnB,IAAI;IAC1B,IAAIqB,EAAEpB,MAAM,KAAK,GAAG;QAClB,OAAO;IACT;IACA,IAAI,SAASqB,IAAI,CAACD,IAAI;QACpB,OAAO;IACT;IACA,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trieb.work/payload-plugin-backup-mongodb",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Payload CMS plugin for MongoDB backup and recovery via Vercel Blob Storage",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"payload",
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"open": "^10.1.0",
|
|
106
106
|
"payload": "3.82.1",
|
|
107
107
|
"prettier": "^3.4.2",
|
|
108
|
+
"prettier-plugin-packagejson": "3.0.2",
|
|
108
109
|
"qs-esm": "8.0.1",
|
|
109
110
|
"react": "19.2.4",
|
|
110
111
|
"react-dom": "19.2.4",
|
|
@@ -149,6 +150,8 @@
|
|
|
149
150
|
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
150
151
|
"dev:turbo": "node ./scripts/dev.mjs --turbo",
|
|
151
152
|
"dev:watch-css": "node ./scripts/watch-backup-css.mjs",
|
|
153
|
+
"format": "prettier --write . --cache --log-level warn",
|
|
154
|
+
"format:check": "prettier --check . --cache --log-level warn",
|
|
152
155
|
"generate:importmap": "pnpm dev:generate-importmap",
|
|
153
156
|
"generate:types": "pnpm dev:generate-types",
|
|
154
157
|
"lint": "eslint \"src/**/*.{ts,tsx}\" \"dev/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
|