@kywi-software/core 0.2.1 → 0.3.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 (354) hide show
  1. package/README.md +57 -2
  2. package/dist/admin/app.d.ts +10 -1
  3. package/dist/admin/app.d.ts.map +1 -1
  4. package/dist/admin/app.js +2 -2
  5. package/dist/admin/app.js.map +1 -1
  6. package/dist/admin/approval-queue.d.ts +3 -1
  7. package/dist/admin/approval-queue.d.ts.map +1 -1
  8. package/dist/admin/approval-queue.js +2 -2
  9. package/dist/admin/approval-queue.js.map +1 -1
  10. package/dist/admin/audience-list.d.ts.map +1 -1
  11. package/dist/admin/audience-list.js +9 -1
  12. package/dist/admin/audience-list.js.map +1 -1
  13. package/dist/admin/chrome.d.ts +7 -1
  14. package/dist/admin/chrome.d.ts.map +1 -1
  15. package/dist/admin/chrome.js +2 -1
  16. package/dist/admin/chrome.js.map +1 -1
  17. package/dist/admin/content-editor-api.d.ts +27 -1
  18. package/dist/admin/content-editor-api.d.ts.map +1 -1
  19. package/dist/admin/content-editor-api.js +60 -2
  20. package/dist/admin/content-editor-api.js.map +1 -1
  21. package/dist/admin/content-form.d.ts +62 -6
  22. package/dist/admin/content-form.d.ts.map +1 -1
  23. package/dist/admin/content-form.js +118 -13
  24. package/dist/admin/content-form.js.map +1 -1
  25. package/dist/admin/content-permissions-tab.d.ts +13 -0
  26. package/dist/admin/content-permissions-tab.d.ts.map +1 -0
  27. package/dist/admin/content-permissions-tab.js +90 -0
  28. package/dist/admin/content-permissions-tab.js.map +1 -0
  29. package/dist/admin/context.d.ts +8 -0
  30. package/dist/admin/context.d.ts.map +1 -1
  31. package/dist/admin/context.js.map +1 -1
  32. package/dist/admin/feed-builder.d.ts.map +1 -1
  33. package/dist/admin/feed-builder.js +20 -1
  34. package/dist/admin/feed-builder.js.map +1 -1
  35. package/dist/admin/field-components.d.ts.map +1 -1
  36. package/dist/admin/field-components.js +26 -1
  37. package/dist/admin/field-components.js.map +1 -1
  38. package/dist/admin/form-builder.js +6 -1
  39. package/dist/admin/form-builder.js.map +1 -1
  40. package/dist/admin/form-helpers.d.ts.map +1 -1
  41. package/dist/admin/form-helpers.js +2 -0
  42. package/dist/admin/form-helpers.js.map +1 -1
  43. package/dist/admin/layout-editor/component-actions.d.ts +18 -0
  44. package/dist/admin/layout-editor/component-actions.d.ts.map +1 -1
  45. package/dist/admin/layout-editor/component-actions.js +19 -0
  46. package/dist/admin/layout-editor/component-actions.js.map +1 -1
  47. package/dist/admin/layout-editor/component-picker-field.d.ts +20 -0
  48. package/dist/admin/layout-editor/component-picker-field.d.ts.map +1 -0
  49. package/dist/admin/layout-editor/component-picker-field.js +16 -0
  50. package/dist/admin/layout-editor/component-picker-field.js.map +1 -0
  51. package/dist/admin/layout-editor/drag-utils.d.ts +48 -0
  52. package/dist/admin/layout-editor/drag-utils.d.ts.map +1 -1
  53. package/dist/admin/layout-editor/drag-utils.js +43 -0
  54. package/dist/admin/layout-editor/drag-utils.js.map +1 -1
  55. package/dist/admin/layout-editor/editor-canvas.d.ts +9 -1
  56. package/dist/admin/layout-editor/editor-canvas.d.ts.map +1 -1
  57. package/dist/admin/layout-editor/editor-canvas.js +41 -15
  58. package/dist/admin/layout-editor/editor-canvas.js.map +1 -1
  59. package/dist/admin/layout-editor/editor-state.d.ts +9 -1
  60. package/dist/admin/layout-editor/editor-state.d.ts.map +1 -1
  61. package/dist/admin/layout-editor/editor-state.js +34 -0
  62. package/dist/admin/layout-editor/editor-state.js.map +1 -1
  63. package/dist/admin/layout-editor/editor.css +83 -0
  64. package/dist/admin/layout-editor/form-picker-field.d.ts +17 -0
  65. package/dist/admin/layout-editor/form-picker-field.d.ts.map +1 -0
  66. package/dist/admin/layout-editor/form-picker-field.js +35 -0
  67. package/dist/admin/layout-editor/form-picker-field.js.map +1 -0
  68. package/dist/admin/layout-editor/layout-editor.d.ts.map +1 -1
  69. package/dist/admin/layout-editor/layout-editor.js +95 -28
  70. package/dist/admin/layout-editor/layout-editor.js.map +1 -1
  71. package/dist/admin/layout-editor/mode-toggle.d.ts.map +1 -1
  72. package/dist/admin/layout-editor/mode-toggle.js +1 -1
  73. package/dist/admin/layout-editor/mode-toggle.js.map +1 -1
  74. package/dist/admin/layout-editor/module-overlay.d.ts +10 -1
  75. package/dist/admin/layout-editor/module-overlay.d.ts.map +1 -1
  76. package/dist/admin/layout-editor/module-overlay.js +3 -3
  77. package/dist/admin/layout-editor/module-overlay.js.map +1 -1
  78. package/dist/admin/layout-editor/props-panel.d.ts +10 -1
  79. package/dist/admin/layout-editor/props-panel.d.ts.map +1 -1
  80. package/dist/admin/layout-editor/props-panel.js +25 -2
  81. package/dist/admin/layout-editor/props-panel.js.map +1 -1
  82. package/dist/admin/layout-editor/section-insert-line.d.ts.map +1 -1
  83. package/dist/admin/layout-editor/section-insert-line.js +1 -1
  84. package/dist/admin/layout-editor/section-insert-line.js.map +1 -1
  85. package/dist/admin/layout-editor/section-overlay.d.ts.map +1 -1
  86. package/dist/admin/layout-editor/section-overlay.js +1 -1
  87. package/dist/admin/layout-editor/section-overlay.js.map +1 -1
  88. package/dist/admin/layout-editor/section-template-picker.d.ts +9 -1
  89. package/dist/admin/layout-editor/section-template-picker.d.ts.map +1 -1
  90. package/dist/admin/layout-editor/section-template-picker.js +45 -2
  91. package/dist/admin/layout-editor/section-template-picker.js.map +1 -1
  92. package/dist/admin/registry.d.ts.map +1 -1
  93. package/dist/admin/registry.js +11 -0
  94. package/dist/admin/registry.js.map +1 -1
  95. package/dist/admin/server.d.ts +15 -2
  96. package/dist/admin/server.d.ts.map +1 -1
  97. package/dist/admin/server.js +37 -1
  98. package/dist/admin/server.js.map +1 -1
  99. package/dist/admin/styles/admin.css +16 -4
  100. package/dist/admin/surface-matcher.d.ts +27 -0
  101. package/dist/admin/surface-matcher.d.ts.map +1 -1
  102. package/dist/admin/surface-matcher.js +11 -0
  103. package/dist/admin/surface-matcher.js.map +1 -1
  104. package/dist/admin/surfaces/approvals/approvals-data.d.ts +29 -9
  105. package/dist/admin/surfaces/approvals/approvals-data.d.ts.map +1 -1
  106. package/dist/admin/surfaces/approvals/approvals-data.js +65 -22
  107. package/dist/admin/surfaces/approvals/approvals-data.js.map +1 -1
  108. package/dist/admin/surfaces/approvals/approvals-index.d.ts.map +1 -1
  109. package/dist/admin/surfaces/approvals/approvals-index.js +47 -14
  110. package/dist/admin/surfaces/approvals/approvals-index.js.map +1 -1
  111. package/dist/admin/surfaces/content/content-edit.d.ts.map +1 -1
  112. package/dist/admin/surfaces/content/content-edit.js +34 -8
  113. package/dist/admin/surfaces/content/content-edit.js.map +1 -1
  114. package/dist/admin/surfaces/content/content-index.d.ts.map +1 -1
  115. package/dist/admin/surfaces/content/content-index.js +29 -2
  116. package/dist/admin/surfaces/content/content-index.js.map +1 -1
  117. package/dist/admin/surfaces/content/content-new.d.ts.map +1 -1
  118. package/dist/admin/surfaces/content/content-new.js +6 -4
  119. package/dist/admin/surfaces/content/content-new.js.map +1 -1
  120. package/dist/admin/surfaces/contentTree/content-tree.d.ts.map +1 -1
  121. package/dist/admin/surfaces/contentTree/content-tree.js +25 -2
  122. package/dist/admin/surfaces/contentTree/content-tree.js.map +1 -1
  123. package/dist/admin/surfaces/contentTypes/content-types-designer.d.ts.map +1 -1
  124. package/dist/admin/surfaces/contentTypes/content-types-designer.js +78 -47
  125. package/dist/admin/surfaces/contentTypes/content-types-designer.js.map +1 -1
  126. package/dist/admin/surfaces/feeds/feed-editor.d.ts.map +1 -1
  127. package/dist/admin/surfaces/feeds/feed-editor.js +21 -4
  128. package/dist/admin/surfaces/feeds/feed-editor.js.map +1 -1
  129. package/dist/admin/surfaces/layouts/layout-editor-client.d.ts.map +1 -1
  130. package/dist/admin/surfaces/layouts/layout-editor-client.js +7 -4
  131. package/dist/admin/surfaces/layouts/layout-editor-client.js.map +1 -1
  132. package/dist/admin/surfaces/media/media-client.d.ts.map +1 -1
  133. package/dist/admin/surfaces/media/media-client.js +10 -2
  134. package/dist/admin/surfaces/media/media-client.js.map +1 -1
  135. package/dist/admin/surfaces/settings/settings-site.d.ts.map +1 -1
  136. package/dist/admin/surfaces/settings/settings-site.js +39 -2
  137. package/dist/admin/surfaces/settings/settings-site.js.map +1 -1
  138. package/dist/admin/surfaces/settings/settings-sites.js +18 -0
  139. package/dist/admin/surfaces/settings/settings-sites.js.map +1 -1
  140. package/dist/admin/surfaces/trash/trash-index.d.ts.map +1 -1
  141. package/dist/admin/surfaces/trash/trash-index.js +45 -8
  142. package/dist/admin/surfaces/trash/trash-index.js.map +1 -1
  143. package/dist/admin/surfaces/users/users-data.d.ts +11 -0
  144. package/dist/admin/surfaces/users/users-data.d.ts.map +1 -1
  145. package/dist/admin/surfaces/users/users-data.js +20 -1
  146. package/dist/admin/surfaces/users/users-data.js.map +1 -1
  147. package/dist/admin/surfaces/webServices/web-services-data.d.ts +11 -1
  148. package/dist/admin/surfaces/webServices/web-services-data.d.ts.map +1 -1
  149. package/dist/admin/surfaces/webServices/web-services-data.js +13 -0
  150. package/dist/admin/surfaces/webServices/web-services-data.js.map +1 -1
  151. package/dist/admin/surfaces/webServices/webhooks.d.ts +3 -0
  152. package/dist/admin/surfaces/webServices/webhooks.d.ts.map +1 -0
  153. package/dist/admin/surfaces/webServices/webhooks.js +69 -0
  154. package/dist/admin/surfaces/webServices/webhooks.js.map +1 -0
  155. package/dist/admin/webhook-manager.d.ts +32 -0
  156. package/dist/admin/webhook-manager.d.ts.map +1 -0
  157. package/dist/admin/webhook-manager.js +62 -0
  158. package/dist/admin/webhook-manager.js.map +1 -0
  159. package/dist/api/admin-features-routes.d.ts.map +1 -1
  160. package/dist/api/admin-features-routes.js +30 -0
  161. package/dist/api/admin-features-routes.js.map +1 -1
  162. package/dist/api/api-key-routes.d.ts.map +1 -1
  163. package/dist/api/api-key-routes.js +6 -1
  164. package/dist/api/api-key-routes.js.map +1 -1
  165. package/dist/api/auth.d.ts +8 -0
  166. package/dist/api/auth.d.ts.map +1 -1
  167. package/dist/api/auth.js +11 -1
  168. package/dist/api/auth.js.map +1 -1
  169. package/dist/api/comment-routes.d.ts.map +1 -1
  170. package/dist/api/comment-routes.js +102 -47
  171. package/dist/api/comment-routes.js.map +1 -1
  172. package/dist/api/config-routes.d.ts +10 -2
  173. package/dist/api/config-routes.d.ts.map +1 -1
  174. package/dist/api/config-routes.js +46 -25
  175. package/dist/api/config-routes.js.map +1 -1
  176. package/dist/api/content-routes.d.ts.map +1 -1
  177. package/dist/api/content-routes.js +41 -0
  178. package/dist/api/content-routes.js.map +1 -1
  179. package/dist/api/content-schemas.d.ts.map +1 -1
  180. package/dist/api/content-schemas.js +34 -1
  181. package/dist/api/content-schemas.js.map +1 -1
  182. package/dist/api/content-type-routes.d.ts.map +1 -1
  183. package/dist/api/content-type-routes.js +58 -3
  184. package/dist/api/content-type-routes.js.map +1 -1
  185. package/dist/api/feed-routes.d.ts +180 -0
  186. package/dist/api/feed-routes.d.ts.map +1 -1
  187. package/dist/api/feed-routes.js +10 -2
  188. package/dist/api/feed-routes.js.map +1 -1
  189. package/dist/api/form-routes.d.ts +6 -0
  190. package/dist/api/form-routes.d.ts.map +1 -1
  191. package/dist/api/form-routes.js +93 -14
  192. package/dist/api/form-routes.js.map +1 -1
  193. package/dist/api/graphql/resolvers.d.ts +4 -4
  194. package/dist/api/index.d.ts.map +1 -1
  195. package/dist/api/index.js +12 -1
  196. package/dist/api/index.js.map +1 -1
  197. package/dist/api/media-routes.d.ts.map +1 -1
  198. package/dist/api/media-routes.js +18 -1
  199. package/dist/api/media-routes.js.map +1 -1
  200. package/dist/api/webhook-routes.d.ts.map +1 -1
  201. package/dist/api/webhook-routes.js +19 -2
  202. package/dist/api/webhook-routes.js.map +1 -1
  203. package/dist/api/workflow-routes.d.ts.map +1 -1
  204. package/dist/api/workflow-routes.js +163 -4
  205. package/dist/api/workflow-routes.js.map +1 -1
  206. package/dist/auth/roles.d.ts +28 -0
  207. package/dist/auth/roles.d.ts.map +1 -1
  208. package/dist/auth/roles.js +49 -0
  209. package/dist/auth/roles.js.map +1 -1
  210. package/dist/config/reserved-field-names.d.ts +29 -0
  211. package/dist/config/reserved-field-names.d.ts.map +1 -0
  212. package/dist/config/reserved-field-names.js +59 -0
  213. package/dist/config/reserved-field-names.js.map +1 -0
  214. package/dist/config/types.d.ts +31 -2
  215. package/dist/config/types.d.ts.map +1 -1
  216. package/dist/config/types.js.map +1 -1
  217. package/dist/db/analytics-queries.d.ts +1 -1
  218. package/dist/db/api-key-queries.d.ts +7 -2
  219. package/dist/db/api-key-queries.d.ts.map +1 -1
  220. package/dist/db/api-key-queries.js +8 -2
  221. package/dist/db/api-key-queries.js.map +1 -1
  222. package/dist/db/apply-snapshot.d.ts +58 -0
  223. package/dist/db/apply-snapshot.d.ts.map +1 -0
  224. package/dist/db/apply-snapshot.js +142 -0
  225. package/dist/db/apply-snapshot.js.map +1 -0
  226. package/dist/db/audit-queries.d.ts +3 -3
  227. package/dist/db/category-queries.d.ts +3 -3
  228. package/dist/db/changeset-audit-queries.d.ts +2 -2
  229. package/dist/db/changeset-queries.d.ts +5 -5
  230. package/dist/db/changeset-queries.d.ts.map +1 -1
  231. package/dist/db/changeset-queries.js +24 -7
  232. package/dist/db/changeset-queries.js.map +1 -1
  233. package/dist/db/content-queries.d.ts +6 -0
  234. package/dist/db/content-queries.d.ts.map +1 -1
  235. package/dist/db/content-queries.js +8 -0
  236. package/dist/db/content-queries.js.map +1 -1
  237. package/dist/db/content-type-queries.d.ts +20 -0
  238. package/dist/db/content-type-queries.d.ts.map +1 -1
  239. package/dist/db/content-type-queries.js +32 -0
  240. package/dist/db/content-type-queries.js.map +1 -1
  241. package/dist/db/dashboard-queries.d.ts.map +1 -1
  242. package/dist/db/dashboard-queries.js +5 -1
  243. package/dist/db/dashboard-queries.js.map +1 -1
  244. package/dist/db/dynamic-schema.d.ts.map +1 -1
  245. package/dist/db/dynamic-schema.js +2 -0
  246. package/dist/db/dynamic-schema.js.map +1 -1
  247. package/dist/db/extension-queries.d.ts +8 -8
  248. package/dist/db/feed-queries.d.ts +3 -3
  249. package/dist/db/index.d.ts +3 -0
  250. package/dist/db/index.d.ts.map +1 -1
  251. package/dist/db/index.js +7 -0
  252. package/dist/db/index.js.map +1 -1
  253. package/dist/db/media-queries.d.ts +31 -7
  254. package/dist/db/media-queries.d.ts.map +1 -1
  255. package/dist/db/media-queries.js +13 -1
  256. package/dist/db/media-queries.js.map +1 -1
  257. package/dist/db/membership-dedupe.d.ts +26 -0
  258. package/dist/db/membership-dedupe.d.ts.map +1 -0
  259. package/dist/db/membership-dedupe.js +68 -0
  260. package/dist/db/membership-dedupe.js.map +1 -0
  261. package/dist/db/revision-queries.d.ts +122 -0
  262. package/dist/db/revision-queries.d.ts.map +1 -0
  263. package/dist/db/revision-queries.js +213 -0
  264. package/dist/db/revision-queries.js.map +1 -0
  265. package/dist/db/schema-apply.d.ts +8 -0
  266. package/dist/db/schema-apply.d.ts.map +1 -1
  267. package/dist/db/schema-apply.js +17 -0
  268. package/dist/db/schema-apply.js.map +1 -1
  269. package/dist/db/schema-field-queries.d.ts +2 -0
  270. package/dist/db/schema-field-queries.d.ts.map +1 -1
  271. package/dist/db/schema-field-queries.js +4 -0
  272. package/dist/db/schema-field-queries.js.map +1 -1
  273. package/dist/db/site-plugin-queries.d.ts +2 -2
  274. package/dist/db/user-queries.d.ts +12 -0
  275. package/dist/db/user-queries.d.ts.map +1 -1
  276. package/dist/db/user-queries.js +18 -4
  277. package/dist/db/user-queries.js.map +1 -1
  278. package/dist/db/version-queries.d.ts +37 -0
  279. package/dist/db/version-queries.d.ts.map +1 -1
  280. package/dist/db/version-queries.js +36 -0
  281. package/dist/db/version-queries.js.map +1 -1
  282. package/dist/db/webhook-queries.d.ts +27 -5
  283. package/dist/db/webhook-queries.d.ts.map +1 -1
  284. package/dist/db/webhook-queries.js +17 -0
  285. package/dist/db/webhook-queries.js.map +1 -1
  286. package/dist/db/workflow-queries.d.ts +2 -2
  287. package/dist/experiments/schema.d.ts +1 -1
  288. package/dist/hooks/index.d.ts +1 -1
  289. package/dist/hooks/index.d.ts.map +1 -1
  290. package/dist/hooks/index.js +1 -1
  291. package/dist/hooks/index.js.map +1 -1
  292. package/dist/hooks/webhook-retry.d.ts +22 -1
  293. package/dist/hooks/webhook-retry.d.ts.map +1 -1
  294. package/dist/hooks/webhook-retry.js +38 -1
  295. package/dist/hooks/webhook-retry.js.map +1 -1
  296. package/dist/host/index.d.ts +28 -0
  297. package/dist/host/index.d.ts.map +1 -1
  298. package/dist/host/index.js +56 -1
  299. package/dist/host/index.js.map +1 -1
  300. package/dist/layout/comments-module.d.ts +18 -0
  301. package/dist/layout/comments-module.d.ts.map +1 -0
  302. package/dist/layout/comments-module.js +130 -0
  303. package/dist/layout/comments-module.js.map +1 -0
  304. package/dist/layout/feed-hydration.d.ts +75 -0
  305. package/dist/layout/feed-hydration.d.ts.map +1 -0
  306. package/dist/layout/feed-hydration.js +158 -0
  307. package/dist/layout/feed-hydration.js.map +1 -0
  308. package/dist/layout/index.d.ts +2 -1
  309. package/dist/layout/index.d.ts.map +1 -1
  310. package/dist/layout/index.js +2 -1
  311. package/dist/layout/index.js.map +1 -1
  312. package/dist/layout/module-render-context.d.ts +13 -0
  313. package/dist/layout/module-render-context.d.ts.map +1 -0
  314. package/dist/layout/module-render-context.js +15 -0
  315. package/dist/layout/module-render-context.js.map +1 -0
  316. package/dist/layout/modules-extended.d.ts.map +1 -1
  317. package/dist/layout/modules-extended.js +92 -13
  318. package/dist/layout/modules-extended.js.map +1 -1
  319. package/dist/layout/registry.d.ts.map +1 -1
  320. package/dist/layout/registry.js +22 -6
  321. package/dist/layout/registry.js.map +1 -1
  322. package/dist/layout/renderer.d.ts +17 -1
  323. package/dist/layout/renderer.d.ts.map +1 -1
  324. package/dist/layout/renderer.js +11 -4
  325. package/dist/layout/renderer.js.map +1 -1
  326. package/dist/nav/index.d.ts +1 -0
  327. package/dist/nav/index.d.ts.map +1 -1
  328. package/dist/nav/index.js +4 -0
  329. package/dist/nav/index.js.map +1 -1
  330. package/dist/nav/url-resolver.d.ts +10 -2
  331. package/dist/nav/url-resolver.d.ts.map +1 -1
  332. package/dist/nav/url-resolver.js +17 -3
  333. package/dist/nav/url-resolver.js.map +1 -1
  334. package/dist/schema/base.d.ts +37 -1
  335. package/dist/schema/base.d.ts.map +1 -1
  336. package/dist/schema/base.js +20 -2
  337. package/dist/schema/base.js.map +1 -1
  338. package/dist/schema/generate.d.ts +8 -0
  339. package/dist/schema/generate.d.ts.map +1 -1
  340. package/dist/schema/generate.js +7 -4
  341. package/dist/schema/generate.js.map +1 -1
  342. package/dist/schema/mapper.d.ts.map +1 -1
  343. package/dist/schema/mapper.js +2 -0
  344. package/dist/schema/mapper.js.map +1 -1
  345. package/dist/schema/mysql-tables.d.ts +34 -0
  346. package/dist/schema/mysql-tables.d.ts.map +1 -1
  347. package/dist/schema/mysql-tables.js +14 -2
  348. package/dist/schema/mysql-tables.js.map +1 -1
  349. package/dist/server.d.ts +1 -0
  350. package/dist/server.d.ts.map +1 -1
  351. package/dist/server.js +1 -0
  352. package/dist/server.js.map +1 -1
  353. package/dist/site/styles.css +384 -0
  354. package/package.json +2 -1
@@ -0,0 +1,69 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * `webServices.webhooks` surface — outgoing webhook endpoint management, the
5
+ * third Web Services surface alongside OAuth Clients + API Keys (#57). Follows
6
+ * the API Keys / OAuth surface pattern exactly: `useAdminContext().fetch` for
7
+ * all requests, core's `WebhookManager` component, and the shared
8
+ * `SecretRevealModal` for the one-time signing-secret reveal on create.
9
+ *
10
+ * Signing secrets are shown once at creation and redacted from every list/read
11
+ * afterwards (the server retains them to sign HMAC payloads — they are NOT
12
+ * hashed, unlike verify-only OAuth client secrets).
13
+ */
14
+ import { useCallback, useEffect, useState } from 'react';
15
+ import { useAdminContext } from '../../context.js';
16
+ import { LoadingState, ErrorState } from '../../primitives.js';
17
+ import { SecretRevealModal } from './secret-reveal-modal.js';
18
+ import { WebhookManager } from '../../webhook-manager.js';
19
+ import { loadWebhooks, createWebhook, deleteWebhook, loadWebhookDeliveries, } from './web-services-data.js';
20
+ export default function WebServicesWebhooksSurface(_props) {
21
+ const { fetch: adminFetch } = useAdminContext();
22
+ const [webhooks, setWebhooks] = useState([]);
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState(null);
25
+ const [revealed, setRevealed] = useState(null);
26
+ const load = useCallback(async () => {
27
+ setLoading(true);
28
+ setError(null);
29
+ try {
30
+ const list = await loadWebhooks(adminFetch);
31
+ list.sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''));
32
+ setWebhooks(list);
33
+ }
34
+ catch (e) {
35
+ setError(e instanceof Error ? e.message : 'Failed to load webhooks');
36
+ }
37
+ finally {
38
+ setLoading(false);
39
+ }
40
+ }, [adminFetch]);
41
+ useEffect(() => {
42
+ load();
43
+ }, [load]);
44
+ const handleCreate = useCallback(async (data) => {
45
+ setError(null);
46
+ try {
47
+ const created = await createWebhook(adminFetch, data);
48
+ // Reveal the signing secret exactly once — it is redacted on every read.
49
+ setRevealed({ url: created.url, secret: created.secret });
50
+ await load();
51
+ }
52
+ catch (e) {
53
+ setError(e instanceof Error ? e.message : 'Failed to create webhook');
54
+ }
55
+ }, [load, adminFetch]);
56
+ const handleDelete = useCallback(async (id) => {
57
+ setError(null);
58
+ try {
59
+ await deleteWebhook(adminFetch, id);
60
+ await load();
61
+ }
62
+ catch (e) {
63
+ setError(e instanceof Error ? e.message : 'Failed to delete webhook');
64
+ }
65
+ }, [load, adminFetch]);
66
+ const handleLoadDeliveries = useCallback((id) => loadWebhookDeliveries(adminFetch, id), [adminFetch]);
67
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "kywi-admin-page-header", children: _jsxs("div", { children: [_jsx("h1", { children: "Webhooks" }), _jsxs("p", { className: "kywi-admin-section-hint", children: ["Register endpoints that Kywi calls when content, comments, or categories change. Each delivery is signed (HMAC-SHA256) with the endpoint\u2019s secret, sent as", ' ', _jsx("code", { children: "X-Kywi-Signature" }), ". Failed deliveries are retried automatically with exponential backoff. The signing secret is shown once at creation and never again."] })] }) }), error && _jsx(ErrorState, { message: error, onRetry: load }), loading ? (_jsx(LoadingState, { label: "Loading webhooks\u2026" })) : (_jsx(WebhookManager, { webhooks: webhooks, onCreateWebhook: data => void handleCreate(data), onDeleteWebhook: id => void handleDelete(id), onLoadDeliveries: handleLoadDeliveries })), revealed && (_jsx(SecretRevealModal, { title: "Webhook signing secret", intro: `Copy the signing secret for “${revealed.url}” now. It is redacted from every later view and will not be shown again.`, label: "Signing secret", value: revealed.secret, onClose: () => setRevealed(null) }))] }));
68
+ }
69
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../../../src/admin/surfaces/webServices/webhooks.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export interface WebhookSummary {
2
+ id: string;
3
+ url: string;
4
+ events: string[];
5
+ isActive: boolean;
6
+ /** True when a signing secret is configured (the secret itself is never listed). */
7
+ secretSet?: boolean;
8
+ createdAt: string;
9
+ }
10
+ export interface WebhookCreateData {
11
+ url: string;
12
+ secret: string;
13
+ events: string[];
14
+ }
15
+ export interface WebhookDeliverySummary {
16
+ id: string;
17
+ event: string;
18
+ status: string;
19
+ attempts: number;
20
+ httpStatus: number | null;
21
+ createdAt: string;
22
+ nextRetryAt: string | null;
23
+ }
24
+ export interface WebhookManagerProps {
25
+ webhooks: WebhookSummary[];
26
+ onCreateWebhook?: (data: WebhookCreateData) => void;
27
+ onDeleteWebhook?: (id: string) => void;
28
+ /** Load recent deliveries for an endpoint (status / attempts log). */
29
+ onLoadDeliveries?: (id: string) => Promise<WebhookDeliverySummary[]>;
30
+ }
31
+ export declare function WebhookManager({ webhooks, onCreateWebhook, onDeleteWebhook, onLoadDeliveries, }: WebhookManagerProps): import("react/jsx-runtime").JSX.Element;
32
+ //# sourceMappingURL=webhook-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-manager.d.ts","sourceRoot":"","sources":["../../src/admin/webhook-manager.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAID,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACnD,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAA;CACrE;AAqBD,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,EAAE,mBAAmB,2CA4JrB"}
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React, { useState } from 'react';
4
+ // ── Available events ────────────────────────────────────────────────────────────
5
+ // Mirrors WEBHOOK_EVENTS in hooks/webhook-dispatch.ts — only events that are
6
+ // actually dispatched are advertised here.
7
+ const AVAILABLE_EVENTS = [
8
+ 'content:afterCreate',
9
+ 'content:afterUpdate',
10
+ 'content:afterPublish',
11
+ 'content:afterDelete',
12
+ 'comment:afterCreate',
13
+ 'comment:afterApprove',
14
+ 'comment:afterDelete',
15
+ 'category:afterCreate',
16
+ 'category:afterUpdate',
17
+ 'category:afterDelete',
18
+ ];
19
+ // ── WebhookManager ──────────────────────────────────────────────────────────────
20
+ export function WebhookManager({ webhooks, onCreateWebhook, onDeleteWebhook, onLoadDeliveries, }) {
21
+ const [showCreateForm, setShowCreateForm] = useState(false);
22
+ const [deleteConfirmId, setDeleteConfirmId] = useState(null);
23
+ const [deliveriesFor, setDeliveriesFor] = useState(null);
24
+ const [deliveries, setDeliveries] = useState([]);
25
+ const [deliveriesLoading, setDeliveriesLoading] = useState(false);
26
+ async function toggleDeliveries(id) {
27
+ if (deliveriesFor === id) {
28
+ setDeliveriesFor(null);
29
+ setDeliveries([]);
30
+ return;
31
+ }
32
+ setDeliveriesFor(id);
33
+ setDeliveries([]);
34
+ if (!onLoadDeliveries)
35
+ return;
36
+ setDeliveriesLoading(true);
37
+ try {
38
+ setDeliveries(await onLoadDeliveries(id));
39
+ }
40
+ finally {
41
+ setDeliveriesLoading(false);
42
+ }
43
+ }
44
+ return (_jsxs("div", { className: "kywi-admin-webhook-manager", children: [_jsxs("div", { className: "kywi-admin-webhook-header", children: [_jsxs("h2", { className: "kywi-admin-webhook-title", children: ["Webhook Endpoints ", _jsxs("span", { className: "kywi-admin-webhook-count", children: ["(", webhooks.length, ")"] })] }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-primary", onClick: () => setShowCreateForm(true), children: "Add Endpoint" })] }), showCreateForm && (_jsx(WebhookCreateForm, { onSubmit: (data) => {
45
+ onCreateWebhook?.(data);
46
+ setShowCreateForm(false);
47
+ }, onCancel: () => setShowCreateForm(false) })), webhooks.length === 0 ? (_jsx("p", { className: "kywi-admin-webhook-empty", children: "No webhook endpoints configured." })) : (_jsxs("table", { className: "kywi-admin-webhook-table", role: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "URL" }), _jsx("th", { children: "Events" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Created" }), _jsx("th", { children: "Actions" })] }) }), _jsx("tbody", { children: webhooks.map(hook => (_jsxs(React.Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { children: _jsx("code", { children: hook.url }) }), _jsx("td", { children: (hook.events ?? []).map(ev => (_jsx("span", { className: "kywi-admin-webhook-event-label", children: ev }, ev))) }), _jsx("td", { children: hook.isActive ? 'Active' : 'Inactive' }), _jsx("td", { children: hook.createdAt ? new Date(hook.createdAt).toLocaleDateString() : '—' }), _jsxs("td", { children: [_jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm", onClick: () => void toggleDeliveries(hook.id), "aria-expanded": deliveriesFor === hook.id, "aria-label": `${deliveriesFor === hook.id ? 'Hide' : 'View'} deliveries for ${hook.url}`, children: deliveriesFor === hook.id ? 'Hide Deliveries' : 'View Deliveries' }), deleteConfirmId === hook.id ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "kywi-admin-confirm-text", children: "Delete this endpoint?" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm kywi-btn-danger", onClick: () => { onDeleteWebhook?.(hook.id); setDeleteConfirmId(null); }, "aria-label": `Confirm delete ${hook.url}`, children: "Yes, Delete" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm", onClick: () => setDeleteConfirmId(null), children: "Cancel" })] })) : (_jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm kywi-btn-danger", onClick: () => setDeleteConfirmId(hook.id), "aria-label": `Delete ${hook.url}`, children: "Delete" }))] })] }), deliveriesFor === hook.id && (_jsx("tr", { children: _jsx("td", { colSpan: 5, children: deliveriesLoading ? (_jsx("p", { className: "kywi-admin-webhook-empty", children: "Loading deliveries\u2026" })) : deliveries.length === 0 ? (_jsx("p", { className: "kywi-admin-webhook-empty", children: "No deliveries recorded yet." })) : (_jsxs("table", { className: "kywi-admin-webhook-deliveries-table", role: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Event" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Attempts" }), _jsx("th", { children: "HTTP" }), _jsx("th", { children: "When" })] }) }), _jsx("tbody", { children: deliveries.map(d => (_jsxs("tr", { children: [_jsx("td", { children: d.event }), _jsx("td", { children: d.status }), _jsx("td", { children: d.attempts }), _jsx("td", { children: d.httpStatus ?? '—' }), _jsx("td", { children: d.createdAt ? new Date(d.createdAt).toLocaleString() : '—' })] }, d.id))) })] })) }) }))] }, hook.id))) })] }))] }));
48
+ }
49
+ function WebhookCreateForm({ onSubmit, onCancel }) {
50
+ const [url, setUrl] = useState('');
51
+ const [secret, setSecret] = useState('');
52
+ const [events, setEvents] = useState([]);
53
+ function handleEventToggle(event, checked) {
54
+ setEvents(prev => checked ? [...prev, event] : prev.filter(e => e !== event));
55
+ }
56
+ function handleSubmit(e) {
57
+ e.preventDefault();
58
+ onSubmit({ url, secret, events });
59
+ }
60
+ return (_jsxs("form", { className: "kywi-admin-webhook-form", onSubmit: handleSubmit, children: [_jsxs("div", { className: "kywi-form-field", children: [_jsx("label", { htmlFor: "webhook-url", children: "Endpoint URL" }), _jsx("input", { id: "webhook-url", type: "url", className: "kywi-input", value: url, onChange: (e) => setUrl(e.target.value), placeholder: "https://example.com/hooks/kywi", required: true })] }), _jsxs("div", { className: "kywi-form-field", children: [_jsx("label", { htmlFor: "webhook-secret", children: "Signing Secret" }), _jsx("input", { id: "webhook-secret", type: "text", className: "kywi-input", value: secret, onChange: (e) => setSecret(e.target.value), minLength: 16, placeholder: "At least 16 characters", required: true }), _jsxs("p", { className: "kywi-admin-section-hint", children: ["Used to sign each payload (HMAC-SHA256, sent as ", _jsx("code", { children: "X-Kywi-Signature" }), "). Shown once here \u2014 store it now; it is redacted from every later view."] })] }), _jsxs("fieldset", { className: "kywi-form-field", children: [_jsx("legend", { children: "Events" }), AVAILABLE_EVENTS.map(event => (_jsxs("label", { className: "kywi-admin-webhook-event-label", children: [_jsx("input", { type: "checkbox", checked: events.includes(event), onChange: (e) => handleEventToggle(event, e.target.checked) }), event] }, event)))] }), _jsxs("div", { className: "kywi-admin-webhook-form-actions", children: [_jsx("button", { type: "submit", className: "kywi-btn kywi-btn-primary", disabled: events.length === 0, children: "Add Endpoint" }), _jsx("button", { type: "button", className: "kywi-btn", onClick: onCancel, children: "Cancel" })] })] }));
61
+ }
62
+ //# sourceMappingURL=webhook-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-manager.js","sourceRoot":"","sources":["../../src/admin/webhook-manager.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"admin-features-routes.d.ts","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AA2BpD,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,UAAU,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,yBAAyB,GAAG,IAAI,CAiChG"}
1
+ {"version":3,"file":"admin-features-routes.d.ts","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,oBAAoB,CAAA;AA2CzE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,UAAU,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,yBAAyB,GAAG,IAAI,CAqDhG"}
@@ -1,9 +1,27 @@
1
1
  import { z } from 'zod';
2
+ import { resolveAdminConfig } from '../config/define.js';
2
3
  import { jsonOk, jsonError, unauthorized, forbidden, validationError } from './response.js';
3
4
  import { resolveAuth } from './auth.js';
4
5
  import { hasMinRole } from '../auth/roles.js';
5
6
  import { toSiteUuid } from './site-context.js';
6
7
  import { getAdminFeatureFlags, setAdminFeatureFlags } from '../db/admin-features-queries.js';
8
+ /**
9
+ * True when a runtime flag key (an admin area or surface id) is held OFF by the
10
+ * config ceiling and therefore cannot be re-enabled at runtime. Mirrors the
11
+ * config-ceiling checks in `isSurfaceEnabled` (surface-matcher.ts): master off,
12
+ * area denylist, or surface-id denylist. The Admin Features surface is never
13
+ * config-locked by an area/surface denylist (the escape hatch), so a superAdmin
14
+ * can always runtime-enable it; only the master switch removes it.
15
+ */
16
+ function isConfigLocked(ceiling, key) {
17
+ if (ceiling.enabled === false)
18
+ return true;
19
+ if (ceiling.features[key] === false)
20
+ return true;
21
+ if (ceiling.disableSurfaces.includes(key))
22
+ return true;
23
+ return false;
24
+ }
7
25
  /**
8
26
  * T16 runtime admin feature-flag routes (plan §4). Thin wrappers over the
9
27
  * per-site `admin.features` settings-key queries.
@@ -52,6 +70,18 @@ export function registerAdminFeaturesRoutes(router, ctx) {
52
70
  if (!parsed.success) {
53
71
  return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
54
72
  }
73
+ // Honest config ceiling (#58): the config hard-ceiling holds at resolution
74
+ // time regardless, so silently storing `{comments:true}` for a config-disabled
75
+ // feature was a lie to anyone scripting the API. Reject any attempt to ENABLE a
76
+ // config-locked feature (nothing is persisted) instead of returning a hollow
77
+ // 200. Setting a locked feature to `false`, or toggling unlocked features, is
78
+ // unaffected — so the admin UI's normal area toggles keep working.
79
+ const ceiling = resolveAdminConfig(config.admin);
80
+ const lockedKeys = Object.keys(parsed.data).filter(key => parsed.data[key] === true && isConfigLocked(ceiling, key));
81
+ if (lockedKeys.length > 0) {
82
+ return jsonError('CONFIG_LOCKED', `Cannot enable feature(s) disabled in kywi.config.ts: ${lockedKeys.join(', ')}. ` +
83
+ 'Config is a hard ceiling over runtime flags — change it in kywi.config.ts.', 409, lockedKeys.map(key => ({ path: key, message: 'Disabled by kywi.config.ts (config-locked)' })));
84
+ }
55
85
  const url = new URL(req.url);
56
86
  const siteId = await toSiteUuid(db, url.searchParams.get('siteId'), config);
57
87
  const flags = await setAdminFeatureFlags(db, siteId, parsed.data);
@@ -1 +1 @@
1
- {"version":3,"file":"admin-features-routes.js","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"admin-features-routes.js","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"api-key-routes.d.ts","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAY7C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAiDzF"}
1
+ {"version":3,"file":"api-key-routes.d.ts","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAY7C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAqDzF"}
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { jsonOk, jsonError, unauthorized, notFound, validationError } from './response.js';
2
+ import { jsonOk, jsonError, unauthorized, forbidden, notFound, validationError } from './response.js';
3
3
  import { createApiKey, listApiKeys, revokeApiKey } from '../db/api-key-queries.js';
4
4
  import { resolveAuth } from './auth.js';
5
5
  const createApiKeySchema = z.object({
@@ -15,6 +15,11 @@ export function registerApiKeyRoutes(router, db, authSecret) {
15
15
  const auth = await authenticate(req);
16
16
  if (!auth.user)
17
17
  return unauthorized();
18
+ // #54: an API key must not be able to mint another API key. Otherwise a
19
+ // read-scoped key could bootstrap a higher-scoped one and escalate past its
20
+ // own ceiling. Key creation is a session-only (human, admin UI) operation.
21
+ if (auth.viaApiKey)
22
+ return forbidden('API keys cannot create API keys');
18
23
  let body;
19
24
  try {
20
25
  body = await req.json();
@@ -1 +1 @@
1
- {"version":3,"file":"api-key-routes.js","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-key-routes.js","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACrG,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAoB,MAAM,0BAA0B,CAAA;AACpG,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAEvC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC;IAC3C,MAAM,EAAE,CAAC,CAAC,KAAK,CACb,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAC3D,CAAC,GAAG,CAAC,CAAC,EAAE,gCAAgC,CAAC;CAC3C,CAAC,CAAA;AAEF,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,EAAU,EAAE,UAAkB;IACjF,KAAK,UAAU,YAAY,CAAC,GAAY;QACtC,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IACtE,CAAC;IAED,6CAA6C;IAC7C,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC1C,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,YAAY,EAAE,CAAA;QACrC,wEAAwE;QACxE,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC,iCAAiC,CAAC,CAAA;QAEvE,IAAI,IAAa,CAAA;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC,aAAa,EAAE,mBAAmB,EAAE,GAAG,CAAC,CAAA;QAC3D,CAAC;QAED,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QACjD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,eAAe,CACpB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAC/E,CAAA;QACH,CAAC;QAED,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAA;QACpC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,MAAuB,CAAC,CAAA;QAElF,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,kDAAkD;IAClD,MAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACzC,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,YAAY,EAAE,CAAA;QAErC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAChD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC,CAAC,CAAA;IAEF,2DAA2D;IAC3D,MAAM,CAAC,MAAM,CAAC,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE;QACxD,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,YAAY,EAAE,CAAA;QAErC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,EAAE,EAAE,MAAM,CAAC,EAAG,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAChE,IAAI,CAAC,OAAO;YAAE,OAAO,QAAQ,CAAC,uCAAuC,CAAC,CAAA;QAEtE,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IAClC,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -3,6 +3,14 @@ import type { KywiDb } from '../db/client.js';
3
3
  export interface AuthResult {
4
4
  user: UserContext | null;
5
5
  error?: string;
6
+ /**
7
+ * True when the request was authenticated by a bearer API key (`kywi_…`)
8
+ * rather than a session/JWT. Credential-minting routes use this to refuse
9
+ * bootstrapping — an API key must never be able to create another API key
10
+ * (a read-scoped key could otherwise mint a higher-scoped one, defeating the
11
+ * scope model, #54). Session auth leaves this falsy, so it is unaffected.
12
+ */
13
+ viaApiKey?: boolean;
6
14
  }
7
15
  export declare function verifyAuthHeader(authHeader: string | null, secret: string): Promise<AuthResult>;
8
16
  export declare function resolveAuth(authHeader: string | null, secret: string, db: KywiDb): Promise<AuthResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAI7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,UAAU,CAAC,CAerB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAK7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,UAAU,CAAC,CAwBrB"}
package/dist/api/auth.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jwtVerify } from 'jose';
2
2
  import { resolveApiKey } from '../db/api-key-queries.js';
3
3
  import { getUserById, getUserGroups, buildUserContext } from '../db/user-queries.js';
4
+ import { effectiveApiKeyRole } from '../auth/roles.js';
4
5
  export async function verifyAuthHeader(authHeader, secret) {
5
6
  if (!authHeader) {
6
7
  return { user: null };
@@ -43,7 +44,16 @@ export async function resolveAuth(authHeader, secret, db) {
43
44
  if (!user || !user.isActive)
44
45
  return { user: null, error: 'API key owner not found or inactive' };
45
46
  const groups = await getUserGroups(db, user.id);
46
- return { user: buildUserContext(user, groups) };
47
+ const ctx = buildUserContext(user, groups);
48
+ // #54: enforce the key's declared scopes as a ceiling on the owner's role.
49
+ // The effective permission is intersection(owner role, key scopes) — NOT a
50
+ // fallback to the owner's role. Otherwise a read-scoped key (the kind handed
51
+ // to a static frontend or CI job) inherits the owner's full role and can
52
+ // POST/PUT/DELETE/publish content. Every route authorizes off `user.role`
53
+ // (canAccessContent / hasMinRole), so capping the role here uniformly gates
54
+ // all API-key traffic. The session/cookie path never reaches this branch, so
55
+ // it is unaffected.
56
+ return { user: { ...ctx, role: effectiveApiKeyRole(ctx.role, result.scopes) }, viaApiKey: true };
47
57
  }
48
58
  return verifyAuthHeader(authHeader, secret);
49
59
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"comment-routes.d.ts","sourceRoot":"","sources":["../../src/api/comment-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAqBxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,UAAU,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,YAAY,CAAA;IAC1B,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;IACzC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAcD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,IAAI,CA6PpF"}
1
+ {"version":3,"file":"comment-routes.d.ts","sourceRoot":"","sources":["../../src/api/comment-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAsBxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,UAAU,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,YAAY,CAAA;IAC1B,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;IACzC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAwBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,IAAI,CA0TpF"}
@@ -6,6 +6,7 @@ import { hasMinRole } from '../auth/roles.js';
6
6
  import { listComments, listSiteComments, getCommentById, createComment, updateComment, updateCommentStatus, deleteComment, flagComment, unflagComment, } from '../db/comment-queries.js';
7
7
  import { isSpam } from '../comments/spam-filter.js';
8
8
  import { getContentById } from '../db/content-queries.js';
9
+ import { checkHoneypot } from './form-routes.js';
9
10
  import { jsonOk, jsonError, notFound, unauthorized, forbidden, validationError } from './response.js';
10
11
  import { listSubscribers } from '../db/comment-subscription-queries.js';
11
12
  import { sendTemplatedEmail, COMMENT_NOTIFICATION_TEMPLATE } from '../email/index.js';
@@ -15,6 +16,12 @@ const createCommentSchema = z.object({
15
16
  authorName: z.string().optional(),
16
17
  authorEmail: z.string().optional(),
17
18
  });
19
+ // Collection-endpoint variant (POST /comments): the content is named in the body
20
+ // rather than the URL path, so the public comment form / Comments layout module
21
+ // can submit without knowing the content's base-type segment.
22
+ const createCommentCollectionSchema = createCommentSchema.extend({
23
+ contentId: z.string().min(1),
24
+ });
18
25
  const updateCommentSchema = z.object({
19
26
  body: z.string().min(1).optional(),
20
27
  status: z.string().optional(),
@@ -22,37 +29,17 @@ const updateCommentSchema = z.object({
22
29
  export function registerCommentRoutes(router, ctx) {
23
30
  const { db, authSecret } = ctx;
24
31
  const commentHooks = createCommentHookDispatcher(ctx.hookRegistry);
25
- // GET /content/:type/:id/comments — list comments on content
26
- router.get('/content/:type/:id/comments', async (req, params) => {
27
- const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
28
- const isEditor = auth.user !== null && hasMinRole(auth.user.role, 'editor');
29
- const url = new URL(req.url);
30
- const statusParam = url.searchParams.get('status') ?? undefined;
31
- let status;
32
- if (isEditor) {
33
- status = statusParam;
34
- }
35
- else {
36
- status = 'approved';
37
- }
38
- const comments = await listComments(db, params.id, status);
39
- return jsonOk(comments);
40
- });
41
- // POST /content/:type/:id/comments — create comment
42
- router.post('/content/:type/:id/comments', async (req, params) => {
43
- const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
44
- let body;
45
- try {
46
- body = await req.json();
47
- }
48
- catch {
49
- return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
50
- }
51
- const parsed = createCommentSchema.safeParse(body);
52
- if (!parsed.success) {
53
- return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
54
- }
55
- const content = await getContentById(db, null, params.id);
32
+ /**
33
+ * Shared create path for both comment-submission endpoints
34
+ * (`POST /content/:type/:id/comments` and the collection `POST /comments`).
35
+ * Validates the target content, honours the content type's `commentable`
36
+ * flag, screens for spam, persists the comment as `pending` (so every new
37
+ * comment lands in the moderation queue), fires the afterCreate hook, and
38
+ * best-effort notifies subscribers. Honeypot + JSON parsing are handled by the
39
+ * callers, which hold the raw request body.
40
+ */
41
+ async function submitComment(auth, contentId, data) {
42
+ const content = await getContentById(db, null, contentId);
56
43
  if (!content)
57
44
  return notFound('Content not found');
58
45
  // Enforce commentable flag on content type
@@ -61,38 +48,38 @@ export function registerCommentRoutes(router, ctx) {
61
48
  if (contentTypeConfig && contentTypeConfig.commentable === false) {
62
49
  return jsonError('FORBIDDEN', 'Comments are not allowed on this content type', 403);
63
50
  }
64
- if (isSpam(parsed.data.body)) {
51
+ if (isSpam(data.body)) {
65
52
  return jsonError('SPAM_REJECTED', 'Comment rejected as spam', 400);
66
53
  }
67
54
  const commentData = {
68
55
  siteId: content.siteId,
69
- contentId: params.id,
70
- body: parsed.data.body,
56
+ contentId,
57
+ body: data.body,
71
58
  status: 'pending',
72
59
  };
73
- if (parsed.data.parentId !== undefined) {
74
- commentData.parentId = parsed.data.parentId;
60
+ if (data.parentId !== undefined) {
61
+ commentData.parentId = data.parentId;
75
62
  }
76
63
  if (auth.user !== null) {
77
64
  commentData.authorId = auth.user.id;
78
65
  }
79
- if (parsed.data.authorName !== undefined) {
80
- commentData.authorName = parsed.data.authorName;
66
+ if (data.authorName !== undefined) {
67
+ commentData.authorName = data.authorName;
81
68
  }
82
- if (parsed.data.authorEmail !== undefined) {
83
- commentData.authorEmail = parsed.data.authorEmail;
69
+ if (data.authorEmail !== undefined) {
70
+ commentData.authorEmail = data.authorEmail;
84
71
  }
85
72
  const comment = await createComment(db, commentData);
86
73
  await commentHooks.afterCreate(comment);
87
74
  // W6: send email notifications to subscribers
88
75
  if (ctx.emailProvider) {
89
76
  try {
90
- const subscribers = await listSubscribers(db, params.id);
91
- const authorEmail = parsed.data.authorEmail ?? (auth.user ? auth.user.id : undefined);
77
+ const subscribers = await listSubscribers(db, contentId);
78
+ const authorEmail = data.authorEmail ?? (auth.user ? auth.user.id : undefined);
92
79
  const contentTitle = content.title ?? 'Untitled';
93
80
  const siteName = ctx.config.sites[0]?.name ?? 'Kywi';
94
81
  const baseUrl = ctx.baseUrl ?? 'http://localhost:3000';
95
- const contentUrl = `${baseUrl}/content/${params.id}`;
82
+ const contentUrl = `${baseUrl}/content/${contentId}`;
96
83
  for (const sub of subscribers) {
97
84
  // Skip the comment author
98
85
  if (authorEmail && sub.email === authorEmail)
@@ -103,8 +90,8 @@ export function registerCommentRoutes(router, ctx) {
103
90
  template: COMMENT_NOTIFICATION_TEMPLATE,
104
91
  vars: {
105
92
  contentTitle,
106
- commentAuthor: parsed.data.authorName ?? 'Someone',
107
- commentBody: parsed.data.body,
93
+ commentAuthor: data.authorName ?? 'Someone',
94
+ commentBody: data.body,
108
95
  siteName,
109
96
  contentUrl,
110
97
  },
@@ -116,15 +103,83 @@ export function registerCommentRoutes(router, ctx) {
116
103
  }
117
104
  }
118
105
  return jsonOk(comment, 201);
106
+ }
107
+ // GET /content/:type/:id/comments — list comments on content
108
+ router.get('/content/:type/:id/comments', async (req, params) => {
109
+ const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
110
+ const isEditor = auth.user !== null && hasMinRole(auth.user.role, 'editor');
111
+ const url = new URL(req.url);
112
+ const statusParam = url.searchParams.get('status') ?? undefined;
113
+ let status;
114
+ if (isEditor) {
115
+ status = statusParam;
116
+ }
117
+ else {
118
+ status = 'approved';
119
+ }
120
+ const comments = await listComments(db, params.id, status);
121
+ return jsonOk(comments);
122
+ });
123
+ // POST /content/:type/:id/comments — create comment (content named in the path)
124
+ router.post('/content/:type/:id/comments', async (req, params) => {
125
+ const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
126
+ let body;
127
+ try {
128
+ body = await req.json();
129
+ }
130
+ catch {
131
+ return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
132
+ }
133
+ // Honeypot — a bot that fills the hidden field is silently accepted (so it
134
+ // can't tell it was blocked) but nothing is persisted (shared with forms).
135
+ if (checkHoneypot(body)) {
136
+ return jsonOk({ status: 'received' }, 200);
137
+ }
138
+ const parsed = createCommentSchema.safeParse(body);
139
+ if (!parsed.success) {
140
+ return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
141
+ }
142
+ return submitComment(auth, params.id, parsed.data);
119
143
  });
120
- // GET /comments — list all site comments (auth+editor)
144
+ // POST /comments — public comment submission (content named in the body). This
145
+ // is the collection endpoint the public comment form / Comments layout module
146
+ // posts to; without it POST /comments matched the GET-only /comments route and
147
+ // returned 405, leaving the whole feature inert end-to-end (#29).
148
+ router.post('/comments', async (req) => {
149
+ const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
150
+ let body;
151
+ try {
152
+ body = await req.json();
153
+ }
154
+ catch {
155
+ return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
156
+ }
157
+ if (checkHoneypot(body)) {
158
+ return jsonOk({ status: 'received' }, 200);
159
+ }
160
+ const parsed = createCommentCollectionSchema.safeParse(body);
161
+ if (!parsed.success) {
162
+ return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
163
+ }
164
+ const { contentId, ...data } = parsed.data;
165
+ return submitComment(auth, contentId, data);
166
+ });
167
+ // GET /comments — dual-mode collection read.
168
+ // • Public: `?contentId=…` returns that content's APPROVED comments only, no
169
+ // auth required — this is what the public Comments module renders.
170
+ // • Admin: no `contentId` → the editor-only site moderation listing.
121
171
  router.get('/comments', async (req) => {
172
+ const url = new URL(req.url);
173
+ const contentIdParam = url.searchParams.get('contentId');
174
+ if (contentIdParam) {
175
+ const comments = await listComments(db, contentIdParam, 'approved');
176
+ return jsonOk(comments);
177
+ }
122
178
  const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
123
179
  if (!auth.user)
124
180
  return unauthorized();
125
181
  if (!hasMinRole(auth.user.role, 'editor'))
126
182
  return forbidden();
127
- const url = new URL(req.url);
128
183
  const statusParam = url.searchParams.get('status') ?? undefined;
129
184
  const siteId = await toSiteUuid(db, url.searchParams.get('siteId'), ctx.config);
130
185
  if (!siteId)