@trieb.work/payload-plugin-backup-mongodb 0.1.5 → 0.2.0

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.
Files changed (97) hide show
  1. package/README.md +260 -90
  2. package/dist/backup-dashboard.css +18 -1
  3. package/dist/collections/BackupTasks.js +8 -1
  4. package/dist/collections/BackupTasks.js.map +1 -1
  5. package/dist/components/BackupDashboard/BackupListCollapsible.client.js.map +1 -1
  6. package/dist/components/BackupDashboard/BackupSettingsModal.client.js +203 -115
  7. package/dist/components/BackupDashboard/BackupSettingsModal.client.js.map +1 -1
  8. package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.js.map +1 -1
  9. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js +1 -1
  10. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js.map +1 -1
  11. package/dist/components/BackupDashboard/TaskActionButton.client.d.ts +1 -1
  12. package/dist/components/BackupDashboard/TaskActionButton.client.js +59 -26
  13. package/dist/components/BackupDashboard/TaskActionButton.client.js.map +1 -1
  14. package/dist/components/BackupDashboard/backupDashboardInlineCss.js +1 -1
  15. package/dist/components/BackupDashboard/backupDashboardInlineCss.js.map +1 -1
  16. package/dist/components/BackupDashboard/index.js +27 -3
  17. package/dist/components/BackupDashboard/index.js.map +1 -1
  18. package/dist/core/archive.js.map +1 -1
  19. package/dist/core/backup.d.ts +4 -3
  20. package/dist/core/backup.js +50 -43
  21. package/dist/core/backup.js.map +1 -1
  22. package/dist/core/backupArchiveRead.d.ts +15 -0
  23. package/dist/core/backupArchiveRead.js +30 -0
  24. package/dist/core/backupArchiveRead.js.map +1 -0
  25. package/dist/core/backupBlobIO.js.map +1 -1
  26. package/dist/core/backupSettings.js +5 -0
  27. package/dist/core/backupSettings.js.map +1 -1
  28. package/dist/core/blobTokenValidate.js.map +1 -1
  29. package/dist/core/restore.d.ts +5 -2
  30. package/dist/core/restore.js +133 -89
  31. package/dist/core/restore.js.map +1 -1
  32. package/dist/core/restorePreview.d.ts +12 -3
  33. package/dist/core/restorePreview.js +17 -13
  34. package/dist/core/restorePreview.js.map +1 -1
  35. package/dist/core/restoreResult.d.ts +34 -0
  36. package/dist/core/restoreResult.js +45 -0
  37. package/dist/core/restoreResult.js.map +1 -0
  38. package/dist/core/storage/config.d.ts +31 -0
  39. package/dist/core/storage/config.js +60 -0
  40. package/dist/core/storage/config.js.map +1 -0
  41. package/dist/core/storage/index.d.ts +23 -0
  42. package/dist/core/storage/index.js +25 -0
  43. package/dist/core/storage/index.js.map +1 -0
  44. package/dist/core/storage/s3.d.ts +35 -0
  45. package/dist/core/storage/s3.js +200 -0
  46. package/dist/core/storage/s3.js.map +1 -0
  47. package/dist/core/storage/types.d.ts +60 -0
  48. package/dist/core/storage/types.js +6 -0
  49. package/dist/core/storage/types.js.map +1 -0
  50. package/dist/core/storage/vercelBlob.d.ts +25 -0
  51. package/dist/core/storage/vercelBlob.js +53 -0
  52. package/dist/core/storage/vercelBlob.js.map +1 -0
  53. package/dist/core/taskProgress.d.ts +10 -4
  54. package/dist/core/taskProgress.js +11 -2
  55. package/dist/core/taskProgress.js.map +1 -1
  56. package/dist/core/vercelBackupCron.js.map +1 -1
  57. package/dist/endpoints/index.js +0 -5
  58. package/dist/endpoints/index.js.map +1 -1
  59. package/dist/endpoints/paths/admin-backup-download.js +8 -7
  60. package/dist/endpoints/paths/admin-backup-download.js.map +1 -1
  61. package/dist/endpoints/paths/admin-delete.js +11 -5
  62. package/dist/endpoints/paths/admin-delete.js.map +1 -1
  63. package/dist/endpoints/paths/admin-manual.js +2 -1
  64. package/dist/endpoints/paths/admin-manual.js.map +1 -1
  65. package/dist/endpoints/paths/admin-preview.js +9 -2
  66. package/dist/endpoints/paths/admin-preview.js.map +1 -1
  67. package/dist/endpoints/paths/admin-restore.js +7 -4
  68. package/dist/endpoints/paths/admin-restore.js.map +1 -1
  69. package/dist/endpoints/paths/admin-settings.js +35 -2
  70. package/dist/endpoints/paths/admin-settings.js.map +1 -1
  71. package/dist/endpoints/paths/cron-list.js +2 -1
  72. package/dist/endpoints/paths/cron-list.js.map +1 -1
  73. package/dist/endpoints/paths/cron-restore.js +14 -4
  74. package/dist/endpoints/paths/cron-restore.js.map +1 -1
  75. package/dist/endpoints/paths/cron-run.js +2 -1
  76. package/dist/endpoints/paths/cron-run.js.map +1 -1
  77. package/dist/endpoints/shared.d.ts +0 -1
  78. package/dist/endpoints/shared.js +0 -6
  79. package/dist/endpoints/shared.js.map +1 -1
  80. package/dist/index.d.ts +5 -3
  81. package/dist/index.js +3 -2
  82. package/dist/index.js.map +1 -1
  83. package/dist/plugin.js.map +1 -1
  84. package/dist/publicApiPaths.d.ts +0 -1
  85. package/dist/publicApiPaths.js +0 -1
  86. package/dist/publicApiPaths.js.map +1 -1
  87. package/dist/types.d.ts +0 -5
  88. package/dist/types.js.map +1 -1
  89. package/dist/utils/blobName.js.map +1 -1
  90. package/dist/utils/dashboardRoleAccess.js.map +1 -1
  91. package/dist/utils/dialogBackdrop.js.map +1 -1
  92. package/dist/utils/hostname.js.map +1 -1
  93. package/dist/utils/maskBlobToken.js.map +1 -1
  94. package/package.json +25 -4
  95. package/dist/endpoints/paths/admin-seed.d.ts +0 -3
  96. package/dist/endpoints/paths/admin-seed.js +0 -47
  97. package/dist/endpoints/paths/admin-seed.js.map +0 -1
@@ -0,0 +1,45 @@
1
+ export function buildRestoreWarnings(result) {
2
+ const warnings = [];
3
+ for (const collection of result.collections){
4
+ if (collection.kind === 'failed') {
5
+ warnings.push(`Collection "${collection.name}": ${collection.error}`);
6
+ }
7
+ }
8
+ if (result.media && !result.media.skippedByOption) {
9
+ for (const failed of result.media.failed){
10
+ warnings.push(`Media "${failed.name}": ${failed.error}`);
11
+ }
12
+ if (result.media.accessFallbackCount > 0) {
13
+ warnings.push(`${result.media.accessFallbackCount} media file(s) uploaded with a fallback blob access level`);
14
+ }
15
+ }
16
+ return warnings;
17
+ }
18
+ export function formatRestoreSummary(result) {
19
+ const restored = result.collections.filter((c)=>c.kind === 'restored');
20
+ const docTotal = restored.reduce((sum, c)=>sum + (c.kind === 'restored' ? c.docCount : 0), 0);
21
+ const skipped = result.collections.filter((c)=>c.kind === 'skipped');
22
+ const failedCollections = result.collections.filter((c)=>c.kind === 'failed');
23
+ const parts = [
24
+ `Restored ${restored.length} collection${restored.length === 1 ? '' : 's'} (${docTotal} document${docTotal === 1 ? '' : 's'})`
25
+ ];
26
+ if (skipped.length > 0) {
27
+ parts.push(`skipped ${skipped.length} collection${skipped.length === 1 ? '' : 's'} (blacklist or empty)`);
28
+ }
29
+ if (result.media?.skippedByOption) {
30
+ parts.push('archive media not restored (option disabled)');
31
+ } else if (result.media) {
32
+ parts.push(`media ${result.media.restored}/${result.media.attempted} file${result.media.attempted === 1 ? '' : 's'}`);
33
+ }
34
+ const failedMedia = result.media?.failed.length ?? 0;
35
+ const errorCount = failedCollections.length + failedMedia;
36
+ if (errorCount > 0) {
37
+ parts.push(`${errorCount} error${errorCount === 1 ? '' : 's'} — see warnings`);
38
+ }
39
+ return parts.join('; ');
40
+ }
41
+ export function restoreTaskStatus(result) {
42
+ return buildRestoreWarnings(result).length > 0 ? 'completed_with_warnings' : 'completed';
43
+ }
44
+
45
+ //# sourceMappingURL=restoreResult.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/restoreResult.ts"],"sourcesContent":["export type RestoreArchiveKind = 'json' | 'tar-gzip'\n\nexport type RestoreCollectionOutcome =\n | { docCount: number; kind: 'restored'; name: string }\n | { error: string; kind: 'failed'; name: string }\n | { kind: 'skipped'; name: string; reason: 'blacklist' | 'empty' }\n\nexport type RestoreMediaOutcome = {\n accessFallbackCount: number\n attempted: number\n failed: Array<{ error: string; name: string }>\n restored: number\n skippedByOption: boolean\n}\n\n/** Structured outcome of a restore run (returned to endpoints and task progress). */\nexport type RestoreBackupResult = {\n archiveKind: RestoreArchiveKind\n collections: RestoreCollectionOutcome[]\n durationMs: number\n media: null | RestoreMediaOutcome\n}\n\nexport function buildRestoreWarnings(result: RestoreBackupResult): string[] {\n const warnings: string[] = []\n\n for (const collection of result.collections) {\n if (collection.kind === 'failed') {\n warnings.push(`Collection \"${collection.name}\": ${collection.error}`)\n }\n }\n\n if (result.media && !result.media.skippedByOption) {\n for (const failed of result.media.failed) {\n warnings.push(`Media \"${failed.name}\": ${failed.error}`)\n }\n if (result.media.accessFallbackCount > 0) {\n warnings.push(\n `${result.media.accessFallbackCount} media file(s) uploaded with a fallback blob access level`,\n )\n }\n }\n\n return warnings\n}\n\nexport function formatRestoreSummary(result: RestoreBackupResult): string {\n const restored = result.collections.filter((c) => c.kind === 'restored')\n const docTotal = restored.reduce((sum, c) => sum + (c.kind === 'restored' ? c.docCount : 0), 0)\n const skipped = result.collections.filter((c) => c.kind === 'skipped')\n const failedCollections = result.collections.filter((c) => c.kind === 'failed')\n\n const parts: string[] = [\n `Restored ${restored.length} collection${restored.length === 1 ? '' : 's'} (${docTotal} document${docTotal === 1 ? '' : 's'})`,\n ]\n\n if (skipped.length > 0) {\n parts.push(\n `skipped ${skipped.length} collection${skipped.length === 1 ? '' : 's'} (blacklist or empty)`,\n )\n }\n\n if (result.media?.skippedByOption) {\n parts.push('archive media not restored (option disabled)')\n } else if (result.media) {\n parts.push(\n `media ${result.media.restored}/${result.media.attempted} file${result.media.attempted === 1 ? '' : 's'}`,\n )\n }\n\n const failedMedia = result.media?.failed.length ?? 0\n const errorCount = failedCollections.length + failedMedia\n if (errorCount > 0) {\n parts.push(`${errorCount} error${errorCount === 1 ? '' : 's'} — see warnings`)\n }\n\n return parts.join('; ')\n}\n\nexport function restoreTaskStatus(\n result: RestoreBackupResult,\n): 'completed' | 'completed_with_warnings' {\n return buildRestoreWarnings(result).length > 0 ? 'completed_with_warnings' : 'completed'\n}\n"],"names":["buildRestoreWarnings","result","warnings","collection","collections","kind","push","name","error","media","skippedByOption","failed","accessFallbackCount","formatRestoreSummary","restored","filter","c","docTotal","reduce","sum","docCount","skipped","failedCollections","parts","length","attempted","failedMedia","errorCount","join","restoreTaskStatus"],"mappings":"AAuBA,OAAO,SAASA,qBAAqBC,MAA2B;IAC9D,MAAMC,WAAqB,EAAE;IAE7B,KAAK,MAAMC,cAAcF,OAAOG,WAAW,CAAE;QAC3C,IAAID,WAAWE,IAAI,KAAK,UAAU;YAChCH,SAASI,IAAI,CAAC,CAAC,YAAY,EAAEH,WAAWI,IAAI,CAAC,GAAG,EAAEJ,WAAWK,KAAK,EAAE;QACtE;IACF;IAEA,IAAIP,OAAOQ,KAAK,IAAI,CAACR,OAAOQ,KAAK,CAACC,eAAe,EAAE;QACjD,KAAK,MAAMC,UAAUV,OAAOQ,KAAK,CAACE,MAAM,CAAE;YACxCT,SAASI,IAAI,CAAC,CAAC,OAAO,EAAEK,OAAOJ,IAAI,CAAC,GAAG,EAAEI,OAAOH,KAAK,EAAE;QACzD;QACA,IAAIP,OAAOQ,KAAK,CAACG,mBAAmB,GAAG,GAAG;YACxCV,SAASI,IAAI,CACX,GAAGL,OAAOQ,KAAK,CAACG,mBAAmB,CAAC,yDAAyD,CAAC;QAElG;IACF;IAEA,OAAOV;AACT;AAEA,OAAO,SAASW,qBAAqBZ,MAA2B;IAC9D,MAAMa,WAAWb,OAAOG,WAAW,CAACW,MAAM,CAAC,CAACC,IAAMA,EAAEX,IAAI,KAAK;IAC7D,MAAMY,WAAWH,SAASI,MAAM,CAAC,CAACC,KAAKH,IAAMG,MAAOH,CAAAA,EAAEX,IAAI,KAAK,aAAaW,EAAEI,QAAQ,GAAG,CAAA,GAAI;IAC7F,MAAMC,UAAUpB,OAAOG,WAAW,CAACW,MAAM,CAAC,CAACC,IAAMA,EAAEX,IAAI,KAAK;IAC5D,MAAMiB,oBAAoBrB,OAAOG,WAAW,CAACW,MAAM,CAAC,CAACC,IAAMA,EAAEX,IAAI,KAAK;IAEtE,MAAMkB,QAAkB;QACtB,CAAC,SAAS,EAAET,SAASU,MAAM,CAAC,WAAW,EAAEV,SAASU,MAAM,KAAK,IAAI,KAAK,IAAI,EAAE,EAAEP,SAAS,SAAS,EAAEA,aAAa,IAAI,KAAK,IAAI,CAAC,CAAC;KAC/H;IAED,IAAII,QAAQG,MAAM,GAAG,GAAG;QACtBD,MAAMjB,IAAI,CACR,CAAC,QAAQ,EAAEe,QAAQG,MAAM,CAAC,WAAW,EAAEH,QAAQG,MAAM,KAAK,IAAI,KAAK,IAAI,qBAAqB,CAAC;IAEjG;IAEA,IAAIvB,OAAOQ,KAAK,EAAEC,iBAAiB;QACjCa,MAAMjB,IAAI,CAAC;IACb,OAAO,IAAIL,OAAOQ,KAAK,EAAE;QACvBc,MAAMjB,IAAI,CACR,CAAC,MAAM,EAAEL,OAAOQ,KAAK,CAACK,QAAQ,CAAC,CAAC,EAAEb,OAAOQ,KAAK,CAACgB,SAAS,CAAC,KAAK,EAAExB,OAAOQ,KAAK,CAACgB,SAAS,KAAK,IAAI,KAAK,KAAK;IAE7G;IAEA,MAAMC,cAAczB,OAAOQ,KAAK,EAAEE,OAAOa,UAAU;IACnD,MAAMG,aAAaL,kBAAkBE,MAAM,GAAGE;IAC9C,IAAIC,aAAa,GAAG;QAClBJ,MAAMjB,IAAI,CAAC,GAAGqB,WAAW,MAAM,EAAEA,eAAe,IAAI,KAAK,IAAI,eAAe,CAAC;IAC/E;IAEA,OAAOJ,MAAMK,IAAI,CAAC;AACpB;AAEA,OAAO,SAASC,kBACd5B,MAA2B;IAE3B,OAAOD,qBAAqBC,QAAQuB,MAAM,GAAG,IAAI,4BAA4B;AAC/E"}
@@ -0,0 +1,31 @@
1
+ import type { BackupStorageKind } from './types';
2
+ /**
3
+ * Resolved S3 configuration. Credentials are optional: when omitted, the AWS SDK's default
4
+ * provider chain is used (env vars `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, shared config,
5
+ * instance/role credentials, …), which is the recommended setup on AWS.
6
+ */
7
+ export interface BackupS3Config {
8
+ accessKeyId?: string;
9
+ bucket: string;
10
+ /** Custom endpoint for S3-compatible stores (Cloudflare R2, MinIO). Omit for AWS S3. */
11
+ endpoint?: string;
12
+ /** Path-style addressing — required for MinIO and some R2 setups. Defaults true when an endpoint is set. */
13
+ forcePathStyle: boolean;
14
+ /** Optional key namespace prepended to every object key (for sharing a bucket). No trailing slash. */
15
+ prefix: string;
16
+ region: string;
17
+ secretAccessKey?: string;
18
+ sessionToken?: string;
19
+ }
20
+ /**
21
+ * Selected backup target. Precedence: explicit `override` (plugin option) → `BACKUP_STORAGE`
22
+ * env var → `vercel-blob` (default, so existing deployments are unaffected).
23
+ */
24
+ export declare function getBackupStorageKind(override?: BackupStorageKind): BackupStorageKind;
25
+ /** True when the S3 target has the minimum configuration (a bucket) to operate. */
26
+ export declare function isS3Configured(): boolean;
27
+ /**
28
+ * Reads S3 configuration from the environment. Throws a descriptive error when the bucket is
29
+ * missing so endpoints can surface a clear failure instead of an opaque SDK error.
30
+ */
31
+ export declare function loadS3Config(): BackupS3Config;
@@ -0,0 +1,60 @@
1
+ function env(name) {
2
+ const v = process.env[name];
3
+ if (typeof v !== 'string') {
4
+ return undefined;
5
+ }
6
+ const trimmed = v.trim();
7
+ return trimmed.length > 0 ? trimmed : undefined;
8
+ }
9
+ function parseBool(raw, fallback) {
10
+ if (raw === undefined) {
11
+ return fallback;
12
+ }
13
+ return [
14
+ '1',
15
+ 'on',
16
+ 'true',
17
+ 'yes'
18
+ ].includes(raw.toLowerCase());
19
+ }
20
+ /**
21
+ * Selected backup target. Precedence: explicit `override` (plugin option) → `BACKUP_STORAGE`
22
+ * env var → `vercel-blob` (default, so existing deployments are unaffected).
23
+ */ export function getBackupStorageKind(override) {
24
+ if (override === 's3' || override === 'vercel-blob') {
25
+ return override;
26
+ }
27
+ const raw = env('BACKUP_STORAGE')?.toLowerCase();
28
+ if (raw === 's3') {
29
+ return 's3';
30
+ }
31
+ return 'vercel-blob';
32
+ }
33
+ /** True when the S3 target has the minimum configuration (a bucket) to operate. */ export function isS3Configured() {
34
+ return Boolean(env('BACKUP_S3_BUCKET'));
35
+ }
36
+ /**
37
+ * Reads S3 configuration from the environment. Throws a descriptive error when the bucket is
38
+ * missing so endpoints can surface a clear failure instead of an opaque SDK error.
39
+ */ export function loadS3Config() {
40
+ const bucket = env('BACKUP_S3_BUCKET');
41
+ if (!bucket) {
42
+ throw new Error('BACKUP_S3_BUCKET is required when BACKUP_STORAGE=s3');
43
+ }
44
+ const endpoint = env('BACKUP_S3_ENDPOINT');
45
+ const rawPrefix = env('BACKUP_S3_PREFIX') ?? '';
46
+ const accessKeyId = env('BACKUP_S3_ACCESS_KEY_ID') ?? env('AWS_ACCESS_KEY_ID');
47
+ const secretAccessKey = env('BACKUP_S3_SECRET_ACCESS_KEY') ?? env('AWS_SECRET_ACCESS_KEY');
48
+ return {
49
+ accessKeyId,
50
+ bucket,
51
+ endpoint,
52
+ forcePathStyle: parseBool(env('BACKUP_S3_FORCE_PATH_STYLE'), Boolean(endpoint)),
53
+ prefix: rawPrefix.replace(/\/+$/, ''),
54
+ region: env('BACKUP_S3_REGION') ?? env('AWS_REGION') ?? 'us-east-1',
55
+ secretAccessKey,
56
+ sessionToken: env('BACKUP_S3_SESSION_TOKEN') ?? env('AWS_SESSION_TOKEN')
57
+ };
58
+ }
59
+
60
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/storage/config.ts"],"sourcesContent":["import type { BackupStorageKind } from './types'\n\n/**\n * Resolved S3 configuration. Credentials are optional: when omitted, the AWS SDK's default\n * provider chain is used (env vars `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, shared config,\n * instance/role credentials, …), which is the recommended setup on AWS.\n */\nexport interface BackupS3Config {\n accessKeyId?: string\n bucket: string\n /** Custom endpoint for S3-compatible stores (Cloudflare R2, MinIO). Omit for AWS S3. */\n endpoint?: string\n /** Path-style addressing — required for MinIO and some R2 setups. Defaults true when an endpoint is set. */\n forcePathStyle: boolean\n /** Optional key namespace prepended to every object key (for sharing a bucket). No trailing slash. */\n prefix: string\n region: string\n secretAccessKey?: string\n sessionToken?: string\n}\n\nfunction env(name: string): string | undefined {\n const v = process.env[name]\n if (typeof v !== 'string') {\n return undefined\n }\n const trimmed = v.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nfunction parseBool(raw: string | undefined, fallback: boolean): boolean {\n if (raw === undefined) {\n return fallback\n }\n return ['1', 'on', 'true', 'yes'].includes(raw.toLowerCase())\n}\n\n/**\n * Selected backup target. Precedence: explicit `override` (plugin option) → `BACKUP_STORAGE`\n * env var → `vercel-blob` (default, so existing deployments are unaffected).\n */\nexport function getBackupStorageKind(override?: BackupStorageKind): BackupStorageKind {\n if (override === 's3' || override === 'vercel-blob') {\n return override\n }\n const raw = env('BACKUP_STORAGE')?.toLowerCase()\n if (raw === 's3') {\n return 's3'\n }\n return 'vercel-blob'\n}\n\n/** True when the S3 target has the minimum configuration (a bucket) to operate. */\nexport function isS3Configured(): boolean {\n return Boolean(env('BACKUP_S3_BUCKET'))\n}\n\n/**\n * Reads S3 configuration from the environment. Throws a descriptive error when the bucket is\n * missing so endpoints can surface a clear failure instead of an opaque SDK error.\n */\nexport function loadS3Config(): BackupS3Config {\n const bucket = env('BACKUP_S3_BUCKET')\n if (!bucket) {\n throw new Error('BACKUP_S3_BUCKET is required when BACKUP_STORAGE=s3')\n }\n const endpoint = env('BACKUP_S3_ENDPOINT')\n const rawPrefix = env('BACKUP_S3_PREFIX') ?? ''\n const accessKeyId = env('BACKUP_S3_ACCESS_KEY_ID') ?? env('AWS_ACCESS_KEY_ID')\n const secretAccessKey = env('BACKUP_S3_SECRET_ACCESS_KEY') ?? env('AWS_SECRET_ACCESS_KEY')\n\n return {\n accessKeyId,\n bucket,\n endpoint,\n forcePathStyle: parseBool(env('BACKUP_S3_FORCE_PATH_STYLE'), Boolean(endpoint)),\n prefix: rawPrefix.replace(/\\/+$/, ''),\n region: env('BACKUP_S3_REGION') ?? env('AWS_REGION') ?? 'us-east-1',\n secretAccessKey,\n sessionToken: env('BACKUP_S3_SESSION_TOKEN') ?? env('AWS_SESSION_TOKEN'),\n }\n}\n"],"names":["env","name","v","process","undefined","trimmed","trim","length","parseBool","raw","fallback","includes","toLowerCase","getBackupStorageKind","override","isS3Configured","Boolean","loadS3Config","bucket","Error","endpoint","rawPrefix","accessKeyId","secretAccessKey","forcePathStyle","prefix","replace","region","sessionToken"],"mappings":"AAqBA,SAASA,IAAIC,IAAY;IACvB,MAAMC,IAAIC,QAAQH,GAAG,CAACC,KAAK;IAC3B,IAAI,OAAOC,MAAM,UAAU;QACzB,OAAOE;IACT;IACA,MAAMC,UAAUH,EAAEI,IAAI;IACtB,OAAOD,QAAQE,MAAM,GAAG,IAAIF,UAAUD;AACxC;AAEA,SAASI,UAAUC,GAAuB,EAAEC,QAAiB;IAC3D,IAAID,QAAQL,WAAW;QACrB,OAAOM;IACT;IACA,OAAO;QAAC;QAAK;QAAM;QAAQ;KAAM,CAACC,QAAQ,CAACF,IAAIG,WAAW;AAC5D;AAEA;;;CAGC,GACD,OAAO,SAASC,qBAAqBC,QAA4B;IAC/D,IAAIA,aAAa,QAAQA,aAAa,eAAe;QACnD,OAAOA;IACT;IACA,MAAML,MAAMT,IAAI,mBAAmBY;IACnC,IAAIH,QAAQ,MAAM;QAChB,OAAO;IACT;IACA,OAAO;AACT;AAEA,iFAAiF,GACjF,OAAO,SAASM;IACd,OAAOC,QAAQhB,IAAI;AACrB;AAEA;;;CAGC,GACD,OAAO,SAASiB;IACd,MAAMC,SAASlB,IAAI;IACnB,IAAI,CAACkB,QAAQ;QACX,MAAM,IAAIC,MAAM;IAClB;IACA,MAAMC,WAAWpB,IAAI;IACrB,MAAMqB,YAAYrB,IAAI,uBAAuB;IAC7C,MAAMsB,cAActB,IAAI,8BAA8BA,IAAI;IAC1D,MAAMuB,kBAAkBvB,IAAI,kCAAkCA,IAAI;IAElE,OAAO;QACLsB;QACAJ;QACAE;QACAI,gBAAgBhB,UAAUR,IAAI,+BAA+BgB,QAAQI;QACrEK,QAAQJ,UAAUK,OAAO,CAAC,QAAQ;QAClCC,QAAQ3B,IAAI,uBAAuBA,IAAI,iBAAiB;QACxDuB;QACAK,cAAc5B,IAAI,8BAA8BA,IAAI;IACtD;AACF"}
@@ -0,0 +1,23 @@
1
+ import type { BackupBlobAccessLevel } from '../backupBlobIO';
2
+ import type { BackupStorageAdapter, BackupStorageKind } from './types';
3
+ export type { BackupS3Config } from './config';
4
+ export { getBackupStorageKind, isS3Configured, loadS3Config } from './config';
5
+ export type { BackupStorageAdapter, BackupStorageKind, BackupStorageObject, BackupStorageRef, BackupStorageValidation, } from './types';
6
+ export interface ResolveBackupStorageOptions {
7
+ /** Vercel Blob access level (ignored for S3). */
8
+ blobAccess?: BackupBlobAccessLevel;
9
+ /** Vercel Blob read/write token (ignored for S3). */
10
+ blobToken?: string;
11
+ /** Force a target; defaults to {@link getBackupStorageKind} (plugin option / env / vercel-blob). */
12
+ kind?: BackupStorageKind;
13
+ }
14
+ /**
15
+ * Builds the backup storage adapter for the active target. Vercel Blob is the default; `s3` is
16
+ * selected via the `BACKUP_STORAGE` env var (or plugin option) and reads `BACKUP_S3_*` config.
17
+ */
18
+ export declare function resolveBackupStorage(options?: ResolveBackupStorageOptions): BackupStorageAdapter;
19
+ /**
20
+ * True when the active target has enough configuration to serve requests. Used by endpoints to
21
+ * return 503 early: Vercel Blob needs a token; S3 needs a bucket.
22
+ */
23
+ export declare function isBackupStorageConfigured(blobToken?: string): boolean;
@@ -0,0 +1,25 @@
1
+ import { getBackupStorageKind, isS3Configured, loadS3Config } from './config';
2
+ import { S3BackupStorage } from './s3';
3
+ import { VercelBlobStorage } from './vercelBlob';
4
+ export { getBackupStorageKind, isS3Configured, loadS3Config } from './config';
5
+ /**
6
+ * Builds the backup storage adapter for the active target. Vercel Blob is the default; `s3` is
7
+ * selected via the `BACKUP_STORAGE` env var (or plugin option) and reads `BACKUP_S3_*` config.
8
+ */ export function resolveBackupStorage(options = {}) {
9
+ const kind = getBackupStorageKind(options.kind);
10
+ if (kind === 's3') {
11
+ return new S3BackupStorage(loadS3Config());
12
+ }
13
+ return new VercelBlobStorage({
14
+ access: options.blobAccess,
15
+ token: options.blobToken
16
+ });
17
+ }
18
+ /**
19
+ * True when the active target has enough configuration to serve requests. Used by endpoints to
20
+ * return 503 early: Vercel Blob needs a token; S3 needs a bucket.
21
+ */ export function isBackupStorageConfigured(blobToken) {
22
+ return getBackupStorageKind() === 's3' ? isS3Configured() : Boolean(blobToken?.trim());
23
+ }
24
+
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/storage/index.ts"],"sourcesContent":["import type { BackupBlobAccessLevel } from '../backupBlobIO'\nimport type { BackupStorageAdapter, BackupStorageKind } from './types'\n\nimport { getBackupStorageKind, isS3Configured, loadS3Config } from './config'\nimport { S3BackupStorage } from './s3'\nimport { VercelBlobStorage } from './vercelBlob'\n\nexport type { BackupS3Config } from './config'\nexport { getBackupStorageKind, isS3Configured, loadS3Config } from './config'\nexport type {\n BackupStorageAdapter,\n BackupStorageKind,\n BackupStorageObject,\n BackupStorageRef,\n BackupStorageValidation,\n} from './types'\n\nexport interface ResolveBackupStorageOptions {\n /** Vercel Blob access level (ignored for S3). */\n blobAccess?: BackupBlobAccessLevel\n /** Vercel Blob read/write token (ignored for S3). */\n blobToken?: string\n /** Force a target; defaults to {@link getBackupStorageKind} (plugin option / env / vercel-blob). */\n kind?: BackupStorageKind\n}\n\n/**\n * Builds the backup storage adapter for the active target. Vercel Blob is the default; `s3` is\n * selected via the `BACKUP_STORAGE` env var (or plugin option) and reads `BACKUP_S3_*` config.\n */\nexport function resolveBackupStorage(\n options: ResolveBackupStorageOptions = {},\n): BackupStorageAdapter {\n const kind = getBackupStorageKind(options.kind)\n if (kind === 's3') {\n return new S3BackupStorage(loadS3Config())\n }\n return new VercelBlobStorage({ access: options.blobAccess, token: options.blobToken })\n}\n\n/**\n * True when the active target has enough configuration to serve requests. Used by endpoints to\n * return 503 early: Vercel Blob needs a token; S3 needs a bucket.\n */\nexport function isBackupStorageConfigured(blobToken?: string): boolean {\n return getBackupStorageKind() === 's3' ? isS3Configured() : Boolean(blobToken?.trim())\n}\n"],"names":["getBackupStorageKind","isS3Configured","loadS3Config","S3BackupStorage","VercelBlobStorage","resolveBackupStorage","options","kind","access","blobAccess","token","blobToken","isBackupStorageConfigured","Boolean","trim"],"mappings":"AAGA,SAASA,oBAAoB,EAAEC,cAAc,EAAEC,YAAY,QAAQ,WAAU;AAC7E,SAASC,eAAe,QAAQ,OAAM;AACtC,SAASC,iBAAiB,QAAQ,eAAc;AAGhD,SAASJ,oBAAoB,EAAEC,cAAc,EAAEC,YAAY,QAAQ,WAAU;AAkB7E;;;CAGC,GACD,OAAO,SAASG,qBACdC,UAAuC,CAAC,CAAC;IAEzC,MAAMC,OAAOP,qBAAqBM,QAAQC,IAAI;IAC9C,IAAIA,SAAS,MAAM;QACjB,OAAO,IAAIJ,gBAAgBD;IAC7B;IACA,OAAO,IAAIE,kBAAkB;QAAEI,QAAQF,QAAQG,UAAU;QAAEC,OAAOJ,QAAQK,SAAS;IAAC;AACtF;AAEA;;;CAGC,GACD,OAAO,SAASC,0BAA0BD,SAAkB;IAC1D,OAAOX,2BAA2B,OAAOC,mBAAmBY,QAAQF,WAAWG;AACjF"}
@@ -0,0 +1,35 @@
1
+ import type { BackupS3Config } from './config';
2
+ import type { BackupStorageAdapter, BackupStorageObject, BackupStorageRef, BackupStorageValidation } from './types';
3
+ /**
4
+ * AWS S3 backup target. The same implementation serves any S3-compatible store (Cloudflare R2,
5
+ * MinIO, …) — only the `endpoint`/`forcePathStyle` config differs, never the code. The AWS SDK is
6
+ * loaded lazily via dynamic import so it stays an optional dependency for Vercel-only installs.
7
+ */
8
+ export declare class S3BackupStorage implements BackupStorageAdapter {
9
+ private readonly cfg;
10
+ private clientPromise;
11
+ private presignerPromise;
12
+ readonly kind = "s3";
13
+ constructor(cfg: BackupS3Config);
14
+ private buildClient;
15
+ private client;
16
+ /** Strip the configured key prefix, returning the provider-agnostic `backups/...` pathname. */
17
+ private fromKey;
18
+ private presigner;
19
+ private presignGet;
20
+ private sdk;
21
+ /** Prepend the configured key prefix to a provider-agnostic `backups/...` pathname. */
22
+ private toKey;
23
+ del(ref: BackupStorageRef): Promise<void>;
24
+ list(prefix: string): Promise<BackupStorageObject[]>;
25
+ openDownloadStream(ref: BackupStorageRef): Promise<{
26
+ contentType: string;
27
+ stream: ReadableStream<Uint8Array<ArrayBufferLike>>;
28
+ } | null>;
29
+ put(pathname: string, body: Buffer | string | Uint8Array): Promise<{
30
+ pathname: string;
31
+ url: string;
32
+ }>;
33
+ read(ref: BackupStorageRef): Promise<Buffer>;
34
+ validate(): Promise<BackupStorageValidation>;
35
+ }
@@ -0,0 +1,200 @@
1
+ /** Presigned download URLs are valid this long — long enough to start a restore/download. */ const SIGNED_URL_TTL_SECONDS = 3600;
2
+ function contentTypeFor(pathname) {
3
+ if (pathname.endsWith('.json')) {
4
+ return 'application/json';
5
+ }
6
+ if (pathname.endsWith('.gz') || pathname.endsWith('.tar.gz')) {
7
+ return 'application/gzip';
8
+ }
9
+ return 'application/octet-stream';
10
+ }
11
+ /**
12
+ * AWS S3 backup target. The same implementation serves any S3-compatible store (Cloudflare R2,
13
+ * MinIO, …) — only the `endpoint`/`forcePathStyle` config differs, never the code. The AWS SDK is
14
+ * loaded lazily via dynamic import so it stays an optional dependency for Vercel-only installs.
15
+ */ export class S3BackupStorage {
16
+ cfg;
17
+ clientPromise = null;
18
+ presignerPromise = null;
19
+ kind = 's3';
20
+ constructor(cfg){
21
+ this.cfg = cfg;
22
+ }
23
+ async buildClient() {
24
+ const { S3Client } = await this.sdk();
25
+ const { accessKeyId, secretAccessKey, sessionToken } = this.cfg;
26
+ return new S3Client({
27
+ ...this.cfg.endpoint ? {
28
+ endpoint: this.cfg.endpoint
29
+ } : {},
30
+ forcePathStyle: this.cfg.forcePathStyle,
31
+ region: this.cfg.region,
32
+ // When keys are absent, fall back to the SDK's default credential provider chain
33
+ // (env vars, shared config, instance/role credentials).
34
+ ...accessKeyId && secretAccessKey ? {
35
+ credentials: {
36
+ accessKeyId,
37
+ secretAccessKey,
38
+ sessionToken
39
+ }
40
+ } : {}
41
+ });
42
+ }
43
+ async client() {
44
+ if (!this.clientPromise) {
45
+ this.clientPromise = this.buildClient();
46
+ }
47
+ return this.clientPromise;
48
+ }
49
+ /** Strip the configured key prefix, returning the provider-agnostic `backups/...` pathname. */ fromKey(key) {
50
+ if (!this.cfg.prefix) {
51
+ return key;
52
+ }
53
+ const lead = `${this.cfg.prefix}/`;
54
+ return key.startsWith(lead) ? key.slice(lead.length) : key;
55
+ }
56
+ async presigner() {
57
+ if (!this.presignerPromise) {
58
+ this.presignerPromise = import('@aws-sdk/s3-request-presigner');
59
+ }
60
+ return this.presignerPromise;
61
+ }
62
+ async presignGet(key) {
63
+ const { GetObjectCommand } = await this.sdk();
64
+ const { getSignedUrl } = await this.presigner();
65
+ const client = await this.client();
66
+ return getSignedUrl(client, new GetObjectCommand({
67
+ Bucket: this.cfg.bucket,
68
+ Key: key
69
+ }), {
70
+ expiresIn: SIGNED_URL_TTL_SECONDS
71
+ });
72
+ }
73
+ async sdk() {
74
+ try {
75
+ return await import('@aws-sdk/client-s3');
76
+ } catch {
77
+ throw new Error('BACKUP_STORAGE=s3 requires the AWS SDK. Install it with: ' + 'pnpm add @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner');
78
+ }
79
+ }
80
+ /** Prepend the configured key prefix to a provider-agnostic `backups/...` pathname. */ toKey(pathname) {
81
+ return this.cfg.prefix ? `${this.cfg.prefix}/${pathname}` : pathname;
82
+ }
83
+ async del(ref) {
84
+ const { DeleteObjectCommand } = await this.sdk();
85
+ const client = await this.client();
86
+ await client.send(new DeleteObjectCommand({
87
+ Bucket: this.cfg.bucket,
88
+ Key: this.toKey(ref.pathname)
89
+ }));
90
+ }
91
+ async list(prefix) {
92
+ const { ListObjectsV2Command } = await this.sdk();
93
+ const client = await this.client();
94
+ const keyPrefix = this.toKey(prefix);
95
+ const objects = [];
96
+ let continuationToken;
97
+ do {
98
+ const res = await client.send(new ListObjectsV2Command({
99
+ Bucket: this.cfg.bucket,
100
+ ContinuationToken: continuationToken,
101
+ Prefix: keyPrefix
102
+ }));
103
+ const pageEntries = [];
104
+ for (const obj of res.Contents ?? []){
105
+ if (!obj.Key) {
106
+ continue;
107
+ }
108
+ pageEntries.push({
109
+ key: obj.Key,
110
+ pathname: this.fromKey(obj.Key),
111
+ size: obj.Size ?? 0,
112
+ uploadedAt: obj.LastModified ?? new Date(0)
113
+ });
114
+ }
115
+ const signed = await Promise.all(pageEntries.map(async (entry)=>{
116
+ const url = await this.presignGet(entry.key);
117
+ return {
118
+ downloadUrl: url,
119
+ pathname: entry.pathname,
120
+ size: entry.size,
121
+ uploadedAt: entry.uploadedAt,
122
+ url
123
+ };
124
+ }));
125
+ objects.push(...signed);
126
+ continuationToken = res.IsTruncated ? res.NextContinuationToken : undefined;
127
+ }while (continuationToken)
128
+ return objects;
129
+ }
130
+ async openDownloadStream(ref) {
131
+ const { GetObjectCommand } = await this.sdk();
132
+ const client = await this.client();
133
+ try {
134
+ const res = await client.send(new GetObjectCommand({
135
+ Bucket: this.cfg.bucket,
136
+ Key: this.toKey(ref.pathname)
137
+ }));
138
+ if (!res.Body) {
139
+ return null;
140
+ }
141
+ const stream = res.Body.transformToWebStream();
142
+ return {
143
+ contentType: res.ContentType || contentTypeFor(ref.pathname),
144
+ stream
145
+ };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ async put(pathname, body) {
151
+ const { Upload } = await import('@aws-sdk/lib-storage');
152
+ const client = await this.client();
153
+ const key = this.toKey(pathname);
154
+ await new Upload({
155
+ client,
156
+ params: {
157
+ Body: typeof body === 'string' ? Buffer.from(body) : body,
158
+ Bucket: this.cfg.bucket,
159
+ ContentType: contentTypeFor(pathname),
160
+ Key: key
161
+ }
162
+ }).done();
163
+ return {
164
+ pathname,
165
+ url: await this.presignGet(key)
166
+ };
167
+ }
168
+ async read(ref) {
169
+ const { GetObjectCommand } = await this.sdk();
170
+ const client = await this.client();
171
+ const res = await client.send(new GetObjectCommand({
172
+ Bucket: this.cfg.bucket,
173
+ Key: this.toKey(ref.pathname)
174
+ }));
175
+ if (!res.Body) {
176
+ throw new Error(`Failed to read backup object (${ref.pathname}): empty body`);
177
+ }
178
+ const bytes = await res.Body.transformToByteArray();
179
+ return Buffer.from(bytes);
180
+ }
181
+ async validate() {
182
+ try {
183
+ const { HeadBucketCommand } = await this.sdk();
184
+ const client = await this.client();
185
+ await client.send(new HeadBucketCommand({
186
+ Bucket: this.cfg.bucket
187
+ }));
188
+ return {
189
+ ok: true
190
+ };
191
+ } catch (error) {
192
+ return {
193
+ error: error instanceof Error ? error.message : 'S3 bucket not reachable',
194
+ ok: false
195
+ };
196
+ }
197
+ }
198
+ }
199
+
200
+ //# sourceMappingURL=s3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/storage/s3.ts"],"sourcesContent":["import type { S3Client } from '@aws-sdk/client-s3'\n\nimport type { BackupS3Config } from './config'\nimport type {\n BackupStorageAdapter,\n BackupStorageObject,\n BackupStorageRef,\n BackupStorageValidation,\n} from './types'\n\n/** Presigned download URLs are valid this long — long enough to start a restore/download. */\nconst SIGNED_URL_TTL_SECONDS = 3600\n\ntype S3PresignerModule = {\n getSignedUrl: (\n client: S3Client,\n command: unknown,\n options: { expiresIn: number },\n ) => Promise<string>\n}\n\nfunction contentTypeFor(pathname: string): string {\n if (pathname.endsWith('.json')) {\n return 'application/json'\n }\n if (pathname.endsWith('.gz') || pathname.endsWith('.tar.gz')) {\n return 'application/gzip'\n }\n return 'application/octet-stream'\n}\n\n/**\n * AWS S3 backup target. The same implementation serves any S3-compatible store (Cloudflare R2,\n * MinIO, …) — only the `endpoint`/`forcePathStyle` config differs, never the code. The AWS SDK is\n * loaded lazily via dynamic import so it stays an optional dependency for Vercel-only installs.\n */\nexport class S3BackupStorage implements BackupStorageAdapter {\n private readonly cfg: BackupS3Config\n\n private clientPromise: null | Promise<S3Client> = null\n private presignerPromise: null | Promise<S3PresignerModule> = null\n readonly kind = 's3'\n\n constructor(cfg: BackupS3Config) {\n this.cfg = cfg\n }\n\n private async buildClient(): Promise<S3Client> {\n const { S3Client } = await this.sdk()\n const { accessKeyId, secretAccessKey, sessionToken } = this.cfg\n return new S3Client({\n ...(this.cfg.endpoint ? { endpoint: this.cfg.endpoint } : {}),\n forcePathStyle: this.cfg.forcePathStyle,\n region: this.cfg.region,\n // When keys are absent, fall back to the SDK's default credential provider chain\n // (env vars, shared config, instance/role credentials).\n ...(accessKeyId && secretAccessKey ?\n { credentials: { accessKeyId, secretAccessKey, sessionToken } }\n : {}),\n })\n }\n\n private async client(): Promise<S3Client> {\n if (!this.clientPromise) {\n this.clientPromise = this.buildClient()\n }\n return this.clientPromise\n }\n\n /** Strip the configured key prefix, returning the provider-agnostic `backups/...` pathname. */\n private fromKey(key: string): string {\n if (!this.cfg.prefix) {\n return key\n }\n const lead = `${this.cfg.prefix}/`\n return key.startsWith(lead) ? key.slice(lead.length) : key\n }\n\n private async presigner() {\n if (!this.presignerPromise) {\n this.presignerPromise = import('@aws-sdk/s3-request-presigner')\n }\n return this.presignerPromise\n }\n\n private async presignGet(key: string): Promise<string> {\n const { GetObjectCommand } = await this.sdk()\n const { getSignedUrl } = await this.presigner()\n const client = await this.client()\n return getSignedUrl(client, new GetObjectCommand({ Bucket: this.cfg.bucket, Key: key }), {\n expiresIn: SIGNED_URL_TTL_SECONDS,\n })\n }\n\n private async sdk() {\n try {\n return await import('@aws-sdk/client-s3')\n } catch {\n throw new Error(\n 'BACKUP_STORAGE=s3 requires the AWS SDK. Install it with: ' +\n 'pnpm add @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner',\n )\n }\n }\n\n /** Prepend the configured key prefix to a provider-agnostic `backups/...` pathname. */\n private toKey(pathname: string): string {\n return this.cfg.prefix ? `${this.cfg.prefix}/${pathname}` : pathname\n }\n\n async del(ref: BackupStorageRef): Promise<void> {\n const { DeleteObjectCommand } = await this.sdk()\n const client = await this.client()\n await client.send(\n new DeleteObjectCommand({ Bucket: this.cfg.bucket, Key: this.toKey(ref.pathname) }),\n )\n }\n\n async list(prefix: string): Promise<BackupStorageObject[]> {\n const { ListObjectsV2Command } = await this.sdk()\n const client = await this.client()\n const keyPrefix = this.toKey(prefix)\n\n const objects: BackupStorageObject[] = []\n let continuationToken: string | undefined\n do {\n const res = await client.send(\n new ListObjectsV2Command({\n Bucket: this.cfg.bucket,\n ContinuationToken: continuationToken,\n Prefix: keyPrefix,\n }),\n )\n const pageEntries: Array<{\n key: string\n pathname: string\n size: number\n uploadedAt: Date\n }> = []\n for (const obj of res.Contents ?? []) {\n if (!obj.Key) {\n continue\n }\n pageEntries.push({\n key: obj.Key,\n pathname: this.fromKey(obj.Key),\n size: obj.Size ?? 0,\n uploadedAt: obj.LastModified ?? new Date(0),\n })\n }\n const signed = await Promise.all(\n pageEntries.map(async (entry) => {\n const url = await this.presignGet(entry.key)\n return {\n downloadUrl: url,\n pathname: entry.pathname,\n size: entry.size,\n uploadedAt: entry.uploadedAt,\n url,\n }\n }),\n )\n objects.push(...signed)\n continuationToken = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (continuationToken)\n\n return objects\n }\n\n async openDownloadStream(ref: BackupStorageRef) {\n const { GetObjectCommand } = await this.sdk()\n const client = await this.client()\n try {\n const res = await client.send(\n new GetObjectCommand({ Bucket: this.cfg.bucket, Key: this.toKey(ref.pathname) }),\n )\n if (!res.Body) {\n return null\n }\n const stream = (\n res.Body as { transformToWebStream: () => ReadableStream<Uint8Array> }\n ).transformToWebStream()\n return {\n contentType: res.ContentType || contentTypeFor(ref.pathname),\n stream,\n }\n } catch {\n return null\n }\n }\n\n async put(pathname: string, body: Buffer | string | Uint8Array) {\n const { Upload } = await import('@aws-sdk/lib-storage')\n const client = await this.client()\n const key = this.toKey(pathname)\n await new Upload({\n client,\n params: {\n Body: typeof body === 'string' ? Buffer.from(body) : body,\n Bucket: this.cfg.bucket,\n ContentType: contentTypeFor(pathname),\n Key: key,\n },\n }).done()\n return { pathname, url: await this.presignGet(key) }\n }\n\n async read(ref: BackupStorageRef): Promise<Buffer> {\n const { GetObjectCommand } = await this.sdk()\n const client = await this.client()\n const res = await client.send(\n new GetObjectCommand({ Bucket: this.cfg.bucket, Key: this.toKey(ref.pathname) }),\n )\n if (!res.Body) {\n throw new Error(`Failed to read backup object (${ref.pathname}): empty body`)\n }\n const bytes = await (\n res.Body as { transformToByteArray: () => Promise<Uint8Array> }\n ).transformToByteArray()\n return Buffer.from(bytes)\n }\n\n async validate(): Promise<BackupStorageValidation> {\n try {\n const { HeadBucketCommand } = await this.sdk()\n const client = await this.client()\n await client.send(new HeadBucketCommand({ Bucket: this.cfg.bucket }))\n return { ok: true }\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : 'S3 bucket not reachable',\n ok: false,\n }\n }\n }\n}\n"],"names":["SIGNED_URL_TTL_SECONDS","contentTypeFor","pathname","endsWith","S3BackupStorage","cfg","clientPromise","presignerPromise","kind","buildClient","S3Client","sdk","accessKeyId","secretAccessKey","sessionToken","endpoint","forcePathStyle","region","credentials","client","fromKey","key","prefix","lead","startsWith","slice","length","presigner","presignGet","GetObjectCommand","getSignedUrl","Bucket","bucket","Key","expiresIn","Error","toKey","del","ref","DeleteObjectCommand","send","list","ListObjectsV2Command","keyPrefix","objects","continuationToken","res","ContinuationToken","Prefix","pageEntries","obj","Contents","push","size","Size","uploadedAt","LastModified","Date","signed","Promise","all","map","entry","url","downloadUrl","IsTruncated","NextContinuationToken","undefined","openDownloadStream","Body","stream","transformToWebStream","contentType","ContentType","put","body","Upload","params","Buffer","from","done","read","bytes","transformToByteArray","validate","HeadBucketCommand","ok","error","message"],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import type { BackupBlobAccessLevel } from '../backupBlobIO';
2
+ /** Selectable backup target. Vercel Blob is the historical default; `s3` covers AWS S3 and any
3
+ * S3-compatible store (Cloudflare R2, MinIO, …) — they share one implementation. */
4
+ export type BackupStorageKind = 's3' | 'vercel-blob';
5
+ /**
6
+ * Backup archive as seen by the dashboard and the cron/list endpoints. The field names match the
7
+ * objects Vercel Blob's `list()` returns so existing consumers (UI, restore-by-`url`) keep working;
8
+ * the S3 adapter synthesizes the same shape (with a presigned `downloadUrl`/`url`).
9
+ */
10
+ export interface BackupStorageObject {
11
+ /** Time-limited URL suitable for downloading the object (presigned for S3). */
12
+ downloadUrl: string;
13
+ /** Object key without any provider prefix, e.g. `backups/manual---db---host---2-1700.json`. */
14
+ pathname: string;
15
+ /** Object size in bytes. */
16
+ size: number;
17
+ /** Upload/last-modified time. */
18
+ uploadedAt: Date | string;
19
+ /** Stable reference used for download/delete (Vercel blob URL; presigned GET for S3). */
20
+ url: string;
21
+ }
22
+ /** Result of probing the target's credentials/reachability. */
23
+ export interface BackupStorageValidation {
24
+ /** Detected Vercel Blob access level, when applicable. */
25
+ access?: BackupBlobAccessLevel;
26
+ /** Short description when validation fails. */
27
+ error?: string;
28
+ ok: boolean;
29
+ }
30
+ /** A reference good enough to read or delete a single archive across providers. */
31
+ export interface BackupStorageRef {
32
+ downloadUrl?: null | string;
33
+ pathname: string;
34
+ url?: null | string;
35
+ }
36
+ /**
37
+ * Provider-agnostic operations the plugin performs on backup archives. Implementations live in
38
+ * `vercelBlob.ts` and `s3.ts`; callers obtain one via {@link resolveBackupStorage}.
39
+ */
40
+ export interface BackupStorageAdapter {
41
+ /** Delete a single archive. */
42
+ del(ref: BackupStorageRef): Promise<void>;
43
+ readonly kind: BackupStorageKind;
44
+ /** List archives under a pathname prefix (e.g. `backups/` or `backups/cron-`). */
45
+ list(prefix: string): Promise<BackupStorageObject[]>;
46
+ /** Open a readable stream for streaming a download response, or null when not found. */
47
+ openDownloadStream(ref: BackupStorageRef): Promise<{
48
+ contentType: string;
49
+ stream: ReadableStream<Uint8Array>;
50
+ } | null>;
51
+ /** Upload an archive at `pathname`. */
52
+ put(pathname: string, body: Buffer | string | Uint8Array): Promise<{
53
+ pathname: string;
54
+ url: string;
55
+ }>;
56
+ /** Read the full bytes of an archive. */
57
+ read(ref: BackupStorageRef): Promise<Buffer>;
58
+ /** Probe credentials / reachability without persisting anything. */
59
+ validate(): Promise<BackupStorageValidation>;
60
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Provider-agnostic operations the plugin performs on backup archives. Implementations live in
3
+ * `vercelBlob.ts` and `s3.ts`; callers obtain one via {@link resolveBackupStorage}.
4
+ */ export { };
5
+
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/storage/types.ts"],"sourcesContent":["import type { BackupBlobAccessLevel } from '../backupBlobIO'\n\n/** Selectable backup target. Vercel Blob is the historical default; `s3` covers AWS S3 and any\n * S3-compatible store (Cloudflare R2, MinIO, …) — they share one implementation. */\nexport type BackupStorageKind = 's3' | 'vercel-blob'\n\n/**\n * Backup archive as seen by the dashboard and the cron/list endpoints. The field names match the\n * objects Vercel Blob's `list()` returns so existing consumers (UI, restore-by-`url`) keep working;\n * the S3 adapter synthesizes the same shape (with a presigned `downloadUrl`/`url`).\n */\nexport interface BackupStorageObject {\n /** Time-limited URL suitable for downloading the object (presigned for S3). */\n downloadUrl: string\n /** Object key without any provider prefix, e.g. `backups/manual---db---host---2-1700.json`. */\n pathname: string\n /** Object size in bytes. */\n size: number\n /** Upload/last-modified time. */\n uploadedAt: Date | string\n /** Stable reference used for download/delete (Vercel blob URL; presigned GET for S3). */\n url: string\n}\n\n/** Result of probing the target's credentials/reachability. */\nexport interface BackupStorageValidation {\n /** Detected Vercel Blob access level, when applicable. */\n access?: BackupBlobAccessLevel\n /** Short description when validation fails. */\n error?: string\n ok: boolean\n}\n\n/** A reference good enough to read or delete a single archive across providers. */\nexport interface BackupStorageRef {\n downloadUrl?: null | string\n pathname: string\n url?: null | string\n}\n\n/**\n * Provider-agnostic operations the plugin performs on backup archives. Implementations live in\n * `vercelBlob.ts` and `s3.ts`; callers obtain one via {@link resolveBackupStorage}.\n */\nexport interface BackupStorageAdapter {\n /** Delete a single archive. */\n del(ref: BackupStorageRef): Promise<void>\n readonly kind: BackupStorageKind\n /** List archives under a pathname prefix (e.g. `backups/` or `backups/cron-`). */\n list(prefix: string): Promise<BackupStorageObject[]>\n /** Open a readable stream for streaming a download response, or null when not found. */\n openDownloadStream(\n ref: BackupStorageRef,\n ): Promise<{ contentType: string; stream: ReadableStream<Uint8Array> } | null>\n /** Upload an archive at `pathname`. */\n put(\n pathname: string,\n body: Buffer | string | Uint8Array,\n ): Promise<{ pathname: string; url: string }>\n /** Read the full bytes of an archive. */\n read(ref: BackupStorageRef): Promise<Buffer>\n /** Probe credentials / reachability without persisting anything. */\n validate(): Promise<BackupStorageValidation>\n}\n"],"names":[],"mappings":"AAwCA;;;CAGC,GACD,WAmBC"}
@@ -0,0 +1,25 @@
1
+ import type { BackupStorageAdapter, BackupStorageObject, BackupStorageRef, BackupStorageValidation } from './types';
2
+ import { type BackupBlobAccessLevel } from '../backupBlobIO';
3
+ /**
4
+ * Vercel Blob backup target — the historical default. This is a thin wrapper around the existing
5
+ * `@vercel/blob` helpers so behaviour (and the test seams that mock `@vercel/blob`) is unchanged;
6
+ * the adapter only generalizes the call shape so S3 can slot in alongside it.
7
+ */
8
+ export declare class VercelBlobStorage implements BackupStorageAdapter {
9
+ private readonly access;
10
+ private readonly token;
11
+ readonly kind = "vercel-blob";
12
+ constructor(options: {
13
+ access?: BackupBlobAccessLevel;
14
+ token?: string;
15
+ });
16
+ del(ref: BackupStorageRef): Promise<void>;
17
+ list(prefix: string): Promise<BackupStorageObject[]>;
18
+ openDownloadStream(ref: BackupStorageRef): Promise<import("../backupBlobIO").BackupBlobDownloadStream | null>;
19
+ put(pathname: string, body: Buffer | string | Uint8Array): Promise<{
20
+ pathname: string;
21
+ url: string;
22
+ }>;
23
+ read(ref: BackupStorageRef): Promise<Buffer>;
24
+ validate(): Promise<BackupStorageValidation>;
25
+ }
@@ -0,0 +1,53 @@
1
+ import { del, list } from '@vercel/blob';
2
+ import { putBackupBlobContent, readBackupBlobContentFlexible, streamBackupBlobForDownload } from '../backupBlobIO';
3
+ import { validateBackupBlobToken } from '../blobTokenValidate';
4
+ /**
5
+ * Vercel Blob backup target — the historical default. This is a thin wrapper around the existing
6
+ * `@vercel/blob` helpers so behaviour (and the test seams that mock `@vercel/blob`) is unchanged;
7
+ * the adapter only generalizes the call shape so S3 can slot in alongside it.
8
+ */ export class VercelBlobStorage {
9
+ access;
10
+ token;
11
+ kind = 'vercel-blob';
12
+ constructor(options){
13
+ this.token = options.token;
14
+ this.access = options.access ?? 'public';
15
+ }
16
+ async del(ref) {
17
+ await del(ref.url ?? ref.pathname, {
18
+ token: this.token
19
+ });
20
+ }
21
+ async list(prefix) {
22
+ const { blobs } = await list({
23
+ limit: 1000,
24
+ prefix,
25
+ token: this.token
26
+ });
27
+ return blobs;
28
+ }
29
+ async openDownloadStream(ref) {
30
+ return streamBackupBlobForDownload({
31
+ blobUrl: ref.url,
32
+ downloadUrl: ref.downloadUrl,
33
+ pathname: ref.pathname,
34
+ preferredAccess: this.access,
35
+ token: this.token ?? ''
36
+ });
37
+ }
38
+ async put(pathname, body) {
39
+ await putBackupBlobContent(pathname, body, this.token, this.access);
40
+ return {
41
+ pathname,
42
+ url: ''
43
+ };
44
+ }
45
+ async read(ref) {
46
+ return readBackupBlobContentFlexible(ref.pathname, ref.downloadUrl ?? ref.url ?? '', this.token ?? '');
47
+ }
48
+ async validate() {
49
+ return validateBackupBlobToken(this.token ?? '');
50
+ }
51
+ }
52
+
53
+ //# sourceMappingURL=vercelBlob.js.map