@structcms/api 0.1.0 → 0.1.1

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.
@@ -49,10 +49,21 @@ module.exports = __toCommonJS(next_exports);
49
49
 
50
50
  // src/media/resolve.ts
51
51
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
52
- var MEDIA_FIELD_SUFFIXES = ["_image", "_media", "_photo", "_thumbnail", "_avatar", "_icon"];
52
+ var MEDIA_FIELD_SUFFIXES = [
53
+ "_image",
54
+ "_media",
55
+ "_photo",
56
+ "_thumbnail",
57
+ "_avatar",
58
+ "_icon",
59
+ "_file",
60
+ "_document",
61
+ "_attachment",
62
+ "_download"
63
+ ];
53
64
  function isMediaField(fieldName) {
54
65
  const lower = fieldName.toLowerCase();
55
- if (lower === "image" || lower === "media" || lower === "photo" || lower === "thumbnail" || lower === "avatar" || lower === "icon") {
66
+ if (lower === "image" || lower === "media" || lower === "photo" || lower === "thumbnail" || lower === "avatar" || lower === "icon" || lower === "file" || lower === "document" || lower === "attachment" || lower === "download") {
56
67
  return true;
57
68
  }
58
69
  return MEDIA_FIELD_SUFFIXES.some((suffix) => lower.endsWith(suffix));
@@ -123,6 +134,7 @@ async function handleGetPageBySlug(adapter, mediaAdapter, slug) {
123
134
  }
124
135
 
125
136
  // src/media/types.ts
137
+ var MAX_FILE_SIZE = 50 * 1024 * 1024;
126
138
  var ALLOWED_MIME_TYPES = [
127
139
  "image/jpeg",
128
140
  "image/png",
@@ -130,6 +142,23 @@ var ALLOWED_MIME_TYPES = [
130
142
  "image/webp",
131
143
  "image/svg+xml"
132
144
  ];
145
+ var ALLOWED_DOCUMENT_MIME_TYPES = [
146
+ "application/pdf",
147
+ "application/msword",
148
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
149
+ "application/vnd.ms-excel",
150
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
151
+ "application/vnd.ms-powerpoint",
152
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
153
+ "text/plain",
154
+ "text/csv",
155
+ "application/zip",
156
+ "application/gzip"
157
+ ];
158
+ var ALL_ALLOWED_MIME_TYPES = [
159
+ ...ALLOWED_MIME_TYPES,
160
+ ...ALLOWED_DOCUMENT_MIME_TYPES
161
+ ];
133
162
 
134
163
  // src/media/handlers.ts
135
164
  var MediaValidationError = class extends Error {
@@ -140,16 +169,25 @@ var MediaValidationError = class extends Error {
140
169
  }
141
170
  };
142
171
  function validateMimeType(mimeType) {
143
- const allowed = ALLOWED_MIME_TYPES;
172
+ const allowed = ALL_ALLOWED_MIME_TYPES;
144
173
  if (!allowed.includes(mimeType)) {
145
174
  throw new MediaValidationError(
146
- `Invalid file type: ${mimeType}. Allowed types: ${ALLOWED_MIME_TYPES.join(", ")}`,
175
+ `Invalid file type: ${mimeType}. Allowed types: ${ALL_ALLOWED_MIME_TYPES.join(", ")}`,
147
176
  "INVALID_MIME_TYPE"
148
177
  );
149
178
  }
150
179
  }
180
+ function validateFileSize(size) {
181
+ if (size > MAX_FILE_SIZE) {
182
+ throw new MediaValidationError(
183
+ `File size exceeds maximum allowed size of ${MAX_FILE_SIZE / 1024 / 1024}MB`,
184
+ "FILE_TOO_LARGE"
185
+ );
186
+ }
187
+ }
151
188
  async function handleUploadMedia(adapter, input) {
152
189
  validateMimeType(input.mimeType);
190
+ validateFileSize(input.size);
153
191
  return adapter.upload(input);
154
192
  }
155
193
  async function handleGetMedia(adapter, id) {
@@ -196,6 +234,12 @@ var SANITIZE_OPTIONS = {
196
234
  function sanitizeString(value) {
197
235
  return (0, import_sanitize_html.default)(value, SANITIZE_OPTIONS);
198
236
  }
237
+ function stripTags(value) {
238
+ return (0, import_sanitize_html.default)(value, {
239
+ allowedTags: [],
240
+ allowedAttributes: {}
241
+ });
242
+ }
199
243
  function sanitizeValue(value) {
200
244
  if (typeof value === "string") {
201
245
  return sanitizeString(value);
@@ -262,10 +306,11 @@ var StorageValidationError = class extends Error {
262
306
  }
263
307
  };
264
308
  async function handleCreatePage(adapter, input) {
265
- if (!input.title.trim()) {
309
+ const sanitizedTitle = stripTags(input.title).trim();
310
+ if (!sanitizedTitle) {
266
311
  throw new StorageValidationError("Page title must not be empty", "EMPTY_TITLE");
267
312
  }
268
- const slug = input.slug?.trim() || generateSlug(input.title);
313
+ const slug = input.slug?.trim() || generateSlug(sanitizedTitle);
269
314
  if (!slug) {
270
315
  throw new StorageValidationError(
271
316
  "Could not generate a valid slug from the provided title",
@@ -278,6 +323,7 @@ async function handleCreatePage(adapter, input) {
278
323
  const sanitizedSections = input.sections ? sanitizeSectionData(input.sections) : void 0;
279
324
  return adapter.createPage({
280
325
  ...input,
326
+ title: sanitizedTitle,
281
327
  slug: uniqueSlug,
282
328
  sections: sanitizedSections
283
329
  });
@@ -286,8 +332,12 @@ async function handleUpdatePage(adapter, input) {
286
332
  if (!input.id.trim()) {
287
333
  throw new StorageValidationError("Page ID must not be empty", "EMPTY_ID");
288
334
  }
289
- if (input.title !== void 0 && !input.title.trim()) {
290
- throw new StorageValidationError("Page title must not be empty", "EMPTY_TITLE");
335
+ let sanitizedTitle;
336
+ if (input.title !== void 0) {
337
+ sanitizedTitle = stripTags(input.title).trim();
338
+ if (!sanitizedTitle) {
339
+ throw new StorageValidationError("Page title must not be empty", "EMPTY_TITLE");
340
+ }
291
341
  }
292
342
  if (input.slug !== void 0) {
293
343
  const slug = input.slug.trim();
@@ -300,7 +350,11 @@ async function handleUpdatePage(adapter, input) {
300
350
  throw new StorageValidationError(`Slug "${slug}" is already in use`, "DUPLICATE_SLUG");
301
351
  }
302
352
  }
303
- const sanitizedInput = input.sections ? { ...input, sections: sanitizeSectionData(input.sections) } : input;
353
+ const sanitizedInput = {
354
+ ...input,
355
+ ...sanitizedTitle !== void 0 && { title: sanitizedTitle },
356
+ ...input.sections && { sections: sanitizeSectionData(input.sections) }
357
+ };
304
358
  return adapter.updatePage(sanitizedInput);
305
359
  }
306
360
  async function handleDeletePage(adapter, id) {
@@ -310,38 +364,46 @@ async function handleDeletePage(adapter, id) {
310
364
  return adapter.deletePage(id);
311
365
  }
312
366
  async function handleCreateNavigation(adapter, input) {
313
- if (!input.name.trim()) {
367
+ const sanitizedName = stripTags(input.name).trim();
368
+ if (!sanitizedName) {
314
369
  throw new StorageValidationError("Navigation name must not be empty", "EMPTY_NAME");
315
370
  }
316
371
  const existingNavigations = await adapter.listNavigations();
317
372
  const existingNames = existingNavigations.map((n) => n.name);
318
- if (existingNames.includes(input.name.trim())) {
373
+ if (existingNames.includes(sanitizedName)) {
319
374
  throw new StorageValidationError(
320
- `Navigation name "${input.name.trim()}" is already in use`,
375
+ `Navigation name "${sanitizedName}" is already in use`,
321
376
  "DUPLICATE_NAME"
322
377
  );
323
378
  }
324
- return adapter.createNavigation(input);
379
+ return adapter.createNavigation({
380
+ ...input,
381
+ name: sanitizedName
382
+ });
325
383
  }
326
384
  async function handleUpdateNavigation(adapter, input) {
327
385
  if (!input.id.trim()) {
328
386
  throw new StorageValidationError("Navigation ID must not be empty", "EMPTY_ID");
329
387
  }
388
+ let sanitizedName;
330
389
  if (input.name !== void 0) {
331
- const name = input.name.trim();
332
- if (!name) {
390
+ sanitizedName = stripTags(input.name).trim();
391
+ if (!sanitizedName) {
333
392
  throw new StorageValidationError("Navigation name must not be empty", "EMPTY_NAME");
334
393
  }
335
394
  const existingNavigations = await adapter.listNavigations();
336
395
  const existingNames = existingNavigations.filter((n) => n.id !== input.id).map((n) => n.name);
337
- if (existingNames.includes(name)) {
396
+ if (existingNames.includes(sanitizedName)) {
338
397
  throw new StorageValidationError(
339
- `Navigation name "${name}" is already in use`,
398
+ `Navigation name "${sanitizedName}" is already in use`,
340
399
  "DUPLICATE_NAME"
341
400
  );
342
401
  }
343
402
  }
344
- return adapter.updateNavigation(input);
403
+ return adapter.updateNavigation({
404
+ ...input,
405
+ ...sanitizedName !== void 0 && { name: sanitizedName }
406
+ });
345
407
  }
346
408
  async function handleDeleteNavigation(adapter, id) {
347
409
  if (!id.trim()) {
@@ -377,7 +439,7 @@ function errorResponse(error, fallbackStatus = 500) {
377
439
  if (error instanceof StorageValidationError || error instanceof MediaValidationError || error instanceof SyntaxError) {
378
440
  return jsonResponse({ error: getErrorMessage(error) }, 400);
379
441
  }
380
- return jsonResponse({ error: getErrorMessage(error) }, fallbackStatus);
442
+ return jsonResponse({ error: "Internal Server Error" }, fallbackStatus);
381
443
  }
382
444
  async function resolveParams(context) {
383
445
  return context.params;
@@ -701,11 +763,51 @@ function createNextPageByIdRoute(config) {
701
763
  }
702
764
  };
703
765
  }
766
+ function getQueryParam(url, name) {
767
+ const queryStart = url.indexOf("?");
768
+ if (queryStart === -1) return null;
769
+ for (const pair of url.slice(queryStart + 1).split("&")) {
770
+ const eqIndex = pair.indexOf("=");
771
+ const key = decodeURIComponent(eqIndex === -1 ? pair : pair.slice(0, eqIndex));
772
+ if (key === name) return eqIndex === -1 ? "" : decodeURIComponent(pair.slice(eqIndex + 1));
773
+ }
774
+ return null;
775
+ }
776
+ function parseMediaFilter(request) {
777
+ if (!request.url) {
778
+ return void 0;
779
+ }
780
+ const category = getQueryParam(request.url, "category");
781
+ const limit = getQueryParam(request.url, "limit");
782
+ const offset = getQueryParam(request.url, "offset");
783
+ const filter = {};
784
+ let hasFilter = false;
785
+ if (category === "image" || category === "document") {
786
+ filter.category = category;
787
+ hasFilter = true;
788
+ }
789
+ if (limit) {
790
+ const parsed = Number.parseInt(limit, 10);
791
+ if (!Number.isNaN(parsed) && parsed > 0) {
792
+ filter.limit = parsed;
793
+ hasFilter = true;
794
+ }
795
+ }
796
+ if (offset) {
797
+ const parsed = Number.parseInt(offset, 10);
798
+ if (!Number.isNaN(parsed) && parsed >= 0) {
799
+ filter.offset = parsed;
800
+ hasFilter = true;
801
+ }
802
+ }
803
+ return hasFilter ? filter : void 0;
804
+ }
704
805
  function createNextMediaRoute(config) {
705
806
  return {
706
- GET: async (_request) => {
807
+ GET: async (request) => {
707
808
  try {
708
- const media = await handleListMedia(config.mediaAdapter);
809
+ const filter = parseMediaFilter(request);
810
+ const media = await handleListMedia(config.mediaAdapter, filter);
709
811
  return jsonResponse(media);
710
812
  } catch (error) {
711
813
  return errorResponse(error);
@@ -966,8 +1068,10 @@ function createNextAuthOAuthRoute(config, Response) {
966
1068
  { status: 400 }
967
1069
  );
968
1070
  }
969
- const message = err instanceof Error ? err.message : "OAuth initialization failed";
970
- return Response.json({ error: { message, code: "OAUTH_ERROR" } }, { status: 500 });
1071
+ return Response.json(
1072
+ { error: { message: "Internal Server Error", code: "OAUTH_ERROR" } },
1073
+ { status: 500 }
1074
+ );
971
1075
  }
972
1076
  };
973
1077
  }
@@ -985,8 +1089,10 @@ function createNextAuthSignInRoute(config, Response) {
985
1089
  { status: 400 }
986
1090
  );
987
1091
  }
988
- const message = err instanceof Error ? err.message : "Sign in failed";
989
- return Response.json({ error: { message, code: "AUTH_ERROR" } }, { status: 401 });
1092
+ return Response.json(
1093
+ { error: { message: "Invalid credentials", code: "AUTH_ERROR" } },
1094
+ { status: 401 }
1095
+ );
990
1096
  }
991
1097
  };
992
1098
  }
@@ -1003,8 +1109,10 @@ function createNextAuthSignOutRoute(config, Response) {
1003
1109
  await handleSignOut(config.authAdapter, token);
1004
1110
  return Response.json({ message: "Signed out successfully" }, { status: 200 });
1005
1111
  } catch (error) {
1006
- const message = error instanceof Error ? error.message : "Sign out failed";
1007
- return Response.json({ error: { message, code: "SIGNOUT_ERROR" } }, { status: 500 });
1112
+ return Response.json(
1113
+ { error: { message: "Internal Server Error", code: "SIGNOUT_ERROR" } },
1114
+ { status: 500 }
1115
+ );
1008
1116
  }
1009
1117
  };
1010
1118
  }
@@ -1027,8 +1135,10 @@ function createNextAuthVerifyRoute(config, Response) {
1027
1135
  }
1028
1136
  return Response.json(user, { status: 200 });
1029
1137
  } catch (error) {
1030
- const message = error instanceof Error ? error.message : "Verification failed";
1031
- return Response.json({ error: { message, code: "VERIFY_ERROR" } }, { status: 401 });
1138
+ return Response.json(
1139
+ { error: { message: "Authentication failed", code: "VERIFY_ERROR" } },
1140
+ { status: 401 }
1141
+ );
1032
1142
  }
1033
1143
  };
1034
1144
  }
@@ -1046,8 +1156,10 @@ function createNextAuthRefreshRoute(config, Response) {
1046
1156
  const session = await handleRefreshSession(config.authAdapter, refreshToken);
1047
1157
  return Response.json(session, { status: 200 });
1048
1158
  } catch (error) {
1049
- const message = error instanceof Error ? error.message : "Refresh failed";
1050
- return Response.json({ error: { message, code: "REFRESH_ERROR" } }, { status: 401 });
1159
+ return Response.json(
1160
+ { error: { message: "Session refresh failed", code: "REFRESH_ERROR" } },
1161
+ { status: 401 }
1162
+ );
1051
1163
  }
1052
1164
  };
1053
1165
  }
@@ -1070,8 +1182,10 @@ function createNextAuthCurrentUserRoute(config, Response) {
1070
1182
  }
1071
1183
  return Response.json(user, { status: 200 });
1072
1184
  } catch (error) {
1073
- const message = error instanceof Error ? error.message : "Failed to get user";
1074
- return Response.json({ error: { message, code: "GET_USER_ERROR" } }, { status: 500 });
1185
+ return Response.json(
1186
+ { error: { message: "Internal Server Error", code: "GET_USER_ERROR" } },
1187
+ { status: 500 }
1188
+ );
1075
1189
  }
1076
1190
  };
1077
1191
  }