@withwiz/cms-kit 0.1.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 (327) hide show
  1. package/README.ko.md +146 -0
  2. package/README.md +146 -0
  3. package/dist/chunk-2REYWBQ6.js +32 -0
  4. package/dist/chunk-2REYWBQ6.js.map +1 -0
  5. package/dist/chunk-2SYUEQ6J.mjs +17 -0
  6. package/dist/chunk-2SYUEQ6J.mjs.map +1 -0
  7. package/dist/chunk-3ILEMZAL.mjs +32 -0
  8. package/dist/chunk-3ILEMZAL.mjs.map +1 -0
  9. package/dist/chunk-3Y2TQCHU.mjs +33 -0
  10. package/dist/chunk-3Y2TQCHU.mjs.map +1 -0
  11. package/dist/chunk-4LSFAAZW.js +1 -0
  12. package/dist/chunk-4LSFAAZW.js.map +1 -0
  13. package/dist/chunk-4W3X7OKF.js +100 -0
  14. package/dist/chunk-4W3X7OKF.js.map +1 -0
  15. package/dist/chunk-5JUIMXNF.js +108 -0
  16. package/dist/chunk-5JUIMXNF.js.map +1 -0
  17. package/dist/chunk-5RDMODGV.mjs +34 -0
  18. package/dist/chunk-5RDMODGV.mjs.map +1 -0
  19. package/dist/chunk-6ES4W2DL.mjs +1 -0
  20. package/dist/chunk-6ES4W2DL.mjs.map +1 -0
  21. package/dist/chunk-6EZ3QE5N.js +71 -0
  22. package/dist/chunk-6EZ3QE5N.js.map +1 -0
  23. package/dist/chunk-6JFERXNE.mjs +16 -0
  24. package/dist/chunk-6JFERXNE.mjs.map +1 -0
  25. package/dist/chunk-7SWW3ZIN.js +17 -0
  26. package/dist/chunk-7SWW3ZIN.js.map +1 -0
  27. package/dist/chunk-AXR56AUD.mjs +1 -0
  28. package/dist/chunk-AXR56AUD.mjs.map +1 -0
  29. package/dist/chunk-BE7JDL64.js +154 -0
  30. package/dist/chunk-BE7JDL64.js.map +1 -0
  31. package/dist/chunk-BHN2SRZC.js +51 -0
  32. package/dist/chunk-BHN2SRZC.js.map +1 -0
  33. package/dist/chunk-BJTO5JO5.mjs +11 -0
  34. package/dist/chunk-BJTO5JO5.mjs.map +1 -0
  35. package/dist/chunk-BWKKHCDG.mjs +164 -0
  36. package/dist/chunk-BWKKHCDG.mjs.map +1 -0
  37. package/dist/chunk-C5WFZJM4.js +218 -0
  38. package/dist/chunk-C5WFZJM4.js.map +1 -0
  39. package/dist/chunk-CGVFB23E.mjs +123 -0
  40. package/dist/chunk-CGVFB23E.mjs.map +1 -0
  41. package/dist/chunk-D2VDTRXO.js +1 -0
  42. package/dist/chunk-D2VDTRXO.js.map +1 -0
  43. package/dist/chunk-D75ADHVM.mjs +181 -0
  44. package/dist/chunk-D75ADHVM.mjs.map +1 -0
  45. package/dist/chunk-DCIF4XJL.js +309 -0
  46. package/dist/chunk-DCIF4XJL.js.map +1 -0
  47. package/dist/chunk-DGUM43GV.js +11 -0
  48. package/dist/chunk-DGUM43GV.js.map +1 -0
  49. package/dist/chunk-DSORCGJK.js +44 -0
  50. package/dist/chunk-DSORCGJK.js.map +1 -0
  51. package/dist/chunk-E5YUAOJI.js +27 -0
  52. package/dist/chunk-E5YUAOJI.js.map +1 -0
  53. package/dist/chunk-EG6NVPP2.js +164 -0
  54. package/dist/chunk-EG6NVPP2.js.map +1 -0
  55. package/dist/chunk-EL7YGOTY.js +1 -0
  56. package/dist/chunk-EL7YGOTY.js.map +1 -0
  57. package/dist/chunk-EM4MQEWV.mjs +100 -0
  58. package/dist/chunk-EM4MQEWV.mjs.map +1 -0
  59. package/dist/chunk-FCCCG5Z7.mjs +108 -0
  60. package/dist/chunk-FCCCG5Z7.mjs.map +1 -0
  61. package/dist/chunk-FEPGJ7WN.mjs +218 -0
  62. package/dist/chunk-FEPGJ7WN.mjs.map +1 -0
  63. package/dist/chunk-HXVWBFCN.js +16 -0
  64. package/dist/chunk-HXVWBFCN.js.map +1 -0
  65. package/dist/chunk-I5YMTJV6.mjs +51 -0
  66. package/dist/chunk-I5YMTJV6.mjs.map +1 -0
  67. package/dist/chunk-IMNWW7Q5.mjs +13 -0
  68. package/dist/chunk-IMNWW7Q5.mjs.map +1 -0
  69. package/dist/chunk-J5N76ASB.mjs +114 -0
  70. package/dist/chunk-J5N76ASB.mjs.map +1 -0
  71. package/dist/chunk-JBV26CUG.mjs +51 -0
  72. package/dist/chunk-JBV26CUG.mjs.map +1 -0
  73. package/dist/chunk-JMWXVXK5.js +53 -0
  74. package/dist/chunk-JMWXVXK5.js.map +1 -0
  75. package/dist/chunk-KWKV2GAH.js +60 -0
  76. package/dist/chunk-KWKV2GAH.js.map +1 -0
  77. package/dist/chunk-LDFE33ML.mjs +53 -0
  78. package/dist/chunk-LDFE33ML.mjs.map +1 -0
  79. package/dist/chunk-LF5LOI7K.mjs +174 -0
  80. package/dist/chunk-LF5LOI7K.mjs.map +1 -0
  81. package/dist/chunk-MMKAMH4U.mjs +16 -0
  82. package/dist/chunk-MMKAMH4U.mjs.map +1 -0
  83. package/dist/chunk-MUHFEEYW.js +33 -0
  84. package/dist/chunk-MUHFEEYW.js.map +1 -0
  85. package/dist/chunk-MUQYK5EO.js +1 -0
  86. package/dist/chunk-MUQYK5EO.js.map +1 -0
  87. package/dist/chunk-OBNALAMG.mjs +71 -0
  88. package/dist/chunk-OBNALAMG.mjs.map +1 -0
  89. package/dist/chunk-OLEYNPW6.js +25 -0
  90. package/dist/chunk-OLEYNPW6.js.map +1 -0
  91. package/dist/chunk-OYJRR3G2.js +123 -0
  92. package/dist/chunk-OYJRR3G2.js.map +1 -0
  93. package/dist/chunk-PLVG7AW3.js +16 -0
  94. package/dist/chunk-PLVG7AW3.js.map +1 -0
  95. package/dist/chunk-PYD4CES4.js +1 -0
  96. package/dist/chunk-PYD4CES4.js.map +1 -0
  97. package/dist/chunk-QACD2U6P.mjs +1 -0
  98. package/dist/chunk-QACD2U6P.mjs.map +1 -0
  99. package/dist/chunk-R5GGIVD4.js +10 -0
  100. package/dist/chunk-R5GGIVD4.js.map +1 -0
  101. package/dist/chunk-RCK5WCU5.mjs +25 -0
  102. package/dist/chunk-RCK5WCU5.mjs.map +1 -0
  103. package/dist/chunk-S6UUSM7S.js +13 -0
  104. package/dist/chunk-S6UUSM7S.js.map +1 -0
  105. package/dist/chunk-S7GOXCRM.js +114 -0
  106. package/dist/chunk-S7GOXCRM.js.map +1 -0
  107. package/dist/chunk-SMV5D5LO.mjs +28 -0
  108. package/dist/chunk-SMV5D5LO.mjs.map +1 -0
  109. package/dist/chunk-T7EVFD4G.mjs +60 -0
  110. package/dist/chunk-T7EVFD4G.mjs.map +1 -0
  111. package/dist/chunk-TLH3V4SY.mjs +1 -0
  112. package/dist/chunk-TLH3V4SY.mjs.map +1 -0
  113. package/dist/chunk-TUQ2CVAK.mjs +10 -0
  114. package/dist/chunk-TUQ2CVAK.mjs.map +1 -0
  115. package/dist/chunk-U6PVJKTB.js +51 -0
  116. package/dist/chunk-U6PVJKTB.js.map +1 -0
  117. package/dist/chunk-UXMMEEX4.mjs +27 -0
  118. package/dist/chunk-UXMMEEX4.mjs.map +1 -0
  119. package/dist/chunk-VVXNFFKI.mjs +44 -0
  120. package/dist/chunk-VVXNFFKI.mjs.map +1 -0
  121. package/dist/chunk-VZZ3RU3Q.js +28 -0
  122. package/dist/chunk-VZZ3RU3Q.js.map +1 -0
  123. package/dist/chunk-W4FWIZ2Y.js +34 -0
  124. package/dist/chunk-W4FWIZ2Y.js.map +1 -0
  125. package/dist/chunk-YEQFKOWD.mjs +154 -0
  126. package/dist/chunk-YEQFKOWD.mjs.map +1 -0
  127. package/dist/chunk-YPBKY4KY.mjs +1 -0
  128. package/dist/chunk-YPBKY4KY.mjs.map +1 -0
  129. package/dist/chunk-YY7K5CXX.js +174 -0
  130. package/dist/chunk-YY7K5CXX.js.map +1 -0
  131. package/dist/chunk-ZFORODOX.js +181 -0
  132. package/dist/chunk-ZFORODOX.js.map +1 -0
  133. package/dist/chunk-ZULSFSDG.mjs +309 -0
  134. package/dist/chunk-ZULSFSDG.mjs.map +1 -0
  135. package/dist/components/AdminManagerBase.d.mts +26 -0
  136. package/dist/components/AdminManagerBase.d.ts +26 -0
  137. package/dist/components/AdminManagerBase.js +13 -0
  138. package/dist/components/AdminManagerBase.js.map +1 -0
  139. package/dist/components/AdminManagerBase.mjs +12 -0
  140. package/dist/components/AdminManagerBase.mjs.map +1 -0
  141. package/dist/components/AdminManagerConfig.d.mts +83 -0
  142. package/dist/components/AdminManagerConfig.d.ts +83 -0
  143. package/dist/components/AdminManagerConfig.js +2 -0
  144. package/dist/components/AdminManagerConfig.js.map +1 -0
  145. package/dist/components/AdminManagerConfig.mjs +2 -0
  146. package/dist/components/AdminManagerConfig.mjs.map +1 -0
  147. package/dist/components/AdminShell.d.mts +30 -0
  148. package/dist/components/AdminShell.d.ts +30 -0
  149. package/dist/components/AdminShell.js +12 -0
  150. package/dist/components/AdminShell.js.map +1 -0
  151. package/dist/components/AdminShell.mjs +11 -0
  152. package/dist/components/AdminShell.mjs.map +1 -0
  153. package/dist/components/JsonLd.d.mts +32 -0
  154. package/dist/components/JsonLd.d.ts +32 -0
  155. package/dist/components/JsonLd.js +11 -0
  156. package/dist/components/JsonLd.js.map +1 -0
  157. package/dist/components/JsonLd.mjs +11 -0
  158. package/dist/components/JsonLd.mjs.map +1 -0
  159. package/dist/components/ResizableImage.d.mts +18 -0
  160. package/dist/components/ResizableImage.d.ts +18 -0
  161. package/dist/components/ResizableImage.js +10 -0
  162. package/dist/components/ResizableImage.js.map +1 -0
  163. package/dist/components/ResizableImage.mjs +9 -0
  164. package/dist/components/ResizableImage.mjs.map +1 -0
  165. package/dist/components/ToggleSwitch.d.mts +11 -0
  166. package/dist/components/ToggleSwitch.d.ts +11 -0
  167. package/dist/components/ToggleSwitch.js +10 -0
  168. package/dist/components/ToggleSwitch.js.map +1 -0
  169. package/dist/components/ToggleSwitch.mjs +9 -0
  170. package/dist/components/ToggleSwitch.mjs.map +1 -0
  171. package/dist/components/image-drop-zone.css +115 -0
  172. package/dist/components/index.d.mts +26 -0
  173. package/dist/components/index.d.ts +26 -0
  174. package/dist/components/index.js +35 -0
  175. package/dist/components/index.js.map +1 -0
  176. package/dist/components/index.mjs +35 -0
  177. package/dist/components/index.mjs.map +1 -0
  178. package/dist/components/toggle-switch.css +55 -0
  179. package/dist/hooks/index.d.mts +45 -0
  180. package/dist/hooks/index.d.ts +45 -0
  181. package/dist/hooks/index.js +25 -0
  182. package/dist/hooks/index.js.map +1 -0
  183. package/dist/hooks/index.mjs +25 -0
  184. package/dist/hooks/index.mjs.map +1 -0
  185. package/dist/hooks/useImageDropZone.d.mts +37 -0
  186. package/dist/hooks/useImageDropZone.d.ts +37 -0
  187. package/dist/hooks/useImageDropZone.js +13 -0
  188. package/dist/hooks/useImageDropZone.js.map +1 -0
  189. package/dist/hooks/useImageDropZone.mjs +12 -0
  190. package/dist/hooks/useImageDropZone.mjs.map +1 -0
  191. package/dist/hooks/useScrollReveal.d.mts +6 -0
  192. package/dist/hooks/useScrollReveal.d.ts +6 -0
  193. package/dist/hooks/useScrollReveal.js +12 -0
  194. package/dist/hooks/useScrollReveal.js.map +1 -0
  195. package/dist/hooks/useScrollReveal.mjs +11 -0
  196. package/dist/hooks/useScrollReveal.mjs.map +1 -0
  197. package/dist/image-drop-zone.css +115 -0
  198. package/dist/index-00a40-FQ.d.mts +136 -0
  199. package/dist/index-00a40-FQ.d.ts +136 -0
  200. package/dist/index.d.mts +35 -0
  201. package/dist/index.d.ts +35 -0
  202. package/dist/index.js +169 -0
  203. package/dist/index.js.map +1 -0
  204. package/dist/index.mjs +169 -0
  205. package/dist/index.mjs.map +1 -0
  206. package/dist/infrastructure/index.d.mts +4 -0
  207. package/dist/infrastructure/index.d.ts +4 -0
  208. package/dist/infrastructure/index.js +20 -0
  209. package/dist/infrastructure/index.js.map +1 -0
  210. package/dist/infrastructure/index.mjs +20 -0
  211. package/dist/infrastructure/index.mjs.map +1 -0
  212. package/dist/infrastructure/middleware/index.d.mts +3 -0
  213. package/dist/infrastructure/middleware/index.d.ts +3 -0
  214. package/dist/infrastructure/middleware/index.js +15 -0
  215. package/dist/infrastructure/middleware/index.js.map +1 -0
  216. package/dist/infrastructure/middleware/index.mjs +15 -0
  217. package/dist/infrastructure/middleware/index.mjs.map +1 -0
  218. package/dist/infrastructure/middleware/wrappers.d.mts +10 -0
  219. package/dist/infrastructure/middleware/wrappers.d.ts +10 -0
  220. package/dist/infrastructure/middleware/wrappers.js +15 -0
  221. package/dist/infrastructure/middleware/wrappers.js.map +1 -0
  222. package/dist/infrastructure/middleware/wrappers.mjs +15 -0
  223. package/dist/infrastructure/middleware/wrappers.mjs.map +1 -0
  224. package/dist/infrastructure/prisma.d.mts +5 -0
  225. package/dist/infrastructure/prisma.d.ts +5 -0
  226. package/dist/infrastructure/prisma.js +12 -0
  227. package/dist/infrastructure/prisma.js.map +1 -0
  228. package/dist/infrastructure/prisma.mjs +12 -0
  229. package/dist/infrastructure/prisma.mjs.map +1 -0
  230. package/dist/services/index.d.mts +22 -0
  231. package/dist/services/index.d.ts +22 -0
  232. package/dist/services/index.js +36 -0
  233. package/dist/services/index.js.map +1 -0
  234. package/dist/services/index.mjs +36 -0
  235. package/dist/services/index.mjs.map +1 -0
  236. package/dist/toggle-switch.css +55 -0
  237. package/dist/types/common.d.mts +14 -0
  238. package/dist/types/common.d.ts +14 -0
  239. package/dist/types/common.js +8 -0
  240. package/dist/types/common.js.map +1 -0
  241. package/dist/types/common.mjs +8 -0
  242. package/dist/types/common.mjs.map +1 -0
  243. package/dist/types/index.d.mts +1 -0
  244. package/dist/types/index.d.ts +1 -0
  245. package/dist/types/index.js +9 -0
  246. package/dist/types/index.js.map +1 -0
  247. package/dist/types/index.mjs +9 -0
  248. package/dist/types/index.mjs.map +1 -0
  249. package/dist/utils/admin-fetch.d.mts +11 -0
  250. package/dist/utils/admin-fetch.d.ts +11 -0
  251. package/dist/utils/admin-fetch.js +13 -0
  252. package/dist/utils/admin-fetch.js.map +1 -0
  253. package/dist/utils/admin-fetch.mjs +13 -0
  254. package/dist/utils/admin-fetch.mjs.map +1 -0
  255. package/dist/utils/api-helpers.d.mts +20 -0
  256. package/dist/utils/api-helpers.d.ts +20 -0
  257. package/dist/utils/api-helpers.js +12 -0
  258. package/dist/utils/api-helpers.js.map +1 -0
  259. package/dist/utils/api-helpers.mjs +12 -0
  260. package/dist/utils/api-helpers.mjs.map +1 -0
  261. package/dist/utils/date.d.mts +5 -0
  262. package/dist/utils/date.d.ts +5 -0
  263. package/dist/utils/date.js +12 -0
  264. package/dist/utils/date.js.map +1 -0
  265. package/dist/utils/date.mjs +12 -0
  266. package/dist/utils/date.mjs.map +1 -0
  267. package/dist/utils/html-sanitizer.d.mts +56 -0
  268. package/dist/utils/html-sanitizer.d.ts +56 -0
  269. package/dist/utils/html-sanitizer.js +11 -0
  270. package/dist/utils/html-sanitizer.js.map +1 -0
  271. package/dist/utils/html-sanitizer.mjs +11 -0
  272. package/dist/utils/html-sanitizer.mjs.map +1 -0
  273. package/dist/utils/image-variant-utils.d.mts +14 -0
  274. package/dist/utils/image-variant-utils.d.ts +14 -0
  275. package/dist/utils/image-variant-utils.js +10 -0
  276. package/dist/utils/image-variant-utils.js.map +1 -0
  277. package/dist/utils/image-variant-utils.mjs +10 -0
  278. package/dist/utils/image-variant-utils.mjs.map +1 -0
  279. package/dist/utils/image-variants.d.mts +13 -0
  280. package/dist/utils/image-variants.d.ts +13 -0
  281. package/dist/utils/image-variants.js +12 -0
  282. package/dist/utils/image-variants.js.map +1 -0
  283. package/dist/utils/image-variants.mjs +12 -0
  284. package/dist/utils/image-variants.mjs.map +1 -0
  285. package/dist/utils/index.d.mts +88 -0
  286. package/dist/utils/index.d.ts +88 -0
  287. package/dist/utils/index.js +92 -0
  288. package/dist/utils/index.js.map +1 -0
  289. package/dist/utils/index.mjs +92 -0
  290. package/dist/utils/index.mjs.map +1 -0
  291. package/dist/utils/jwt.d.mts +19 -0
  292. package/dist/utils/jwt.d.ts +19 -0
  293. package/dist/utils/jwt.js +9 -0
  294. package/dist/utils/jwt.js.map +1 -0
  295. package/dist/utils/jwt.mjs +9 -0
  296. package/dist/utils/jwt.mjs.map +1 -0
  297. package/dist/utils/r2-helpers.d.mts +9 -0
  298. package/dist/utils/r2-helpers.d.ts +9 -0
  299. package/dist/utils/r2-helpers.js +20 -0
  300. package/dist/utils/r2-helpers.js.map +1 -0
  301. package/dist/utils/r2-helpers.mjs +20 -0
  302. package/dist/utils/r2-helpers.mjs.map +1 -0
  303. package/dist/utils/r2-storage.d.mts +38 -0
  304. package/dist/utils/r2-storage.d.ts +38 -0
  305. package/dist/utils/r2-storage.js +17 -0
  306. package/dist/utils/r2-storage.js.map +1 -0
  307. package/dist/utils/r2-storage.mjs +17 -0
  308. package/dist/utils/r2-storage.mjs.map +1 -0
  309. package/dist/utils/route-params.d.mts +3 -0
  310. package/dist/utils/route-params.d.ts +3 -0
  311. package/dist/utils/route-params.js +8 -0
  312. package/dist/utils/route-params.js.map +1 -0
  313. package/dist/utils/route-params.mjs +8 -0
  314. package/dist/utils/route-params.mjs.map +1 -0
  315. package/dist/validators/index.d.mts +2 -0
  316. package/dist/validators/index.d.ts +2 -0
  317. package/dist/validators/index.js +11 -0
  318. package/dist/validators/index.js.map +1 -0
  319. package/dist/validators/index.mjs +11 -0
  320. package/dist/validators/index.mjs.map +1 -0
  321. package/dist/validators/shared.d.mts +6 -0
  322. package/dist/validators/shared.d.ts +6 -0
  323. package/dist/validators/shared.js +10 -0
  324. package/dist/validators/shared.js.map +1 -0
  325. package/dist/validators/shared.mjs +10 -0
  326. package/dist/validators/shared.mjs.map +1 -0
  327. package/package.json +257 -0
@@ -0,0 +1,164 @@
1
+ // src/config/index.ts
2
+ var _A = "/admin";
3
+ var _API_A = "/api" + _A;
4
+ var _AUTH = _API_A + "/auth";
5
+ var DEFAULT_ROUTES = {
6
+ loginPath: _A + "/login",
7
+ postLoginRedirect: _A + "/dashboard",
8
+ meEndpoint: _AUTH + "/me",
9
+ logoutEndpoint: _AUTH + "/logout",
10
+ refreshEndpoint: _AUTH + "/refresh",
11
+ uploadEndpoint: _API_A + "/upload"
12
+ };
13
+ var DEFAULT_TRUSTED_IFRAME_ORIGINS = [
14
+ "https://www.youtube.com/",
15
+ "https://youtube.com/",
16
+ "https://www.youtube-nocookie.com/",
17
+ "https://player.vimeo.com/"
18
+ ];
19
+ var DEFAULT_ACCESS_TOKEN_EXPIRY = "2h";
20
+ var DEFAULT_REFRESH_TOKEN_EXPIRY = "7d";
21
+ var DEFAULT_JWT_ALGORITHM = "HS256";
22
+ var JWT_SECRET_MIN_LENGTH = 32;
23
+ var _config = {};
24
+ function setCmsConfig(config) {
25
+ _config = {
26
+ ..._config,
27
+ ...config,
28
+ brand: { ..._config.brand, ...config.brand },
29
+ routes: { ..._config.routes, ...config.routes },
30
+ jwt: { ..._config.jwt, ...config.jwt },
31
+ sanitizer: { ..._config.sanitizer, ...config.sanitizer },
32
+ storage: {
33
+ ..._config.storage,
34
+ ...config.storage,
35
+ r2: { ..._config.storage?.r2, ...config.storage?.r2 }
36
+ },
37
+ rateLimit: { ..._config.rateLimit, ...config.rateLimit }
38
+ };
39
+ }
40
+ function resetCmsConfig() {
41
+ _config = {};
42
+ _warnedKeys.clear();
43
+ }
44
+ function getCmsConfig() {
45
+ return _config;
46
+ }
47
+ var NS = "@withwiz/cms-kit:";
48
+ var _warnedKeys = /* @__PURE__ */ new Set();
49
+ function warnOnceMissingConfig(key, message) {
50
+ if (_warnedKeys.has(key)) return;
51
+ _warnedKeys.add(key);
52
+ console.warn(`${NS} ${message}`);
53
+ }
54
+ function namespacedError(message) {
55
+ return new Error(`${NS} ${message}`);
56
+ }
57
+ function resolveBrandConfig(suppliedViaProps = false) {
58
+ const b = _config.brand ?? {};
59
+ const navConfigured = Array.isArray(b.navItems);
60
+ const brandConfigured = typeof b.brandLabel === "string";
61
+ if (!suppliedViaProps && !navConfigured && !brandConfigured) {
62
+ warnOnceMissingConfig(
63
+ "brand.nav",
64
+ "brand/navigation is not configured. Rendering a neutral empty admin shell. Inject `setCmsConfig({ brand: { brandLabel, navItems } })` to supply brand and navigation."
65
+ );
66
+ }
67
+ return {
68
+ brandLabel: brandConfigured ? b.brandLabel : null,
69
+ brandHref: b.brandHref ?? "/",
70
+ adminHref: b.adminHref ?? _A,
71
+ navItems: navConfigured ? b.navItems : []
72
+ };
73
+ }
74
+ function resolveRouteConfig() {
75
+ const r = _config.routes ?? {};
76
+ return {
77
+ loginPath: r.loginPath ?? DEFAULT_ROUTES.loginPath,
78
+ postLoginRedirect: r.postLoginRedirect ?? DEFAULT_ROUTES.postLoginRedirect,
79
+ meEndpoint: r.meEndpoint ?? DEFAULT_ROUTES.meEndpoint,
80
+ logoutEndpoint: r.logoutEndpoint ?? DEFAULT_ROUTES.logoutEndpoint,
81
+ refreshEndpoint: r.refreshEndpoint ?? DEFAULT_ROUTES.refreshEndpoint,
82
+ uploadEndpoint: r.uploadEndpoint ?? DEFAULT_ROUTES.uploadEndpoint
83
+ };
84
+ }
85
+ function resolveJwtConfig() {
86
+ const j = _config.jwt ?? {};
87
+ const accessTokenExpiry = j.accessTokenExpiry ?? process.env.JWT_EXPIRES_IN ?? DEFAULT_ACCESS_TOKEN_EXPIRY;
88
+ const refreshTokenExpiry = j.refreshTokenExpiry ?? process.env.JWT_REFRESH_TOKEN_EXPIRES_IN ?? DEFAULT_REFRESH_TOKEN_EXPIRY;
89
+ const algorithm = j.algorithm ?? DEFAULT_JWT_ALGORITHM;
90
+ const secret = j.secret ?? process.env.JWT_SECRET;
91
+ if (secret === void 0 || secret === null || secret === "") {
92
+ throw namespacedError(
93
+ "JWT signing secret is missing. There is NO safe default for a signing secret. Inject `setCmsConfig({ jwt: { secret } })` or set the `JWT_SECRET` environment variable."
94
+ );
95
+ }
96
+ if (typeof secret !== "string" || secret.length < JWT_SECRET_MIN_LENGTH) {
97
+ throw namespacedError(
98
+ `JWT signing secret is too weak: it must be at least ${JWT_SECRET_MIN_LENGTH} characters (documented policy). A short/weak secret produces forgeable tokens and has no safe default. Provide a stronger \`JWT_SECRET\` / injected jwt.secret.`
99
+ );
100
+ }
101
+ return { secret, accessTokenExpiry, refreshTokenExpiry, algorithm };
102
+ }
103
+ function resolveTrustedIframeOrigins() {
104
+ const s = _config.sanitizer ?? {};
105
+ return s.trustedIframeOrigins ?? DEFAULT_TRUSTED_IFRAME_ORIGINS;
106
+ }
107
+ function resolveStorageConfig() {
108
+ const st = _config.storage ?? {};
109
+ return {
110
+ inlineKeyPrefixes: Array.isArray(st.inlineKeyPrefixes) ? st.inlineKeyPrefixes : null,
111
+ publicBaseUrl: typeof st.publicBaseUrl === "string" ? st.publicBaseUrl : null
112
+ };
113
+ }
114
+ function resolveR2CredentialsConfig() {
115
+ const r2 = _config.storage?.r2 ?? {};
116
+ const pick = (injected, env) => typeof injected === "string" && injected.length > 0 ? injected : typeof env === "string" && env.length > 0 ? env : null;
117
+ return {
118
+ accountId: pick(r2.accountId, process.env.R2_ACCOUNT_ID),
119
+ accessKeyId: pick(r2.accessKeyId, process.env.R2_ACCESS_KEY_ID),
120
+ secretAccessKey: pick(r2.secretAccessKey, process.env.R2_SECRET_ACCESS_KEY),
121
+ bucketName: pick(r2.bucketName, process.env.R2_BUCKET_NAME)
122
+ };
123
+ }
124
+ function resolveR2PublicUrl() {
125
+ const st = _config.storage ?? {};
126
+ if (typeof st.publicBaseUrl === "string" && st.publicBaseUrl.length > 0) {
127
+ return st.publicBaseUrl;
128
+ }
129
+ if (typeof process.env.R2_PUBLIC_URL === "string" && process.env.R2_PUBLIC_URL.length > 0) {
130
+ return process.env.R2_PUBLIC_URL;
131
+ }
132
+ return null;
133
+ }
134
+ function resolveClientIdentity(headers) {
135
+ const rl = _config.rateLimit ?? {};
136
+ if (typeof rl.identityExtractor === "function") {
137
+ return rl.identityExtractor(headers);
138
+ }
139
+ return "cms-kit:shared-anon";
140
+ }
141
+ function resolveRateLimitEnabled() {
142
+ const rl = _config.rateLimit ?? {};
143
+ if (typeof rl.enabled === "boolean") return rl.enabled;
144
+ return process.env.RATE_LIMIT_ENABLED !== "false";
145
+ }
146
+
147
+ export {
148
+ JWT_SECRET_MIN_LENGTH,
149
+ setCmsConfig,
150
+ resetCmsConfig,
151
+ getCmsConfig,
152
+ warnOnceMissingConfig,
153
+ namespacedError,
154
+ resolveBrandConfig,
155
+ resolveRouteConfig,
156
+ resolveJwtConfig,
157
+ resolveTrustedIframeOrigins,
158
+ resolveStorageConfig,
159
+ resolveR2CredentialsConfig,
160
+ resolveR2PublicUrl,
161
+ resolveClientIdentity,
162
+ resolveRateLimitEnabled
163
+ };
164
+ //# sourceMappingURL=chunk-BWKKHCDG.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/index.ts"],"sourcesContent":["/**\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 * rate-limit client identity 를 해석한다.\n *\n * 안전 기본값: spoofable `x-forwarded-for` 의 첫 값을 무조건 신뢰하지 않는다.\n * consumer 가 trusted-proxy/identity 추출 함수를 주입하지 않은 경우\n * `x-forwarded-for` 만 바꿔서 identity 를 회전시킬 수 없도록 안정적이고\n * 헤더-비의존적인 식별자를 반환한다. consumer 주입 추출기가 있으면 그것을\n * 그대로 사용한다.\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 // 안전 기본: XFF 를 무조건 신뢰하지 않는다. proxy topology 를 모르므로\n // 어떤 단일 client 가 헤더만 바꿔 무한 회전하는 것을 막기 위해, 신뢰 가능한\n // 비-spoofable 단일 식별자(고정 버킷)를 사용한다. 127.0.0.1 같은\n // 매직 fallback 은 제거한다 — consumer 는 자신의 hop 수를 아는\n // `rateLimit.identityExtractor` 로 override 한다.\n return 'cms-kit:shared-anon';\n}\n\n/** rate-limit 활성화 여부 (inject > legacy RATE_LIMIT_ENABLED env > 기본 활성). */\nexport function resolveRateLimitEnabled(): boolean {\n const rl = _config.rateLimit ?? {};\n if (typeof rl.enabled === 'boolean') return rl.enabled;\n return process.env.RATE_LIMIT_ENABLED !== 'false';\n}\n"],"mappings":";AAsJA,IAAM,KAAK;AACX,IAAM,SAAS,SAAS;AACxB,IAAM,QAAQ,SAAS;AACvB,IAAM,iBAA2C;AAAA,EAC/C,WAAW,KAAK;AAAA,EAChB,mBAAmB,KAAK;AAAA,EACxB,YAAY,QAAQ;AAAA,EACpB,gBAAgB,QAAQ;AAAA,EACxB,iBAAiB,QAAQ;AAAA,EACzB,gBAAgB,SAAS;AAC3B;AAEA,IAAM,iCAAoD;AAAA,EACxD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,8BAA8B;AACpC,IAAM,+BAA+B;AACrC,IAAM,wBAAwB;AAGvB,IAAM,wBAAwB;AAMrC,IAAI,UAAqB,CAAC;AAMnB,SAAS,aAAa,QAAyB;AACpD,YAAU;AAAA,IACR,GAAG;AAAA,IACH,GAAG;AAAA,IACH,OAAO,EAAE,GAAG,QAAQ,OAAO,GAAG,OAAO,MAAM;AAAA,IAC3C,QAAQ,EAAE,GAAG,QAAQ,QAAQ,GAAG,OAAO,OAAO;AAAA,IAC9C,KAAK,EAAE,GAAG,QAAQ,KAAK,GAAG,OAAO,IAAI;AAAA,IACrC,WAAW,EAAE,GAAG,QAAQ,WAAW,GAAG,OAAO,UAAU;AAAA,IACvD,SAAS;AAAA,MACP,GAAG,QAAQ;AAAA,MACX,GAAG,OAAO;AAAA,MACV,IAAI,EAAE,GAAG,QAAQ,SAAS,IAAI,GAAG,OAAO,SAAS,GAAG;AAAA,IACtD;AAAA,IACA,WAAW,EAAE,GAAG,QAAQ,WAAW,GAAG,OAAO,UAAU;AAAA,EACzD;AACF;AAGO,SAAS,iBAAuB;AACrC,YAAU,CAAC;AACX,cAAY,MAAM;AACpB;AAGO,SAAS,eAA0B;AACxC,SAAO;AACT;AAMA,IAAM,KAAK;AACX,IAAM,cAAc,oBAAI,IAAY;AAM7B,SAAS,sBAAsB,KAAa,SAAuB;AACxE,MAAI,YAAY,IAAI,GAAG,EAAG;AAC1B,cAAY,IAAI,GAAG;AAGnB,UAAQ,KAAK,GAAG,EAAE,IAAI,OAAO,EAAE;AACjC;AAGO,SAAS,gBAAgB,SAAwB;AACtD,SAAO,IAAI,MAAM,GAAG,EAAE,IAAI,OAAO,EAAE;AACrC;AAaO,SAAS,mBAAmB,mBAAmB,OAKpD;AACA,QAAM,IAAI,QAAQ,SAAS,CAAC;AAC5B,QAAM,gBAAgB,MAAM,QAAQ,EAAE,QAAQ;AAC9C,QAAM,kBAAkB,OAAO,EAAE,eAAe;AAEhD,MAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,iBAAiB;AAC3D;AAAA,MACE;AAAA,MACA;AAAA,IAEF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,YAAY,kBAAmB,EAAE,aAAwB;AAAA,IACzD,WAAW,EAAE,aAAa;AAAA,IAC1B,WAAW,EAAE,aAAa;AAAA,IAC1B,UAAU,gBAAiB,EAAE,WAA4B,CAAC;AAAA,EAC5D;AACF;AAGO,SAAS,qBAA+C;AAC7D,QAAM,IAAI,QAAQ,UAAU,CAAC;AAC7B,SAAO;AAAA,IACL,WAAW,EAAE,aAAa,eAAe;AAAA,IACzC,mBAAmB,EAAE,qBAAqB,eAAe;AAAA,IACzD,YAAY,EAAE,cAAc,eAAe;AAAA,IAC3C,gBAAgB,EAAE,kBAAkB,eAAe;AAAA,IACnD,iBAAiB,EAAE,mBAAmB,eAAe;AAAA,IACrD,gBAAgB,EAAE,kBAAkB,eAAe;AAAA,EACrD;AACF;AAQO,SAAS,mBAKd;AACA,QAAM,IAAI,QAAQ,OAAO,CAAC;AAE1B,QAAM,oBACJ,EAAE,qBAAqB,QAAQ,IAAI,kBAAkB;AACvD,QAAM,qBACJ,EAAE,sBACF,QAAQ,IAAI,gCACZ;AACF,QAAM,YAAY,EAAE,aAAa;AAEjC,QAAM,SAAS,EAAE,UAAU,QAAQ,IAAI;AAEvC,MAAI,WAAW,UAAa,WAAW,QAAQ,WAAW,IAAI;AAC5D,UAAM;AAAA,MACJ;AAAA,IAGF;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,YAAY,OAAO,SAAS,uBAAuB;AACvE,UAAM;AAAA,MACJ,uDAAuD,qBAAqB;AAAA,IAI9E;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,mBAAmB,oBAAoB,UAAU;AACpE;AAGO,SAAS,8BAAiD;AAC/D,QAAM,IAAI,QAAQ,aAAa,CAAC;AAChC,SAAO,EAAE,wBAAwB;AACnC;AAGO,SAAS,uBAGd;AACA,QAAM,KAAK,QAAQ,WAAW,CAAC;AAC/B,SAAO;AAAA,IACL,mBAAmB,MAAM,QAAQ,GAAG,iBAAiB,IACjD,GAAG,oBACH;AAAA,IACJ,eACE,OAAO,GAAG,kBAAkB,WAAW,GAAG,gBAAgB;AAAA,EAC9D;AACF;AAgBO,SAAS,6BAKd;AACA,QAAM,KAAK,QAAQ,SAAS,MAAM,CAAC;AACnC,QAAM,OAAO,CAAC,UAA8B,QAC1C,OAAO,aAAa,YAAY,SAAS,SAAS,IAC9C,WACA,OAAO,QAAQ,YAAY,IAAI,SAAS,IACtC,MACA;AAER,SAAO;AAAA,IACL,WAAW,KAAK,GAAG,WAAW,QAAQ,IAAI,aAAa;AAAA,IACvD,aAAa,KAAK,GAAG,aAAa,QAAQ,IAAI,gBAAgB;AAAA,IAC9D,iBAAiB,KAAK,GAAG,iBAAiB,QAAQ,IAAI,oBAAoB;AAAA,IAC1E,YAAY,KAAK,GAAG,YAAY,QAAQ,IAAI,cAAc;AAAA,EAC5D;AACF;AAYO,SAAS,qBAAoC;AAClD,QAAM,KAAK,QAAQ,WAAW,CAAC;AAC/B,MAAI,OAAO,GAAG,kBAAkB,YAAY,GAAG,cAAc,SAAS,GAAG;AACvE,WAAO,GAAG;AAAA,EACZ;AACA,MACE,OAAO,QAAQ,IAAI,kBAAkB,YACrC,QAAQ,IAAI,cAAc,SAAS,GACnC;AACA,WAAO,QAAQ,IAAI;AAAA,EACrB;AACA,SAAO;AACT;AAWO,SAAS,sBAAsB,SAA0B;AAC9D,QAAM,KAAK,QAAQ,aAAa,CAAC;AACjC,MAAI,OAAO,GAAG,sBAAsB,YAAY;AAC9C,WAAO,GAAG,kBAAkB,OAAO;AAAA,EACrC;AAMA,SAAO;AACT;AAGO,SAAS,0BAAmC;AACjD,QAAM,KAAK,QAAQ,aAAa,CAAC;AACjC,MAAI,OAAO,GAAG,YAAY,UAAW,QAAO,GAAG;AAC/C,SAAO,QAAQ,IAAI,uBAAuB;AAC5C;","names":[]}
@@ -0,0 +1,218 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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
+
3
+ var _chunkKWKV2GAHjs = require('./chunk-KWKV2GAH.js');
4
+
5
+
6
+
7
+ var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
8
+
9
+ // src/components/AdminShell.tsx
10
+ var _react = require('react');
11
+ var _link = require('next/link'); var _link2 = _interopRequireDefault(_link);
12
+ var _navigation = require('next/navigation');
13
+ var _dynamic = require('next/dynamic'); var _dynamic2 = _interopRequireDefault(_dynamic);
14
+ var _jsxruntime = require('react/jsx-runtime');
15
+ var Toaster = _dynamic2.default.call(void 0,
16
+ () => Promise.resolve().then(() => _interopRequireWildcard(require("sonner"))).then((m) => m.Toaster),
17
+ { ssr: false }
18
+ );
19
+ function AdminShell({
20
+ children,
21
+ brandLabel,
22
+ brandHref,
23
+ adminHref,
24
+ navItems,
25
+ loginPath,
26
+ meEndpoint,
27
+ logoutEndpoint
28
+ }) {
29
+ const pathname = _navigation.usePathname.call(void 0, );
30
+ const router = _navigation.useRouter.call(void 0, );
31
+ const routerRef = _react.useRef.call(void 0, router);
32
+ routerRef.current = router;
33
+ const brandSuppliedViaProps = brandLabel !== void 0 || navItems !== void 0;
34
+ const brandCfg = _chunkEG6NVPP2js.resolveBrandConfig.call(void 0, brandSuppliedViaProps);
35
+ const routeCfg = _chunkEG6NVPP2js.resolveRouteConfig.call(void 0, );
36
+ const resolvedBrandLabel = _nullishCoalesce(_nullishCoalesce(brandLabel, () => ( brandCfg.brandLabel)), () => ( null));
37
+ const resolvedBrandHref = _nullishCoalesce(brandHref, () => ( brandCfg.brandHref));
38
+ const resolvedAdminHref = _nullishCoalesce(adminHref, () => ( brandCfg.adminHref));
39
+ const resolvedNav = _nullishCoalesce(navItems, () => ( brandCfg.navItems));
40
+ const resolvedLoginPath = _nullishCoalesce(loginPath, () => ( routeCfg.loginPath));
41
+ const resolvedMeEndpoint = _nullishCoalesce(meEndpoint, () => ( routeCfg.meEndpoint));
42
+ const resolvedLogoutEndpoint = _nullishCoalesce(logoutEndpoint, () => ( routeCfg.logoutEndpoint));
43
+ const [checking, setChecking] = _react.useState.call(void 0, true);
44
+ const [user, setUser] = _react.useState.call(void 0, null);
45
+ const [mobileOpen, setMobileOpen] = _react.useState.call(void 0, false);
46
+ const [collapsed, setCollapsed] = _react.useState.call(void 0, () => {
47
+ if (typeof window !== "undefined") {
48
+ return localStorage.getItem("admin_sidebar_collapsed") === "true";
49
+ }
50
+ return false;
51
+ });
52
+ const DEFAULT_WIDTH = 200;
53
+ const MIN_WIDTH = 200;
54
+ const MAX_WIDTH = 400;
55
+ const [sidebarWidth, setSidebarWidth] = _react.useState.call(void 0, () => {
56
+ if (typeof window !== "undefined") {
57
+ const saved = localStorage.getItem("admin_sidebar_width");
58
+ return saved ? Number(saved) : DEFAULT_WIDTH;
59
+ }
60
+ return DEFAULT_WIDTH;
61
+ });
62
+ const isResizing = _react.useRef.call(void 0, false);
63
+ const widthRef = _react.useRef.call(void 0, sidebarWidth);
64
+ const [dragging, setDragging] = _react.useState.call(void 0, false);
65
+ const isLoginPage = pathname === resolvedLoginPath;
66
+ _react.useEffect.call(void 0, () => {
67
+ if (isLoginPage) {
68
+ setChecking(false);
69
+ return;
70
+ }
71
+ let cancelled = false;
72
+ async function checkAuth() {
73
+ try {
74
+ const res = await _chunkKWKV2GAHjs.adminFetch.call(void 0, resolvedMeEndpoint);
75
+ if (cancelled) return;
76
+ if (!res.ok) {
77
+ routerRef.current.replace(resolvedLoginPath);
78
+ return;
79
+ }
80
+ const data = await res.json();
81
+ if (data.success && _optionalChain([data, 'access', _ => _.data, 'optionalAccess', _2 => _2.user])) {
82
+ const u = data.data.user;
83
+ setUser(
84
+ (prev) => _optionalChain([prev, 'optionalAccess', _3 => _3.email]) === u.email && _optionalChain([prev, 'optionalAccess', _4 => _4.id]) === u.id ? prev : u
85
+ );
86
+ }
87
+ } catch (e2) {
88
+ if (!cancelled) {
89
+ routerRef.current.replace(resolvedLoginPath);
90
+ return;
91
+ }
92
+ }
93
+ setChecking(false);
94
+ }
95
+ checkAuth();
96
+ return () => {
97
+ cancelled = true;
98
+ };
99
+ }, [isLoginPage]);
100
+ const handleMouseMove = _react.useCallback.call(void 0, (e) => {
101
+ if (!isResizing.current) return;
102
+ const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, e.clientX));
103
+ widthRef.current = newWidth;
104
+ setSidebarWidth(newWidth);
105
+ }, []);
106
+ const handleMouseUp = _react.useCallback.call(void 0, () => {
107
+ if (!isResizing.current) return;
108
+ isResizing.current = false;
109
+ setDragging(false);
110
+ document.body.style.cursor = "";
111
+ document.body.style.userSelect = "";
112
+ localStorage.setItem("admin_sidebar_width", String(widthRef.current));
113
+ document.removeEventListener("mousemove", handleMouseMove);
114
+ document.removeEventListener("mouseup", handleMouseUp);
115
+ }, [handleMouseMove]);
116
+ if (isLoginPage) {
117
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _jsxruntime.Fragment, { children });
118
+ }
119
+ if (checking) {
120
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "admin-auth-loading", children: "\uC778\uC99D \uD655\uC778 \uC911..." });
121
+ }
122
+ async function handleLogout() {
123
+ await fetch(resolvedLogoutEndpoint, { method: "POST", credentials: "same-origin" });
124
+ router.replace(resolvedLoginPath);
125
+ }
126
+ function toggleSidebar() {
127
+ setCollapsed((prev) => {
128
+ const next = !prev;
129
+ localStorage.setItem("admin_sidebar_collapsed", String(next));
130
+ return next;
131
+ });
132
+ }
133
+ function startResize(e) {
134
+ e.preventDefault();
135
+ isResizing.current = true;
136
+ setDragging(true);
137
+ document.body.style.cursor = "col-resize";
138
+ document.body.style.userSelect = "none";
139
+ document.addEventListener("mousemove", handleMouseMove);
140
+ document.addEventListener("mouseup", handleMouseUp);
141
+ }
142
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: `admin-layout${collapsed ? " admin-sidebar-collapsed" : ""}${dragging ? " admin-resizing" : ""}${mobileOpen ? " admin-sidebar-mobile-open" : ""}`, children: [
143
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
144
+ "button",
145
+ {
146
+ className: "admin-mobile-toggle",
147
+ onClick: () => setMobileOpen(true),
148
+ "aria-label": "\uBA54\uB274 \uC5F4\uAE30",
149
+ children: "\u2630"
150
+ }
151
+ ),
152
+ mobileOpen && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "admin-sidebar-overlay", onClick: () => setMobileOpen(false) }),
153
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
154
+ "aside",
155
+ {
156
+ className: "admin-sidebar",
157
+ style: !collapsed ? { width: sidebarWidth } : void 0,
158
+ children: [
159
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "admin-sidebar-header", children: [
160
+ !collapsed && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "admin-sidebar-logo", children: [
161
+ resolvedBrandLabel && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "a", { href: resolvedBrandHref, className: "admin-logo-home", title: "\uC0AC\uC774\uD2B8 \uBCF4\uAE30", target: "_blank", rel: "noopener noreferrer", children: resolvedBrandLabel }),
162
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: resolvedAdminHref, className: "admin-logo-admin", children: "Admin" })
163
+ ] }),
164
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
165
+ "button",
166
+ {
167
+ className: "admin-sidebar-toggle",
168
+ onClick: () => {
169
+ if (mobileOpen) {
170
+ setMobileOpen(false);
171
+ } else {
172
+ toggleSidebar();
173
+ }
174
+ },
175
+ title: collapsed ? "\uBA54\uB274 \uD3BC\uCE58\uAE30" : "\uBA54\uB274 \uC811\uAE30",
176
+ children: mobileOpen ? "\u2715" : collapsed ? "\u203A" : "\u2039"
177
+ }
178
+ )
179
+ ] }),
180
+ !collapsed && user && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "admin-sidebar-user", children: user.email }),
181
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "nav", { className: "admin-sidebar-nav", children: resolvedNav.map((item) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
182
+ _link2.default,
183
+ {
184
+ href: item.href,
185
+ className: "admin-sidebar-link",
186
+ title: item.label,
187
+ onClick: () => setMobileOpen(false),
188
+ children: collapsed ? item.glyph : item.label
189
+ },
190
+ item.href
191
+ )) }),
192
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "admin-sidebar-footer", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { className: "admin-sidebar-logout", onClick: handleLogout, title: "\uB85C\uADF8\uC544\uC6C3", children: collapsed ? "\u2715" : "\uB85C\uADF8\uC544\uC6C3" }) }),
193
+ !collapsed && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
194
+ "div",
195
+ {
196
+ className: "admin-sidebar-resize",
197
+ onMouseDown: startResize
198
+ }
199
+ )
200
+ ]
201
+ }
202
+ ),
203
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
204
+ "main",
205
+ {
206
+ className: "admin-main",
207
+ style: !collapsed ? { marginLeft: sidebarWidth, width: `calc(100vw - ${sidebarWidth}px)` } : void 0,
208
+ children
209
+ }
210
+ ),
211
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Toaster, { position: "top-center", richColors: true, closeButton: true, duration: 3e3 })
212
+ ] });
213
+ }
214
+
215
+
216
+
217
+ exports.AdminShell = AdminShell;
218
+ //# sourceMappingURL=chunk-C5WFZJM4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-C5WFZJM4.js","../src/components/AdminShell.tsx"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-C5WFZJM4.js","sourcesContent":[null,"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport dynamic from \"next/dynamic\";\n\nconst Toaster = dynamic(\n () => import(\"sonner\").then((m) => m.Toaster),\n { ssr: false }\n);\nimport { adminFetch } from \"../utils/admin-fetch\";\nimport {\n resolveBrandConfig,\n resolveRouteConfig,\n type CmsNavItem,\n} from \"../config\";\n\ninterface AdminUser {\n id: string;\n email: string;\n name: string | null;\n role: string;\n}\n\n/**\n * AdminShell props (spec.md §4.1 / Sprint 1 C1/C2).\n *\n * brand/nav/route 는 더 이상 하드코딩되지 않는다. 우선순위:\n * props > §5 config boundary (`setCmsConfig`) > 안전 중립 기본값\n * (빈 nav + 1회 `@withwiz/cms-kit:` warn, route 는 레거시 기본 경로).\n */\nexport interface AdminShellProps {\n children: React.ReactNode;\n /** 브랜드 라벨 (사이드바 홈 링크 텍스트). 미지정 시 §5 config. */\n brandLabel?: string;\n /** 브랜드 홈 링크 href. */\n brandHref?: string;\n /** admin 링크 href. */\n adminHref?: string;\n /** 순서 있는 nav 항목 목록 (label/href/glyph). */\n navItems?: CmsNavItem[];\n /** 로그인 페이지 경로 (라우트 가드/리다이렉트). */\n loginPath?: string;\n /** \"현재 사용자\" 엔드포인트. */\n meEndpoint?: string;\n /** 로그아웃 엔드포인트. */\n logoutEndpoint?: string;\n}\n\nexport default function AdminShell({\n children,\n brandLabel,\n brandHref,\n adminHref,\n navItems,\n loginPath,\n meEndpoint,\n logoutEndpoint,\n}: AdminShellProps) {\n const pathname = usePathname();\n const router = useRouter();\n const routerRef = useRef(router);\n routerRef.current = router;\n\n // props > §5 config boundary > safe default. The single\n // @withwiz/cms-kit-namespaced warn-once fires ONLY when neither props NOR §5\n // config supply brand/nav (props are a valid injection — suppress warn).\n const brandSuppliedViaProps =\n brandLabel !== undefined || navItems !== undefined;\n const brandCfg = resolveBrandConfig(brandSuppliedViaProps);\n const routeCfg = resolveRouteConfig();\n const resolvedBrandLabel =\n brandLabel ?? brandCfg.brandLabel ?? null;\n const resolvedBrandHref = brandHref ?? brandCfg.brandHref;\n const resolvedAdminHref = adminHref ?? brandCfg.adminHref;\n const resolvedNav: CmsNavItem[] = navItems ?? brandCfg.navItems;\n const resolvedLoginPath = loginPath ?? routeCfg.loginPath;\n const resolvedMeEndpoint = meEndpoint ?? routeCfg.meEndpoint;\n const resolvedLogoutEndpoint = logoutEndpoint ?? routeCfg.logoutEndpoint;\n\n const [checking, setChecking] = useState(true);\n const [user, setUser] = useState<AdminUser | null>(null);\n const [mobileOpen, setMobileOpen] = useState(false);\n const [collapsed, setCollapsed] = useState(() => {\n if (typeof window !== \"undefined\") {\n return localStorage.getItem(\"admin_sidebar_collapsed\") === \"true\";\n }\n return false;\n });\n\n const DEFAULT_WIDTH = 200;\n const MIN_WIDTH = 200;\n const MAX_WIDTH = 400;\n\n const [sidebarWidth, setSidebarWidth] = useState(() => {\n if (typeof window !== \"undefined\") {\n const saved = localStorage.getItem(\"admin_sidebar_width\");\n return saved ? Number(saved) : DEFAULT_WIDTH;\n }\n return DEFAULT_WIDTH;\n });\n const isResizing = useRef(false);\n const widthRef = useRef(sidebarWidth);\n const [dragging, setDragging] = useState(false);\n\n const isLoginPage = pathname === resolvedLoginPath;\n\n useEffect(() => {\n if (isLoginPage) {\n setChecking(false);\n return;\n }\n\n let cancelled = false;\n\n async function checkAuth() {\n try {\n const res = await adminFetch(resolvedMeEndpoint);\n if (cancelled) return;\n\n if (!res.ok) {\n routerRef.current.replace(resolvedLoginPath);\n return;\n }\n\n const data = await res.json();\n if (data.success && data.data?.user) {\n const u = data.data.user as AdminUser;\n setUser((prev) =>\n prev?.email === u.email && prev?.id === u.id ? prev : u\n );\n }\n } catch {\n if (!cancelled) {\n routerRef.current.replace(resolvedLoginPath);\n return;\n }\n }\n\n setChecking(false);\n }\n\n checkAuth();\n return () => { cancelled = true; };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isLoginPage]);\n\n const handleMouseMove = useCallback((e: MouseEvent) => {\n if (!isResizing.current) return;\n const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, e.clientX));\n widthRef.current = newWidth;\n setSidebarWidth(newWidth);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n if (!isResizing.current) return;\n isResizing.current = false;\n setDragging(false);\n document.body.style.cursor = \"\";\n document.body.style.userSelect = \"\";\n localStorage.setItem(\"admin_sidebar_width\", String(widthRef.current));\n document.removeEventListener(\"mousemove\", handleMouseMove);\n document.removeEventListener(\"mouseup\", handleMouseUp);\n }, [handleMouseMove]);\n\n if (isLoginPage) {\n return <>{children}</>;\n }\n\n if (checking) {\n return <div className=\"admin-auth-loading\">인증 확인 중...</div>;\n }\n\n async function handleLogout() {\n await fetch(resolvedLogoutEndpoint, { method: \"POST\", credentials: \"same-origin\" });\n router.replace(resolvedLoginPath);\n }\n\n function toggleSidebar() {\n setCollapsed((prev) => {\n const next = !prev;\n localStorage.setItem(\"admin_sidebar_collapsed\", String(next));\n return next;\n });\n }\n\n function startResize(e: React.MouseEvent) {\n e.preventDefault();\n isResizing.current = true;\n setDragging(true);\n document.body.style.cursor = \"col-resize\";\n document.body.style.userSelect = \"none\";\n document.addEventListener(\"mousemove\", handleMouseMove);\n document.addEventListener(\"mouseup\", handleMouseUp);\n }\n\n return (\n <div className={`admin-layout${collapsed ? \" admin-sidebar-collapsed\" : \"\"}${dragging ? \" admin-resizing\" : \"\"}${mobileOpen ? \" admin-sidebar-mobile-open\" : \"\"}`}>\n <button\n className=\"admin-mobile-toggle\"\n onClick={() => setMobileOpen(true)}\n aria-label=\"메뉴 열기\"\n >\n ☰\n </button>\n {mobileOpen && (\n <div className=\"admin-sidebar-overlay\" onClick={() => setMobileOpen(false)} />\n )}\n <aside\n className=\"admin-sidebar\"\n style={!collapsed ? { width: sidebarWidth } : undefined}\n >\n <div className=\"admin-sidebar-header\">\n {!collapsed && (\n <div className=\"admin-sidebar-logo\">\n {resolvedBrandLabel && (\n <a href={resolvedBrandHref} className=\"admin-logo-home\" title=\"사이트 보기\" target=\"_blank\" rel=\"noopener noreferrer\">{resolvedBrandLabel}</a>\n )}\n <Link href={resolvedAdminHref} className=\"admin-logo-admin\">Admin</Link>\n </div>\n )}\n <button\n className=\"admin-sidebar-toggle\"\n onClick={() => {\n if (mobileOpen) {\n setMobileOpen(false);\n } else {\n toggleSidebar();\n }\n }}\n title={collapsed ? \"메뉴 펼치기\" : \"메뉴 접기\"}\n >\n {mobileOpen ? \"✕\" : collapsed ? \"›\" : \"‹\"}\n </button>\n </div>\n {!collapsed && user && (\n <div className=\"admin-sidebar-user\">{user.email}</div>\n )}\n <nav className=\"admin-sidebar-nav\">\n {resolvedNav.map((item) => (\n <Link\n key={item.href}\n href={item.href}\n className=\"admin-sidebar-link\"\n title={item.label}\n onClick={() => setMobileOpen(false)}\n >\n {collapsed ? item.glyph : item.label}\n </Link>\n ))}\n </nav>\n <div className=\"admin-sidebar-footer\">\n <button className=\"admin-sidebar-logout\" onClick={handleLogout} title=\"로그아웃\">\n {collapsed ? \"✕\" : \"로그아웃\"}\n </button>\n </div>\n {!collapsed && (\n <div\n className=\"admin-sidebar-resize\"\n onMouseDown={startResize}\n />\n )}\n </aside>\n <main\n className=\"admin-main\"\n style={!collapsed ? { marginLeft: sidebarWidth, width: `calc(100vw - ${sidebarWidth}px)` } : undefined}\n >{children}</main>\n <Toaster position=\"top-center\" richColors closeButton duration={3000} />\n </div>\n );\n}\n"]}
@@ -0,0 +1,123 @@
1
+ import {
2
+ resolveTrustedIframeOrigins
3
+ } from "./chunk-BWKKHCDG.mjs";
4
+ import {
5
+ __require
6
+ } from "./chunk-BJTO5JO5.mjs";
7
+
8
+ // src/utils/html-sanitizer.ts
9
+ var STRIP_TAGS_WITH_CONTENT = /(<\s*\/?\s*(script|object|embed|applet|form|input|textarea|select|button)\b[^>]*>)/gi;
10
+ var UNTRUSTED_IFRAME = /<\s*iframe\b[^>]*>[\s\S]*?<\s*\/\s*iframe\s*>/gi;
11
+ var STRIP_TAG_CONTENT = /<\s*(script|style)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi;
12
+ var EVENT_HANDLER_ATTRS = /\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi;
13
+ var DANGEROUS_PROTOCOL = /(href|src|action)\s*=\s*["']\s*(javascript|vbscript|data\s*:(?!image\/))[^"']*["']/gi;
14
+ var cachedDomPurify;
15
+ function tryLoadDomPurify() {
16
+ if (cachedDomPurify !== void 0) return cachedDomPurify;
17
+ try {
18
+ const mod = __require("isomorphic-dompurify");
19
+ const candidate = mod.default ?? mod;
20
+ if (candidate && typeof candidate.sanitize === "function") {
21
+ cachedDomPurify = candidate;
22
+ return candidate;
23
+ }
24
+ } catch {
25
+ }
26
+ cachedDomPurify = null;
27
+ return null;
28
+ }
29
+ function regexSanitize(html, trustedOrigins) {
30
+ let result = html;
31
+ result = result.replace(STRIP_TAG_CONTENT, "");
32
+ result = result.replace(STRIP_TAGS_WITH_CONTENT, "");
33
+ result = result.replace(UNTRUSTED_IFRAME, (match) => {
34
+ const srcMatch = match.match(/\bsrc\s*=\s*["']([^"']+)["']/i);
35
+ if (srcMatch) {
36
+ const src = srcMatch[1].trim();
37
+ if (trustedOrigins.some((origin) => src.startsWith(origin))) {
38
+ return match;
39
+ }
40
+ }
41
+ return "";
42
+ });
43
+ result = result.replace(EVENT_HANDLER_ATTRS, "");
44
+ result = result.replace(DANGEROUS_PROTOCOL, '$1=""');
45
+ return result;
46
+ }
47
+ function dompurifySanitize(html, purify, trustedOrigins, config) {
48
+ const options = {
49
+ // iframe 은 hook 에서 origin 검증 후 허용
50
+ ADD_TAGS: ["iframe"],
51
+ ADD_ATTR: ["allowfullscreen", "frameborder", "allow"],
52
+ FORBID_TAGS: [
53
+ "script",
54
+ "object",
55
+ "embed",
56
+ "applet",
57
+ "form",
58
+ "input",
59
+ "textarea",
60
+ "select",
61
+ "button",
62
+ "style"
63
+ ],
64
+ FORBID_ATTR: [
65
+ "onerror",
66
+ "onload",
67
+ "onclick",
68
+ "onmouseover",
69
+ "onfocus",
70
+ "onblur"
71
+ ]
72
+ };
73
+ if (config.allowedTags) options.ALLOWED_TAGS = config.allowedTags;
74
+ if (config.allowedAttributes) {
75
+ const flat = /* @__PURE__ */ new Set();
76
+ for (const attrs of Object.values(config.allowedAttributes)) {
77
+ for (const a of attrs) flat.add(a);
78
+ }
79
+ options.ALLOWED_ATTR = Array.from(flat);
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 = node.getAttribute?.("src") ?? "";
86
+ const trusted = trustedOrigins.some(
87
+ (origin) => src.startsWith(origin)
88
+ );
89
+ if (!trusted) {
90
+ node.remove?.();
91
+ }
92
+ }
93
+ });
94
+ }
95
+ try {
96
+ return purify.sanitize(html, options);
97
+ } finally {
98
+ if (typeof purifyWithHook.removeHook === "function") {
99
+ purifyWithHook.removeHook("uponSanitizeElement");
100
+ }
101
+ }
102
+ }
103
+ function createSanitizer(config = {}) {
104
+ return function sanitize(html) {
105
+ if (!html) return html;
106
+ const trustedOrigins = config.trustedIframeOrigins ?? resolveTrustedIframeOrigins();
107
+ const purify = tryLoadDomPurify();
108
+ if (purify) {
109
+ return dompurifySanitize(html, purify, trustedOrigins, config);
110
+ }
111
+ return regexSanitize(html, trustedOrigins);
112
+ };
113
+ }
114
+ function sanitizeHtmlContent(html) {
115
+ return defaultSanitize(html);
116
+ }
117
+ var defaultSanitize = createSanitizer();
118
+
119
+ export {
120
+ createSanitizer,
121
+ sanitizeHtmlContent
122
+ };
123
+ //# sourceMappingURL=chunk-CGVFB23E.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/html-sanitizer.ts"],"sourcesContent":["/**\n * 서버사이드 HTML 새니타이저 (spec.md §4.6 / Sprint 1 S1).\n *\n * 위험한 요소(script, iframe, object 등)와 이벤트 핸들러 속성을 제거하면서\n * 블록 에디터가 사용하는 안전한 HTML 구조를 보존한다.\n *\n * - 1차(기본): `isomorphic-dompurify` (실제 jsdom+DOMPurify DOM/parser\n * allowlist 새니타이저 — 손수 만든 regex 가 아님). 동적으로 로드한다.\n * - fallback: regex 기반 새니타이저는 *방어 심층(defense-in-depth)* 으로만\n * 남겨둔다 (optional dep 미설치 시). 더 이상 1차 경로가 아니다.\n * - `createSanitizer(config)` 로 신뢰 iframe origin 을 주입할 수 있다.\n *\n * `sanitizeHtmlContent` 는 하위 호환을 위해 기본 안전 설정으로 동작한다\n * (I1 — 동일 이름, 동일 `string|null|undefined → string|null` 시그니처).\n * 기본 신뢰 iframe origin 은 §5 config boundary 를 통해 consumer 가\n * override 할 수 있고, 미설정 시 현재 YouTube/Vimeo 집합이 그대로 쓰인다\n * (unconfigured 동작 불변 — I3 보호).\n *\n * `../../withwiz-blog-core/src/utils/html-sanitizer.ts` 와 *메커니즘 동일*\n * (`createSanitizer` factory + `tryLoadDomPurify()` 동적 require +\n * `uponSanitizeElement` iframe-origin hook + regex defense-in-depth fallback).\n */\n\nimport { resolveTrustedIframeOrigins } from '../config';\n\n// ── 정규식 패턴 (defense-in-depth fallback 전용) ──\n\n/** 항상 제거할 태그 (내용 포함) */\nconst STRIP_TAGS_WITH_CONTENT =\n /(<\\s*\\/?\\s*(script|object|embed|applet|form|input|textarea|select|button)\\b[^>]*>)/gi;\n\n/** 신뢰되지 않는 iframe 매칭 */\nconst UNTRUSTED_IFRAME = /<\\s*iframe\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*iframe\\s*>/gi;\n\n/** script/style 태그 사이 콘텐츠 */\nconst STRIP_TAG_CONTENT = /<\\s*(script|style)\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*\\1\\s*>/gi;\n\n/** 이벤트 핸들러 속성 (onclick, onerror, onload 등) */\nconst EVENT_HANDLER_ATTRS = /\\s+on\\w+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)/gi;\n\n/** 위험한 URL 프로토콜 (href/src 속성 내) */\nconst DANGEROUS_PROTOCOL =\n /(href|src|action)\\s*=\\s*[\"']\\s*(javascript|vbscript|data\\s*:(?!image\\/))[^\"']*[\"']/gi;\n\n// ── 설정 인터페이스 ──\n\n/** 새니타이저 설정 */\nexport interface SanitizerConfig {\n /**\n * 신뢰할 수 있는 iframe origin 목록 (prefix 매칭).\n * 미지정 시 §5 config boundary 의 기본값(YouTube/Vimeo) 사용.\n */\n trustedIframeOrigins?: readonly string[];\n /** DOMPurify 사용 시 허용할 태그 화이트리스트. */\n allowedTags?: string[];\n /** DOMPurify 사용 시 허용할 속성 화이트리스트. */\n allowedAttributes?: Record<string, string[]>;\n}\n\n// ── DOMPurify 동적 로딩 (선택적 / 실제 사용되는 1차 경로) ──\n\ntype DOMPurifyLike = {\n sanitize: (dirty: string, options?: Record<string, unknown>) => string;\n};\n\nlet cachedDomPurify: DOMPurifyLike | null | undefined;\n\n/**\n * 선택적 의존성 `isomorphic-dompurify` (실제 DOM allowlist 새니타이저) 를\n * 동기적으로 로드한다. 설치되지 않았거나 로드 실패 시에만 null 을 반환하고,\n * 호출자는 regex defense-in-depth fallback 을 쓴다.\n */\nfunction tryLoadDomPurify(): DOMPurifyLike | null {\n if (cachedDomPurify !== undefined) return cachedDomPurify;\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require('isomorphic-dompurify') as unknown;\n const candidate =\n (mod as { default?: DOMPurifyLike }).default ?? (mod as DOMPurifyLike);\n if (candidate && typeof candidate.sanitize === 'function') {\n cachedDomPurify = candidate;\n return candidate;\n }\n } catch {\n // 모듈 미설치 — regex fallback 사용\n }\n cachedDomPurify = null;\n return null;\n}\n\n// ── 정규식 기반 새니타이저 (defense-in-depth fallback) ──\n\nfunction regexSanitize(html: string, trustedOrigins: readonly string[]): string {\n let result = html;\n\n // 1. script/style 태그 사이 콘텐츠 제거\n result = result.replace(STRIP_TAG_CONTENT, '');\n\n // 2. 위험한 태그 제거\n result = result.replace(STRIP_TAGS_WITH_CONTENT, '');\n\n // 2b. 신뢰되지 않는 iframe 제거 (신뢰 origin 유지)\n result = result.replace(UNTRUSTED_IFRAME, (match) => {\n const srcMatch = match.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i);\n if (srcMatch) {\n const src = srcMatch[1].trim();\n if (trustedOrigins.some((origin) => src.startsWith(origin))) {\n return match;\n }\n }\n return '';\n });\n\n // 3. 이벤트 핸들러 속성 제거\n result = result.replace(EVENT_HANDLER_ATTRS, '');\n\n // 4. 위험한 URL 프로토콜 무력화\n result = result.replace(DANGEROUS_PROTOCOL, '$1=\"\"');\n\n return result;\n}\n\n// ── DOMPurify 기반 새니타이저 (1차 경로) ──\n\nfunction dompurifySanitize(\n html: string,\n purify: DOMPurifyLike,\n trustedOrigins: readonly string[],\n config: SanitizerConfig,\n): string {\n const options: Record<string, unknown> = {\n // iframe 은 hook 에서 origin 검증 후 허용\n ADD_TAGS: ['iframe'],\n ADD_ATTR: ['allowfullscreen', 'frameborder', 'allow'],\n FORBID_TAGS: [\n 'script',\n 'object',\n 'embed',\n 'applet',\n 'form',\n 'input',\n 'textarea',\n 'select',\n 'button',\n 'style',\n ],\n FORBID_ATTR: [\n 'onerror',\n 'onload',\n 'onclick',\n 'onmouseover',\n 'onfocus',\n 'onblur',\n ],\n };\n\n if (config.allowedTags) options.ALLOWED_TAGS = config.allowedTags;\n if (config.allowedAttributes) {\n const flat = new Set<string>();\n for (const attrs of Object.values(config.allowedAttributes)) {\n for (const a of attrs) flat.add(a);\n }\n options.ALLOWED_ATTR = Array.from(flat);\n }\n\n const purifyWithHook = purify as DOMPurifyLike & {\n addHook?: (hook: string, cb: (node: Element) => void) => void;\n removeHook?: (hook: string) => void;\n };\n if (typeof purifyWithHook.addHook === 'function') {\n purifyWithHook.addHook('uponSanitizeElement', (node) => {\n if (node.nodeName && node.nodeName.toLowerCase() === 'iframe') {\n const src = (node as Element).getAttribute?.('src') ?? '';\n const trusted = trustedOrigins.some((origin) =>\n src.startsWith(origin),\n );\n if (!trusted) {\n (node as Element).remove?.();\n }\n }\n });\n }\n try {\n return purify.sanitize(html, options);\n } finally {\n if (typeof purifyWithHook.removeHook === 'function') {\n purifyWithHook.removeHook('uponSanitizeElement');\n }\n }\n}\n\n// ── Public API ──\n\n/**\n * 새니타이저 팩토리.\n *\n * DOMPurify(isomorphic-dompurify) 가 설치되어 있으면 그것을 *1차* 로 사용하고,\n * 없을 때만 regex defense-in-depth fallback 으로 동작한다.\n *\n * @example\n * const sanitize = createSanitizer({\n * trustedIframeOrigins: ['https://www.youtube.com/', 'https://www.loom.com/'],\n * });\n * const safe = sanitize(userHtml);\n */\nexport function createSanitizer(\n config: SanitizerConfig = {},\n): (html: string | null | undefined) => string | null {\n return function sanitize(html: string | null | undefined): string | null {\n if (!html) return html as string | null;\n // 신뢰 origin 은 point-of-use 에서 §5 boundary 를 통해 해석한다\n // (lazy — import 시점에 config 가 없어도 throw 하지 않음).\n const trustedOrigins =\n config.trustedIframeOrigins ?? resolveTrustedIframeOrigins();\n const purify = tryLoadDomPurify();\n if (purify) {\n return dompurifySanitize(html, purify, trustedOrigins, config);\n }\n return regexSanitize(html, trustedOrigins);\n };\n}\n\n/**\n * 리치 HTML 콘텐츠에서 위험한 요소와 속성을 제거한다.\n * 기본 안전 설정으로 동작한다 (하위 호환, I1). 호스트별 신뢰 origin 확장은\n * `createSanitizer({ trustedIframeOrigins: [...] })` 또는 §5\n * `setCmsConfig({ sanitizer: { trustedIframeOrigins } })` 로 한다.\n */\nexport function sanitizeHtmlContent(\n html: string | null | undefined,\n): string | null {\n return defaultSanitize(html);\n}\n\n/** 모듈 로드 시점에 1회 만들어지는 기본 새니타이저 (실행은 lazy). */\nconst defaultSanitize = createSanitizer();\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ "use strict";//# sourceMappingURL=chunk-D2VDTRXO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-D2VDTRXO.js"],"names":[],"mappings":"AAAA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-D2VDTRXO.js"}