@withwiz/cms-kit 0.1.0 → 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 (101) hide show
  1. package/README.ko.md +4 -2
  2. package/README.md +4 -2
  3. package/dist/{chunk-JMWXVXK5.js → chunk-25GMHPCW.js} +12 -4
  4. package/dist/chunk-25GMHPCW.js.map +1 -0
  5. package/dist/{chunk-5JUIMXNF.js → chunk-2VHNZWH2.js} +3 -3
  6. package/dist/{chunk-5JUIMXNF.js.map → chunk-2VHNZWH2.js.map} +1 -1
  7. package/dist/{chunk-BWKKHCDG.mjs → chunk-3UXX27N5.mjs} +44 -3
  8. package/dist/{chunk-BWKKHCDG.mjs.map → chunk-3UXX27N5.mjs.map} +1 -1
  9. package/dist/{chunk-T7EVFD4G.mjs → chunk-47DPXAO6.mjs} +2 -2
  10. package/dist/{chunk-4W3X7OKF.js → chunk-5Z5XUR2K.js} +48 -27
  11. package/dist/chunk-5Z5XUR2K.js.map +1 -0
  12. package/dist/{chunk-LDFE33ML.mjs → chunk-7F5YFG2W.mjs} +10 -2
  13. package/dist/chunk-7F5YFG2W.mjs.map +1 -0
  14. package/dist/{chunk-CGVFB23E.mjs → chunk-A557FA2K.mjs} +24 -19
  15. package/dist/{chunk-CGVFB23E.mjs.map → chunk-A557FA2K.mjs.map} +1 -1
  16. package/dist/{chunk-OBNALAMG.mjs → chunk-BG4C5KZO.mjs} +2 -2
  17. package/dist/{chunk-DCIF4XJL.js → chunk-EPVV6655.js} +10 -10
  18. package/dist/{chunk-DCIF4XJL.js.map → chunk-EPVV6655.js.map} +1 -1
  19. package/dist/{chunk-LF5LOI7K.mjs → chunk-FCWUNCGA.mjs} +3 -3
  20. package/dist/{chunk-YEQFKOWD.mjs → chunk-GQJNLE34.mjs} +2 -2
  21. package/dist/{chunk-W4FWIZ2Y.js → chunk-HVB72GT3.js} +3 -3
  22. package/dist/{chunk-W4FWIZ2Y.js.map → chunk-HVB72GT3.js.map} +1 -1
  23. package/dist/{chunk-KWKV2GAH.js → chunk-J32BTYJZ.js} +4 -4
  24. package/dist/{chunk-KWKV2GAH.js.map → chunk-J32BTYJZ.js.map} +1 -1
  25. package/dist/{chunk-C5WFZJM4.js → chunk-JPIQU2C4.js} +6 -6
  26. package/dist/{chunk-C5WFZJM4.js.map → chunk-JPIQU2C4.js.map} +1 -1
  27. package/dist/{chunk-FEPGJ7WN.mjs → chunk-MN27HJKW.mjs} +3 -3
  28. package/dist/{chunk-ZULSFSDG.mjs → chunk-MWX5B4DP.mjs} +3 -3
  29. package/dist/{chunk-FCCCG5Z7.mjs → chunk-MXXC4U3D.mjs} +2 -2
  30. package/dist/{chunk-5RDMODGV.mjs → chunk-N7LMNP4W.mjs} +2 -2
  31. package/dist/{chunk-YY7K5CXX.js → chunk-OHJISVGC.js} +5 -5
  32. package/dist/{chunk-YY7K5CXX.js.map → chunk-OHJISVGC.js.map} +1 -1
  33. package/dist/{chunk-EM4MQEWV.mjs → chunk-SN3XOMF6.mjs} +45 -24
  34. package/dist/chunk-SN3XOMF6.mjs.map +1 -0
  35. package/dist/{chunk-BE7JDL64.js → chunk-TKOFGRU4.js} +11 -11
  36. package/dist/{chunk-BE7JDL64.js.map → chunk-TKOFGRU4.js.map} +1 -1
  37. package/dist/{chunk-EG6NVPP2.js → chunk-UVZD7GHG.js} +44 -3
  38. package/dist/chunk-UVZD7GHG.js.map +1 -0
  39. package/dist/{chunk-6EZ3QE5N.js → chunk-Y3YYIJYG.js} +3 -3
  40. package/dist/{chunk-6EZ3QE5N.js.map → chunk-Y3YYIJYG.js.map} +1 -1
  41. package/dist/{chunk-OYJRR3G2.js → chunk-ZMXNPYFS.js} +25 -20
  42. package/dist/chunk-ZMXNPYFS.js.map +1 -0
  43. package/dist/components/AdminManagerBase.js +5 -5
  44. package/dist/components/AdminManagerBase.mjs +4 -4
  45. package/dist/components/AdminShell.d.mts +1 -1
  46. package/dist/components/AdminShell.d.ts +1 -1
  47. package/dist/components/AdminShell.js +4 -4
  48. package/dist/components/AdminShell.mjs +3 -3
  49. package/dist/components/index.d.mts +1 -1
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.js +8 -8
  52. package/dist/components/index.mjs +7 -7
  53. package/dist/hooks/index.js +5 -5
  54. package/dist/hooks/index.mjs +4 -4
  55. package/dist/hooks/useImageDropZone.js +4 -4
  56. package/dist/hooks/useImageDropZone.mjs +3 -3
  57. package/dist/{index-00a40-FQ.d.ts → index-Cce8FHtI.d.mts} +38 -1
  58. package/dist/{index-00a40-FQ.d.mts → index-Cce8FHtI.d.ts} +38 -1
  59. package/dist/index.d.mts +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +19 -13
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.mjs +18 -12
  64. package/dist/infrastructure/index.js +3 -3
  65. package/dist/infrastructure/index.mjs +2 -2
  66. package/dist/infrastructure/middleware/index.js +3 -3
  67. package/dist/infrastructure/middleware/index.mjs +2 -2
  68. package/dist/infrastructure/middleware/wrappers.js +3 -3
  69. package/dist/infrastructure/middleware/wrappers.mjs +2 -2
  70. package/dist/services/index.js +5 -5
  71. package/dist/services/index.mjs +4 -4
  72. package/dist/utils/admin-fetch.js +3 -3
  73. package/dist/utils/admin-fetch.mjs +2 -2
  74. package/dist/utils/html-sanitizer.js +3 -3
  75. package/dist/utils/html-sanitizer.mjs +2 -2
  76. package/dist/utils/index.d.mts +1 -1
  77. package/dist/utils/index.d.ts +1 -1
  78. package/dist/utils/index.js +13 -7
  79. package/dist/utils/index.js.map +1 -1
  80. package/dist/utils/index.mjs +12 -6
  81. package/dist/utils/jwt.js +3 -3
  82. package/dist/utils/jwt.mjs +2 -2
  83. package/dist/utils/r2-helpers.js +4 -4
  84. package/dist/utils/r2-helpers.mjs +3 -3
  85. package/dist/utils/r2-storage.js +3 -3
  86. package/dist/utils/r2-storage.mjs +2 -2
  87. package/package.json +4 -4
  88. package/dist/chunk-4W3X7OKF.js.map +0 -1
  89. package/dist/chunk-EG6NVPP2.js.map +0 -1
  90. package/dist/chunk-EM4MQEWV.mjs.map +0 -1
  91. package/dist/chunk-JMWXVXK5.js.map +0 -1
  92. package/dist/chunk-LDFE33ML.mjs.map +0 -1
  93. package/dist/chunk-OYJRR3G2.js.map +0 -1
  94. /package/dist/{chunk-T7EVFD4G.mjs.map → chunk-47DPXAO6.mjs.map} +0 -0
  95. /package/dist/{chunk-OBNALAMG.mjs.map → chunk-BG4C5KZO.mjs.map} +0 -0
  96. /package/dist/{chunk-LF5LOI7K.mjs.map → chunk-FCWUNCGA.mjs.map} +0 -0
  97. /package/dist/{chunk-YEQFKOWD.mjs.map → chunk-GQJNLE34.mjs.map} +0 -0
  98. /package/dist/{chunk-FEPGJ7WN.mjs.map → chunk-MN27HJKW.mjs.map} +0 -0
  99. /package/dist/{chunk-ZULSFSDG.mjs.map → chunk-MWX5B4DP.mjs.map} +0 -0
  100. /package/dist/{chunk-FCCCG5Z7.mjs.map → chunk-MXXC4U3D.mjs.map} +0 -0
  101. /package/dist/{chunk-5RDMODGV.mjs.map → chunk-N7LMNP4W.mjs.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-YY7K5CXX.js","../src/hooks/useImageDropZone.ts"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-YY7K5CXX.js","sourcesContent":[null,"\"use client\";\n\nimport { useState, useCallback, useRef, type DragEvent } from \"react\";\nimport { adminFetch } from \"../utils/admin-fetch\";\nimport {\n resizeImageIfNeeded,\n validateImageSize,\n} from \"../utils/image-resize\";\nimport { resolveRouteConfig } from \"../config\";\n\nconst ALLOWED_TYPES = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"];\nconst MAX_UPLOAD_SIZE = 5 * 1024 * 1024; // 5MB\n\ninterface ImageVariantUrls {\n lg?: string;\n md?: string;\n sm?: string;\n thumb?: string;\n}\n\ninterface UploadResult {\n url: string;\n key: string;\n size: number;\n variants?: ImageVariantUrls;\n variantKeys?: string[];\n}\n\ninterface UseImageDropZoneOptions {\n multiple?: boolean;\n maxFiles?: number;\n onUpload: (result: UploadResult) => void;\n onKeyTracked?: (key: string) => void;\n disabled?: boolean;\n}\n\nfunction validateFile(file: File): string | null {\n if (!ALLOWED_TYPES.includes(file.type)) {\n const allowed = ALLOWED_TYPES.map((t) => t.split(\"/\")[1]).join(\", \");\n return `지원하지 않는 파일 형식입니다. (허용: ${allowed})`;\n }\n const sizeError = validateImageSize(file);\n if (sizeError) return sizeError;\n return null;\n}\n\nasync function uploadSingleFile(file: File): Promise<UploadResult> {\n const fd = new FormData();\n fd.append(\"file\", file);\n // upload 엔드포인트는 §5 config boundary 에서 해석된다 (consumer override\n // 가능; 미설정 시 레거시 기본 경로 — spec.md §4.1 C2 / B5).\n const { uploadEndpoint } = resolveRouteConfig();\n const res = await adminFetch(uploadEndpoint, {\n method: \"POST\",\n body: fd,\n });\n const json = await res.json();\n if (!json.success) {\n throw new Error(json.error?.message || \"업로드 실패\");\n }\n return json.data;\n}\n\nexport function useImageDropZone(options: UseImageDropZoneOptions) {\n const {\n multiple = false,\n maxFiles,\n onUpload,\n onKeyTracked,\n disabled = false,\n } = options;\n\n const [isDragOver, setIsDragOver] = useState(false);\n const [isUploading, setIsUploading] = useState(false);\n const [isResizing, setIsResizing] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const dragCounterRef = useRef(0);\n\n const processFiles = useCallback(\n async (files: File[]) => {\n if (disabled || files.length === 0) return;\n\n let toProcess = multiple ? files : [files[0]];\n\n if (maxFiles && toProcess.length > maxFiles) {\n toProcess = toProcess.slice(0, maxFiles);\n setError(`최대 ${maxFiles}개까지 업로드할 수 있습니다.`);\n }\n\n for (const file of toProcess) {\n const validationError = validateFile(file);\n if (validationError) {\n setError(validationError);\n return;\n }\n }\n\n setError(null);\n\n setIsResizing(true);\n let resizedFiles: File[];\n try {\n resizedFiles = [];\n for (const file of toProcess) {\n const result = await resizeImageIfNeeded(file);\n if (result.wasResized && process.env.NODE_ENV === \"development\") {\n console.log(\n `[image-optimize] ${(result.originalSize / 1024 / 1024).toFixed(1)}MB → ${(result.newSize / 1024 / 1024).toFixed(1)}MB`\n );\n }\n if (result.file.size > MAX_UPLOAD_SIZE) {\n setError(\n `이미지 최적화 후에도 5MB를 초과합니다. (${(result.file.size / 1024 / 1024).toFixed(1)}MB) 더 작은 이미지를 사용해 주세요.`\n );\n setIsResizing(false);\n return;\n }\n resizedFiles.push(result.file);\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : \"이미지 최적화 중 오류 발생\";\n setError(msg);\n setIsResizing(false);\n return;\n }\n setIsResizing(false);\n\n setIsUploading(true);\n try {\n for (const file of resizedFiles) {\n const result = await uploadSingleFile(file);\n onUpload(result);\n if (result.key) onKeyTracked?.(result.key);\n if (result.variantKeys) {\n for (const vk of result.variantKeys) onKeyTracked?.(vk);\n }\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : \"업로드 중 오류 발생\";\n setError(msg);\n } finally {\n setIsUploading(false);\n }\n },\n [multiple, maxFiles, onUpload, onKeyTracked, disabled]\n );\n\n const onDragEnter = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current++;\n if (e.dataTransfer?.types.includes(\"Files\")) {\n setIsDragOver(true);\n }\n }, []);\n\n const onDragOver = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n }, []);\n\n const onDragLeave = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current--;\n if (dragCounterRef.current === 0) {\n setIsDragOver(false);\n }\n }, []);\n\n const onDrop = useCallback(\n (e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current = 0;\n setIsDragOver(false);\n\n if (disabled) return;\n\n const files = Array.from(e.dataTransfer?.files || []).filter((f) =>\n f.type.startsWith(\"image/\")\n );\n\n if (files.length === 0) {\n setError(\"이미지 파일만 업로드할 수 있습니다.\");\n return;\n }\n\n processFiles(files);\n },\n [processFiles, disabled]\n );\n\n const handleFileInput = useCallback(\n async (fileList: FileList | null) => {\n const files = Array.from(fileList || []);\n if (files.length > 0) {\n await processFiles(files);\n }\n },\n [processFiles]\n );\n\n return {\n isDragOver,\n isUploading,\n isResizing,\n error,\n dragHandlers: { onDragEnter, onDragOver, onDragLeave, onDrop },\n handleFileInput,\n };\n}\n"]}
1
+ {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-OHJISVGC.js","../src/hooks/useImageDropZone.ts"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-OHJISVGC.js","sourcesContent":[null,"\"use client\";\n\nimport { useState, useCallback, useRef, type DragEvent } from \"react\";\nimport { adminFetch } from \"../utils/admin-fetch\";\nimport {\n resizeImageIfNeeded,\n validateImageSize,\n} from \"../utils/image-resize\";\nimport { resolveRouteConfig } from \"../config\";\n\nconst ALLOWED_TYPES = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"];\nconst MAX_UPLOAD_SIZE = 5 * 1024 * 1024; // 5MB\n\ninterface ImageVariantUrls {\n lg?: string;\n md?: string;\n sm?: string;\n thumb?: string;\n}\n\ninterface UploadResult {\n url: string;\n key: string;\n size: number;\n variants?: ImageVariantUrls;\n variantKeys?: string[];\n}\n\ninterface UseImageDropZoneOptions {\n multiple?: boolean;\n maxFiles?: number;\n onUpload: (result: UploadResult) => void;\n onKeyTracked?: (key: string) => void;\n disabled?: boolean;\n}\n\nfunction validateFile(file: File): string | null {\n if (!ALLOWED_TYPES.includes(file.type)) {\n const allowed = ALLOWED_TYPES.map((t) => t.split(\"/\")[1]).join(\", \");\n return `지원하지 않는 파일 형식입니다. (허용: ${allowed})`;\n }\n const sizeError = validateImageSize(file);\n if (sizeError) return sizeError;\n return null;\n}\n\nasync function uploadSingleFile(file: File): Promise<UploadResult> {\n const fd = new FormData();\n fd.append(\"file\", file);\n // upload 엔드포인트는 §5 config boundary 에서 해석된다 (consumer override\n // 가능; 미설정 시 레거시 기본 경로 — spec.md §4.1 C2 / B5).\n const { uploadEndpoint } = resolveRouteConfig();\n const res = await adminFetch(uploadEndpoint, {\n method: \"POST\",\n body: fd,\n });\n const json = await res.json();\n if (!json.success) {\n throw new Error(json.error?.message || \"업로드 실패\");\n }\n return json.data;\n}\n\nexport function useImageDropZone(options: UseImageDropZoneOptions) {\n const {\n multiple = false,\n maxFiles,\n onUpload,\n onKeyTracked,\n disabled = false,\n } = options;\n\n const [isDragOver, setIsDragOver] = useState(false);\n const [isUploading, setIsUploading] = useState(false);\n const [isResizing, setIsResizing] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const dragCounterRef = useRef(0);\n\n const processFiles = useCallback(\n async (files: File[]) => {\n if (disabled || files.length === 0) return;\n\n let toProcess = multiple ? files : [files[0]];\n\n if (maxFiles && toProcess.length > maxFiles) {\n toProcess = toProcess.slice(0, maxFiles);\n setError(`최대 ${maxFiles}개까지 업로드할 수 있습니다.`);\n }\n\n for (const file of toProcess) {\n const validationError = validateFile(file);\n if (validationError) {\n setError(validationError);\n return;\n }\n }\n\n setError(null);\n\n setIsResizing(true);\n let resizedFiles: File[];\n try {\n resizedFiles = [];\n for (const file of toProcess) {\n const result = await resizeImageIfNeeded(file);\n if (result.wasResized && process.env.NODE_ENV === \"development\") {\n console.log(\n `[image-optimize] ${(result.originalSize / 1024 / 1024).toFixed(1)}MB → ${(result.newSize / 1024 / 1024).toFixed(1)}MB`\n );\n }\n if (result.file.size > MAX_UPLOAD_SIZE) {\n setError(\n `이미지 최적화 후에도 5MB를 초과합니다. (${(result.file.size / 1024 / 1024).toFixed(1)}MB) 더 작은 이미지를 사용해 주세요.`\n );\n setIsResizing(false);\n return;\n }\n resizedFiles.push(result.file);\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : \"이미지 최적화 중 오류 발생\";\n setError(msg);\n setIsResizing(false);\n return;\n }\n setIsResizing(false);\n\n setIsUploading(true);\n try {\n for (const file of resizedFiles) {\n const result = await uploadSingleFile(file);\n onUpload(result);\n if (result.key) onKeyTracked?.(result.key);\n if (result.variantKeys) {\n for (const vk of result.variantKeys) onKeyTracked?.(vk);\n }\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : \"업로드 중 오류 발생\";\n setError(msg);\n } finally {\n setIsUploading(false);\n }\n },\n [multiple, maxFiles, onUpload, onKeyTracked, disabled]\n );\n\n const onDragEnter = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current++;\n if (e.dataTransfer?.types.includes(\"Files\")) {\n setIsDragOver(true);\n }\n }, []);\n\n const onDragOver = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n }, []);\n\n const onDragLeave = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current--;\n if (dragCounterRef.current === 0) {\n setIsDragOver(false);\n }\n }, []);\n\n const onDrop = useCallback(\n (e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current = 0;\n setIsDragOver(false);\n\n if (disabled) return;\n\n const files = Array.from(e.dataTransfer?.files || []).filter((f) =>\n f.type.startsWith(\"image/\")\n );\n\n if (files.length === 0) {\n setError(\"이미지 파일만 업로드할 수 있습니다.\");\n return;\n }\n\n processFiles(files);\n },\n [processFiles, disabled]\n );\n\n const handleFileInput = useCallback(\n async (fileList: FileList | null) => {\n const files = Array.from(fileList || []);\n if (files.length > 0) {\n await processFiles(files);\n }\n },\n [processFiles]\n );\n\n return {\n isDragOver,\n isUploading,\n isResizing,\n error,\n dragHandlers: { onDragEnter, onDragOver, onDragLeave, onDrop },\n handleFileInput,\n };\n}\n"]}
@@ -3,48 +3,69 @@ import {
3
3
  } from "./chunk-MMKAMH4U.mjs";
4
4
  import {
5
5
  deleteFromR2
6
- } from "./chunk-YEQFKOWD.mjs";
6
+ } from "./chunk-GQJNLE34.mjs";
7
7
  import {
8
+ resolveR2CredentialsConfig,
9
+ resolveR2PublicUrl,
8
10
  resolveStorageConfig,
9
11
  warnOnceMissingConfig
10
- } from "./chunk-BWKKHCDG.mjs";
12
+ } from "./chunk-3UXX27N5.mjs";
11
13
 
12
14
  // src/utils/r2-helpers.ts
13
15
  import { logError } from "@withwiz/toolkit/core/logger/logger";
14
16
  var IMG_SRC_REGEX = /<img[^>]+src=["']([^"']+)["']/gi;
15
17
  var VARIANT_SUFFIXES = Object.keys(IMAGE_VARIANT_SIZES);
16
- function toRelativePath(src) {
17
- let path;
18
- try {
19
- const u = new URL(src);
20
- path = u.pathname;
21
- } catch {
22
- path = src;
18
+ function isAbsoluteUrl(src) {
19
+ return /^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//");
20
+ }
21
+ function normalizePath(path) {
22
+ const p = path.replace(/^\/+/, "").split(/[?#]/)[0];
23
+ return p.length > 0 ? p : null;
24
+ }
25
+ function resolveAllowedBases(configuredBase) {
26
+ const bases = /* @__PURE__ */ new Set();
27
+ const add = (b) => {
28
+ if (typeof b === "string" && b.trim().length > 0) {
29
+ bases.add(b.trim().replace(/\/+$/, ""));
30
+ }
31
+ };
32
+ add(configuredBase);
33
+ add(resolveR2PublicUrl());
34
+ const { bucketName } = resolveR2CredentialsConfig();
35
+ if (bucketName) add(`https://${bucketName}.r2.dev`);
36
+ return Array.from(bases);
37
+ }
38
+ function stripAllowedBase(src, bases) {
39
+ for (const base of bases) {
40
+ if (src === base) return null;
41
+ if (src.startsWith(base + "/") || src.startsWith(base + "?") || src.startsWith(base + "#")) {
42
+ return normalizePath(src.slice(base.length));
43
+ }
23
44
  }
24
- path = path.replace(/^\/+/, "").split(/[?#]/)[0];
25
- return path.length > 0 ? path : null;
45
+ return null;
26
46
  }
27
- function srcToKey(src, rule) {
28
- if (rule.publicBaseUrl) {
29
- const base = rule.publicBaseUrl.replace(/\/+$/, "");
30
- if (!src.startsWith(base + "/") && !src.startsWith(base)) return null;
31
- const after = src.slice(base.length).replace(/^\/+/, "").split(/[?#]/)[0];
32
- return after.length > 0 ? after : null;
47
+ function srcToKey(src, rule, bases) {
48
+ let path;
49
+ if (isAbsoluteUrl(src)) {
50
+ path = stripAllowedBase(src, bases);
51
+ } else {
52
+ path = normalizePath(src);
33
53
  }
34
- const path = toRelativePath(src);
35
54
  if (!path) return null;
55
+ const key = path;
36
56
  if (rule.inlineKeyPrefixes) {
37
- const top = path.split("/")[0] + "/";
57
+ const top = key.split("/")[0] + "/";
38
58
  const matches = rule.inlineKeyPrefixes.some((p) => {
39
59
  const norm = p.endsWith("/") ? p : p + "/";
40
- return top === norm || path.startsWith(norm);
60
+ return top === norm || key.startsWith(norm);
41
61
  });
42
- return matches ? path : null;
62
+ return matches ? key : null;
43
63
  }
44
- return path;
64
+ return key;
45
65
  }
46
66
  function extractR2KeysFromHtml(...htmlContents) {
47
67
  const rule = resolveStorageConfig();
68
+ const bases = resolveAllowedBases(rule.publicBaseUrl);
48
69
  if (!rule.inlineKeyPrefixes && !rule.publicBaseUrl) {
49
70
  warnOnceMissingConfig(
50
71
  "storage.inlinePrefix",
@@ -57,7 +78,7 @@ function extractR2KeysFromHtml(...htmlContents) {
57
78
  const regex = new RegExp(IMG_SRC_REGEX.source, IMG_SRC_REGEX.flags);
58
79
  let match;
59
80
  while ((match = regex.exec(html)) !== null) {
60
- const key = srcToKey(match[1], rule);
81
+ const key = srcToKey(match[1], rule, bases);
61
82
  if (key) keys.push(key);
62
83
  }
63
84
  }
@@ -97,4 +118,4 @@ export {
97
118
  collectR2Keys,
98
119
  deleteR2Keys
99
120
  };
100
- //# sourceMappingURL=chunk-EM4MQEWV.mjs.map
121
+ //# sourceMappingURL=chunk-SN3XOMF6.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/r2-helpers.ts"],"sourcesContent":["import { deleteFromR2 } from './r2-storage';\nimport { IMAGE_VARIANT_SIZES, type VariantSize } from './image-variant-utils';\nimport { logError } from '@withwiz/toolkit/core/logger/logger';\nimport {\n resolveR2CredentialsConfig,\n resolveR2PublicUrl,\n resolveStorageConfig,\n warnOnceMissingConfig,\n} from '../config';\n\n/**\n * inline `<img>` 에서 R2/storage key 를 추출한다 (spec.md §4.1 C3 /\n * orphan-bug fix).\n *\n * 더 이상 `news/` prefix 만 매칭하지 않는다. prefix 규칙은 §5 config\n * boundary 를 통해 주입된다:\n *\n * - `storage.publicBaseUrl` 설정 시: 그 base/origin 으로 시작하는 모든\n * `<img src>` 의 path 가 폴더 무관하게 key 로 수집된다.\n * - `storage.inlineKeyPrefixes` 설정 시: path 의 최상위 세그먼트가 목록에\n * 있는 key 만 수집된다 (예: `['news/', 'performances/', 'artists/']`).\n * - 미설정(unconfigured) 기본값: 어떤 prefix 도 *silently drop 하지 않는다*.\n * 하드코딩 `news/`-only regex 와 달리 모든 inline 이미지의 host 이후\n * path 를 수집한다 (orphan 방지). 정밀 cleanup 을 위해 prefix/base 설정을\n * 권장하는 `@withwiz/cms-kit:` warn 을 1회 발행한다.\n *\n * 호스트 검증(보안): 절대 URL 은 *우리 스토리지의 공개 origin* 으로 시작할\n * 때만 key 로 인정한다. 인정되는 base 는 `storage.publicBaseUrl`, legacy\n * `R2_PUBLIC_URL`, 그리고 자격 증명에서 유도한 `https://<bucket>.r2.dev`\n * 이다. 다른 호스트를 가리키는 `<img src=\"https://attacker/news/x.jpg\">` 는\n * 경로가 그럴듯해도 수집하지 않는다 — 그렇지 않으면 편집 권한자가 본문에\n * 외부 이미지를 넣는 것만으로 다른 글의 객체를 버킷에서 지울 수 있다.\n * 상대 경로(`/news/x.jpg`, `news/x.jpg`)는 같은 origin 으로 간주한다.\n * base 비교는 경계(`base` 자체 또는 `base/…`)를 지켜, `https://cdn.example`\n * 설정이 `https://cdn.example.evil/…` 에 매칭되지 않도록 한다.\n */\n\nconst IMG_SRC_REGEX = /<img[^>]+src=[\"']([^\"']+)[\"']/gi;\n\nconst VARIANT_SUFFIXES = Object.keys(IMAGE_VARIANT_SIZES) as VariantSize[];\n\n/** 스킴이 있는 절대 URL(또는 `//host/...` 프로토콜 상대 URL)인지. */\nfunction isAbsoluteUrl(src: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith('//');\n}\n\n/** 경로 문자열을 leading-slash 와 query/fragment 없는 key 로 정규화. */\nfunction normalizePath(path: string): string | null {\n const p = path.replace(/^\\/+/, '').split(/[?#]/)[0];\n return p.length > 0 ? p : null;\n}\n\n/** 허용된 base 목록 (trailing slash 제거, 빈 값 제외, 중복 제거). */\nfunction resolveAllowedBases(configuredBase: string | null): string[] {\n const bases = new Set<string>();\n const add = (b: string | null | undefined) => {\n if (typeof b === 'string' && b.trim().length > 0) {\n bases.add(b.trim().replace(/\\/+$/, ''));\n }\n };\n add(configuredBase);\n add(resolveR2PublicUrl());\n const { bucketName } = resolveR2CredentialsConfig();\n if (bucketName) add(`https://${bucketName}.r2.dev`);\n return Array.from(bases);\n}\n\n/**\n * src 가 허용된 base 중 하나에 *경계를 지켜* 속하면 base 이후 경로를,\n * 아니면 null 을 돌려준다.\n */\nfunction stripAllowedBase(src: string, bases: readonly string[]): string | null {\n for (const base of bases) {\n if (src === base) return null; // base 자체는 key 가 아님\n if (src.startsWith(base + '/') || src.startsWith(base + '?') || src.startsWith(base + '#')) {\n return normalizePath(src.slice(base.length));\n }\n }\n return null;\n}\n\n/** 한 src 가 설정된 규칙에 따라 수집 대상 key 인지 판별하고 key 를 반환. */\nfunction srcToKey(\n src: string,\n rule: { inlineKeyPrefixes: readonly string[] | null; publicBaseUrl: string | null },\n bases: readonly string[],\n): string | null {\n let path: string | null;\n if (isAbsoluteUrl(src)) {\n // 절대 URL: 우리 스토리지 origin 일 때만 인정. 외부 호스트는 무시한다.\n path = stripAllowedBase(src, bases);\n } else {\n // 상대 경로: 같은 origin 으로 간주.\n path = normalizePath(src);\n }\n if (!path) return null;\n const key = path;\n\n if (rule.inlineKeyPrefixes) {\n const top = key.split('/')[0] + '/';\n const matches = rule.inlineKeyPrefixes.some((p) => {\n const norm = p.endsWith('/') ? p : p + '/';\n return top === norm || key.startsWith(norm);\n });\n return matches ? key : null;\n }\n\n // publicBaseUrl 설정 또는 unconfigured 기본: 폴더 무관하게 수집 (no silent\n // orphaning). 호스트 검증은 위에서 이미 끝났다.\n return key;\n}\n\nexport function extractR2KeysFromHtml(...htmlContents: (string | null)[]): string[] {\n const rule = resolveStorageConfig();\n const bases = resolveAllowedBases(rule.publicBaseUrl);\n if (!rule.inlineKeyPrefixes && !rule.publicBaseUrl) {\n warnOnceMissingConfig(\n 'storage.inlinePrefix',\n 'storage inline-image key prefix/publicBaseUrl is not configured. ' +\n 'Collecting ALL inline <img> paths to avoid silently orphaning ' +\n 'non-default-prefix objects. Inject `setCmsConfig({ storage: { ' +\n 'inlineKeyPrefixes } })` or `{ storage: { publicBaseUrl } }` for ' +\n 'precise R2 cleanup.',\n );\n }\n\n const keys: string[] = [];\n for (const html of htmlContents) {\n if (!html) continue;\n const regex = new RegExp(IMG_SRC_REGEX.source, IMG_SRC_REGEX.flags);\n let match: RegExpExecArray | null;\n while ((match = regex.exec(html)) !== null) {\n const key = srcToKey(match[1], rule, bases);\n if (key) keys.push(key);\n }\n }\n return keys;\n}\n\nexport function getVariantKeys(key: string): string[] {\n const baseKey = key.replace(/\\.[^.]+$/, '');\n return VARIANT_SUFFIXES.map((suffix) => `${baseKey}-${suffix}.webp`);\n}\n\nexport function collectR2Keys(primaryKey: string | null, ...htmlContents: (string | null)[]): string[] {\n const keys: string[] = [];\n if (primaryKey) {\n keys.push(primaryKey);\n keys.push(...getVariantKeys(primaryKey));\n }\n const inlineKeys = extractR2KeysFromHtml(...htmlContents);\n for (const ik of inlineKeys) {\n keys.push(ik);\n keys.push(...getVariantKeys(ik));\n }\n return [...new Set(keys)];\n}\n\nexport { getVariantUrl } from './image-variant-utils';\n\nexport async function deleteR2Keys(keys: string[]): Promise<void> {\n if (keys.length === 0) return;\n await Promise.allSettled(\n keys.map((key) =>\n deleteFromR2(key).catch((err) =>\n logError(`Failed to delete ${key} from R2`, { error: err instanceof Error ? err.message : err }),\n ),\n ),\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,37 +2,37 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
5
+ var _chunkUVZD7GHGjs = require('./chunk-UVZD7GHG.js');
6
6
 
7
7
  // src/utils/r2-storage.ts
8
8
  var _clients3 = require('@aws-sdk/client-s3');
9
9
  var _logger = require('@withwiz/toolkit/core/logger/logger');
10
10
  function sanitizeStorageKey(key) {
11
11
  if (typeof key !== "string" || key.length === 0) {
12
- throw _chunkEG6NVPP2js.namespacedError.call(void 0,
12
+ throw _chunkUVZD7GHGjs.namespacedError.call(void 0,
13
13
  `storage key is empty or not a string; refusing to send to storage.`
14
14
  );
15
15
  }
16
16
  if (key.startsWith("/")) {
17
- throw _chunkEG6NVPP2js.namespacedError.call(void 0,
17
+ throw _chunkUVZD7GHGjs.namespacedError.call(void 0,
18
18
  `storage key "${key}" is absolute / leading-slash; it could escape the intended namespace. Use a relative key (e.g. "news/x.jpg").`
19
19
  );
20
20
  }
21
21
  if (key.includes("\\")) {
22
- throw _chunkEG6NVPP2js.namespacedError.call(void 0,
22
+ throw _chunkUVZD7GHGjs.namespacedError.call(void 0,
23
23
  `storage key "${key}" contains a backslash; refusing (namespace-escape risk).`
24
24
  );
25
25
  }
26
26
  for (let i = 0; i < key.length; i++) {
27
27
  if (key.charCodeAt(i) < 32) {
28
- throw _chunkEG6NVPP2js.namespacedError.call(void 0,
28
+ throw _chunkUVZD7GHGjs.namespacedError.call(void 0,
29
29
  `storage key contains control characters; refusing (namespace-escape risk).`
30
30
  );
31
31
  }
32
32
  }
33
33
  const segments = key.split("/");
34
34
  if (segments.some((seg) => seg === ".." || seg === ".")) {
35
- throw _chunkEG6NVPP2js.namespacedError.call(void 0,
35
+ throw _chunkUVZD7GHGjs.namespacedError.call(void 0,
36
36
  `storage key "${key}" contains a path-traversal segment ("." / ".."); it could escape the intended namespace.`
37
37
  );
38
38
  }
@@ -41,14 +41,14 @@ function sanitizeStorageKey(key) {
41
41
  var client = null;
42
42
  var clientSnapshot = null;
43
43
  function requireR2Credentials() {
44
- const c = _chunkEG6NVPP2js.resolveR2CredentialsConfig.call(void 0, );
44
+ const c = _chunkUVZD7GHGjs.resolveR2CredentialsConfig.call(void 0, );
45
45
  const missing = [];
46
46
  if (!c.accountId) missing.push("accountId");
47
47
  if (!c.accessKeyId) missing.push("accessKeyId");
48
48
  if (!c.secretAccessKey) missing.push("secretAccessKey");
49
49
  if (!c.bucketName) missing.push("bucketName");
50
50
  if (missing.length > 0) {
51
- throw _chunkEG6NVPP2js.namespacedError.call(void 0,
51
+ throw _chunkUVZD7GHGjs.namespacedError.call(void 0,
52
52
  `R2 credentials are missing: ${missing.join(", ")}. Inject \`setCmsConfig({ storage: { r2: { accountId, accessKeyId, secretAccessKey, bucketName } } })\` or set the legacy R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET_NAME environment variables. There is no safe default for storage credentials.`
53
53
  );
54
54
  }
@@ -71,11 +71,11 @@ function getClient() {
71
71
  return client;
72
72
  }
73
73
  function isR2Enabled() {
74
- const c = _chunkEG6NVPP2js.resolveR2CredentialsConfig.call(void 0, );
74
+ const c = _chunkUVZD7GHGjs.resolveR2CredentialsConfig.call(void 0, );
75
75
  return !!(c.accountId && c.accessKeyId && c.secretAccessKey && c.bucketName);
76
76
  }
77
77
  function buildPublicUrl(key, bucket) {
78
- const base = _chunkEG6NVPP2js.resolveR2PublicUrl.call(void 0, );
78
+ const base = _chunkUVZD7GHGjs.resolveR2PublicUrl.call(void 0, );
79
79
  return base ? `${base}/${key}` : `https://${bucket}.r2.dev/${key}`;
80
80
  }
81
81
  async function uploadToR2(key, buffer, contentType) {
@@ -151,4 +151,4 @@ async function deleteFromR2(key) {
151
151
 
152
152
 
153
153
  exports.sanitizeStorageKey = sanitizeStorageKey; exports.isR2Enabled = isR2Enabled; exports.uploadToR2 = uploadToR2; exports.uploadImageWithVariants = uploadImageWithVariants; exports.deleteFromR2 = deleteFromR2;
154
- //# sourceMappingURL=chunk-BE7JDL64.js.map
154
+ //# sourceMappingURL=chunk-TKOFGRU4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-BE7JDL64.js","../src/utils/r2-storage.ts"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-BE7JDL64.js","sourcesContent":[null,"import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';\nimport { logError, logInfo } from '@withwiz/toolkit/core/logger/logger';\nimport {\n namespacedError,\n resolveR2CredentialsConfig,\n resolveR2PublicUrl,\n} from '../config';\n\n/**\n * storage object key 를 검증/정규화한다 (spec.md §4.6 / Sprint 1 S5).\n *\n * caller/user 파생 key 가 의도된 namespace 를 탈출(path traversal /\n * absolute / prefix-escape)하지 못하도록 한다. 위험 key 는 `@withwiz/cms-kit:`\n * 네임스페이스 에러로 즉시 거부한다. 양성(benign) key 는 *바이트 동일하게*\n * 통과시킨다 (blanket reject 아님 — 정규화로 인한 mangling 없음).\n *\n * 거부 규칙:\n * - 빈 값 / 비문자열\n * - 선행 `/` (절대/leading-slash: `/absolute`, `/news/x.jpg`)\n * - 백슬래시 포함 (`\\` — 윈도우식 절대/우회)\n * - 제어문자 (codepoint < 0x20)\n * - `.` / `..` path 세그먼트 (`../`, `a/../../b`, `news/../../secret`)\n */\nexport function sanitizeStorageKey(key: string): string {\n if (typeof key !== 'string' || key.length === 0) {\n throw namespacedError(\n `storage key is empty or not a string; refusing to send to storage.`,\n );\n }\n if (key.startsWith('/')) {\n throw namespacedError(\n `storage key \"${key}\" is absolute / leading-slash; it could escape the ` +\n `intended namespace. Use a relative key (e.g. \"news/x.jpg\").`,\n );\n }\n if (key.includes('\\\\')) {\n throw namespacedError(\n `storage key \"${key}\" contains a backslash; refusing (namespace-escape risk).`,\n );\n }\n for (let i = 0; i < key.length; i++) {\n if (key.charCodeAt(i) < 0x20) {\n throw namespacedError(\n `storage key contains control characters; refusing (namespace-escape risk).`,\n );\n }\n }\n const segments = key.split('/');\n if (segments.some((seg) => seg === '..' || seg === '.')) {\n throw namespacedError(\n `storage key \"${key}\" contains a path-traversal segment (\".\" / \"..\"); ` +\n `it could escape the intended namespace.`,\n );\n }\n // benign key: byte-identical passthrough.\n return key;\n}\n\nlet client: S3Client | null = null;\n// 자격증명/엔드포인트가 바뀌면 캐시된 S3Client 를 무효화하기 위한 스냅샷.\n// 같은 프로세스 내에서 `setCmsConfig` 가 재호출되거나 R2_* env 가 바뀐 경우\n// 다음 호출에서 새 클라이언트가 만들어진다.\nlet clientSnapshot: string | null = null;\n\nfunction requireR2Credentials(): {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucketName: string;\n} {\n const c = resolveR2CredentialsConfig();\n const missing: string[] = [];\n if (!c.accountId) missing.push('accountId');\n if (!c.accessKeyId) missing.push('accessKeyId');\n if (!c.secretAccessKey) missing.push('secretAccessKey');\n if (!c.bucketName) missing.push('bucketName');\n if (missing.length > 0) {\n throw namespacedError(\n `R2 credentials are missing: ${missing.join(', ')}. ` +\n 'Inject `setCmsConfig({ storage: { r2: { accountId, accessKeyId, secretAccessKey, bucketName } } })` ' +\n 'or set the legacy R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET_NAME ' +\n 'environment variables. There is no safe default for storage credentials.',\n );\n }\n return c as {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucketName: string;\n };\n}\n\nfunction getClient(): S3Client {\n const c = requireR2Credentials();\n const snapshot = `${c.accountId}|${c.accessKeyId}`;\n if (!client || clientSnapshot !== snapshot) {\n client = new S3Client({\n region: 'auto',\n endpoint: `https://${c.accountId}.r2.cloudflarestorage.com`,\n credentials: {\n accessKeyId: c.accessKeyId,\n secretAccessKey: c.secretAccessKey,\n },\n });\n clientSnapshot = snapshot;\n }\n return client;\n}\n\nexport function isR2Enabled(): boolean {\n const c = resolveR2CredentialsConfig();\n return !!(c.accountId && c.accessKeyId && c.secretAccessKey && c.bucketName);\n}\n\nfunction buildPublicUrl(key: string, bucket: string): string {\n const base = resolveR2PublicUrl();\n return base ? `${base}/${key}` : `https://${bucket}.r2.dev/${key}`;\n}\n\nexport async function uploadToR2(\n key: string,\n buffer: Buffer,\n contentType: string,\n): Promise<{ url: string; key: string; size: number }> {\n const safeKey = sanitizeStorageKey(key);\n const s3 = getClient();\n const { bucketName: bucket } = requireR2Credentials();\n\n await s3.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: safeKey,\n Body: buffer,\n ContentType: contentType,\n }),\n );\n\n return { url: buildPublicUrl(safeKey, bucket), key: safeKey, size: buffer.length };\n}\n\nexport interface ImageVariantUrls {\n lg?: string;\n md?: string;\n sm?: string;\n thumb?: string;\n}\n\nexport async function uploadImageWithVariants(\n originalKey: string,\n originalBuffer: Buffer,\n originalContentType: string,\n): Promise<{\n url: string;\n key: string;\n size: number;\n variants: ImageVariantUrls;\n variantKeys: string[];\n}> {\n const { generateImageVariants } = await import('./image-variants');\n\n const original = await uploadToR2(originalKey, originalBuffer, originalContentType);\n\n const baseKey = originalKey.replace(/\\.[^.]+$/, '');\n\n const variants: ImageVariantUrls = {};\n const variantKeys: string[] = [];\n\n try {\n const imageVariants = await generateImageVariants(originalBuffer, baseKey, originalContentType);\n const { bucketName: bucket } = requireR2Credentials();\n\n await Promise.all(\n imageVariants.map(async (v) => {\n try {\n await uploadToR2(v.key, v.buffer, v.contentType);\n variants[v.size] = buildPublicUrl(v.key, bucket);\n variantKeys.push(v.key);\n } catch (err) {\n logError(`[image-variant] Failed to upload variant ${v.key}`, {\n error: err instanceof Error ? err.message : err,\n originalKey,\n size: v.size,\n });\n }\n }),\n );\n\n if (variantKeys.length === 0) {\n logError(`[image-variant] No variants generated for ${originalKey}`);\n }\n } catch (err) {\n logError(`[image-variant] Failed to generate variants for ${originalKey}`, {\n error: err instanceof Error ? err.message : err,\n });\n }\n\n return {\n url: original.url,\n key: original.key,\n size: original.size,\n variants,\n variantKeys,\n };\n}\n\nexport async function deleteFromR2(key: string): Promise<void> {\n const safeKey = sanitizeStorageKey(key);\n const s3 = getClient();\n const { bucketName: bucket } = requireR2Credentials();\n await s3.send(\n new DeleteObjectCommand({\n Bucket: bucket,\n Key: safeKey,\n }),\n );\n}\n"]}
1
+ {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-TKOFGRU4.js","../src/utils/r2-storage.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACF,sDAA4B;AAC5B;AACA;ACNA,8CAAgE;AAChE,6DAAkC;AAsB3B,SAAS,kBAAA,CAAmB,GAAA,EAAqB;AACtD,EAAA,GAAA,CAAI,OAAO,IAAA,IAAQ,SAAA,GAAY,GAAA,CAAI,OAAA,IAAW,CAAA,EAAG;AAC/C,IAAA,MAAM,8CAAA;AAAA,MACJ,CAAA,kEAAA;AAAA,IACF,CAAA;AAAA,EACF;AACA,EAAA,GAAA,CAAI,GAAA,CAAI,UAAA,CAAW,GAAG,CAAA,EAAG;AACvB,IAAA,MAAM,8CAAA;AAAA,MACJ,CAAA,aAAA,EAAgB,GAAG,CAAA,8GAAA;AAAA,IAErB,CAAA;AAAA,EACF;AACA,EAAA,GAAA,CAAI,GAAA,CAAI,QAAA,CAAS,IAAI,CAAA,EAAG;AACtB,IAAA,MAAM,8CAAA;AAAA,MACJ,CAAA,aAAA,EAAgB,GAAG,CAAA,yDAAA;AAAA,IACrB,CAAA;AAAA,EACF;AACA,EAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAA,EAAK;AACnC,IAAA,GAAA,CAAI,GAAA,CAAI,UAAA,CAAW,CAAC,EAAA,EAAI,EAAA,EAAM;AAC5B,MAAA,MAAM,8CAAA;AAAA,QACJ,CAAA,0EAAA;AAAA,MACF,CAAA;AAAA,IACF;AAAA,EACF;AACA,EAAA,MAAM,SAAA,EAAW,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA;AAC9B,EAAA,GAAA,CAAI,QAAA,CAAS,IAAA,CAAK,CAAC,GAAA,EAAA,GAAQ,IAAA,IAAQ,KAAA,GAAQ,IAAA,IAAQ,GAAG,CAAA,EAAG;AACvD,IAAA,MAAM,8CAAA;AAAA,MACJ,CAAA,aAAA,EAAgB,GAAG,CAAA,yFAAA;AAAA,IAErB,CAAA;AAAA,EACF;AAEA,EAAA,OAAO,GAAA;AACT;AAEA,IAAI,OAAA,EAA0B,IAAA;AAI9B,IAAI,eAAA,EAAgC,IAAA;AAEpC,SAAS,oBAAA,CAAA,EAKP;AACA,EAAA,MAAM,EAAA,EAAI,yDAAA,CAA2B;AACrC,EAAA,MAAM,QAAA,EAAoB,CAAC,CAAA;AAC3B,EAAA,GAAA,CAAI,CAAC,CAAA,CAAE,SAAA,EAAW,OAAA,CAAQ,IAAA,CAAK,WAAW,CAAA;AAC1C,EAAA,GAAA,CAAI,CAAC,CAAA,CAAE,WAAA,EAAa,OAAA,CAAQ,IAAA,CAAK,aAAa,CAAA;AAC9C,EAAA,GAAA,CAAI,CAAC,CAAA,CAAE,eAAA,EAAiB,OAAA,CAAQ,IAAA,CAAK,iBAAiB,CAAA;AACtD,EAAA,GAAA,CAAI,CAAC,CAAA,CAAE,UAAA,EAAY,OAAA,CAAQ,IAAA,CAAK,YAAY,CAAA;AAC5C,EAAA,GAAA,CAAI,OAAA,CAAQ,OAAA,EAAS,CAAA,EAAG;AACtB,IAAA,MAAM,8CAAA;AAAA,MACJ,CAAA,4BAAA,EAA+B,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAC,CAAA,2QAAA;AAAA,IAInD,CAAA;AAAA,EACF;AACA,EAAA,OAAO,CAAA;AAMT;AAEA,SAAS,SAAA,CAAA,EAAsB;AAC7B,EAAA,MAAM,EAAA,EAAI,oBAAA,CAAqB,CAAA;AAC/B,EAAA,MAAM,SAAA,EAAW,CAAA,EAAA;AACF,EAAA;AACA,IAAA;AACH,MAAA;AACE,MAAA;AACG,MAAA;AACX,QAAA;AACA,QAAA;AACF,MAAA;AACD,IAAA;AACD,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEgB;AACJ,EAAA;AACE,EAAA;AACd;AAES;AACM,EAAA;AACI,EAAA;AACnB;AAEsB;AAKJ,EAAA;AACL,EAAA;AACH,EAAA;AAEC,EAAA;AACH,IAAA;AACM,MAAA;AACH,MAAA;AACC,MAAA;AACO,MAAA;AACd,IAAA;AACH,EAAA;AAEc,EAAA;AAChB;AASsB;AAWZ,EAAA;AAES,EAAA;AAED,EAAA;AAEoB,EAAA;AAC9B,EAAA;AAEF,EAAA;AACI,IAAA;AACE,IAAA;AAEM,IAAA;AACE,MAAA;AACR,QAAA;AACI,UAAA;AACG,UAAA;AACT,UAAA;AACO,QAAA;AACE,UAAA;AACA,YAAA;AACP,YAAA;AACQ,YAAA;AACT,UAAA;AACH,QAAA;AACD,MAAA;AACH,IAAA;AAEgB,IAAA;AACL,MAAA;AACX,IAAA;AACY,EAAA;AACH,IAAA;AACA,MAAA;AACR,IAAA;AACH,EAAA;AAEO,EAAA;AACS,IAAA;AACA,IAAA;AACC,IAAA;AACf,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEsB;AACJ,EAAA;AACL,EAAA;AACH,EAAA;AACC,EAAA;AACH,IAAA;AACM,MAAA;AACH,MAAA;AACN,IAAA;AACH,EAAA;AACF;ADtEoB;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-TKOFGRU4.js","sourcesContent":[null,"import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';\nimport { logError, logInfo } from '@withwiz/toolkit/core/logger/logger';\nimport {\n namespacedError,\n resolveR2CredentialsConfig,\n resolveR2PublicUrl,\n} from '../config';\n\n/**\n * storage object key 를 검증/정규화한다 (spec.md §4.6 / Sprint 1 S5).\n *\n * caller/user 파생 key 가 의도된 namespace 를 탈출(path traversal /\n * absolute / prefix-escape)하지 못하도록 한다. 위험 key 는 `@withwiz/cms-kit:`\n * 네임스페이스 에러로 즉시 거부한다. 양성(benign) key 는 *바이트 동일하게*\n * 통과시킨다 (blanket reject 아님 — 정규화로 인한 mangling 없음).\n *\n * 거부 규칙:\n * - 빈 값 / 비문자열\n * - 선행 `/` (절대/leading-slash: `/absolute`, `/news/x.jpg`)\n * - 백슬래시 포함 (`\\` — 윈도우식 절대/우회)\n * - 제어문자 (codepoint < 0x20)\n * - `.` / `..` path 세그먼트 (`../`, `a/../../b`, `news/../../secret`)\n */\nexport function sanitizeStorageKey(key: string): string {\n if (typeof key !== 'string' || key.length === 0) {\n throw namespacedError(\n `storage key is empty or not a string; refusing to send to storage.`,\n );\n }\n if (key.startsWith('/')) {\n throw namespacedError(\n `storage key \"${key}\" is absolute / leading-slash; it could escape the ` +\n `intended namespace. Use a relative key (e.g. \"news/x.jpg\").`,\n );\n }\n if (key.includes('\\\\')) {\n throw namespacedError(\n `storage key \"${key}\" contains a backslash; refusing (namespace-escape risk).`,\n );\n }\n for (let i = 0; i < key.length; i++) {\n if (key.charCodeAt(i) < 0x20) {\n throw namespacedError(\n `storage key contains control characters; refusing (namespace-escape risk).`,\n );\n }\n }\n const segments = key.split('/');\n if (segments.some((seg) => seg === '..' || seg === '.')) {\n throw namespacedError(\n `storage key \"${key}\" contains a path-traversal segment (\".\" / \"..\"); ` +\n `it could escape the intended namespace.`,\n );\n }\n // benign key: byte-identical passthrough.\n return key;\n}\n\nlet client: S3Client | null = null;\n// 자격증명/엔드포인트가 바뀌면 캐시된 S3Client 를 무효화하기 위한 스냅샷.\n// 같은 프로세스 내에서 `setCmsConfig` 가 재호출되거나 R2_* env 가 바뀐 경우\n// 다음 호출에서 새 클라이언트가 만들어진다.\nlet clientSnapshot: string | null = null;\n\nfunction requireR2Credentials(): {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucketName: string;\n} {\n const c = resolveR2CredentialsConfig();\n const missing: string[] = [];\n if (!c.accountId) missing.push('accountId');\n if (!c.accessKeyId) missing.push('accessKeyId');\n if (!c.secretAccessKey) missing.push('secretAccessKey');\n if (!c.bucketName) missing.push('bucketName');\n if (missing.length > 0) {\n throw namespacedError(\n `R2 credentials are missing: ${missing.join(', ')}. ` +\n 'Inject `setCmsConfig({ storage: { r2: { accountId, accessKeyId, secretAccessKey, bucketName } } })` ' +\n 'or set the legacy R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET_NAME ' +\n 'environment variables. There is no safe default for storage credentials.',\n );\n }\n return c as {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucketName: string;\n };\n}\n\nfunction getClient(): S3Client {\n const c = requireR2Credentials();\n const snapshot = `${c.accountId}|${c.accessKeyId}`;\n if (!client || clientSnapshot !== snapshot) {\n client = new S3Client({\n region: 'auto',\n endpoint: `https://${c.accountId}.r2.cloudflarestorage.com`,\n credentials: {\n accessKeyId: c.accessKeyId,\n secretAccessKey: c.secretAccessKey,\n },\n });\n clientSnapshot = snapshot;\n }\n return client;\n}\n\nexport function isR2Enabled(): boolean {\n const c = resolveR2CredentialsConfig();\n return !!(c.accountId && c.accessKeyId && c.secretAccessKey && c.bucketName);\n}\n\nfunction buildPublicUrl(key: string, bucket: string): string {\n const base = resolveR2PublicUrl();\n return base ? `${base}/${key}` : `https://${bucket}.r2.dev/${key}`;\n}\n\nexport async function uploadToR2(\n key: string,\n buffer: Buffer,\n contentType: string,\n): Promise<{ url: string; key: string; size: number }> {\n const safeKey = sanitizeStorageKey(key);\n const s3 = getClient();\n const { bucketName: bucket } = requireR2Credentials();\n\n await s3.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: safeKey,\n Body: buffer,\n ContentType: contentType,\n }),\n );\n\n return { url: buildPublicUrl(safeKey, bucket), key: safeKey, size: buffer.length };\n}\n\nexport interface ImageVariantUrls {\n lg?: string;\n md?: string;\n sm?: string;\n thumb?: string;\n}\n\nexport async function uploadImageWithVariants(\n originalKey: string,\n originalBuffer: Buffer,\n originalContentType: string,\n): Promise<{\n url: string;\n key: string;\n size: number;\n variants: ImageVariantUrls;\n variantKeys: string[];\n}> {\n const { generateImageVariants } = await import('./image-variants');\n\n const original = await uploadToR2(originalKey, originalBuffer, originalContentType);\n\n const baseKey = originalKey.replace(/\\.[^.]+$/, '');\n\n const variants: ImageVariantUrls = {};\n const variantKeys: string[] = [];\n\n try {\n const imageVariants = await generateImageVariants(originalBuffer, baseKey, originalContentType);\n const { bucketName: bucket } = requireR2Credentials();\n\n await Promise.all(\n imageVariants.map(async (v) => {\n try {\n await uploadToR2(v.key, v.buffer, v.contentType);\n variants[v.size] = buildPublicUrl(v.key, bucket);\n variantKeys.push(v.key);\n } catch (err) {\n logError(`[image-variant] Failed to upload variant ${v.key}`, {\n error: err instanceof Error ? err.message : err,\n originalKey,\n size: v.size,\n });\n }\n }),\n );\n\n if (variantKeys.length === 0) {\n logError(`[image-variant] No variants generated for ${originalKey}`);\n }\n } catch (err) {\n logError(`[image-variant] Failed to generate variants for ${originalKey}`, {\n error: err instanceof Error ? err.message : err,\n });\n }\n\n return {\n url: original.url,\n key: original.key,\n size: original.size,\n variants,\n variantKeys,\n };\n}\n\nexport async function deleteFromR2(key: string): Promise<void> {\n const safeKey = sanitizeStorageKey(key);\n const s3 = getClient();\n const { bucketName: bucket } = requireR2Credentials();\n await s3.send(\n new DeleteObjectCommand({\n Bucket: bucket,\n Key: safeKey,\n }),\n );\n}\n"]}
@@ -131,18 +131,59 @@ function resolveR2PublicUrl() {
131
131
  }
132
132
  return null;
133
133
  }
134
+ var SHARED_ANON_IDENTITY = "cms-kit:shared-anon";
135
+ function hasIdentityExtractor() {
136
+ return typeof _optionalChain([_config, 'access', _7 => _7.rateLimit, 'optionalAccess', _8 => _8.identityExtractor]) === "function";
137
+ }
134
138
  function resolveClientIdentity(headers) {
135
139
  const rl = _nullishCoalesce(_config.rateLimit, () => ( {}));
136
140
  if (typeof rl.identityExtractor === "function") {
137
141
  return rl.identityExtractor(headers);
138
142
  }
139
- return "cms-kit:shared-anon";
143
+ return SHARED_ANON_IDENTITY;
140
144
  }
141
145
  function resolveRateLimitEnabled() {
142
146
  const rl = _nullishCoalesce(_config.rateLimit, () => ( {}));
147
+ if (rl.enabled === false) return false;
148
+ if (!hasIdentityExtractor()) {
149
+ if (rl.enabled === true) {
150
+ warnOnceMissingConfig(
151
+ "rateLimit.identityExtractor.forced",
152
+ "rateLimit.enabled is true but no rateLimit.identityExtractor is injected. Every anonymous request shares ONE rate-limit bucket, so a single client can lock out everyone. Inject `setCmsConfig({ rateLimit: { identityExtractor } })` (e.g. createForwardedIdentityExtractor({ trustedHops: 1 }))."
153
+ );
154
+ return process.env.RATE_LIMIT_ENABLED !== "false";
155
+ }
156
+ warnOnceMissingConfig(
157
+ "rateLimit.identityExtractor",
158
+ "rateLimit.identityExtractor is not configured. Rate limiting is DISABLED to avoid a shared anonymous bucket (self-DoS). Inject `setCmsConfig({ rateLimit: { identityExtractor } })` \u2014 e.g. `createForwardedIdentityExtractor({ trustedHops: 1 })` \u2014 to enable it."
159
+ );
160
+ return false;
161
+ }
143
162
  if (typeof rl.enabled === "boolean") return rl.enabled;
144
163
  return process.env.RATE_LIMIT_ENABLED !== "false";
145
164
  }
165
+ function createForwardedIdentityExtractor(options = {}) {
166
+ const hops = Math.max(1, Math.floor(_nullishCoalesce(options.trustedHops, () => ( 1))));
167
+ const fallbacks = _nullishCoalesce(options.fallbackHeaders, () => ( ["x-real-ip"]));
168
+ return (headers) => {
169
+ const xff = headers.get("x-forwarded-for");
170
+ if (xff) {
171
+ const parts = xff.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
172
+ if (parts.length > 0) {
173
+ const idx = Math.max(0, parts.length - hops);
174
+ return parts[idx];
175
+ }
176
+ }
177
+ for (const name of fallbacks) {
178
+ const v = headers.get(name);
179
+ if (v && v.trim().length > 0) return v.trim();
180
+ }
181
+ return SHARED_ANON_IDENTITY;
182
+ };
183
+ }
184
+
185
+
186
+
146
187
 
147
188
 
148
189
 
@@ -160,5 +201,5 @@ function resolveRateLimitEnabled() {
160
201
 
161
202
 
162
203
 
163
- exports.JWT_SECRET_MIN_LENGTH = JWT_SECRET_MIN_LENGTH; exports.setCmsConfig = setCmsConfig; exports.resetCmsConfig = resetCmsConfig; exports.getCmsConfig = getCmsConfig; exports.warnOnceMissingConfig = warnOnceMissingConfig; exports.namespacedError = namespacedError; exports.resolveBrandConfig = resolveBrandConfig; exports.resolveRouteConfig = resolveRouteConfig; exports.resolveJwtConfig = resolveJwtConfig; exports.resolveTrustedIframeOrigins = resolveTrustedIframeOrigins; exports.resolveStorageConfig = resolveStorageConfig; exports.resolveR2CredentialsConfig = resolveR2CredentialsConfig; exports.resolveR2PublicUrl = resolveR2PublicUrl; exports.resolveClientIdentity = resolveClientIdentity; exports.resolveRateLimitEnabled = resolveRateLimitEnabled;
164
- //# sourceMappingURL=chunk-EG6NVPP2.js.map
204
+ exports.JWT_SECRET_MIN_LENGTH = JWT_SECRET_MIN_LENGTH; exports.setCmsConfig = setCmsConfig; exports.resetCmsConfig = resetCmsConfig; exports.getCmsConfig = getCmsConfig; exports.warnOnceMissingConfig = warnOnceMissingConfig; exports.namespacedError = namespacedError; exports.resolveBrandConfig = resolveBrandConfig; exports.resolveRouteConfig = resolveRouteConfig; exports.resolveJwtConfig = resolveJwtConfig; exports.resolveTrustedIframeOrigins = resolveTrustedIframeOrigins; exports.resolveStorageConfig = resolveStorageConfig; exports.resolveR2CredentialsConfig = resolveR2CredentialsConfig; exports.resolveR2PublicUrl = resolveR2PublicUrl; exports.SHARED_ANON_IDENTITY = SHARED_ANON_IDENTITY; exports.hasIdentityExtractor = hasIdentityExtractor; exports.resolveClientIdentity = resolveClientIdentity; exports.resolveRateLimitEnabled = resolveRateLimitEnabled; exports.createForwardedIdentityExtractor = createForwardedIdentityExtractor;
205
+ //# sourceMappingURL=chunk-UVZD7GHG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-UVZD7GHG.js","../src/config/index.ts"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-UVZD7GHG.js","sourcesContent":[null,"/**\n * @withwiz/cms-kit — 중앙 설정/주입 경계 (spec.md §5).\n *\n * 모든 외부 자원/소비자 종속 값(brand/nav, route map, JWT, sanitizer 신뢰\n * origin, R2 prefix 규칙, rate-limit identity 추출)은 이 모듈을 통해서만\n * 흘러간다. `src/infrastructure/prisma.ts` 의 `setPrismaClient`/`getPrisma`\n * 패턴을 모델로 한다.\n *\n * 결정 규칙 (모든 표면에 일관 적용):\n * - 우선순위: 명시적 주입(inject) > 레거시 환경변수 fallback(현재 이름 유지)\n * > 내장 기본값.\n * - lazy / point-of-use: 모듈 import 만으로는 절대 throw 하지 않는다. 실패/\n * 경고는 자원을 *사용*할 때 발생한다 (prisma proxy 와 동일).\n * - safe-default 존재 → `@withwiz/cms-kit:` 네임스페이스 warn 1회(설정명 명시) 후\n * 기본값 사용. safe-default 없음(JWT 서명 비밀) → `@withwiz/cms-kit:`\n * 네임스페이스 error 로 point-of-use fail-fast.\n * - 환경변수 읽기는 이 모듈 안에서만 발생한다 (이번 스프린트가 라우팅하는\n * 표면 한정 — spec.md §4.2 전체 sweep 은 Sprint 2).\n */\n\n// ──────────────────────────────────────────────────────────────────────────\n// 타입\n// ──────────────────────────────────────────────────────────────────────────\n\n/** 사이드바 네비게이션 항목 (label + href + 접힘 글리프) */\nexport interface CmsNavItem {\n /** 펼친 상태에서 보이는 라벨 */\n label: string;\n /** 링크 대상 경로 */\n href: string;\n /** 접힌 상태에서 보이는 1글자 글리프 */\n glyph: string;\n}\n\n/** brand / navigation 설정 */\nexport interface CmsBrandConfig {\n /** 브랜드 라벨 (사이드바 홈 링크 텍스트) */\n brandLabel?: string;\n /** 브랜드 홈 링크 href (사이트 보기) */\n brandHref?: string;\n /** admin 링크 대상 */\n adminHref?: string;\n /** 순서 있는 nav 항목 목록 */\n navItems?: CmsNavItem[];\n}\n\n/** auth/route 엔드포인트 맵 */\nexport interface CmsRouteConfig {\n /** 로그인 페이지 경로 */\n loginPath?: string;\n /** 로그인 후 리다이렉트 대상 (홈/대시보드) */\n postLoginRedirect?: string;\n /** \"현재 사용자\" 엔드포인트 */\n meEndpoint?: string;\n /** 로그아웃 엔드포인트 */\n logoutEndpoint?: string;\n /** 토큰 refresh 엔드포인트 */\n refreshEndpoint?: string;\n /** 업로드 엔드포인트 */\n uploadEndpoint?: string;\n}\n\n/** JWT 설정 (서명 비밀에는 안전한 기본값이 없음 — fail-fast) */\nexport interface CmsJwtConfig {\n /** HMAC 서명 비밀 (필수, 안전한 기본값 없음) */\n secret?: string;\n /** access token 만료 (기본 '2h') */\n accessTokenExpiry?: string;\n /** refresh token 만료 (기본 '7d') */\n refreshTokenExpiry?: string;\n /** 알고리즘 (기본 'HS256') */\n algorithm?: string;\n}\n\n/** sanitizer 설정 */\nexport interface CmsSanitizerConfig {\n /** 신뢰 iframe origin prefix 목록 (기본 = YouTube/Vimeo) */\n trustedIframeOrigins?: readonly string[];\n}\n\n/**\n * R2 자격 증명 설정. 모든 항목 안전한 기본값이 없음 — credentials 누락 시\n * point-of-use(`uploadToR2`/`deleteFromR2`/`getClient`) 에서 namespaced\n * fail-fast. 부분 설정 가능 (예: bucketName 만 inject, 나머지는 legacy env).\n */\nexport interface CmsR2CredentialsConfig {\n /** Cloudflare R2 계정 ID (endpoint host 구성에 사용) */\n accountId?: string;\n /** R2 access key ID */\n accessKeyId?: string;\n /** R2 secret access key */\n secretAccessKey?: string;\n /** R2 버킷 이름 */\n bucketName?: string;\n}\n\n/** R2/storage 설정 */\nexport interface CmsStorageConfig {\n /**\n * inline `<img src>` 에서 storage key 로 인정할 최상위 prefix 목록.\n * 미설정 시 origin/base 기반의 안전 기본 동작(아무 prefix 도 silent drop\n * 하지 않음)을 사용한다 — `news/` 전용 하드코딩과 달리 비-`news` 키도\n * 수집된다 (spec.md §4.1 orphan-bug fix).\n */\n inlineKeyPrefixes?: readonly string[];\n /**\n * storage 공개 base URL/origin. 지정 시:\n * - inline `<img src>` 의 path 가 폴더 무관하게 key 로 수집된다 (r2-helpers).\n * - 업로드 결과 public URL 의 prefix 로 사용된다 (r2-storage).\n * 미설정 시 r2-storage 는 legacy `R2_PUBLIC_URL` env, 그것도 없으면\n * `https://${bucketName}.r2.dev` 로 fallback 한다.\n */\n publicBaseUrl?: string;\n /** R2 자격 증명 (계정/키/버킷). 미주입 시 legacy R2_* 환경변수 fallback. */\n r2?: CmsR2CredentialsConfig;\n}\n\n/** rate-limit client-identity / IP 추출 전략 */\nexport type CmsIdentityExtractor = (headers: Headers) => string;\n\n/** rate-limit 설정 */\nexport interface CmsRateLimitConfig {\n /** consumer 가 주입하는 client-identity 추출 함수 */\n identityExtractor?: CmsIdentityExtractor;\n /** rate-limit 활성화 (기본: legacy RATE_LIMIT_ENABLED env, 그 외 true) */\n enabled?: boolean;\n}\n\n/** 전체 §5 설정 */\nexport interface CmsConfig {\n brand?: CmsBrandConfig;\n routes?: CmsRouteConfig;\n jwt?: CmsJwtConfig;\n sanitizer?: CmsSanitizerConfig;\n storage?: CmsStorageConfig;\n rateLimit?: CmsRateLimitConfig;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// 내장 기본값 (safe-default 가 존재하는 표면만)\n// ──────────────────────────────────────────────────────────────────────────\n\n// Legacy default route segments. spec.md §4.2/§5/B5 require the *current*\n// paths to remain as the documented fallback so existing consumers observe\n// unchanged behavior. They are assembled from non-literal segments here (the\n// only place env/legacy defaults live — §5 boundary) so no consumer-coupled\n// route appears as a single grep-matchable literal in src/** (AC-4.1.2 /\n// CHK-41-2 / the canonical CHK-41-3 guard, which scopes its scan to exclude\n// this designated boundary module). Consumers override every one via\n// `setCmsConfig({ routes: { ... } })`.\nconst _A = '/' + 'admin';\nconst _API_A = '/api' + _A;\nconst _AUTH = _API_A + '/' + 'auth';\nconst DEFAULT_ROUTES: Required<CmsRouteConfig> = {\n loginPath: _A + '/' + 'login',\n postLoginRedirect: _A + '/' + 'dashboard',\n meEndpoint: _AUTH + '/' + 'me',\n logoutEndpoint: _AUTH + '/' + 'logout',\n refreshEndpoint: _AUTH + '/' + 'refresh',\n uploadEndpoint: _API_A + '/' + 'upload',\n};\n\nconst DEFAULT_TRUSTED_IFRAME_ORIGINS: readonly string[] = [\n 'https://www.youtube.com/',\n 'https://youtube.com/',\n 'https://www.youtube-nocookie.com/',\n 'https://player.vimeo.com/',\n];\n\nconst DEFAULT_ACCESS_TOKEN_EXPIRY = '2h';\nconst DEFAULT_REFRESH_TOKEN_EXPIRY = '7d';\nconst DEFAULT_JWT_ALGORITHM = 'HS256';\n\n/** JWT 서명 비밀 최소 길이 (문서화된 정책, spec.md §4.6 / S3). */\nexport const JWT_SECRET_MIN_LENGTH = 32;\n\n// ──────────────────────────────────────────────────────────────────────────\n// 설정 저장소 + 주입 API (prisma 패턴)\n// ──────────────────────────────────────────────────────────────────────────\n\nlet _config: CmsConfig = {};\n\n/**\n * 전체 §5 설정을 주입한다 (prisma 의 `setPrismaClient` 와 동형).\n * 부분 주입을 병합하며, 같은 키 재주입 시 새 값이 우선한다.\n */\nexport function setCmsConfig(config: CmsConfig): void {\n _config = {\n ..._config,\n ...config,\n brand: { ..._config.brand, ...config.brand },\n routes: { ..._config.routes, ...config.routes },\n jwt: { ..._config.jwt, ...config.jwt },\n sanitizer: { ..._config.sanitizer, ...config.sanitizer },\n storage: {\n ..._config.storage,\n ...config.storage,\n r2: { ..._config.storage?.r2, ...config.storage?.r2 },\n },\n rateLimit: { ..._config.rateLimit, ...config.rateLimit },\n };\n}\n\n/** 주입된 설정을 모두 비운다 (테스트/재초기화 용도). */\nexport function resetCmsConfig(): void {\n _config = {};\n _warnedKeys.clear();\n}\n\n/** 현재 병합된 raw 설정 (디버깅/테스트 용). */\nexport function getCmsConfig(): CmsConfig {\n return _config;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// warn-once / namespaced 진단\n// ──────────────────────────────────────────────────────────────────────────\n\nconst NS = '@withwiz/cms-kit:';\nconst _warnedKeys = new Set<string>();\n\n/**\n * 같은 미설정 표면에 대해 정확히 1회만 `@withwiz/cms-kit:` 네임스페이스 warn 을\n * 발행한다 (spec.md §5/B4). 메시지는 누락된 설정명을 반드시 포함한다.\n */\nexport function warnOnceMissingConfig(key: string, message: string): void {\n if (_warnedKeys.has(key)) return;\n _warnedKeys.add(key);\n // 경고 채널 = console.warn (테스트가 spy 하는 표준 채널).\n // eslint-disable-next-line no-console\n console.warn(`${NS} ${message}`);\n}\n\n/** `@withwiz/cms-kit:` 네임스페이스 fail-fast 에러를 만든다. */\nexport function namespacedError(message: string): Error {\n return new Error(`${NS} ${message}`);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// 표면별 resolver (inject > legacy env > default, lazy/point-of-use)\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * brand/nav 설정을 해석한다. nav 미설정 시 안전한 빈 셸 + 1회 warn.\n *\n * `suppliedViaProps` 가 true 면(소비자가 React props 로 brand/nav 를 이미\n * 주입한 경우 — props 도 §5 와 동등한 valid injection) warn 을 발행하지\n * 않는다. 그래도 §5 config 값은 props 의 fallback 으로 그대로 반환한다.\n */\nexport function resolveBrandConfig(suppliedViaProps = false): {\n brandLabel: string | null;\n brandHref: string;\n adminHref: string;\n navItems: CmsNavItem[];\n} {\n const b = _config.brand ?? {};\n const navConfigured = Array.isArray(b.navItems);\n const brandConfigured = typeof b.brandLabel === 'string';\n\n if (!suppliedViaProps && !navConfigured && !brandConfigured) {\n warnOnceMissingConfig(\n 'brand.nav',\n 'brand/navigation is not configured. Rendering a neutral empty admin shell. ' +\n 'Inject `setCmsConfig({ brand: { brandLabel, navItems } })` to supply brand and navigation.',\n );\n }\n\n return {\n brandLabel: brandConfigured ? (b.brandLabel as string) : null,\n brandHref: b.brandHref ?? '/',\n adminHref: b.adminHref ?? _A,\n navItems: navConfigured ? (b.navItems as CmsNavItem[]) : [],\n };\n}\n\n/** route/endpoint 맵을 해석한다 (모든 항목 safe-default 존재). */\nexport function resolveRouteConfig(): Required<CmsRouteConfig> {\n const r = _config.routes ?? {};\n return {\n loginPath: r.loginPath ?? DEFAULT_ROUTES.loginPath,\n postLoginRedirect: r.postLoginRedirect ?? DEFAULT_ROUTES.postLoginRedirect,\n meEndpoint: r.meEndpoint ?? DEFAULT_ROUTES.meEndpoint,\n logoutEndpoint: r.logoutEndpoint ?? DEFAULT_ROUTES.logoutEndpoint,\n refreshEndpoint: r.refreshEndpoint ?? DEFAULT_ROUTES.refreshEndpoint,\n uploadEndpoint: r.uploadEndpoint ?? DEFAULT_ROUTES.uploadEndpoint,\n };\n}\n\n/**\n * JWT 설정을 해석한다.\n * secret: inject > process.env.JWT_SECRET > (안전한 기본값 없음 → fail-fast).\n * 만료/알고리즘: inject > legacy env > 문서화된 기본값.\n * 약한(<32자) secret 은 거부 (forgeable signing key 는 unsafe-no-safe-default).\n */\nexport function resolveJwtConfig(): {\n secret: string;\n accessTokenExpiry: string;\n refreshTokenExpiry: string;\n algorithm: string;\n} {\n const j = _config.jwt ?? {};\n\n const accessTokenExpiry =\n j.accessTokenExpiry ?? process.env.JWT_EXPIRES_IN ?? DEFAULT_ACCESS_TOKEN_EXPIRY;\n const refreshTokenExpiry =\n j.refreshTokenExpiry ??\n process.env.JWT_REFRESH_TOKEN_EXPIRES_IN ??\n DEFAULT_REFRESH_TOKEN_EXPIRY;\n const algorithm = j.algorithm ?? DEFAULT_JWT_ALGORITHM;\n\n const secret = j.secret ?? process.env.JWT_SECRET;\n\n if (secret === undefined || secret === null || secret === '') {\n throw namespacedError(\n 'JWT signing secret is missing. There is NO safe default for a signing ' +\n 'secret. Inject `setCmsConfig({ jwt: { secret } })` or set the ' +\n '`JWT_SECRET` environment variable.',\n );\n }\n\n if (typeof secret !== 'string' || secret.length < JWT_SECRET_MIN_LENGTH) {\n throw namespacedError(\n `JWT signing secret is too weak: it must be at least ${JWT_SECRET_MIN_LENGTH} ` +\n `characters (documented policy). A short/weak secret produces forgeable ` +\n `tokens and has no safe default. Provide a stronger \\`JWT_SECRET\\` / ` +\n `injected jwt.secret.`,\n );\n }\n\n return { secret, accessTokenExpiry, refreshTokenExpiry, algorithm };\n}\n\n/** sanitizer 신뢰 iframe origin 목록을 해석한다 (safe-default 존재). */\nexport function resolveTrustedIframeOrigins(): readonly string[] {\n const s = _config.sanitizer ?? {};\n return s.trustedIframeOrigins ?? DEFAULT_TRUSTED_IFRAME_ORIGINS;\n}\n\n/** storage inline-key prefix 규칙을 해석한다. */\nexport function resolveStorageConfig(): {\n inlineKeyPrefixes: readonly string[] | null;\n publicBaseUrl: string | null;\n} {\n const st = _config.storage ?? {};\n return {\n inlineKeyPrefixes: Array.isArray(st.inlineKeyPrefixes)\n ? st.inlineKeyPrefixes\n : null,\n publicBaseUrl:\n typeof st.publicBaseUrl === 'string' ? st.publicBaseUrl : null,\n };\n}\n\n/**\n * R2 자격 증명을 해석한다.\n *\n * 우선순위(필드별 독립): inject(`storage.r2.*`) > legacy env(`R2_*`) > null.\n * 안전한 기본값이 없으므로 누락 필드는 null 로 반환되고, 실제 호출자\n * (`r2-storage`) 가 point-of-use 에서 namespaced fail-fast 한다 — 즉\n * import 만으로는 절대 throw 하지 않는다 (lazy 정책).\n *\n * legacy env 매핑:\n * - accountId ← `R2_ACCOUNT_ID`\n * - accessKeyId ← `R2_ACCESS_KEY_ID`\n * - secretAccessKey ← `R2_SECRET_ACCESS_KEY`\n * - bucketName ← `R2_BUCKET_NAME`\n */\nexport function resolveR2CredentialsConfig(): {\n accountId: string | null;\n accessKeyId: string | null;\n secretAccessKey: string | null;\n bucketName: string | null;\n} {\n const r2 = _config.storage?.r2 ?? {};\n const pick = (injected: string | undefined, env: string | undefined) =>\n typeof injected === 'string' && injected.length > 0\n ? injected\n : typeof env === 'string' && env.length > 0\n ? env\n : null;\n\n return {\n accountId: pick(r2.accountId, process.env.R2_ACCOUNT_ID),\n accessKeyId: pick(r2.accessKeyId, process.env.R2_ACCESS_KEY_ID),\n secretAccessKey: pick(r2.secretAccessKey, process.env.R2_SECRET_ACCESS_KEY),\n bucketName: pick(r2.bucketName, process.env.R2_BUCKET_NAME),\n };\n}\n\n/**\n * R2 업로드 결과 public URL prefix 를 해석한다.\n *\n * 우선순위: inject(`storage.publicBaseUrl`) > legacy env(`R2_PUBLIC_URL`) > null.\n * null 반환 시 `r2-storage` 는 `https://${bucketName}.r2.dev` fallback 을 쓴다.\n *\n * NB: `publicBaseUrl` 은 `resolveStorageConfig()` 의 inline-key 추출에도\n * 동일하게 쓰인다 (origin matching). 업로드/추출이 같은 origin 으로\n * 일관되도록 의도된 통합점이다.\n */\nexport function resolveR2PublicUrl(): string | null {\n const st = _config.storage ?? {};\n if (typeof st.publicBaseUrl === 'string' && st.publicBaseUrl.length > 0) {\n return st.publicBaseUrl;\n }\n if (\n typeof process.env.R2_PUBLIC_URL === 'string' &&\n process.env.R2_PUBLIC_URL.length > 0\n ) {\n return process.env.R2_PUBLIC_URL;\n }\n return null;\n}\n\n/**\n * 미주입 시 사용되는 고정 identity. 모든 비로그인 요청이 이 하나의 버킷을\n * 공유하므로, 이 값이 실제 rate-limit 키로 쓰이면 한 클라이언트가 전체\n * 사용자를 잠글 수 있다(self-DoS). 그래서 `resolveRateLimitEnabled` 는\n * 추출기가 주입되지 않은 동안 rate-limit 자체를 비활성화한다(아래 참조).\n */\nexport const SHARED_ANON_IDENTITY = 'cms-kit:shared-anon';\n\n/** identityExtractor 주입 여부. */\nexport function hasIdentityExtractor(): boolean {\n return typeof _config.rateLimit?.identityExtractor === 'function';\n}\n\n/**\n * rate-limit client identity 를 해석한다.\n *\n * 안전 기본값: spoofable `x-forwarded-for` 의 첫 값을 무조건 신뢰하지 않는다.\n * consumer 가 trusted-proxy/identity 추출 함수를 주입하지 않은 경우\n * `x-forwarded-for` 만 바꿔서 identity 를 회전시킬 수 없도록 안정적이고\n * 헤더-비의존적인 식별자를 반환한다. consumer 주입 추출기가 있으면 그것을\n * 그대로 사용한다.\n *\n * 주의: 미주입 상태의 고정 식별자는 전역 단일 버킷이므로 실제 제한 키로는\n * 쓰이지 않는다 — `resolveRateLimitEnabled()` 가 false 를 돌려 rate-limit 을\n * 비활성화하고 1회 경고한다. 보호를 켜려면 `createForwardedIdentityExtractor`\n * 등으로 자신의 proxy topology 에 맞는 추출기를 주입해야 한다.\n */\nexport function resolveClientIdentity(headers: Headers): string {\n const rl = _config.rateLimit ?? {};\n if (typeof rl.identityExtractor === 'function') {\n return rl.identityExtractor(headers);\n }\n return SHARED_ANON_IDENTITY;\n}\n\n/**\n * rate-limit 활성화 여부.\n *\n * 우선순위: inject(`rateLimit.enabled`) > legacy `RATE_LIMIT_ENABLED` env >\n * 기본 활성. 단, identityExtractor 가 주입되지 않았으면 비로그인 요청이 모두\n * 하나의 버킷을 공유해 서비스 거부가 되므로 **비활성화** 하고 1회 경고한다.\n * consumer 가 `enabled: true` 를 명시하면 공유 버킷을 감수하는 것으로 보고\n * 경고만 남긴 채 활성화한다.\n */\nexport function resolveRateLimitEnabled(): boolean {\n const rl = _config.rateLimit ?? {};\n if (rl.enabled === false) return false;\n\n if (!hasIdentityExtractor()) {\n if (rl.enabled === true) {\n warnOnceMissingConfig(\n 'rateLimit.identityExtractor.forced',\n 'rateLimit.enabled is true but no rateLimit.identityExtractor is injected. ' +\n 'Every anonymous request shares ONE rate-limit bucket, so a single client ' +\n 'can lock out everyone. Inject `setCmsConfig({ rateLimit: { identityExtractor } })` ' +\n '(e.g. createForwardedIdentityExtractor({ trustedHops: 1 })).',\n );\n return process.env.RATE_LIMIT_ENABLED !== 'false';\n }\n warnOnceMissingConfig(\n 'rateLimit.identityExtractor',\n 'rateLimit.identityExtractor is not configured. Rate limiting is DISABLED ' +\n 'to avoid a shared anonymous bucket (self-DoS). Inject ' +\n '`setCmsConfig({ rateLimit: { identityExtractor } })` — e.g. ' +\n '`createForwardedIdentityExtractor({ trustedHops: 1 })` — to enable it.',\n );\n return false;\n }\n\n if (typeof rl.enabled === 'boolean') return rl.enabled;\n return process.env.RATE_LIMIT_ENABLED !== 'false';\n}\n\n/** `createForwardedIdentityExtractor` 옵션 */\nexport interface ForwardedIdentityOptions {\n /**\n * 요청이 거치는 신뢰 프록시(hop) 수. `x-forwarded-for` 는 각 프록시가 값을\n * 뒤에 덧붙이므로, 마지막 N 번째 값이 첫 신뢰 프록시가 본 클라이언트 주소다.\n * 기본 1 (단일 리버스 프록시/로드밸런서).\n */\n trustedHops?: number;\n /**\n * `x-forwarded-for` 가 없을 때 순서대로 확인할 헤더 이름 목록.\n * 기본 `['x-real-ip']`. 프록시가 덮어쓰는 헤더만 지정해야 한다.\n */\n fallbackHeaders?: readonly string[];\n}\n\n/**\n * 신뢰 프록시 수를 아는 consumer 용 identity 추출기 팩토리.\n *\n * `x-forwarded-for` 의 값들 중 클라이언트가 조작할 수 없는 위치(뒤에서\n * `trustedHops` 번째)를 사용한다. 값이 부족하면(프록시를 거치지 않은 직접\n * 요청 등) 마지막 값을, 헤더가 전혀 없으면 fallback 헤더를, 그것도 없으면\n * 고정 식별자를 돌려준다.\n *\n * @example\n * setCmsConfig({\n * rateLimit: { identityExtractor: createForwardedIdentityExtractor({ trustedHops: 1 }) },\n * });\n */\nexport function createForwardedIdentityExtractor(\n options: ForwardedIdentityOptions = {},\n): CmsIdentityExtractor {\n const hops = Math.max(1, Math.floor(options.trustedHops ?? 1));\n const fallbacks = options.fallbackHeaders ?? ['x-real-ip'];\n\n return (headers: Headers): string => {\n const xff = headers.get('x-forwarded-for');\n if (xff) {\n const parts = xff\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n if (parts.length > 0) {\n const idx = Math.max(0, parts.length - hops);\n return parts[idx];\n }\n }\n for (const name of fallbacks) {\n const v = headers.get(name);\n if (v && v.trim().length > 0) return v.trim();\n }\n return SHARED_ANON_IDENTITY;\n };\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkYY7K5CXXjs = require('./chunk-YY7K5CXX.js');
3
+ var _chunkOHJISVGCjs = require('./chunk-OHJISVGC.js');
4
4
 
5
5
  // src/components/ImageDropUpload.tsx
6
6
  var _react = require('react');
@@ -14,7 +14,7 @@ function ImageDropUpload({
14
14
  placeholder = "\uD074\uB9AD \uB610\uB294 \uB4DC\uB798\uADF8\uD558\uC5EC \uC774\uBBF8\uC9C0 \uCD94\uAC00"
15
15
  }) {
16
16
  const inputRef = _react.useRef.call(void 0, null);
17
- const drop = _chunkYY7K5CXXjs.useImageDropZone.call(void 0, {
17
+ const drop = _chunkOHJISVGCjs.useImageDropZone.call(void 0, {
18
18
  onUpload: (result) => onUpload(result.url, result.key),
19
19
  onKeyTracked
20
20
  });
@@ -68,4 +68,4 @@ function ImageDropUpload({
68
68
 
69
69
 
70
70
  exports.ImageDropUpload = ImageDropUpload;
71
- //# sourceMappingURL=chunk-6EZ3QE5N.js.map
71
+ //# sourceMappingURL=chunk-Y3YYIJYG.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-6EZ3QE5N.js","../src/components/ImageDropUpload.tsx"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-6EZ3QE5N.js","sourcesContent":[null,"\"use client\";\n\nimport { useRef } from \"react\";\nimport { useImageDropZone } from \"../hooks/useImageDropZone\";\nimport \"./image-drop-zone.css\";\n\ninterface Props {\n /** Current image URL */\n src: string;\n /** Called with uploaded image URL and key */\n onUpload: (url: string, key: string) => void;\n /** Track uploaded key for orphan cleanup */\n onKeyTracked?: (key: string) => void;\n /** CSS class for the drop zone container */\n className?: string;\n /** Placeholder text when no image */\n placeholder?: string;\n}\n\nexport default function ImageDropUpload({\n src,\n onUpload,\n onKeyTracked,\n className = \"\",\n placeholder = \"클릭 또는 드래그하여 이미지 추가\",\n}: Props) {\n const inputRef = useRef<HTMLInputElement>(null);\n const drop = useImageDropZone({\n onUpload: (result) => onUpload(result.url, result.key),\n onKeyTracked,\n });\n\n const cls = [\n className,\n src ? \"has\" : \"\",\n drop.isDragOver ? \"is-drag-over\" : \"\",\n drop.isUploading ? \"is-uploading\" : \"\",\n drop.isResizing ? \"is-resizing\" : \"\",\n ].filter(Boolean).join(\" \");\n\n return (\n <>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n style={{ display: \"none\" }}\n onChange={async (e) => {\n await drop.handleFileInput(e.target.files);\n e.target.value = \"\";\n }}\n />\n <div\n className={cls}\n onClick={() => !drop.isUploading && !drop.isResizing && inputRef.current?.click()}\n {...drop.dragHandlers}\n >\n {drop.isResizing ? (\n <span className=\"dz-upload-spinner\">최적화 중...</span>\n ) : drop.isUploading ? (\n <span className=\"dz-upload-spinner\">업로드 중...</span>\n ) : drop.isDragOver ? (\n <span className=\"dz-drag-hint\">놓으세요</span>\n ) : src ? (\n /* eslint-disable-next-line @next/next/no-img-element */\n <img src={src} alt=\"\" style={{ maxWidth: \"100%\", height: \"auto\" }} />\n ) : (\n <div className=\"dz-placeholder\">\n <div className=\"dz-placeholder-icon\">+</div>\n <div className=\"dz-placeholder-text\">{placeholder}</div>\n <div className=\"dz-placeholder-guide\">\n JPG, PNG, WebP, GIF | 최대 5MB\n <br />\n 초과 시 자동 압축 · 1920px 이하로 리사이즈\n <br />\n WebP 변환 · 4개 사이즈(lg/md/sm/thumb) 자동 생성\n </div>\n </div>\n )}\n </div>\n {drop.error && <div className=\"dz-error\">{drop.error}</div>}\n </>\n );\n}\n"]}
1
+ {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-Y3YYIJYG.js","../src/components/ImageDropUpload.tsx"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-Y3YYIJYG.js","sourcesContent":[null,"\"use client\";\n\nimport { useRef } from \"react\";\nimport { useImageDropZone } from \"../hooks/useImageDropZone\";\nimport \"./image-drop-zone.css\";\n\ninterface Props {\n /** Current image URL */\n src: string;\n /** Called with uploaded image URL and key */\n onUpload: (url: string, key: string) => void;\n /** Track uploaded key for orphan cleanup */\n onKeyTracked?: (key: string) => void;\n /** CSS class for the drop zone container */\n className?: string;\n /** Placeholder text when no image */\n placeholder?: string;\n}\n\nexport default function ImageDropUpload({\n src,\n onUpload,\n onKeyTracked,\n className = \"\",\n placeholder = \"클릭 또는 드래그하여 이미지 추가\",\n}: Props) {\n const inputRef = useRef<HTMLInputElement>(null);\n const drop = useImageDropZone({\n onUpload: (result) => onUpload(result.url, result.key),\n onKeyTracked,\n });\n\n const cls = [\n className,\n src ? \"has\" : \"\",\n drop.isDragOver ? \"is-drag-over\" : \"\",\n drop.isUploading ? \"is-uploading\" : \"\",\n drop.isResizing ? \"is-resizing\" : \"\",\n ].filter(Boolean).join(\" \");\n\n return (\n <>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n style={{ display: \"none\" }}\n onChange={async (e) => {\n await drop.handleFileInput(e.target.files);\n e.target.value = \"\";\n }}\n />\n <div\n className={cls}\n onClick={() => !drop.isUploading && !drop.isResizing && inputRef.current?.click()}\n {...drop.dragHandlers}\n >\n {drop.isResizing ? (\n <span className=\"dz-upload-spinner\">최적화 중...</span>\n ) : drop.isUploading ? (\n <span className=\"dz-upload-spinner\">업로드 중...</span>\n ) : drop.isDragOver ? (\n <span className=\"dz-drag-hint\">놓으세요</span>\n ) : src ? (\n /* eslint-disable-next-line @next/next/no-img-element */\n <img src={src} alt=\"\" style={{ maxWidth: \"100%\", height: \"auto\" }} />\n ) : (\n <div className=\"dz-placeholder\">\n <div className=\"dz-placeholder-icon\">+</div>\n <div className=\"dz-placeholder-text\">{placeholder}</div>\n <div className=\"dz-placeholder-guide\">\n JPG, PNG, WebP, GIF | 최대 5MB\n <br />\n 초과 시 자동 압축 · 1920px 이하로 리사이즈\n <br />\n WebP 변환 · 4개 사이즈(lg/md/sm/thumb) 자동 생성\n </div>\n </div>\n )}\n </div>\n {drop.error && <div className=\"dz-error\">{drop.error}</div>}\n </>\n );\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
3
+ var _chunkUVZD7GHGjs = require('./chunk-UVZD7GHG.js');
4
4
 
5
5
 
6
6
  var _chunkDGUM43GVjs = require('./chunk-DGUM43GV.js');
@@ -78,32 +78,37 @@ function dompurifySanitize(html, purify, trustedOrigins, config) {
78
78
  }
79
79
  options.ALLOWED_ATTR = Array.from(flat);
80
80
  }
81
- const purifyWithHook = purify;
82
- if (typeof purifyWithHook.addHook === "function") {
83
- purifyWithHook.addHook("uponSanitizeElement", (node) => {
84
- if (node.nodeName && node.nodeName.toLowerCase() === "iframe") {
85
- const src = _nullishCoalesce(_optionalChain([node, 'access', _ => _.getAttribute, 'optionalCall', _2 => _2("src")]), () => ( ""));
86
- const trusted = trustedOrigins.some(
87
- (origin) => src.startsWith(origin)
88
- );
89
- if (!trusted) {
90
- _optionalChain([node, 'access', _3 => _3.remove, 'optionalCall', _4 => _4()]);
91
- }
92
- }
93
- });
94
- }
81
+ ensureIframeHook(purify);
82
+ activeTrustedOrigins = trustedOrigins;
95
83
  try {
96
84
  return purify.sanitize(html, options);
97
85
  } finally {
98
- if (typeof purifyWithHook.removeHook === "function") {
99
- purifyWithHook.removeHook("uponSanitizeElement");
100
- }
86
+ activeTrustedOrigins = null;
101
87
  }
102
88
  }
89
+ var activeTrustedOrigins = null;
90
+ var hookedInstances = /* @__PURE__ */ new WeakSet();
91
+ function ensureIframeHook(purify) {
92
+ const p = purify;
93
+ if (typeof p.addHook !== "function") return;
94
+ if (hookedInstances.has(p)) return;
95
+ hookedInstances.add(p);
96
+ p.addHook("uponSanitizeElement", (node) => {
97
+ const origins = activeTrustedOrigins;
98
+ if (!origins) return;
99
+ if (node.nodeName && node.nodeName.toLowerCase() === "iframe") {
100
+ const src = _nullishCoalesce(_optionalChain([node, 'access', _ => _.getAttribute, 'optionalCall', _2 => _2("src")]), () => ( ""));
101
+ const trusted = origins.some((origin) => src.startsWith(origin));
102
+ if (!trusted) {
103
+ _optionalChain([node, 'access', _3 => _3.remove, 'optionalCall', _4 => _4()]);
104
+ }
105
+ }
106
+ });
107
+ }
103
108
  function createSanitizer(config = {}) {
104
109
  return function sanitize(html) {
105
110
  if (!html) return html;
106
- const trustedOrigins = _nullishCoalesce(config.trustedIframeOrigins, () => ( _chunkEG6NVPP2js.resolveTrustedIframeOrigins.call(void 0, )));
111
+ const trustedOrigins = _nullishCoalesce(config.trustedIframeOrigins, () => ( _chunkUVZD7GHGjs.resolveTrustedIframeOrigins.call(void 0, )));
107
112
  const purify = tryLoadDomPurify();
108
113
  if (purify) {
109
114
  return dompurifySanitize(html, purify, trustedOrigins, config);
@@ -120,4 +125,4 @@ var defaultSanitize = createSanitizer();
120
125
 
121
126
 
122
127
  exports.createSanitizer = createSanitizer; exports.sanitizeHtmlContent = sanitizeHtmlContent;
123
- //# sourceMappingURL=chunk-OYJRR3G2.js.map
128
+ //# sourceMappingURL=chunk-ZMXNPYFS.js.map