@porulle/core 0.10.8 → 0.13.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 (360) hide show
  1. package/dist/auth/auth-schema-guard.d.ts +2444 -0
  2. package/dist/auth/auth-schema-guard.d.ts.map +1 -0
  3. package/dist/auth/auth-schema-guard.js +113 -0
  4. package/dist/auth/auth-schema.d.ts +228 -0
  5. package/dist/auth/auth-schema.d.ts.map +1 -1
  6. package/dist/auth/auth-schema.js +24 -2
  7. package/dist/auth/middleware.d.ts +1 -0
  8. package/dist/auth/middleware.d.ts.map +1 -1
  9. package/dist/auth/middleware.js +13 -5
  10. package/dist/auth/org.d.ts +8 -2
  11. package/dist/auth/org.d.ts.map +1 -1
  12. package/dist/auth/org.js +12 -4
  13. package/dist/auth/organization-guard.d.ts +5 -0
  14. package/dist/auth/organization-guard.d.ts.map +1 -0
  15. package/dist/auth/organization-guard.js +124 -0
  16. package/dist/auth/permissions.d.ts +10 -0
  17. package/dist/auth/permissions.d.ts.map +1 -1
  18. package/dist/auth/permissions.js +31 -7
  19. package/dist/auth/role-authority.d.ts +44 -0
  20. package/dist/auth/role-authority.d.ts.map +1 -0
  21. package/dist/auth/role-authority.js +85 -0
  22. package/dist/auth/setup.d.ts.map +1 -1
  23. package/dist/auth/setup.js +17 -0
  24. package/dist/auth/strict-org-resolution.d.ts.map +1 -1
  25. package/dist/auth/strict-org-resolution.js +9 -1
  26. package/dist/auth/types.d.ts +1 -1
  27. package/dist/auth/types.d.ts.map +1 -1
  28. package/dist/config/defaults.d.ts.map +1 -1
  29. package/dist/config/defaults.js +2 -0
  30. package/dist/config/types.d.ts +33 -4
  31. package/dist/config/types.d.ts.map +1 -1
  32. package/dist/hooks/checkout.d.ts.map +1 -1
  33. package/dist/hooks/checkout.js +10 -9
  34. package/dist/index.d.ts +9 -6
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5 -3
  37. package/dist/interfaces/rest/audit-middleware.d.ts.map +1 -1
  38. package/dist/interfaces/rest/audit-middleware.js +2 -0
  39. package/dist/interfaces/rest/customer-portal.d.ts.map +1 -1
  40. package/dist/interfaces/rest/customer-portal.js +9 -8
  41. package/dist/interfaces/rest/index.d.ts.map +1 -1
  42. package/dist/interfaces/rest/index.js +4 -0
  43. package/dist/interfaces/rest/route-coverage.d.ts +14 -0
  44. package/dist/interfaces/rest/route-coverage.d.ts.map +1 -0
  45. package/dist/interfaces/rest/route-coverage.js +81 -0
  46. package/dist/interfaces/rest/router.d.ts +7 -2
  47. package/dist/interfaces/rest/router.d.ts.map +1 -1
  48. package/dist/interfaces/rest/router.js +8 -4
  49. package/dist/interfaces/rest/routes/admin/compensation-failures.d.ts.map +1 -1
  50. package/dist/interfaces/rest/routes/admin/compensation-failures.js +7 -5
  51. package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts +5 -0
  52. package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts.map +1 -0
  53. package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +22 -0
  54. package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts +5 -0
  55. package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts.map +1 -0
  56. package/dist/interfaces/rest/routes/admin/entity-field-definitions.js +39 -0
  57. package/dist/interfaces/rest/routes/admin/permissions.d.ts +1 -1
  58. package/dist/interfaces/rest/routes/admin/permissions.d.ts.map +1 -1
  59. package/dist/interfaces/rest/routes/admin/permissions.js +2 -0
  60. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
  61. package/dist/interfaces/rest/routes/admin/staff.js +126 -66
  62. package/dist/interfaces/rest/routes/admin-jobs.js +3 -3
  63. package/dist/interfaces/rest/routes/audit.js +3 -3
  64. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  65. package/dist/interfaces/rest/routes/carts.js +13 -10
  66. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  67. package/dist/interfaces/rest/routes/catalog.js +47 -4
  68. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  69. package/dist/interfaces/rest/routes/checkout.js +30 -35
  70. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -1
  71. package/dist/interfaces/rest/routes/documents.js +10 -5
  72. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  73. package/dist/interfaces/rest/routes/inventory.js +2 -0
  74. package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
  75. package/dist/interfaces/rest/routes/media.js +7 -3
  76. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  77. package/dist/interfaces/rest/routes/orders.js +26 -8
  78. package/dist/interfaces/rest/routes/pricing.js +2 -2
  79. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  80. package/dist/interfaces/rest/routes/promotions.js +13 -10
  81. package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
  82. package/dist/interfaces/rest/routes/search.js +14 -1
  83. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts +134 -0
  84. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts.map +1 -0
  85. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.js +26 -0
  86. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts +592 -0
  87. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts.map +1 -0
  88. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.js +109 -0
  89. package/dist/interfaces/rest/schemas/carts.d.ts +7 -0
  90. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  91. package/dist/interfaces/rest/schemas/catalog.d.ts +1364 -401
  92. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  93. package/dist/interfaces/rest/schemas/catalog.js +86 -0
  94. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  95. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  96. package/dist/interfaces/rest/schemas/orders.d.ts +204 -0
  97. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  98. package/dist/interfaces/rest/schemas/pricing.d.ts +1 -0
  99. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  100. package/dist/interfaces/rest/schemas/responses.d.ts +71 -0
  101. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  102. package/dist/interfaces/rest/schemas/search.d.ts +1 -0
  103. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  104. package/dist/interfaces/rest/schemas/search.js +1 -0
  105. package/dist/interfaces/rest/utils.d.ts +15 -0
  106. package/dist/interfaces/rest/utils.d.ts.map +1 -1
  107. package/dist/interfaces/rest/utils.js +45 -2
  108. package/dist/kernel/compensation/executor.js +2 -2
  109. package/dist/kernel/database/tx-context.d.ts +8 -0
  110. package/dist/kernel/database/tx-context.d.ts.map +1 -1
  111. package/dist/kernel/database/tx-context.js +13 -0
  112. package/dist/kernel/hooks/create-context.d.ts +2 -0
  113. package/dist/kernel/hooks/create-context.d.ts.map +1 -1
  114. package/dist/kernel/hooks/create-context.js +1 -0
  115. package/dist/kernel/hooks/types.d.ts +2 -0
  116. package/dist/kernel/hooks/types.d.ts.map +1 -1
  117. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  118. package/dist/kernel/plugin/manifest.js +9 -5
  119. package/dist/modules/analytics/service.js +2 -2
  120. package/dist/modules/audit/service.js +3 -3
  121. package/dist/modules/cart/repository/index.d.ts +13 -7
  122. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  123. package/dist/modules/cart/repository/index.js +16 -10
  124. package/dist/modules/cart/schema.d.ts +17 -0
  125. package/dist/modules/cart/schema.d.ts.map +1 -1
  126. package/dist/modules/cart/schema.js +4 -0
  127. package/dist/modules/cart/service.d.ts +22 -9
  128. package/dist/modules/cart/service.d.ts.map +1 -1
  129. package/dist/modules/cart/service.js +57 -33
  130. package/dist/modules/catalog/brand-service.js +7 -7
  131. package/dist/modules/catalog/category-service.js +7 -7
  132. package/dist/modules/catalog/entity-service.d.ts +19 -10
  133. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  134. package/dist/modules/catalog/entity-service.js +180 -51
  135. package/dist/modules/catalog/module.d.ts +6 -1
  136. package/dist/modules/catalog/module.d.ts.map +1 -1
  137. package/dist/modules/catalog/module.js +6 -1
  138. package/dist/modules/catalog/ownership.d.ts +5 -0
  139. package/dist/modules/catalog/ownership.d.ts.map +1 -0
  140. package/dist/modules/catalog/ownership.js +11 -0
  141. package/dist/modules/catalog/read-policy.d.ts +9 -0
  142. package/dist/modules/catalog/read-policy.d.ts.map +1 -0
  143. package/dist/modules/catalog/read-policy.js +8 -0
  144. package/dist/modules/catalog/repository/index.d.ts +55 -3
  145. package/dist/modules/catalog/repository/index.d.ts.map +1 -1
  146. package/dist/modules/catalog/repository/index.js +335 -6
  147. package/dist/modules/catalog/revision.d.ts +4 -0
  148. package/dist/modules/catalog/revision.d.ts.map +1 -0
  149. package/dist/modules/catalog/revision.js +33 -0
  150. package/dist/modules/catalog/schema.d.ts +979 -110
  151. package/dist/modules/catalog/schema.d.ts.map +1 -1
  152. package/dist/modules/catalog/schema.js +112 -2
  153. package/dist/modules/catalog/schemas.d.ts +2 -0
  154. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  155. package/dist/modules/catalog/schemas.js +2 -0
  156. package/dist/modules/catalog/service.d.ts +80 -15
  157. package/dist/modules/catalog/service.d.ts.map +1 -1
  158. package/dist/modules/catalog/service.js +522 -14
  159. package/dist/modules/channels/adapter.d.ts +85 -0
  160. package/dist/modules/channels/adapter.d.ts.map +1 -1
  161. package/dist/modules/channels/adapter.js +9 -1
  162. package/dist/modules/customers/module.d.ts.map +1 -1
  163. package/dist/modules/customers/module.js +1 -0
  164. package/dist/modules/customers/service.d.ts +2 -0
  165. package/dist/modules/customers/service.d.ts.map +1 -1
  166. package/dist/modules/customers/service.js +21 -20
  167. package/dist/modules/documents/module.d.ts.map +1 -1
  168. package/dist/modules/documents/module.js +1 -0
  169. package/dist/modules/documents/service.d.ts +18 -4
  170. package/dist/modules/documents/service.d.ts.map +1 -1
  171. package/dist/modules/documents/service.js +54 -16
  172. package/dist/modules/fulfillment/module.d.ts.map +1 -1
  173. package/dist/modules/fulfillment/module.js +1 -0
  174. package/dist/modules/fulfillment/service.d.ts +2 -0
  175. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  176. package/dist/modules/fulfillment/service.js +15 -6
  177. package/dist/modules/inventory/service.d.ts.map +1 -1
  178. package/dist/modules/inventory/service.js +15 -14
  179. package/dist/modules/media/module.d.ts +68 -0
  180. package/dist/modules/media/module.d.ts.map +1 -1
  181. package/dist/modules/media/module.js +2 -0
  182. package/dist/modules/media/schema.d.ts +70 -0
  183. package/dist/modules/media/schema.d.ts.map +1 -1
  184. package/dist/modules/media/schema.js +20 -2
  185. package/dist/modules/media/service.d.ts +8 -1
  186. package/dist/modules/media/service.d.ts.map +1 -1
  187. package/dist/modules/media/service.js +71 -23
  188. package/dist/modules/orders/guest-access.d.ts +27 -0
  189. package/dist/modules/orders/guest-access.d.ts.map +1 -0
  190. package/dist/modules/orders/guest-access.js +46 -0
  191. package/dist/modules/orders/module.d.ts.map +1 -1
  192. package/dist/modules/orders/module.js +1 -0
  193. package/dist/modules/orders/quote.d.ts.map +1 -1
  194. package/dist/modules/orders/quote.js +1 -0
  195. package/dist/modules/orders/repository/index.d.ts +1 -1
  196. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  197. package/dist/modules/orders/repository/index.js +2 -2
  198. package/dist/modules/orders/schema.d.ts +17 -0
  199. package/dist/modules/orders/schema.d.ts.map +1 -1
  200. package/dist/modules/orders/schema.js +5 -3
  201. package/dist/modules/orders/service.d.ts +16 -7
  202. package/dist/modules/orders/service.d.ts.map +1 -1
  203. package/dist/modules/orders/service.js +184 -55
  204. package/dist/modules/pricing/module.d.ts.map +1 -1
  205. package/dist/modules/pricing/module.js +1 -0
  206. package/dist/modules/pricing/schema.d.ts +17 -0
  207. package/dist/modules/pricing/schema.d.ts.map +1 -1
  208. package/dist/modules/pricing/schema.js +1 -0
  209. package/dist/modules/pricing/schemas.d.ts +1 -0
  210. package/dist/modules/pricing/schemas.d.ts.map +1 -1
  211. package/dist/modules/pricing/schemas.js +1 -0
  212. package/dist/modules/pricing/service.d.ts +2 -0
  213. package/dist/modules/pricing/service.d.ts.map +1 -1
  214. package/dist/modules/pricing/service.js +18 -12
  215. package/dist/modules/promotions/module.d.ts.map +1 -1
  216. package/dist/modules/promotions/module.js +1 -0
  217. package/dist/modules/promotions/service.d.ts +3 -0
  218. package/dist/modules/promotions/service.d.ts.map +1 -1
  219. package/dist/modules/promotions/service.js +19 -11
  220. package/dist/modules/search/adapter.d.ts +8 -0
  221. package/dist/modules/search/adapter.d.ts.map +1 -1
  222. package/dist/modules/search/module.d.ts.map +1 -1
  223. package/dist/modules/search/module.js +3 -0
  224. package/dist/modules/search/service.d.ts +5 -0
  225. package/dist/modules/search/service.d.ts.map +1 -1
  226. package/dist/modules/search/service.js +131 -11
  227. package/dist/modules/settings/module.d.ts.map +1 -1
  228. package/dist/modules/settings/module.js +1 -0
  229. package/dist/modules/settings/service.d.ts +3 -0
  230. package/dist/modules/settings/service.d.ts.map +1 -1
  231. package/dist/modules/settings/service.js +6 -4
  232. package/dist/modules/shipping/service.js +9 -9
  233. package/dist/modules/tax/module.d.ts.map +1 -1
  234. package/dist/modules/tax/module.js +1 -0
  235. package/dist/modules/tax/service.d.ts +3 -0
  236. package/dist/modules/tax/service.d.ts.map +1 -1
  237. package/dist/modules/tax/service.js +11 -9
  238. package/dist/modules/webhooks/hook.js +2 -2
  239. package/dist/modules/webhooks/module.d.ts.map +1 -1
  240. package/dist/modules/webhooks/module.js +1 -0
  241. package/dist/modules/webhooks/service.d.ts +2 -0
  242. package/dist/modules/webhooks/service.d.ts.map +1 -1
  243. package/dist/modules/webhooks/service.js +4 -4
  244. package/dist/runtime/kernel-modules.d.ts +73 -0
  245. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  246. package/dist/runtime/server.d.ts.map +1 -1
  247. package/dist/runtime/server.js +10 -2
  248. package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
  249. package/dist/test-utils/create-pglite-adapter.js +35 -13
  250. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  251. package/dist/test-utils/create-test-config.js +4 -0
  252. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  253. package/dist/test-utils/rest-api-test-utils.js +2 -0
  254. package/dist/test-utils/test-actors.js +2 -2
  255. package/dist/utils/id.d.ts +2 -0
  256. package/dist/utils/id.d.ts.map +1 -1
  257. package/dist/utils/id.js +11 -0
  258. package/package.json +6 -5
  259. package/src/auth/auth-schema-guard.ts +143 -0
  260. package/src/auth/auth-schema.ts +50 -19
  261. package/src/auth/middleware.ts +17 -5
  262. package/src/auth/org.ts +15 -3
  263. package/src/auth/organization-guard.ts +158 -0
  264. package/src/auth/permissions.ts +31 -8
  265. package/src/auth/role-authority.ts +107 -0
  266. package/src/auth/setup.ts +17 -0
  267. package/src/auth/strict-org-resolution.ts +7 -1
  268. package/src/auth/types.ts +1 -1
  269. package/src/config/defaults.ts +2 -0
  270. package/src/config/types.ts +32 -4
  271. package/src/hooks/checkout.ts +19 -12
  272. package/src/index.ts +30 -6
  273. package/src/interfaces/rest/audit-middleware.ts +2 -0
  274. package/src/interfaces/rest/customer-portal.ts +31 -24
  275. package/src/interfaces/rest/index.ts +4 -0
  276. package/src/interfaces/rest/route-coverage.ts +103 -0
  277. package/src/interfaces/rest/router.ts +16 -10
  278. package/src/interfaces/rest/routes/admin/compensation-failures.ts +8 -5
  279. package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +37 -0
  280. package/src/interfaces/rest/routes/admin/entity-field-definitions.ts +89 -0
  281. package/src/interfaces/rest/routes/admin/permissions.ts +3 -1
  282. package/src/interfaces/rest/routes/admin/staff.ts +161 -72
  283. package/src/interfaces/rest/routes/admin-jobs.ts +3 -3
  284. package/src/interfaces/rest/routes/audit.ts +3 -3
  285. package/src/interfaces/rest/routes/carts.ts +17 -7
  286. package/src/interfaces/rest/routes/catalog.ts +96 -0
  287. package/src/interfaces/rest/routes/checkout.ts +41 -38
  288. package/src/interfaces/rest/routes/documents.ts +15 -1
  289. package/src/interfaces/rest/routes/inventory.ts +2 -0
  290. package/src/interfaces/rest/routes/media.ts +8 -3
  291. package/src/interfaces/rest/routes/orders.ts +27 -8
  292. package/src/interfaces/rest/routes/pricing.ts +2 -2
  293. package/src/interfaces/rest/routes/promotions.ts +12 -9
  294. package/src/interfaces/rest/routes/search.ts +19 -1
  295. package/src/interfaces/rest/schemas/admin-custom-field-proposals.ts +29 -0
  296. package/src/interfaces/rest/schemas/admin-entity-field-definitions.ts +120 -0
  297. package/src/interfaces/rest/schemas/catalog.ts +92 -0
  298. package/src/interfaces/rest/schemas/search.ts +2 -0
  299. package/src/interfaces/rest/utils.ts +61 -2
  300. package/src/kernel/compensation/executor.ts +2 -2
  301. package/src/kernel/database/tx-context.ts +25 -0
  302. package/src/kernel/hooks/create-context.ts +3 -0
  303. package/src/kernel/hooks/types.ts +2 -0
  304. package/src/kernel/plugin/manifest.ts +9 -5
  305. package/src/modules/analytics/service.ts +2 -2
  306. package/src/modules/audit/service.ts +3 -3
  307. package/src/modules/cart/repository/index.ts +22 -8
  308. package/src/modules/cart/schema.ts +4 -0
  309. package/src/modules/cart/service.ts +62 -27
  310. package/src/modules/catalog/brand-service.ts +7 -7
  311. package/src/modules/catalog/category-service.ts +7 -7
  312. package/src/modules/catalog/entity-service.ts +188 -53
  313. package/src/modules/catalog/module.ts +15 -0
  314. package/src/modules/catalog/ownership.ts +17 -0
  315. package/src/modules/catalog/read-policy.ts +16 -0
  316. package/src/modules/catalog/repository/index.ts +566 -4
  317. package/src/modules/catalog/revision.ts +35 -0
  318. package/src/modules/catalog/schema.ts +169 -1
  319. package/src/modules/catalog/schemas.ts +2 -0
  320. package/src/modules/catalog/service.ts +766 -26
  321. package/src/modules/channels/adapter.ts +107 -1
  322. package/src/modules/customers/module.ts +2 -0
  323. package/src/modules/customers/service.ts +23 -22
  324. package/src/modules/documents/module.ts +2 -0
  325. package/src/modules/documents/service.ts +74 -11
  326. package/src/modules/fulfillment/module.ts +2 -0
  327. package/src/modules/fulfillment/service.ts +17 -6
  328. package/src/modules/inventory/service.ts +15 -24
  329. package/src/modules/media/module.ts +2 -0
  330. package/src/modules/media/schema.ts +23 -2
  331. package/src/modules/media/service.ts +89 -28
  332. package/src/modules/orders/guest-access.ts +55 -0
  333. package/src/modules/orders/module.ts +1 -0
  334. package/src/modules/orders/quote.ts +1 -0
  335. package/src/modules/orders/repository/index.ts +2 -0
  336. package/src/modules/orders/schema.ts +5 -3
  337. package/src/modules/orders/service.ts +302 -52
  338. package/src/modules/pricing/module.ts +2 -0
  339. package/src/modules/pricing/schema.ts +1 -0
  340. package/src/modules/pricing/schemas.ts +1 -0
  341. package/src/modules/pricing/service.ts +21 -18
  342. package/src/modules/promotions/module.ts +2 -0
  343. package/src/modules/promotions/service.ts +25 -16
  344. package/src/modules/search/adapter.ts +8 -0
  345. package/src/modules/search/module.ts +3 -0
  346. package/src/modules/search/service.ts +142 -12
  347. package/src/modules/settings/module.ts +2 -0
  348. package/src/modules/settings/service.ts +8 -4
  349. package/src/modules/shipping/service.ts +9 -9
  350. package/src/modules/tax/module.ts +1 -0
  351. package/src/modules/tax/service.ts +13 -9
  352. package/src/modules/webhooks/hook.ts +2 -2
  353. package/src/modules/webhooks/module.ts +2 -0
  354. package/src/modules/webhooks/service.ts +6 -4
  355. package/src/runtime/server.ts +16 -2
  356. package/src/test-utils/create-pglite-adapter.ts +34 -12
  357. package/src/test-utils/create-test-config.ts +10 -6
  358. package/src/test-utils/rest-api-test-utils.ts +2 -0
  359. package/src/test-utils/test-actors.ts +2 -2
  360. package/src/utils/id.ts +20 -0
@@ -4,7 +4,7 @@ import type { Kernel } from "../../../../runtime/kernel.js";
4
4
  import type { CommerceConfig } from "../../../../config/types.js";
5
5
  import type { DrizzleDatabase } from "../../../../kernel/database/drizzle-db.js";
6
6
  import { member, user, invitation } from "../../../../auth/auth-schema.js";
7
- import { resolveOrgId } from "../../../../auth/org.js";
7
+ import { resolveOrgIdForCommerce } from "../../../../auth/org.js";
8
8
  import { makeId } from "../../../../utils/id.js";
9
9
  import {
10
10
  listStaffRoute,
@@ -15,6 +15,15 @@ import {
15
15
  updateStaffRoleRoute,
16
16
  revokeStaffRoute,
17
17
  } from "../../schemas/admin-staff.js";
18
+ import type { Actor } from "../../../../auth/types.js";
19
+ import {
20
+ canGrantRole,
21
+ granterForRole,
22
+ isCompositeRole,
23
+ isOwnerRole,
24
+ roleRank,
25
+ validRoles,
26
+ } from "../../../../auth/role-authority.js";
18
27
  import { type AppEnv, requirePerm } from "../../utils.js";
19
28
 
20
29
  /**
@@ -34,39 +43,28 @@ export function adminStaffRoutes(kernel: Kernel) {
34
43
  router.use("/staff/invitations", requirePerm("staff:manage"));
35
44
  router.use("/staff/roles", requirePerm("staff:manage"));
36
45
 
37
- function validRoles(): Set<string> {
38
- return new Set([
39
- ...Object.keys(config.auth?.roles ?? {}),
40
- "owner",
41
- "admin",
42
- "member",
43
- ]);
44
- }
45
-
46
46
  function invalidRole(c: { json: (d: unknown, s: number) => unknown }, role: string) {
47
47
  return c.json(
48
48
  {
49
49
  error: {
50
50
  code: "VALIDATION_FAILED",
51
- message: `Unknown role "${role}". Valid roles: ${[...validRoles()].join(", ")}.`,
51
+ message: `Unknown role "${role}". Valid roles: ${[...validRoles(config)].join(", ")}.`,
52
52
  },
53
53
  },
54
54
  422,
55
55
  );
56
56
  }
57
57
 
58
- const BUILTIN_ROLE_RANK: Record<string, number> = {
59
- owner: 3,
60
- admin: 2,
61
- };
62
- const CUSTOM_ROLE_RANK = 1;
63
-
64
- function roleRank(role: string): number {
65
- return BUILTIN_ROLE_RANK[role] ?? CUSTOM_ROLE_RANK;
58
+ function rankOf(role: string | null | undefined): number {
59
+ return roleRank(config, role);
66
60
  }
67
61
 
68
- function canGrantRole(actorRole: string, targetRole: string): boolean {
69
- return roleRank(actorRole) >= roleRank(targetRole);
62
+ function actorCanGrantRole(actor: Actor | null, targetRole: string): boolean {
63
+ return canGrantRole(
64
+ config,
65
+ actor ? { role: actor.role, permissions: actor.permissions } : null,
66
+ targetRole,
67
+ );
70
68
  }
71
69
 
72
70
  function insufficientPrivilege(
@@ -84,16 +82,81 @@ export function adminStaffRoutes(kernel: Kernel) {
84
82
  );
85
83
  }
86
84
 
87
- async function countOwners(orgId: string): Promise<number> {
88
- const rows = await db
89
- .select({ id: member.id })
90
- .from(member)
91
- .where(and(eq(member.organizationId, orgId), eq(member.role, "owner")));
92
- return rows.length;
85
+ /**
86
+ * Runs a membership mutation with the organization's member rows locked, so
87
+ * the last-owner check and the write it guards cannot be interleaved.
88
+ *
89
+ * Counting owners and then mutating as two statements let two owners revoke
90
+ * each other concurrently: both read `count = 2`, both proceed, and the
91
+ * organization is left with none. The lock makes the second transaction
92
+ * re-read after the first commits, so it sees the count it must respect.
93
+ *
94
+ * The whole member set is locked, not just the owner rows, because an owner
95
+ * is any member whose role string contains `owner` — a predicate SQL cannot
96
+ * express as cheaply as the JS filter below. Membership writes are rare
97
+ * administrative actions, so serialising them per organization is free.
98
+ * `ORDER BY id` fixes the lock order and keeps two concurrent revocations
99
+ * from deadlocking each other.
100
+ */
101
+ async function withMembershipLock<T>(
102
+ orgId: string,
103
+ run: (tx: DrizzleDatabase, ownerIds: string[]) => Promise<T>,
104
+ ): Promise<T> {
105
+ return kernel.database.transaction(async (raw) => {
106
+ const tx = raw as DrizzleDatabase;
107
+ const locked = await tx
108
+ .select({ id: member.id, role: member.role })
109
+ .from(member)
110
+ .where(eq(member.organizationId, orgId))
111
+ .orderBy(member.id)
112
+ .for("update");
113
+ const ownerIds = locked
114
+ .filter((row) => isOwnerRole(row.role))
115
+ .map((row) => row.id);
116
+ return run(tx, ownerIds);
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Cancels the member's pending invitations that their role can no longer
122
+ * grant. Acceptance re-checks the inviter's authority too, so this is not
123
+ * what closes the takeover — it keeps the invitation list honest and stops a
124
+ * dead grant sitting there for the rest of its seven-day life.
125
+ *
126
+ * `newRole` is null when the membership is being revoked outright.
127
+ */
128
+ async function cancelUngrantableInvitations(
129
+ orgId: string,
130
+ inviterUserId: string,
131
+ newRole: string | null,
132
+ ): Promise<void> {
133
+ const pending = await db
134
+ .select({ id: invitation.id, role: invitation.role })
135
+ .from(invitation)
136
+ .where(
137
+ and(
138
+ eq(invitation.organizationId, orgId),
139
+ eq(invitation.inviterId, inviterUserId),
140
+ eq(invitation.status, "pending"),
141
+ ),
142
+ );
143
+
144
+ const dead = pending.filter(
145
+ (row) =>
146
+ newRole === null ||
147
+ !canGrantRole(config, granterForRole(config, newRole), row.role ?? "member"),
148
+ );
149
+
150
+ for (const row of dead) {
151
+ await db
152
+ .update(invitation)
153
+ .set({ status: "canceled" })
154
+ .where(eq(invitation.id, row.id));
155
+ }
93
156
  }
94
157
 
95
158
  router.openapi(listStaffRoute, async (c) => {
96
- const orgId = resolveOrgId(c.get("actor"));
159
+ const orgId = resolveOrgIdForCommerce(c.get("actor"), config);
97
160
  const rows = await db
98
161
  .select({
99
162
  id: member.id,
@@ -113,10 +176,10 @@ export function adminStaffRoutes(kernel: Kernel) {
113
176
  router.openapi(createStaffRoute, async (c) => {
114
177
  const body = c.req.valid("json") as { userId: string; role: string };
115
178
  const actor = c.get("actor");
116
- const orgId = resolveOrgId(actor);
179
+ const orgId = resolveOrgIdForCommerce(actor, config);
117
180
 
118
- if (!validRoles().has(body.role)) return invalidRole(c, body.role);
119
- if (!canGrantRole(actor!.role, body.role)) return insufficientPrivilege(c, body.role);
181
+ if (isCompositeRole(body.role) || !validRoles(config).has(body.role)) return invalidRole(c, body.role);
182
+ if (!actorCanGrantRole(actor, body.role)) return insufficientPrivilege(c, body.role);
120
183
 
121
184
  const users = await db.select().from(user).where(eq(user.id, body.userId));
122
185
  if (users.length === 0) {
@@ -151,10 +214,10 @@ export function adminStaffRoutes(kernel: Kernel) {
151
214
  router.openapi(inviteStaffRoute, async (c) => {
152
215
  const body = c.req.valid("json") as { email: string; role: string };
153
216
  const actor = c.get("actor");
154
- const orgId = resolveOrgId(actor);
155
-
156
- if (!validRoles().has(body.role)) return invalidRole(c, body.role);
217
+ const orgId = resolveOrgIdForCommerce(actor, config);
157
218
 
219
+ if (isCompositeRole(body.role) || !validRoles(config).has(body.role)) return invalidRole(c, body.role);
220
+ if (!actorCanGrantRole(actor, body.role)) return insufficientPrivilege(c, body.role);
158
221
  const rows = await db
159
222
  .insert(invitation)
160
223
  .values({
@@ -164,14 +227,14 @@ export function adminStaffRoutes(kernel: Kernel) {
164
227
  role: body.role,
165
228
  status: "pending",
166
229
  expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
167
- inviterId: actor!.userId,
230
+ inviterId: actor?.userId ?? "system",
168
231
  })
169
232
  .returning();
170
233
  return c.json({ data: rows[0] }, 201);
171
234
  });
172
235
 
173
236
  router.openapi(listStaffInvitationsRoute, async (c) => {
174
- const orgId = resolveOrgId(c.get("actor"));
237
+ const orgId = resolveOrgIdForCommerce(c.get("actor"), config);
175
238
  const rows = await db
176
239
  .select()
177
240
  .from(invitation)
@@ -192,67 +255,93 @@ export function adminStaffRoutes(kernel: Kernel) {
192
255
  router.openapi(updateStaffRoleRoute, async (c) => {
193
256
  const body = c.req.valid("json") as { role: string };
194
257
  const actor = c.get("actor");
195
- const orgId = resolveOrgId(actor);
258
+ const orgId = resolveOrgIdForCommerce(actor, config);
196
259
  const id = c.req.param("id");
197
260
 
198
- if (!validRoles().has(body.role)) return invalidRole(c, body.role);
199
- if (!canGrantRole(actor!.role, body.role)) return insufficientPrivilege(c, body.role);
200
-
201
- const rows = await db
202
- .select()
203
- .from(member)
204
- .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
205
- const target = rows[0];
206
- if (!target) {
261
+ if (isCompositeRole(body.role) || !validRoles(config).has(body.role)) return invalidRole(c, body.role);
262
+ if (!actorCanGrantRole(actor, body.role)) return insufficientPrivilege(c, body.role);
263
+
264
+ const outcome = await withMembershipLock(orgId, async (tx, ownerIds) => {
265
+ const rows = await tx
266
+ .select()
267
+ .from(member)
268
+ .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
269
+ const target = rows[0];
270
+ if (!target) return { kind: "not-found" } as const;
271
+
272
+ // SEC-18/R-02: the actor must also outrank (or equal) the target's CURRENT
273
+ // role, else an admin could demote an owner they do not outrank.
274
+ if (rankOf(actor!.role) < rankOf(target.role)) {
275
+ return { kind: "outranked", role: target.role } as const;
276
+ }
277
+
278
+ if (isOwnerRole(target.role) && !isOwnerRole(body.role) && ownerIds.length <= 1) {
279
+ return { kind: "last-owner" } as const;
280
+ }
281
+
282
+ const updated = await tx
283
+ .update(member)
284
+ .set({ role: body.role })
285
+ .where(eq(member.id, id))
286
+ .returning();
287
+ return { kind: "updated", target, row: updated[0] } as const;
288
+ });
289
+
290
+ if (outcome.kind === "not-found") {
207
291
  return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
208
292
  }
209
-
210
- // SEC-18/R-02: the actor must also outrank (or equal) the target's CURRENT
211
- // role, else an admin could demote an owner they do not outrank.
212
- if (!canGrantRole(actor!.role, target.role)) return insufficientPrivilege(c, target.role);
213
-
214
- if (target.role === "owner" && body.role !== "owner" && (await countOwners(orgId)) <= 1) {
293
+ if (outcome.kind === "outranked") return insufficientPrivilege(c, outcome.role);
294
+ if (outcome.kind === "last-owner") {
215
295
  return c.json(
216
296
  { error: { code: "VALIDATION_FAILED", message: "Cannot demote the organization's last owner." } },
217
297
  422,
218
298
  );
219
299
  }
220
300
 
221
- const updated = await db
222
- .update(member)
223
- .set({ role: body.role })
224
- .where(eq(member.id, id))
225
- .returning();
226
- return c.json({ data: updated[0] });
301
+ await cancelUngrantableInvitations(orgId, outcome.target.userId, body.role);
302
+ return c.json({ data: outcome.row });
227
303
  });
228
304
 
229
305
  // @ts-expect-error -- openapi handler union return type
230
306
  router.openapi(revokeStaffRoute, async (c) => {
231
307
  const actor = c.get("actor");
232
- const orgId = resolveOrgId(actor);
308
+ const orgId = resolveOrgIdForCommerce(actor, config);
233
309
  const id = c.req.param("id");
234
310
 
235
- const rows = await db
236
- .select()
237
- .from(member)
238
- .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
239
- const target = rows[0];
240
- if (!target) {
311
+ const outcome = await withMembershipLock(orgId, async (tx, ownerIds) => {
312
+ const rows = await tx
313
+ .select()
314
+ .from(member)
315
+ .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
316
+ const target = rows[0];
317
+ if (!target) return { kind: "not-found" } as const;
318
+
319
+ // SEC-18/R-02: the actor must outrank (or equal) the target's current role
320
+ // to revoke it — an admin cannot revoke an owner.
321
+ if (rankOf(actor!.role) < rankOf(target.role)) {
322
+ return { kind: "outranked", role: target.role } as const;
323
+ }
324
+
325
+ if (isOwnerRole(target.role) && ownerIds.length <= 1) {
326
+ return { kind: "last-owner" } as const;
327
+ }
328
+
329
+ await tx.delete(member).where(eq(member.id, id));
330
+ return { kind: "deleted", target } as const;
331
+ });
332
+
333
+ if (outcome.kind === "not-found") {
241
334
  return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
242
335
  }
243
-
244
- // SEC-18/R-02: the actor must outrank (or equal) the target's current role
245
- // to revoke it — an admin cannot revoke an owner.
246
- if (!canGrantRole(actor!.role, target.role)) return insufficientPrivilege(c, target.role);
247
-
248
- if (target.role === "owner" && (await countOwners(orgId)) <= 1) {
336
+ if (outcome.kind === "outranked") return insufficientPrivilege(c, outcome.role);
337
+ if (outcome.kind === "last-owner") {
249
338
  return c.json(
250
339
  { error: { code: "VALIDATION_FAILED", message: "Cannot revoke the organization's last owner." } },
251
340
  422,
252
341
  );
253
342
  }
254
343
 
255
- await db.delete(member).where(eq(member.id, id));
344
+ await cancelUngrantableInvitations(orgId, outcome.target.userId, null);
256
345
  return c.json({ data: { deleted: true } });
257
346
  });
258
347
 
@@ -5,7 +5,7 @@ import type { Kernel } from "../../../runtime/kernel.js";
5
5
  import { commerceJobs } from "../../../kernel/jobs/schema.js";
6
6
  import { listFailedJobsRoute, retryJobRoute } from "../schemas/admin-jobs.js";
7
7
  import { type AppEnv, requirePerm } from "../utils.js";
8
- import { resolveOrgId } from "../../../auth/org.js";
8
+ import { resolveOrgIdForCommerce } from "../../../auth/org.js";
9
9
 
10
10
  type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
11
11
 
@@ -17,7 +17,7 @@ export function adminJobRoutes(kernel: Kernel) {
17
17
 
18
18
  router.openapi(listFailedJobsRoute, async (c) => {
19
19
  const actor = c.get("actor");
20
- const orgId = resolveOrgId(actor);
20
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
21
21
  const limit = Math.min(Number(c.req.query("limit") ?? "50"), 100);
22
22
  const conditions = [eq(commerceJobs.status, "failed")];
23
23
  // Scope to org unless wildcard admin
@@ -37,7 +37,7 @@ export function adminJobRoutes(kernel: Kernel) {
37
37
  // @ts-expect-error -- openapi handler union return type
38
38
  router.openapi(retryJobRoute, async (c) => {
39
39
  const actor = c.get("actor");
40
- const orgId = resolveOrgId(actor);
40
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
41
41
  const id = c.req.param("id");
42
42
  const conditions = [eq(commerceJobs.id, id)];
43
43
  // Scope to org unless wildcard admin
@@ -2,7 +2,7 @@ import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { Kernel } from "../../../runtime/kernel.js";
3
3
  import { listAuditRoute, listEntityAuditRoute } from "../schemas/audit.js";
4
4
  import { type AppEnv, requirePerm } from "../utils.js";
5
- import { resolveOrgId } from "../../../auth/org.js";
5
+ import { resolveOrgIdForCommerce } from "../../../auth/org.js";
6
6
 
7
7
  export function auditRoutes(kernel: Kernel) {
8
8
  const router = new OpenAPIHono<AppEnv>();
@@ -15,7 +15,7 @@ export function auditRoutes(kernel: Kernel) {
15
15
 
16
16
  router.openapi(listAuditRoute, async (c) => {
17
17
  const actor = c.get("actor");
18
- const orgId = resolveOrgId(actor);
18
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
19
19
  const entries = await kernel.services.audit.list({
20
20
  organizationId: orgId,
21
21
  entityType: c.req.query("entityType"),
@@ -37,7 +37,7 @@ export function auditRoutes(kernel: Kernel) {
37
37
 
38
38
  router.openapi(listEntityAuditRoute, async (c) => {
39
39
  const actor = c.get("actor");
40
- const orgId = resolveOrgId(actor);
40
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
41
41
  const entries = await kernel.services.audit.listForEntity({
42
42
  organizationId: orgId,
43
43
  entityType: c.req.param("entityType"),
@@ -2,16 +2,22 @@ import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { Kernel } from "../../../runtime/kernel.js";
3
3
  import type { CreateCartInput, AddCartItemInput } from "../../../modules/cart/schemas.js";
4
4
  import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute, listCartsRoute, recoverCartRoute } from "../schemas/carts.js";
5
- import { type AppEnv, mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
5
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus, parsePagination, requireMethodPerm, requirePerm } from "../utils.js";
6
6
 
7
7
  export function cartRoutes(kernel: Kernel) {
8
8
  const router = new OpenAPIHono<AppEnv>();
9
9
 
10
+ router.use("/", requireMethodPerm(["GET"], "cart:manage"));
11
+ router.use("/:id/recover", requirePerm("cart:manage"));
12
+
10
13
  // @ts-expect-error -- openapi() enforces strict response typing but our handler
11
14
  // returns union responses (201 | 400 | 422). The route definition documents the
12
15
  // contract; the handler returns dynamic status.
13
16
  router.openapi(createCartRoute, async (c) => {
14
17
  const actor = c.get("actor");
18
+ if (!actor) {
19
+ return c.json({ error: { code: "NOT_FOUND", message: "Cart not found." } }, 404);
20
+ }
15
21
  const result = await kernel.services.cart.create(c.req.valid("json") as CreateCartInput, actor);
16
22
  if (!result.ok)
17
23
  return c.json(
@@ -74,17 +80,12 @@ export function cartRoutes(kernel: Kernel) {
74
80
  // @ts-expect-error -- openapi handler union return type
75
81
  router.openapi(getCartRoute, async (c) => {
76
82
  const actor = c.get("actor");
77
- // Guest carts are gated by the cart secret; clients pass it as either
78
- // ?secret=... or X-Cart-Secret. Customer carts ignore the secret and
79
- // require ownership instead.
80
- const secretParam = c.req.query("secret");
81
83
  const secretHeader = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret");
82
- const secret = secretParam ?? secretHeader ?? undefined;
83
84
  const result = await kernel.services.cart.getById(
84
85
  c.req.param("id"),
85
86
  actor,
86
87
  undefined,
87
- secret,
88
+ secretHeader ?? undefined,
88
89
  );
89
90
  if (!result.ok)
90
91
  return c.json(
@@ -98,9 +99,12 @@ export function cartRoutes(kernel: Kernel) {
98
99
  // returns union responses (201 | 400 | 422). The route definition documents the
99
100
  // contract; the handler returns dynamic status.
100
101
  router.openapi(addCartItemRoute, async (c) => {
102
+ const secret = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
101
103
  const result = await kernel.services.cart.addItem(
102
104
  { ...c.req.valid("json"), cartId: c.req.param("id") } as AddCartItemInput,
103
105
  c.get("actor"),
106
+ undefined,
107
+ secret,
104
108
  );
105
109
  if (!result.ok)
106
110
  return c.json(
@@ -115,6 +119,7 @@ export function cartRoutes(kernel: Kernel) {
115
119
  // contract; the handler returns dynamic status.
116
120
  router.openapi(updateCartItemQuantityRoute, async (c) => {
117
121
  const body = c.req.valid("json");
122
+ const secret = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
118
123
  const result = await kernel.services.cart.updateQuantity(
119
124
  {
120
125
  cartId: c.req.param("id"),
@@ -122,6 +127,8 @@ export function cartRoutes(kernel: Kernel) {
122
127
  quantity: body.quantity,
123
128
  },
124
129
  c.get("actor"),
130
+ undefined,
131
+ secret,
125
132
  );
126
133
  if (!result.ok)
127
134
  return c.json(
@@ -133,10 +140,13 @@ export function cartRoutes(kernel: Kernel) {
133
140
 
134
141
  // @ts-expect-error -- openapi handler union return type
135
142
  router.openapi(removeCartItemRoute, async (c) => {
143
+ const secret = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
136
144
  const result = await kernel.services.cart.removeItem(
137
145
  c.req.param("id"),
138
146
  c.req.param("itemId"),
139
147
  c.get("actor"),
148
+ undefined,
149
+ secret,
140
150
  );
141
151
  if (!result.ok)
142
152
  return c.json(
@@ -19,6 +19,8 @@ import {
19
19
  listEntitiesRoute,
20
20
  getEntityRoute,
21
21
  getEntityAttributesRoute,
22
+ listFieldOwnershipRoute,
23
+ setFieldOwnershipRoute,
22
24
  listCategoriesRoute,
23
25
  listBrandsRoute,
24
26
  deleteEntityRoute,
@@ -34,6 +36,8 @@ import {
34
36
  createEntityRoute,
35
37
  updateEntityRoute,
36
38
  setEntityAttributesRoute,
39
+ approveCustomFieldRoute,
40
+ rejectCustomFieldRoute,
37
41
  createCategoryRoute,
38
42
  archiveCategoryRoute,
39
43
  restoreCategoryRoute,
@@ -54,11 +58,67 @@ import {
54
58
  parseInclude,
55
59
  parsePagination,
56
60
  parseSort,
61
+ requirePerm,
57
62
  } from "../utils.js";
58
63
 
59
64
  export function catalogRoutes(kernel: Kernel) {
60
65
  const router = new OpenAPIHono<AppEnv>();
61
66
 
67
+ router.use("/entities/*", requirePerm("catalog:read"));
68
+ router.use("/categories", requirePerm("catalog:read"));
69
+ router.use("/brands", requirePerm("catalog:read"));
70
+
71
+ router.use("/entities/:id/custom-fields/:fieldName/approve", requirePerm("catalog:update"));
72
+ router.use("/entities/:id/custom-fields/:fieldName/reject", requirePerm("catalog:update"));
73
+ router.use("/categories/:categoryId", requirePerm("catalog:update"));
74
+ router.use("/categories/:categoryId/*", requirePerm("catalog:update"));
75
+ router.use("/brands/:brandId", requirePerm("catalog:update"));
76
+ router.use("/brands/:brandId/*", requirePerm("catalog:update"));
77
+ router.use("/options/:optionTypeId/values", requirePerm("catalog:update"));
78
+
79
+ // @ts-expect-error -- openapi handler union return type
80
+ router.openapi(listFieldOwnershipRoute, async (c) => {
81
+ const result = await kernel.services.catalog.listFieldOwnership(
82
+ c.req.param("id"),
83
+ c.get("actor"),
84
+ undefined,
85
+ c.req.query("storeId"),
86
+ );
87
+ if (!result.ok) {
88
+ return c.json(
89
+ mapErrorToResponse(result.error),
90
+ mapErrorToStatus(result.error),
91
+ );
92
+ }
93
+ return c.json({ data: result.value });
94
+ });
95
+
96
+ // @ts-expect-error -- openapi handler union return type
97
+ router.openapi(setFieldOwnershipRoute, async (c) => {
98
+ const body = c.req.valid("json") as {
99
+ fieldPath: string;
100
+ owner: "platform" | "store" | "shared";
101
+ storeId?: string | null;
102
+ variantId?: string | null;
103
+ };
104
+ const result = await kernel.services.catalog.setFieldOwner(
105
+ c.req.param("id"),
106
+ body.fieldPath,
107
+ body.storeId ?? null,
108
+ body.owner,
109
+ c.get("actor"),
110
+ undefined,
111
+ body.variantId ?? null,
112
+ );
113
+ if (!result.ok) {
114
+ return c.json(
115
+ mapErrorToResponse(result.error),
116
+ mapErrorToStatus(result.error),
117
+ );
118
+ }
119
+ return c.json({ data: { updated: true } });
120
+ });
121
+
62
122
  // @ts-expect-error -- openapi handler union return type
63
123
  router.openapi(listEntitiesRoute, async (c) => {
64
124
  try {
@@ -101,12 +161,14 @@ export function catalogRoutes(kernel: Kernel) {
101
161
  includeMedia: include.has("media"),
102
162
  includeInventory: include.has("inventory"),
103
163
  includePricing: include.has("pricing"),
164
+ includeCustomFields: include.has("customFields"),
104
165
  };
105
166
  const hydrated = await Promise.all(
106
167
  result.value.items.map(async (item) => {
107
168
  const full = await kernel.services.catalog.getById(
108
169
  item.id,
109
170
  includeOptions,
171
+ c.get("actor"),
110
172
  );
111
173
  return full.ok ? full.value : item;
112
174
  }),
@@ -143,6 +205,7 @@ export function catalogRoutes(kernel: Kernel) {
143
205
  includeMedia: include.has("media"),
144
206
  includeInventory: include.has("inventory"),
145
207
  includePricing: include.has("pricing"),
208
+ includeCustomFields: include.has("customFields"),
146
209
  };
147
210
 
148
211
  const actor = c.get("actor");
@@ -268,11 +331,44 @@ export function catalogRoutes(kernel: Kernel) {
268
331
  return c.json({ data: { updated: true } });
269
332
  });
270
333
 
334
+ // @ts-expect-error -- openapi handler union return type
335
+ router.openapi(approveCustomFieldRoute, async (c) => {
336
+ const result = await kernel.services.catalog.approveCustomField(
337
+ c.req.param("id"),
338
+ c.req.param("fieldName"),
339
+ c.req.query("locale") ?? "en",
340
+ c.get("actor"),
341
+ );
342
+ if (!result.ok)
343
+ return c.json(
344
+ mapErrorToResponse(result.error),
345
+ mapErrorToStatus(result.error),
346
+ );
347
+ return c.json({ data: result.value });
348
+ });
349
+
350
+ // @ts-expect-error -- openapi handler union return type
351
+ router.openapi(rejectCustomFieldRoute, async (c) => {
352
+ const result = await kernel.services.catalog.rejectCustomField(
353
+ c.req.param("id"),
354
+ c.req.param("fieldName"),
355
+ c.req.query("locale") ?? "en",
356
+ c.get("actor"),
357
+ );
358
+ if (!result.ok)
359
+ return c.json(
360
+ mapErrorToResponse(result.error),
361
+ mapErrorToStatus(result.error),
362
+ );
363
+ return c.json({ data: result.value });
364
+ });
365
+
271
366
  // @ts-expect-error -- openapi handler union return type
272
367
  router.openapi(getEntityAttributesRoute, async (c) => {
273
368
  const result = await kernel.services.catalog.getAttributes(
274
369
  c.req.param("id"),
275
370
  c.req.param("locale"),
371
+ c.get("actor"),
276
372
  );
277
373
  if (!result.ok)
278
374
  return c.json(