@morphika/andami 0.2.9 → 0.2.10
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 +8 -3
- package/admin/backups.ts +7 -0
- package/app/admin/backups/page.tsx +1 -0
- package/app/admin/layout.tsx +327 -274
- package/app/api/admin/backups/export/route.ts +76 -0
- package/app/api/admin/backups/prepare-export/route.ts +56 -0
- package/app/api/admin/backups/restore/route.ts +131 -0
- package/app/api/admin/backups/restore-data/route.ts +424 -0
- package/app/api/admin/backups/status/route.ts +35 -0
- package/app/api/custom-sections/[id]/route.ts +9 -5
- package/components/admin/backups/BackupsPage.tsx +1581 -0
- package/components/builder/CustomSectionInstanceCard.tsx +7 -3
- package/components/builder/ReadOnlyFrame.tsx +4 -2
- package/components/builder/settings-panel/CustomSectionSettings.tsx +5 -3
- package/lib/backup/export.ts +377 -0
- package/lib/backup/manifest.ts +121 -0
- package/lib/backup/r2-helpers.ts +294 -0
- package/lib/backup/restore.ts +266 -0
- package/lib/backup/sanity-ops.ts +194 -0
- package/lib/builder/serializer/normalizers.ts +1 -0
- package/lib/builder/store-canvas.ts +4 -0
- package/lib/builder/store.ts +1 -0
- package/lib/builder/types.ts +4 -0
- package/lib/security.ts +30 -0
- package/lib/security.ts.new +27 -0
- package/lib/storage/types.ts +33 -1
- package/lib/version.ts +7 -4
- package/package.json +17 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanity bulk operations for backup restore.
|
|
3
|
+
*
|
|
4
|
+
* Provides wipe (delete all) and restore (createOrReplace) for the
|
|
5
|
+
* 5 Andami document types. Uses the writeClient for mutations and
|
|
6
|
+
* adminClient for reads. Operations are batched to stay within
|
|
7
|
+
* Sanity's transaction limits (~200 mutations per transaction).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { writeClient } from "../sanity/writeClient";
|
|
11
|
+
import { adminClient } from "../sanity/client";
|
|
12
|
+
import { logger } from "../logger";
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================
|
|
17
|
+
|
|
18
|
+
/** All Sanity document types managed by Andami */
|
|
19
|
+
export const ANDAMI_DOC_TYPES = [
|
|
20
|
+
"page",
|
|
21
|
+
"siteSettings",
|
|
22
|
+
"siteStyles",
|
|
23
|
+
"assetRegistry",
|
|
24
|
+
"customSection",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type AndamiDocType = (typeof ANDAMI_DOC_TYPES)[number];
|
|
28
|
+
|
|
29
|
+
/** Max mutations per Sanity transaction (conservative limit) */
|
|
30
|
+
const BATCH_SIZE = 200;
|
|
31
|
+
|
|
32
|
+
// ============================================
|
|
33
|
+
// Wipe operations
|
|
34
|
+
// ============================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Delete ALL Andami documents from Sanity.
|
|
38
|
+
* Fetches document IDs per type, then batch-deletes them.
|
|
39
|
+
*
|
|
40
|
+
* Returns a summary of how many documents were deleted per type.
|
|
41
|
+
*/
|
|
42
|
+
export async function wipeAllDocuments(): Promise<Record<string, number>> {
|
|
43
|
+
const summary: Record<string, number> = {};
|
|
44
|
+
|
|
45
|
+
for (const docType of ANDAMI_DOC_TYPES) {
|
|
46
|
+
try {
|
|
47
|
+
// Fetch all _id values for this type
|
|
48
|
+
const ids: string[] = await adminClient.fetch(
|
|
49
|
+
`*[_type == $type]._id`,
|
|
50
|
+
{ type: docType }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!ids || ids.length === 0) {
|
|
54
|
+
summary[docType] = 0;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Delete in batches
|
|
59
|
+
let deleted = 0;
|
|
60
|
+
for (let i = 0; i < ids.length; i += BATCH_SIZE) {
|
|
61
|
+
const batch = ids.slice(i, i + BATCH_SIZE);
|
|
62
|
+
const tx = writeClient.transaction();
|
|
63
|
+
|
|
64
|
+
for (const id of batch) {
|
|
65
|
+
// Delete both the published and draft versions
|
|
66
|
+
tx.delete(id);
|
|
67
|
+
tx.delete(`drafts.${id}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await tx.commit({ visibility: "async" });
|
|
71
|
+
deleted += batch.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
summary[docType] = deleted;
|
|
75
|
+
logger.info(
|
|
76
|
+
"[Backup:Restore]",
|
|
77
|
+
`Wiped ${deleted} ${docType} document(s)`
|
|
78
|
+
);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.error(
|
|
81
|
+
"[Backup:Restore]",
|
|
82
|
+
`Failed to wipe ${docType} documents`,
|
|
83
|
+
err
|
|
84
|
+
);
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Wipe failed for ${docType}: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return summary;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================
|
|
95
|
+
// Restore operations
|
|
96
|
+
// ============================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Restore documents of a given type into Sanity.
|
|
100
|
+
* Uses createOrReplace to preserve original _id values.
|
|
101
|
+
*
|
|
102
|
+
* - Array types (page, customSection): expects an array of docs
|
|
103
|
+
* - Singleton types (siteSettings, siteStyles, assetRegistry): expects a single doc
|
|
104
|
+
*
|
|
105
|
+
* Returns the number of documents restored.
|
|
106
|
+
*/
|
|
107
|
+
export async function restoreDocuments(
|
|
108
|
+
docType: AndamiDocType,
|
|
109
|
+
docs: unknown
|
|
110
|
+
): Promise<number> {
|
|
111
|
+
// Normalize to array
|
|
112
|
+
const docArray = Array.isArray(docs) ? docs : docs ? [docs] : [];
|
|
113
|
+
if (docArray.length === 0) return 0;
|
|
114
|
+
|
|
115
|
+
let restored = 0;
|
|
116
|
+
const failed: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < docArray.length; i += BATCH_SIZE) {
|
|
119
|
+
const batch = docArray.slice(i, i + BATCH_SIZE);
|
|
120
|
+
const tx = writeClient.transaction();
|
|
121
|
+
|
|
122
|
+
for (const doc of batch) {
|
|
123
|
+
if (!doc || typeof doc !== "object") continue;
|
|
124
|
+
|
|
125
|
+
const d = doc as Record<string, unknown>;
|
|
126
|
+
|
|
127
|
+
// Safety net: verify the document's _type matches the expected type
|
|
128
|
+
// for this batch. If it doesn't, skip the document rather than
|
|
129
|
+
// silently overwriting _type (which would make Sanity store it under
|
|
130
|
+
// the wrong type and later surface as a broken document). This guards
|
|
131
|
+
// against future bugs where a wrong key/type mapping is passed in.
|
|
132
|
+
if (d._type && d._type !== docType) {
|
|
133
|
+
logger.warn(
|
|
134
|
+
"[Backup:Restore]",
|
|
135
|
+
`Skipping document ${String(d._id)} — type mismatch (${String(d._type)} ≠ ${docType})`
|
|
136
|
+
);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
d._type = docType;
|
|
140
|
+
|
|
141
|
+
// Remove _rev — Sanity generates this on write.
|
|
142
|
+
// Keeping it would cause "document already exists with different revision" errors.
|
|
143
|
+
delete d._rev;
|
|
144
|
+
|
|
145
|
+
// L-11: drop `_updatedAt` so Sanity stamps a fresh value on write.
|
|
146
|
+
// `_updatedAt` is an audit field that semantically should reflect the
|
|
147
|
+
// time of the *restore*, not the time of the original save. Leaving
|
|
148
|
+
// it on the payload would back-date the document and confuse any
|
|
149
|
+
// "recently edited" UI in the Studio.
|
|
150
|
+
//
|
|
151
|
+
// `_createdAt` however is intentionally preserved: it's a provenance
|
|
152
|
+
// field that should survive a backup round-trip (otherwise every
|
|
153
|
+
// restore looks like the entire dataset was created today). Sanity
|
|
154
|
+
// accepts `_createdAt` on createOrReplace and honours it, unlike
|
|
155
|
+
// `_updatedAt` which it always regenerates.
|
|
156
|
+
delete d._updatedAt;
|
|
157
|
+
|
|
158
|
+
// NOTE: the export stores raw references as { _ref, _type: "reference" }.
|
|
159
|
+
// No post-processing needed here — Sanity accepts them directly.
|
|
160
|
+
|
|
161
|
+
tx.createOrReplace(d as { _id: string; _type: string });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await tx.commit({ visibility: "async" });
|
|
166
|
+
restored += batch.length;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// Log which batch failed but continue with remaining batches
|
|
169
|
+
const batchIds = batch
|
|
170
|
+
.map((d: Record<string, unknown>) => d._id || "?")
|
|
171
|
+
.join(", ");
|
|
172
|
+
logger.error(
|
|
173
|
+
"[Backup:Restore]",
|
|
174
|
+
`Batch restore failed for ${docType} [${batchIds}]`,
|
|
175
|
+
err
|
|
176
|
+
);
|
|
177
|
+
failed.push(...batch.map((d: Record<string, unknown>) => String(d._id || "?")));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (failed.length > 0) {
|
|
182
|
+
logger.warn(
|
|
183
|
+
"[Backup:Restore]",
|
|
184
|
+
`${failed.length} ${docType} document(s) failed to restore: ${failed.join(", ")}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
logger.info(
|
|
189
|
+
"[Backup:Restore]",
|
|
190
|
+
`Restored ${restored}/${docArray.length} ${docType} document(s)`
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return restored;
|
|
194
|
+
}
|
|
@@ -213,6 +213,7 @@ export function documentToState(doc: Page): Omit<BuilderState, "isDirty" | "isSa
|
|
|
213
213
|
draftMode: doc.draft_mode ?? true,
|
|
214
214
|
rows,
|
|
215
215
|
_customSectionCache: {},
|
|
216
|
+
_customSectionRefetchTick: 0,
|
|
216
217
|
// BUG-014 fix: flag whether colors came from the document
|
|
217
218
|
_hasDocumentPageSettings: hasDocumentPageSettings,
|
|
218
219
|
pageSettings: {
|
|
@@ -93,6 +93,10 @@ export function createCanvasActions(set: StoreSet, get: StoreGet) {
|
|
|
93
93
|
selectedProjectCardKey: null,
|
|
94
94
|
// m1 fix: only mark page dirty if the section was actually saved (Session 110)
|
|
95
95
|
isDirty: wasSaved ? true : state.isDirty,
|
|
96
|
+
// Bump refetch tick so CustomSectionInstanceCard re-fetches fresh data
|
|
97
|
+
_customSectionRefetchTick: wasSaved
|
|
98
|
+
? state._customSectionRefetchTick + 1
|
|
99
|
+
: state._customSectionRefetchTick,
|
|
96
100
|
// Reset history back to page context
|
|
97
101
|
_history: [],
|
|
98
102
|
_future: [],
|
package/lib/builder/store.ts
CHANGED
package/lib/builder/types.ts
CHANGED
|
@@ -239,6 +239,10 @@ export interface BuilderState {
|
|
|
239
239
|
* Used by SortableRow to merge base settings with per-instance overrides. */
|
|
240
240
|
_customSectionCache: Record<string, import("../../lib/sanity/types").SectionV2Settings>;
|
|
241
241
|
|
|
242
|
+
/** Counter incremented after a custom section is saved via the section editor.
|
|
243
|
+
* Used as a useEffect dependency in CustomSectionInstanceCard to trigger refetch. */
|
|
244
|
+
_customSectionRefetchTick: number;
|
|
245
|
+
|
|
242
246
|
/** Live preview overlay from color picker — shown on canvas without persisting to Sanity.
|
|
243
247
|
* Set while user is dragging in the color picker; cleared on close. */
|
|
244
248
|
colorPickerPreview: {
|
package/lib/security.ts
CHANGED
|
@@ -359,6 +359,32 @@ export async function decryptToken(stored: string): Promise<string> {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
// ─── Rate Limiting (#32) ──────────────────────────────────────────────────
|
|
362
|
+
//
|
|
363
|
+
// IMPORTANT — this is a best-effort, PER-INSTANCE in-memory limiter. It is
|
|
364
|
+
// intentionally simple and runs entirely inside the Node.js process that
|
|
365
|
+
// handles the request. That design has known blind spots in a serverless
|
|
366
|
+
// environment like Vercel:
|
|
367
|
+
//
|
|
368
|
+
// - Every cold start wipes `rateLimitBuckets`, so a caller that hits a
|
|
369
|
+
// newly-warm instance resets their counter even if they exceeded the
|
|
370
|
+
// limit on a sibling instance moments earlier.
|
|
371
|
+
// - Vercel routes requests across many concurrent invocations. Two
|
|
372
|
+
// requests from the same IP can hit two different instances and both
|
|
373
|
+
// see an empty bucket.
|
|
374
|
+
// - Horizontal scale makes the effective limit ~ maxRequests * N per
|
|
375
|
+
// window, where N is the live instance count.
|
|
376
|
+
//
|
|
377
|
+
// For the backups flow specifically (L-1 in docs/audits/BACKUPS-V2-AUDIT.md),
|
|
378
|
+
// the "1 restore per 5 minutes" guarantee is therefore soft. It still
|
|
379
|
+
// prevents accidental double-submits from the admin UI (same browser, same
|
|
380
|
+
// instance within the keep-alive window) but it is NOT a hard rate limit
|
|
381
|
+
// and MUST NOT be relied on for adversarial scenarios. Auth + CSRF are the
|
|
382
|
+
// real access gates.
|
|
383
|
+
//
|
|
384
|
+
// A stronger implementation would back this with Redis / Upstash / Vercel KV
|
|
385
|
+
// so all invocations share the same counters. That is tracked as a
|
|
386
|
+
// follow-up ("pending infra decision") in the audit doc — not implemented
|
|
387
|
+
// here because it requires an infra choice by the host instance.
|
|
362
388
|
|
|
363
389
|
interface RateLimitBucket {
|
|
364
390
|
count: number;
|
|
@@ -383,6 +409,10 @@ function cleanupRateLimitBuckets(windowMs: number) {
|
|
|
383
409
|
* #32: Simple in-memory rate limiter for mutation endpoints.
|
|
384
410
|
* Returns true if the request should be allowed, false if rate limited.
|
|
385
411
|
*
|
|
412
|
+
* Best-effort semantics only (see block comment above). Do not treat this
|
|
413
|
+
* as a hard security boundary — use it to protect against honest double-
|
|
414
|
+
* submits and accidental loops, not against a determined attacker.
|
|
415
|
+
*
|
|
386
416
|
* @param key - Unique key for the rate limit bucket (e.g. "r2-delete:IP")
|
|
387
417
|
* @param maxRequests - Max requests per window
|
|
388
418
|
* @param windowMs - Window size in milliseconds
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared security utilities for input validation and sanitization.
|
|
3
|
+
* Used across API routes and components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ─── Request Body Size Limits ─────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/** Default max body size for JSON API routes: 1MB */
|
|
9
|
+
export const MAX_JSON_BODY_SIZE = 1 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/** Max body size for page builder saves (larger due to block content): 5MB */
|
|
12
|
+
export const MAX_PAGE_BODY_SIZE = 5 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a request's Content-Length exceeds the specified limit.
|
|
16
|
+
* Returns true if the body is too large.
|
|
17
|
+
* #11: Also rejects requests without Content-Length header (chunked encoding bypass).
|
|
18
|
+
*/
|
|
19
|
+
export function isBodyTooLarge(request: Request, maxBytes: number): boolean {
|
|
20
|
+
const contentLength = request.headers.get("content-length");
|
|
21
|
+
if (!contentLength) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const size = parseInt(contentLength, 10);
|
|
25
|
+
if (isNaN(size) || size > maxBytes) return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
package/lib/storage/types.ts
CHANGED
|
@@ -91,24 +91,46 @@ export interface StorageAdapter {
|
|
|
91
91
|
// ============================================
|
|
92
92
|
|
|
93
93
|
const MIME_TYPES: Record<string, string> = {
|
|
94
|
+
// Raster / vector images
|
|
94
95
|
jpg: "image/jpeg",
|
|
95
96
|
jpeg: "image/jpeg",
|
|
96
97
|
png: "image/png",
|
|
97
98
|
webp: "image/webp",
|
|
99
|
+
avif: "image/avif",
|
|
98
100
|
gif: "image/gif",
|
|
99
101
|
svg: "image/svg+xml",
|
|
102
|
+
// Video
|
|
100
103
|
mp4: "video/mp4",
|
|
101
104
|
webm: "video/webm",
|
|
102
105
|
mov: "video/quicktime",
|
|
106
|
+
// Documents
|
|
103
107
|
pdf: "application/pdf",
|
|
108
|
+
// Fonts — served from R2 when instances use custom typography via
|
|
109
|
+
// the /api/admin/styles/fonts uploader. Correct MIME matters for
|
|
110
|
+
// browser caching, CORS font fetches, and presigned-URL signatures.
|
|
111
|
+
woff: "font/woff",
|
|
112
|
+
woff2: "font/woff2",
|
|
113
|
+
ttf: "font/ttf",
|
|
114
|
+
otf: "font/otf",
|
|
104
115
|
};
|
|
105
116
|
|
|
106
|
-
/** Supported media file extensions (for filtering during scans) */
|
|
117
|
+
/** Supported media file extensions (for filtering during asset-browser scans) */
|
|
107
118
|
export const MEDIA_EXTENSIONS = new Set([
|
|
108
119
|
"jpg", "jpeg", "png", "webp", "gif", "svg",
|
|
109
120
|
"mp4", "webm", "mov",
|
|
110
121
|
]);
|
|
111
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Extensions allowed in backup restore uploads.
|
|
125
|
+
* Superset of MEDIA_EXTENSIONS — includes fonts and documents so a full
|
|
126
|
+
* site backup can round-trip without losing typography or PDF assets.
|
|
127
|
+
*/
|
|
128
|
+
export const BACKUP_ASSET_EXTENSIONS = new Set<string>([
|
|
129
|
+
...MEDIA_EXTENSIONS,
|
|
130
|
+
"pdf",
|
|
131
|
+
"woff", "woff2", "ttf", "otf",
|
|
132
|
+
]);
|
|
133
|
+
|
|
112
134
|
/** Get MIME type from a filename or extension */
|
|
113
135
|
export function getMimeType(filenameOrExt: string): string {
|
|
114
136
|
const ext = filenameOrExt.includes(".")
|
|
@@ -123,3 +145,13 @@ export function isMediaFile(key: string | undefined): boolean {
|
|
|
123
145
|
const ext = key.split(".").pop()?.toLowerCase() || "";
|
|
124
146
|
return MEDIA_EXTENSIONS.has(ext);
|
|
125
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a filename/key is allowed as a backup asset
|
|
151
|
+
* (media files + fonts + PDFs).
|
|
152
|
+
*/
|
|
153
|
+
export function isBackupAssetFile(key: string | undefined): boolean {
|
|
154
|
+
if (!key) return false;
|
|
155
|
+
const ext = key.split(".").pop()?.toLowerCase() || "";
|
|
156
|
+
return BACKUP_ASSET_EXTENSIONS.has(ext);
|
|
157
|
+
}
|
package/lib/version.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Framework version —
|
|
3
|
-
*
|
|
4
|
-
* the
|
|
2
|
+
* Framework version — kept in sync with package.json at publish time by
|
|
3
|
+
* `scripts/prepack.mjs`. Do not edit manually; any change here will be
|
|
4
|
+
* overwritten on the next `npm publish`.
|
|
5
|
+
*
|
|
6
|
+
* Exposed as a plain constant so it can be imported without reading
|
|
7
|
+
* package.json at runtime.
|
|
5
8
|
*/
|
|
6
|
-
export const ANDAMI_VERSION = "0.
|
|
9
|
+
export const ANDAMI_VERSION = "0.2.10";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphika/andami",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -83,6 +83,9 @@
|
|
|
83
83
|
"./lib/editor/*": "./lib/editor/*.ts",
|
|
84
84
|
"./lib/animation/*": "./lib/animation/*.ts",
|
|
85
85
|
"./lib/shader/glsl": "./lib/shader/glsl/index.ts",
|
|
86
|
+
"./lib/backup/manifest": "./lib/backup/manifest.ts",
|
|
87
|
+
"./lib/backup/export": "./lib/backup/export.ts",
|
|
88
|
+
"./lib/backup/r2-helpers": "./lib/backup/r2-helpers.ts",
|
|
86
89
|
"./lib/storage": "./lib/storage/index.ts",
|
|
87
90
|
"./lib/storage/*": "./lib/storage/*.ts",
|
|
88
91
|
"./lib/contexts/*": "./lib/contexts/*.tsx",
|
|
@@ -135,6 +138,7 @@
|
|
|
135
138
|
"./admin/assets": "./admin/assets.ts",
|
|
136
139
|
"./admin/database": "./admin/database.ts",
|
|
137
140
|
"./admin/settings": "./admin/settings.ts",
|
|
141
|
+
"./admin/backups": "./admin/backups.ts",
|
|
138
142
|
"./admin/setup": "./admin/setup.ts",
|
|
139
143
|
"./components/admin/setup-wizard": "./components/admin/setup-wizard/index.ts",
|
|
140
144
|
"./components/admin/setup-wizard/*": "./components/admin/setup-wizard/*.tsx",
|
|
@@ -173,6 +177,13 @@
|
|
|
173
177
|
"./api/projects": "./app/api/projects/route.ts",
|
|
174
178
|
"./api/assets": "./app/api/assets/[...path]/route.ts",
|
|
175
179
|
"./api/custom-sections": "./app/api/custom-sections/[id]/route.ts",
|
|
180
|
+
"./api/admin/backups/export": "./app/api/admin/backups/export/route.ts",
|
|
181
|
+
"./api/admin/backups/status": "./app/api/admin/backups/status/route.ts",
|
|
182
|
+
"./api/admin/backups/restore": "./app/api/admin/backups/restore/route.ts",
|
|
183
|
+
"./api/admin/backups/prepare-export": "./app/api/admin/backups/prepare-export/route.ts",
|
|
184
|
+
"./api/admin/backups/restore-data": "./app/api/admin/backups/restore-data/route.ts",
|
|
185
|
+
"./lib/backup/restore": "./lib/backup/restore.ts",
|
|
186
|
+
"./lib/backup/sanity-ops": "./lib/backup/sanity-ops.ts",
|
|
176
187
|
"./api/draft-mode/enable": "./app/api/draft-mode/enable/route.ts",
|
|
177
188
|
"./api/draft-mode/disable": "./app/api/draft-mode/disable/route.ts"
|
|
178
189
|
},
|
|
@@ -182,6 +193,7 @@
|
|
|
182
193
|
"react-dom": ">=19.0.0"
|
|
183
194
|
},
|
|
184
195
|
"dependencies": {
|
|
196
|
+
"archiver": "^7.0.1",
|
|
185
197
|
"@aws-sdk/client-s3": "^3.1021.0",
|
|
186
198
|
"@aws-sdk/s3-request-presigner": "^3.1021.0",
|
|
187
199
|
"@dnd-kit/core": "^6.3.1",
|
|
@@ -196,12 +208,16 @@
|
|
|
196
208
|
"@tiptap/pm": "^2.12.0",
|
|
197
209
|
"@tiptap/react": "^2.12.0",
|
|
198
210
|
"@tiptap/starter-kit": "^2.12.0",
|
|
211
|
+
"jszip": "^3.10.1",
|
|
199
212
|
"next-sanity": "^12.1.5",
|
|
200
213
|
"ogl": "^1.0.8",
|
|
201
214
|
"sanity": "^5.17.1",
|
|
215
|
+
"unzipper": "^0.12.3",
|
|
202
216
|
"zustand": "^5.0.12"
|
|
203
217
|
},
|
|
204
218
|
"devDependencies": {
|
|
219
|
+
"@types/archiver": "^6.0.3",
|
|
220
|
+
"@types/unzipper": "^0.10.10",
|
|
205
221
|
"@tailwindcss/postcss": "^4",
|
|
206
222
|
"@testing-library/dom": "^10.4.1",
|
|
207
223
|
"@testing-library/jest-dom": "^6.6.3",
|