@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
@@ -1,9 +1,10 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import { eq, and } from "drizzle-orm";
3
3
  import { member, user, invitation } from "../../../../auth/auth-schema.js";
4
- import { resolveOrgId } from "../../../../auth/org.js";
4
+ import { resolveOrgIdForCommerce } from "../../../../auth/org.js";
5
5
  import { makeId } from "../../../../utils/id.js";
6
6
  import { listStaffRoute, createStaffRoute, inviteStaffRoute, listStaffInvitationsRoute, listStaffRolesRoute, updateStaffRoleRoute, revokeStaffRoute, } from "../../schemas/admin-staff.js";
7
+ import { canGrantRole, granterForRole, isCompositeRole, isOwnerRole, roleRank, validRoles, } from "../../../../auth/role-authority.js";
7
8
  import { requirePerm } from "../../utils.js";
8
9
  /**
9
10
  * Admin staff / RBAC surface (issue #46).
@@ -20,32 +21,19 @@ export function adminStaffRoutes(kernel) {
20
21
  router.use("/staff/:id", requirePerm("staff:manage"));
21
22
  router.use("/staff/invitations", requirePerm("staff:manage"));
22
23
  router.use("/staff/roles", requirePerm("staff:manage"));
23
- function validRoles() {
24
- return new Set([
25
- ...Object.keys(config.auth?.roles ?? {}),
26
- "owner",
27
- "admin",
28
- "member",
29
- ]);
30
- }
31
24
  function invalidRole(c, role) {
32
25
  return c.json({
33
26
  error: {
34
27
  code: "VALIDATION_FAILED",
35
- message: `Unknown role "${role}". Valid roles: ${[...validRoles()].join(", ")}.`,
28
+ message: `Unknown role "${role}". Valid roles: ${[...validRoles(config)].join(", ")}.`,
36
29
  },
37
30
  }, 422);
38
31
  }
39
- const BUILTIN_ROLE_RANK = {
40
- owner: 3,
41
- admin: 2,
42
- };
43
- const CUSTOM_ROLE_RANK = 1;
44
- function roleRank(role) {
45
- return BUILTIN_ROLE_RANK[role] ?? CUSTOM_ROLE_RANK;
32
+ function rankOf(role) {
33
+ return roleRank(config, role);
46
34
  }
47
- function canGrantRole(actorRole, targetRole) {
48
- return roleRank(actorRole) >= roleRank(targetRole);
35
+ function actorCanGrantRole(actor, targetRole) {
36
+ return canGrantRole(config, actor ? { role: actor.role, permissions: actor.permissions } : null, targetRole);
49
37
  }
50
38
  function insufficientPrivilege(c, targetRole) {
51
39
  return c.json({
@@ -55,15 +43,61 @@ export function adminStaffRoutes(kernel) {
55
43
  },
56
44
  }, 403);
57
45
  }
58
- async function countOwners(orgId) {
59
- const rows = await db
60
- .select({ id: member.id })
61
- .from(member)
62
- .where(and(eq(member.organizationId, orgId), eq(member.role, "owner")));
63
- return rows.length;
46
+ /**
47
+ * Runs a membership mutation with the organization's member rows locked, so
48
+ * the last-owner check and the write it guards cannot be interleaved.
49
+ *
50
+ * Counting owners and then mutating as two statements let two owners revoke
51
+ * each other concurrently: both read `count = 2`, both proceed, and the
52
+ * organization is left with none. The lock makes the second transaction
53
+ * re-read after the first commits, so it sees the count it must respect.
54
+ *
55
+ * The whole member set is locked, not just the owner rows, because an owner
56
+ * is any member whose role string contains `owner` — a predicate SQL cannot
57
+ * express as cheaply as the JS filter below. Membership writes are rare
58
+ * administrative actions, so serialising them per organization is free.
59
+ * `ORDER BY id` fixes the lock order and keeps two concurrent revocations
60
+ * from deadlocking each other.
61
+ */
62
+ async function withMembershipLock(orgId, run) {
63
+ return kernel.database.transaction(async (raw) => {
64
+ const tx = raw;
65
+ const locked = await tx
66
+ .select({ id: member.id, role: member.role })
67
+ .from(member)
68
+ .where(eq(member.organizationId, orgId))
69
+ .orderBy(member.id)
70
+ .for("update");
71
+ const ownerIds = locked
72
+ .filter((row) => isOwnerRole(row.role))
73
+ .map((row) => row.id);
74
+ return run(tx, ownerIds);
75
+ });
76
+ }
77
+ /**
78
+ * Cancels the member's pending invitations that their role can no longer
79
+ * grant. Acceptance re-checks the inviter's authority too, so this is not
80
+ * what closes the takeover — it keeps the invitation list honest and stops a
81
+ * dead grant sitting there for the rest of its seven-day life.
82
+ *
83
+ * `newRole` is null when the membership is being revoked outright.
84
+ */
85
+ async function cancelUngrantableInvitations(orgId, inviterUserId, newRole) {
86
+ const pending = await db
87
+ .select({ id: invitation.id, role: invitation.role })
88
+ .from(invitation)
89
+ .where(and(eq(invitation.organizationId, orgId), eq(invitation.inviterId, inviterUserId), eq(invitation.status, "pending")));
90
+ const dead = pending.filter((row) => newRole === null ||
91
+ !canGrantRole(config, granterForRole(config, newRole), row.role ?? "member"));
92
+ for (const row of dead) {
93
+ await db
94
+ .update(invitation)
95
+ .set({ status: "canceled" })
96
+ .where(eq(invitation.id, row.id));
97
+ }
64
98
  }
65
99
  router.openapi(listStaffRoute, async (c) => {
66
- const orgId = resolveOrgId(c.get("actor"));
100
+ const orgId = resolveOrgIdForCommerce(c.get("actor"), config);
67
101
  const rows = await db
68
102
  .select({
69
103
  id: member.id,
@@ -82,10 +116,10 @@ export function adminStaffRoutes(kernel) {
82
116
  router.openapi(createStaffRoute, async (c) => {
83
117
  const body = c.req.valid("json");
84
118
  const actor = c.get("actor");
85
- const orgId = resolveOrgId(actor);
86
- if (!validRoles().has(body.role))
119
+ const orgId = resolveOrgIdForCommerce(actor, config);
120
+ if (isCompositeRole(body.role) || !validRoles(config).has(body.role))
87
121
  return invalidRole(c, body.role);
88
- if (!canGrantRole(actor.role, body.role))
122
+ if (!actorCanGrantRole(actor, body.role))
89
123
  return insufficientPrivilege(c, body.role);
90
124
  const users = await db.select().from(user).where(eq(user.id, body.userId));
91
125
  if (users.length === 0) {
@@ -114,9 +148,11 @@ export function adminStaffRoutes(kernel) {
114
148
  router.openapi(inviteStaffRoute, async (c) => {
115
149
  const body = c.req.valid("json");
116
150
  const actor = c.get("actor");
117
- const orgId = resolveOrgId(actor);
118
- if (!validRoles().has(body.role))
151
+ const orgId = resolveOrgIdForCommerce(actor, config);
152
+ if (isCompositeRole(body.role) || !validRoles(config).has(body.role))
119
153
  return invalidRole(c, body.role);
154
+ if (!actorCanGrantRole(actor, body.role))
155
+ return insufficientPrivilege(c, body.role);
120
156
  const rows = await db
121
157
  .insert(invitation)
122
158
  .values({
@@ -126,13 +162,13 @@ export function adminStaffRoutes(kernel) {
126
162
  role: body.role,
127
163
  status: "pending",
128
164
  expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
129
- inviterId: actor.userId,
165
+ inviterId: actor?.userId ?? "system",
130
166
  })
131
167
  .returning();
132
168
  return c.json({ data: rows[0] }, 201);
133
169
  });
134
170
  router.openapi(listStaffInvitationsRoute, async (c) => {
135
- const orgId = resolveOrgId(c.get("actor"));
171
+ const orgId = resolveOrgIdForCommerce(c.get("actor"), config);
136
172
  const rows = await db
137
173
  .select()
138
174
  .from(invitation)
@@ -151,55 +187,79 @@ export function adminStaffRoutes(kernel) {
151
187
  router.openapi(updateStaffRoleRoute, async (c) => {
152
188
  const body = c.req.valid("json");
153
189
  const actor = c.get("actor");
154
- const orgId = resolveOrgId(actor);
190
+ const orgId = resolveOrgIdForCommerce(actor, config);
155
191
  const id = c.req.param("id");
156
- if (!validRoles().has(body.role))
192
+ if (isCompositeRole(body.role) || !validRoles(config).has(body.role))
157
193
  return invalidRole(c, body.role);
158
- if (!canGrantRole(actor.role, body.role))
194
+ if (!actorCanGrantRole(actor, body.role))
159
195
  return insufficientPrivilege(c, body.role);
160
- const rows = await db
161
- .select()
162
- .from(member)
163
- .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
164
- const target = rows[0];
165
- if (!target) {
196
+ const outcome = await withMembershipLock(orgId, async (tx, ownerIds) => {
197
+ const rows = await tx
198
+ .select()
199
+ .from(member)
200
+ .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
201
+ const target = rows[0];
202
+ if (!target)
203
+ return { kind: "not-found" };
204
+ // SEC-18/R-02: the actor must also outrank (or equal) the target's CURRENT
205
+ // role, else an admin could demote an owner they do not outrank.
206
+ if (rankOf(actor.role) < rankOf(target.role)) {
207
+ return { kind: "outranked", role: target.role };
208
+ }
209
+ if (isOwnerRole(target.role) && !isOwnerRole(body.role) && ownerIds.length <= 1) {
210
+ return { kind: "last-owner" };
211
+ }
212
+ const updated = await tx
213
+ .update(member)
214
+ .set({ role: body.role })
215
+ .where(eq(member.id, id))
216
+ .returning();
217
+ return { kind: "updated", target, row: updated[0] };
218
+ });
219
+ if (outcome.kind === "not-found") {
166
220
  return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
167
221
  }
168
- // SEC-18/R-02: the actor must also outrank (or equal) the target's CURRENT
169
- // role, else an admin could demote an owner they do not outrank.
170
- if (!canGrantRole(actor.role, target.role))
171
- return insufficientPrivilege(c, target.role);
172
- if (target.role === "owner" && body.role !== "owner" && (await countOwners(orgId)) <= 1) {
222
+ if (outcome.kind === "outranked")
223
+ return insufficientPrivilege(c, outcome.role);
224
+ if (outcome.kind === "last-owner") {
173
225
  return c.json({ error: { code: "VALIDATION_FAILED", message: "Cannot demote the organization's last owner." } }, 422);
174
226
  }
175
- const updated = await db
176
- .update(member)
177
- .set({ role: body.role })
178
- .where(eq(member.id, id))
179
- .returning();
180
- return c.json({ data: updated[0] });
227
+ await cancelUngrantableInvitations(orgId, outcome.target.userId, body.role);
228
+ return c.json({ data: outcome.row });
181
229
  });
182
230
  // @ts-expect-error -- openapi handler union return type
183
231
  router.openapi(revokeStaffRoute, async (c) => {
184
232
  const actor = c.get("actor");
185
- const orgId = resolveOrgId(actor);
233
+ const orgId = resolveOrgIdForCommerce(actor, config);
186
234
  const id = c.req.param("id");
187
- const rows = await db
188
- .select()
189
- .from(member)
190
- .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
191
- const target = rows[0];
192
- if (!target) {
235
+ const outcome = await withMembershipLock(orgId, async (tx, ownerIds) => {
236
+ const rows = await tx
237
+ .select()
238
+ .from(member)
239
+ .where(and(eq(member.organizationId, orgId), eq(member.id, id)));
240
+ const target = rows[0];
241
+ if (!target)
242
+ return { kind: "not-found" };
243
+ // SEC-18/R-02: the actor must outrank (or equal) the target's current role
244
+ // to revoke it — an admin cannot revoke an owner.
245
+ if (rankOf(actor.role) < rankOf(target.role)) {
246
+ return { kind: "outranked", role: target.role };
247
+ }
248
+ if (isOwnerRole(target.role) && ownerIds.length <= 1) {
249
+ return { kind: "last-owner" };
250
+ }
251
+ await tx.delete(member).where(eq(member.id, id));
252
+ return { kind: "deleted", target };
253
+ });
254
+ if (outcome.kind === "not-found") {
193
255
  return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
194
256
  }
195
- // SEC-18/R-02: the actor must outrank (or equal) the target's current role
196
- // to revoke it — an admin cannot revoke an owner.
197
- if (!canGrantRole(actor.role, target.role))
198
- return insufficientPrivilege(c, target.role);
199
- if (target.role === "owner" && (await countOwners(orgId)) <= 1) {
257
+ if (outcome.kind === "outranked")
258
+ return insufficientPrivilege(c, outcome.role);
259
+ if (outcome.kind === "last-owner") {
200
260
  return c.json({ error: { code: "VALIDATION_FAILED", message: "Cannot revoke the organization's last owner." } }, 422);
201
261
  }
202
- await db.delete(member).where(eq(member.id, id));
262
+ await cancelUngrantableInvitations(orgId, outcome.target.userId, null);
203
263
  return c.json({ data: { deleted: true } });
204
264
  });
205
265
  return router;
@@ -3,14 +3,14 @@ import { eq, and, desc } from "drizzle-orm";
3
3
  import { commerceJobs } from "../../../kernel/jobs/schema.js";
4
4
  import { listFailedJobsRoute, retryJobRoute } from "../schemas/admin-jobs.js";
5
5
  import { requirePerm } from "../utils.js";
6
- import { resolveOrgId } from "../../../auth/org.js";
6
+ import { resolveOrgIdForCommerce } from "../../../auth/org.js";
7
7
  export function adminJobRoutes(kernel) {
8
8
  const router = new OpenAPIHono();
9
9
  const db = kernel.database.db;
10
10
  router.use("/jobs/failed", requirePerm("jobs:admin"));
11
11
  router.openapi(listFailedJobsRoute, async (c) => {
12
12
  const actor = c.get("actor");
13
- const orgId = resolveOrgId(actor);
13
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
14
14
  const limit = Math.min(Number(c.req.query("limit") ?? "50"), 100);
15
15
  const conditions = [eq(commerceJobs.status, "failed")];
16
16
  // Scope to org unless wildcard admin
@@ -28,7 +28,7 @@ export function adminJobRoutes(kernel) {
28
28
  // @ts-expect-error -- openapi handler union return type
29
29
  router.openapi(retryJobRoute, async (c) => {
30
30
  const actor = c.get("actor");
31
- const orgId = resolveOrgId(actor);
31
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
32
32
  const id = c.req.param("id");
33
33
  const conditions = [eq(commerceJobs.id, id)];
34
34
  // Scope to org unless wildcard admin
@@ -1,7 +1,7 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import { listAuditRoute, listEntityAuditRoute } from "../schemas/audit.js";
3
3
  import { requirePerm } from "../utils.js";
4
- import { resolveOrgId } from "../../../auth/org.js";
4
+ import { resolveOrgIdForCommerce } from "../../../auth/org.js";
5
5
  export function auditRoutes(kernel) {
6
6
  const router = new OpenAPIHono();
7
7
  /**
@@ -11,7 +11,7 @@ export function auditRoutes(kernel) {
11
11
  router.use("/", requirePerm("audit:read"));
12
12
  router.openapi(listAuditRoute, async (c) => {
13
13
  const actor = c.get("actor");
14
- const orgId = resolveOrgId(actor);
14
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
15
15
  const entries = await kernel.services.audit.list({
16
16
  organizationId: orgId,
17
17
  entityType: c.req.query("entityType"),
@@ -31,7 +31,7 @@ export function auditRoutes(kernel) {
31
31
  router.use("/:entityType/:entityId", requirePerm("audit:read"));
32
32
  router.openapi(listEntityAuditRoute, async (c) => {
33
33
  const actor = c.get("actor");
34
- const orgId = resolveOrgId(actor);
34
+ const orgId = resolveOrgIdForCommerce(actor, kernel.config);
35
35
  const entries = await kernel.services.audit.listForEntity({
36
36
  organizationId: orgId,
37
37
  entityType: c.req.param("entityType"),
@@ -1 +1 @@
1
- {"version":3,"file":"carts.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/carts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGzD,OAAO,EAAE,KAAK,MAAM,EAAyD,MAAM,aAAa,CAAC;AAEjG,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,gCA+IxC"}
1
+ {"version":3,"file":"carts.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/carts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGzD,OAAO,EAAE,KAAK,MAAM,EAAyF,MAAM,aAAa,CAAC;AAEjI,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,gCAyJxC"}
@@ -1,13 +1,18 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute, listCartsRoute, recoverCartRoute } from "../schemas/carts.js";
3
- import { mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
3
+ import { mapErrorToResponse, mapErrorToStatus, parsePagination, requireMethodPerm, requirePerm } from "../utils.js";
4
4
  export function cartRoutes(kernel) {
5
5
  const router = new OpenAPIHono();
6
+ router.use("/", requireMethodPerm(["GET"], "cart:manage"));
7
+ router.use("/:id/recover", requirePerm("cart:manage"));
6
8
  // @ts-expect-error -- openapi() enforces strict response typing but our handler
7
9
  // returns union responses (201 | 400 | 422). The route definition documents the
8
10
  // contract; the handler returns dynamic status.
9
11
  router.openapi(createCartRoute, async (c) => {
10
12
  const actor = c.get("actor");
13
+ if (!actor) {
14
+ return c.json({ error: { code: "NOT_FOUND", message: "Cart not found." } }, 404);
15
+ }
11
16
  const result = await kernel.services.cart.create(c.req.valid("json"), actor);
12
17
  if (!result.ok)
13
18
  return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
@@ -53,13 +58,8 @@ export function cartRoutes(kernel) {
53
58
  // @ts-expect-error -- openapi handler union return type
54
59
  router.openapi(getCartRoute, async (c) => {
55
60
  const actor = c.get("actor");
56
- // Guest carts are gated by the cart secret; clients pass it as either
57
- // ?secret=... or X-Cart-Secret. Customer carts ignore the secret and
58
- // require ownership instead.
59
- const secretParam = c.req.query("secret");
60
61
  const secretHeader = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret");
61
- const secret = secretParam ?? secretHeader ?? undefined;
62
- const result = await kernel.services.cart.getById(c.req.param("id"), actor, undefined, secret);
62
+ const result = await kernel.services.cart.getById(c.req.param("id"), actor, undefined, secretHeader ?? undefined);
63
63
  if (!result.ok)
64
64
  return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
65
65
  return c.json({ data: result.value });
@@ -68,7 +68,8 @@ export function cartRoutes(kernel) {
68
68
  // returns union responses (201 | 400 | 422). The route definition documents the
69
69
  // contract; the handler returns dynamic status.
70
70
  router.openapi(addCartItemRoute, async (c) => {
71
- const result = await kernel.services.cart.addItem({ ...c.req.valid("json"), cartId: c.req.param("id") }, c.get("actor"));
71
+ const secret = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
72
+ const result = await kernel.services.cart.addItem({ ...c.req.valid("json"), cartId: c.req.param("id") }, c.get("actor"), undefined, secret);
72
73
  if (!result.ok)
73
74
  return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
74
75
  return c.json({ data: result.value }, 201);
@@ -78,18 +79,20 @@ export function cartRoutes(kernel) {
78
79
  // contract; the handler returns dynamic status.
79
80
  router.openapi(updateCartItemQuantityRoute, async (c) => {
80
81
  const body = c.req.valid("json");
82
+ const secret = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
81
83
  const result = await kernel.services.cart.updateQuantity({
82
84
  cartId: c.req.param("id"),
83
85
  itemId: c.req.param("itemId"),
84
86
  quantity: body.quantity,
85
- }, c.get("actor"));
87
+ }, c.get("actor"), undefined, secret);
86
88
  if (!result.ok)
87
89
  return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
88
90
  return c.json({ data: result.value });
89
91
  });
90
92
  // @ts-expect-error -- openapi handler union return type
91
93
  router.openapi(removeCartItemRoute, async (c) => {
92
- const result = await kernel.services.cart.removeItem(c.req.param("id"), c.req.param("itemId"), c.get("actor"));
94
+ const secret = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
95
+ const result = await kernel.services.cart.removeItem(c.req.param("id"), c.req.param("itemId"), c.get("actor"), undefined, secret);
93
96
  if (!result.ok)
94
97
  return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
95
98
  return c.json({ data: { deleted: true } });
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAgDzD,OAAO,EACL,KAAK,MAAM,EAMZ,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,gCAuf3C"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAoDzD,OAAO,EACL,KAAK,MAAM,EAOZ,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,gCAklB3C"}
@@ -1,8 +1,35 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
- import { listEntitiesRoute, getEntityRoute, getEntityAttributesRoute, listCategoriesRoute, listBrandsRoute, deleteEntityRoute, deleteCategoryRoute, deleteBrandRoute, removeEntityFromCategoryRoute, removeEntityFromBrandRoute, publishEntityRoute, archiveEntityRoute, discontinueEntityRoute, addEntityToCategoryRoute, addEntityToBrandRoute, createEntityRoute, updateEntityRoute, setEntityAttributesRoute, createCategoryRoute, archiveCategoryRoute, restoreCategoryRoute, updateCategoryRoute, createBrandRoute, updateBrandRoute, createOptionTypeRoute, createOptionValueRoute, createVariantRoute, generateVariantsRoute, quickVariantRoute, bulkVariantsRoute, } from "../schemas/catalog.js";
3
- import { mapErrorToResponse, mapErrorToStatus, parseInclude, parsePagination, parseSort, } from "../utils.js";
2
+ import { listEntitiesRoute, getEntityRoute, getEntityAttributesRoute, listFieldOwnershipRoute, setFieldOwnershipRoute, listCategoriesRoute, listBrandsRoute, deleteEntityRoute, deleteCategoryRoute, deleteBrandRoute, removeEntityFromCategoryRoute, removeEntityFromBrandRoute, publishEntityRoute, archiveEntityRoute, discontinueEntityRoute, addEntityToCategoryRoute, addEntityToBrandRoute, createEntityRoute, updateEntityRoute, setEntityAttributesRoute, approveCustomFieldRoute, rejectCustomFieldRoute, createCategoryRoute, archiveCategoryRoute, restoreCategoryRoute, updateCategoryRoute, createBrandRoute, updateBrandRoute, createOptionTypeRoute, createOptionValueRoute, createVariantRoute, generateVariantsRoute, quickVariantRoute, bulkVariantsRoute, } from "../schemas/catalog.js";
3
+ import { mapErrorToResponse, mapErrorToStatus, parseInclude, parsePagination, parseSort, requirePerm, } from "../utils.js";
4
4
  export function catalogRoutes(kernel) {
5
5
  const router = new OpenAPIHono();
6
+ router.use("/entities/*", requirePerm("catalog:read"));
7
+ router.use("/categories", requirePerm("catalog:read"));
8
+ router.use("/brands", requirePerm("catalog:read"));
9
+ router.use("/entities/:id/custom-fields/:fieldName/approve", requirePerm("catalog:update"));
10
+ router.use("/entities/:id/custom-fields/:fieldName/reject", requirePerm("catalog:update"));
11
+ router.use("/categories/:categoryId", requirePerm("catalog:update"));
12
+ router.use("/categories/:categoryId/*", requirePerm("catalog:update"));
13
+ router.use("/brands/:brandId", requirePerm("catalog:update"));
14
+ router.use("/brands/:brandId/*", requirePerm("catalog:update"));
15
+ router.use("/options/:optionTypeId/values", requirePerm("catalog:update"));
16
+ // @ts-expect-error -- openapi handler union return type
17
+ router.openapi(listFieldOwnershipRoute, async (c) => {
18
+ const result = await kernel.services.catalog.listFieldOwnership(c.req.param("id"), c.get("actor"), undefined, c.req.query("storeId"));
19
+ if (!result.ok) {
20
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
21
+ }
22
+ return c.json({ data: result.value });
23
+ });
24
+ // @ts-expect-error -- openapi handler union return type
25
+ router.openapi(setFieldOwnershipRoute, async (c) => {
26
+ const body = c.req.valid("json");
27
+ const result = await kernel.services.catalog.setFieldOwner(c.req.param("id"), body.fieldPath, body.storeId ?? null, body.owner, c.get("actor"), undefined, body.variantId ?? null);
28
+ if (!result.ok) {
29
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
30
+ }
31
+ return c.json({ data: { updated: true } });
32
+ });
6
33
  // @ts-expect-error -- openapi handler union return type
7
34
  router.openapi(listEntitiesRoute, async (c) => {
8
35
  try {
@@ -43,9 +70,10 @@ export function catalogRoutes(kernel) {
43
70
  includeMedia: include.has("media"),
44
71
  includeInventory: include.has("inventory"),
45
72
  includePricing: include.has("pricing"),
73
+ includeCustomFields: include.has("customFields"),
46
74
  };
47
75
  const hydrated = await Promise.all(result.value.items.map(async (item) => {
48
- const full = await kernel.services.catalog.getById(item.id, includeOptions);
76
+ const full = await kernel.services.catalog.getById(item.id, includeOptions, c.get("actor"));
49
77
  return full.ok ? full.value : item;
50
78
  }));
51
79
  withIncludes = hydrated;
@@ -77,6 +105,7 @@ export function catalogRoutes(kernel) {
77
105
  includeMedia: include.has("media"),
78
106
  includeInventory: include.has("inventory"),
79
107
  includePricing: include.has("pricing"),
108
+ includeCustomFields: include.has("customFields"),
80
109
  };
81
110
  const actor = c.get("actor");
82
111
  const result = isUUID
@@ -146,8 +175,22 @@ export function catalogRoutes(kernel) {
146
175
  return c.json({ data: { updated: true } });
147
176
  });
148
177
  // @ts-expect-error -- openapi handler union return type
178
+ router.openapi(approveCustomFieldRoute, async (c) => {
179
+ const result = await kernel.services.catalog.approveCustomField(c.req.param("id"), c.req.param("fieldName"), c.req.query("locale") ?? "en", c.get("actor"));
180
+ if (!result.ok)
181
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
182
+ return c.json({ data: result.value });
183
+ });
184
+ // @ts-expect-error -- openapi handler union return type
185
+ router.openapi(rejectCustomFieldRoute, async (c) => {
186
+ const result = await kernel.services.catalog.rejectCustomField(c.req.param("id"), c.req.param("fieldName"), c.req.query("locale") ?? "en", c.get("actor"));
187
+ if (!result.ok)
188
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
189
+ return c.json({ data: result.value });
190
+ });
191
+ // @ts-expect-error -- openapi handler union return type
149
192
  router.openapi(getEntityAttributesRoute, async (c) => {
150
- const result = await kernel.services.catalog.getAttributes(c.req.param("id"), c.req.param("locale"));
193
+ const result = await kernel.services.catalog.getAttributes(c.req.param("id"), c.req.param("locale"), c.get("actor"));
151
194
  if (!result.ok)
152
195
  return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
153
196
  return c.json({ data: result.value });
@@ -1 +1 @@
1
- {"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAoBzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAMhF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,EAC1C,KAAK,EAAE,KAAK,GAAG,IAAI,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmC7B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCAgV5C"}
1
+ {"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAoBzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAMhF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,EAC1C,KAAK,EAAE,KAAK,GAAG,IAAI,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmC7B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCAmV5C"}
@@ -6,8 +6,8 @@ import { createHookContext } from "../../../kernel/hooks/create-context.js";
6
6
  import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
7
7
  import { isCommerceError } from "../../../kernel/errors.js";
8
8
  import { assertPermission } from "../../../auth/permissions.js";
9
- import { resolveOrgId } from "../../../auth/org.js";
10
- import { makeDeterministicId, makeId } from "../../../utils/id.js";
9
+ import { resolveOrgIdForCommerce } from "../../../auth/org.js";
10
+ import { makeDeterministicId, makeId, makeIdempotencyScope } from "../../../utils/id.js";
11
11
  /**
12
12
  * SEC-07 — resolve the customer profile a checkout order is attributed to.
13
13
  * A self-service actor may ONLY attribute the order to its own customer
@@ -60,42 +60,32 @@ export function checkoutRoutes(kernel) {
60
60
  router.openapi(checkoutRoute, async (c) => {
61
61
  const body = c.req.valid("json");
62
62
  const actor = c.get("actor");
63
- // Idempotent replay: a re-submitted checkout (offline POS queue, network
64
- // retry) returns the already-created order BEFORE the pipeline runs — no
65
- // double payment authorization, no duplicate order.
63
+ const cartSecret = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
64
+ // Idempotent replay is authorized by the order service before payment runs.
65
+ // This preflight only preserves the no-double-payment fast path; the create
66
+ // primitive repeats the same binding for direct callers and race losers.
67
+ let replay;
66
68
  if (body.idempotencyKey) {
67
- const replay = await kernel.services.orders.getByIdempotencyKey(body.idempotencyKey, actor);
68
- if (replay.ok && replay.value) {
69
- // IDOR guard: an idempotency key must only replay the requester's OWN
70
- // order. Without this, a same-org customer who supplies/guesses another
71
- // customer's key would receive that customer's order (PII, totals, lines).
72
- let canActForOthers = false;
73
- try {
74
- assertPermission(actor, "customers:read");
75
- canActForOthers = true;
76
- }
77
- catch {
78
- canActForOthers = false;
79
- }
80
- const ownCustomer = actor
81
- ? await resolveCheckoutCustomerUuid(kernel.services.customers, actor, undefined)
82
- : undefined;
83
- const replayCustomer = replay.value.customerId ?? null;
84
- const ownsOrder = (replayCustomer !== null && replayCustomer === ownCustomer) ||
85
- (replayCustomer === null && !ownCustomer);
86
- if (canActForOthers || ownsOrder) {
87
- return c.json({ data: replay.value }, 201);
88
- }
89
- return c.json({
90
- error: {
91
- code: "IDEMPOTENCY_CONFLICT",
92
- message: "Idempotency key does not belong to this account.",
93
- },
94
- }, 409);
69
+ replay = await kernel.services.orders.getByIdempotencyKey(body.idempotencyKey, actor, undefined, cartSecret);
70
+ if (!replay.ok) {
71
+ return c.json(mapErrorToResponse(replay.error), mapErrorToStatus(replay.error));
95
72
  }
73
+ if (replay.value)
74
+ return c.json({ data: replay.value }, 201);
75
+ }
76
+ // Authenticate the cart once at the HTTP boundary. Guest checkout is
77
+ // authorized by the cart secret; customer checkout is authorized by the
78
+ // customer cart ownership check. The hook pipeline uses its explicit
79
+ // internal cart-read path instead of carrying this credential inward.
80
+ const cartAccess = await kernel.services.cart.getById(body.cartId, actor, undefined, cartSecret);
81
+ if (!cartAccess.ok) {
82
+ return c.json(mapErrorToResponse(cartAccess.error), mapErrorToStatus(cartAccess.error));
96
83
  }
84
+ const idempotencyScope = body.idempotencyKey
85
+ ? await makeIdempotencyScope(actor, cartSecret, body.cartId)
86
+ : undefined;
97
87
  const orderId = body.idempotencyKey
98
- ? await makeDeterministicId(`checkout:${resolveOrgId(actor)}:${body.idempotencyKey}`)
88
+ ? await makeDeterministicId(`checkout:${resolveOrgIdForCommerce(actor, kernel.config)}:${idempotencyScope}:${body.idempotencyKey}`)
99
89
  : makeId();
100
90
  const checkoutData = {
101
91
  checkoutId: makeId(),
@@ -166,6 +156,7 @@ export function checkoutRoutes(kernel) {
166
156
  context: { moduleName: "checkout" },
167
157
  origin: "rest",
168
158
  database: { db: kernel.database.db },
159
+ commerceConfig: kernel.config,
169
160
  });
170
161
  let pipelineStage = "validate-and-calculate";
171
162
  try {
@@ -237,6 +228,7 @@ export function checkoutRoutes(kernel) {
237
228
  pipelineStage = "create-order";
238
229
  const order = await kernel.services.orders.create(orderPayload, actor, undefined, {
239
230
  trustedPricing: true,
231
+ ...(cartSecret !== undefined ? { guestCredential: cartSecret } : {}),
240
232
  });
241
233
  if (!order.ok) {
242
234
  return c.json(mapErrorToResponse(order.error), mapErrorToStatus(order.error));
@@ -304,7 +296,10 @@ export function checkoutRoutes(kernel) {
304
296
  // Validation, payment authorization, or compensated completion may fail
305
297
  // after the cart's atomic active -> checking_out claim. Release only
306
298
  // that transient state so the shopper can correct the problem and retry.
307
- await kernel.services.cart.releaseCheckoutClaim(body.cartId).catch((releaseError) => {
299
+ // Releasing is scoped to this attempt's claim token: an attempt that lost
300
+ // the claim never held it, so its failure cannot reopen a cart another
301
+ // checkout is still working through.
302
+ await kernel.services.cart.releaseCheckoutClaim(body.cartId, checkoutData.checkoutId).catch((releaseError) => {
308
303
  console.error("[checkout] Failed to release cart claim:", {
309
304
  cartId: body.cartId,
310
305
  error: releaseError instanceof Error ? releaseError.message : String(releaseError),
@@ -1 +1 @@
1
- {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAOzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCA6C5C"}
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAOzD,OAAO,EAAE,KAAK,MAAM,EAAwD,MAAM,aAAa,CAAC;AAEhG;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCA2D5C"}