@porulle/core 0.7.0 → 0.9.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 (325) hide show
  1. package/dist/auth/middleware.d.ts.map +1 -1
  2. package/dist/auth/middleware.js +26 -7
  3. package/dist/auth/setup.d.ts.map +1 -1
  4. package/dist/auth/setup.js +8 -5
  5. package/dist/config/defaults.d.ts.map +1 -1
  6. package/dist/config/defaults.js +7 -0
  7. package/dist/config/types.d.ts +26 -1
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/generated/plugin-manifest.d.ts +3 -0
  10. package/dist/generated/plugin-manifest.d.ts.map +1 -1
  11. package/dist/generated/plugin-manifest.js +1 -0
  12. package/dist/hooks/checkout-completion.d.ts.map +1 -1
  13. package/dist/hooks/checkout-completion.js +2 -2
  14. package/dist/hooks/checkout.d.ts.map +1 -1
  15. package/dist/hooks/checkout.js +17 -2
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/interfaces/rest/index.d.ts.map +1 -1
  20. package/dist/interfaces/rest/index.js +12 -0
  21. package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
  22. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
  23. package/dist/interfaces/rest/routes/admin/staff.js +206 -0
  24. package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
  25. package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
  26. package/dist/interfaces/rest/routes/analytics.js +24 -0
  27. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  28. package/dist/interfaces/rest/routes/carts.js +39 -2
  29. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  30. package/dist/interfaces/rest/routes/catalog.js +17 -1
  31. package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
  32. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  33. package/dist/interfaces/rest/routes/checkout.js +97 -21
  34. package/dist/interfaces/rest/routes/documents.d.ts +10 -0
  35. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
  36. package/dist/interfaces/rest/routes/documents.js +40 -0
  37. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  38. package/dist/interfaces/rest/routes/inventory.js +13 -0
  39. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  40. package/dist/interfaces/rest/routes/orders.js +111 -1
  41. package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
  42. package/dist/interfaces/rest/routes/pricing.js +36 -1
  43. package/dist/interfaces/rest/routes/settings.d.ts +5 -0
  44. package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
  45. package/dist/interfaces/rest/routes/settings.js +50 -0
  46. package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
  47. package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
  48. package/dist/interfaces/rest/routes/shipping.js +68 -0
  49. package/dist/interfaces/rest/routes/tax.d.ts +5 -0
  50. package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
  51. package/dist/interfaces/rest/routes/tax.js +68 -0
  52. package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
  53. package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
  54. package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
  55. package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
  56. package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
  57. package/dist/interfaces/rest/schemas/analytics.js +33 -0
  58. package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
  59. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  60. package/dist/interfaces/rest/schemas/carts.js +38 -0
  61. package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
  62. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  63. package/dist/interfaces/rest/schemas/catalog.js +48 -0
  64. package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
  65. package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
  66. package/dist/interfaces/rest/schemas/checkout.js +4 -0
  67. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  68. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  69. package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
  70. package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
  71. package/dist/interfaces/rest/schemas/documents.js +65 -0
  72. package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
  73. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  74. package/dist/interfaces/rest/schemas/orders.js +234 -1
  75. package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
  76. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  77. package/dist/interfaces/rest/schemas/pricing.js +70 -0
  78. package/dist/interfaces/rest/schemas/responses.d.ts +190 -0
  79. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  80. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  81. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  82. package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
  83. package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
  84. package/dist/interfaces/rest/schemas/settings.js +79 -0
  85. package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
  86. package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
  87. package/dist/interfaces/rest/schemas/shipping.js +146 -0
  88. package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
  89. package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
  90. package/dist/interfaces/rest/schemas/tax.js +132 -0
  91. package/dist/kernel/database/schema.d.ts +4 -0
  92. package/dist/kernel/database/schema.d.ts.map +1 -1
  93. package/dist/kernel/database/schema.js +8 -0
  94. package/dist/kernel/database/scoped-db.d.ts +2 -1
  95. package/dist/kernel/database/scoped-db.d.ts.map +1 -1
  96. package/dist/kernel/database/scoped-db.js +62 -1
  97. package/dist/kernel/hooks/types.d.ts +1 -1
  98. package/dist/kernel/hooks/types.d.ts.map +1 -1
  99. package/dist/kernel/plugin/manifest.d.ts +26 -0
  100. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  101. package/dist/kernel/plugin/manifest.js +17 -2
  102. package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
  103. package/dist/modules/analytics/drizzle-adapter.js +16 -3
  104. package/dist/modules/analytics/module.d.ts.map +1 -1
  105. package/dist/modules/analytics/module.js +6 -0
  106. package/dist/modules/analytics/reports.d.ts +58 -0
  107. package/dist/modules/analytics/reports.d.ts.map +1 -0
  108. package/dist/modules/analytics/reports.js +249 -0
  109. package/dist/modules/analytics/service.d.ts +10 -0
  110. package/dist/modules/analytics/service.d.ts.map +1 -1
  111. package/dist/modules/analytics/service.js +14 -0
  112. package/dist/modules/cart/repository/index.d.ts +7 -0
  113. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  114. package/dist/modules/cart/repository/index.js +25 -1
  115. package/dist/modules/cart/schema.d.ts +17 -0
  116. package/dist/modules/cart/schema.d.ts.map +1 -1
  117. package/dist/modules/cart/schema.js +2 -0
  118. package/dist/modules/cart/schemas.d.ts +1 -0
  119. package/dist/modules/cart/schemas.d.ts.map +1 -1
  120. package/dist/modules/cart/schemas.js +1 -0
  121. package/dist/modules/cart/service.d.ts +32 -0
  122. package/dist/modules/cart/service.d.ts.map +1 -1
  123. package/dist/modules/cart/service.js +61 -2
  124. package/dist/modules/catalog/entity-service.d.ts +21 -0
  125. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  126. package/dist/modules/catalog/entity-service.js +166 -5
  127. package/dist/modules/catalog/schema.d.ts +34 -0
  128. package/dist/modules/catalog/schema.d.ts.map +1 -1
  129. package/dist/modules/catalog/schema.js +5 -0
  130. package/dist/modules/catalog/schemas.d.ts +3 -0
  131. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  132. package/dist/modules/catalog/schemas.js +3 -0
  133. package/dist/modules/catalog/service.d.ts +36 -0
  134. package/dist/modules/catalog/service.d.ts.map +1 -1
  135. package/dist/modules/catalog/service.js +6 -0
  136. package/dist/modules/customers/service.d.ts.map +1 -1
  137. package/dist/modules/customers/service.js +6 -0
  138. package/dist/modules/documents/module.d.ts +7 -0
  139. package/dist/modules/documents/module.d.ts.map +1 -0
  140. package/dist/modules/documents/module.js +12 -0
  141. package/dist/modules/documents/pdf.d.ts +25 -0
  142. package/dist/modules/documents/pdf.d.ts.map +1 -0
  143. package/dist/modules/documents/pdf.js +80 -0
  144. package/dist/modules/documents/render.d.ts +32 -0
  145. package/dist/modules/documents/render.d.ts.map +1 -0
  146. package/dist/modules/documents/render.js +190 -0
  147. package/dist/modules/documents/repository/index.d.ts +25 -0
  148. package/dist/modules/documents/repository/index.d.ts.map +1 -0
  149. package/dist/modules/documents/repository/index.js +47 -0
  150. package/dist/modules/documents/schema.d.ts +214 -0
  151. package/dist/modules/documents/schema.d.ts.map +1 -0
  152. package/dist/modules/documents/schema.js +42 -0
  153. package/dist/modules/documents/service.d.ts +51 -0
  154. package/dist/modules/documents/service.d.ts.map +1 -0
  155. package/dist/modules/documents/service.js +119 -0
  156. package/dist/modules/fulfillment/service.d.ts +18 -0
  157. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  158. package/dist/modules/fulfillment/service.js +85 -1
  159. package/dist/modules/orders/repository/index.d.ts +18 -1
  160. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  161. package/dist/modules/orders/repository/index.js +86 -1
  162. package/dist/modules/orders/schema.d.ts +391 -0
  163. package/dist/modules/orders/schema.d.ts.map +1 -1
  164. package/dist/modules/orders/schema.js +56 -1
  165. package/dist/modules/orders/service.d.ts +102 -3
  166. package/dist/modules/orders/service.d.ts.map +1 -1
  167. package/dist/modules/orders/service.js +653 -29
  168. package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
  169. package/dist/modules/orders/stale-order-cleanup.js +13 -1
  170. package/dist/modules/pricing/repository/index.d.ts +5 -0
  171. package/dist/modules/pricing/repository/index.d.ts.map +1 -1
  172. package/dist/modules/pricing/repository/index.js +19 -0
  173. package/dist/modules/pricing/service.d.ts +18 -0
  174. package/dist/modules/pricing/service.d.ts.map +1 -1
  175. package/dist/modules/pricing/service.js +43 -5
  176. package/dist/modules/settings/module.d.ts +6 -0
  177. package/dist/modules/settings/module.d.ts.map +1 -0
  178. package/dist/modules/settings/module.js +11 -0
  179. package/dist/modules/settings/repository/index.d.ts +16 -0
  180. package/dist/modules/settings/repository/index.d.ts.map +1 -0
  181. package/dist/modules/settings/repository/index.js +41 -0
  182. package/dist/modules/settings/schema.d.ts +121 -0
  183. package/dist/modules/settings/schema.d.ts.map +1 -0
  184. package/dist/modules/settings/schema.js +24 -0
  185. package/dist/modules/settings/service.d.ts +26 -0
  186. package/dist/modules/settings/service.d.ts.map +1 -0
  187. package/dist/modules/settings/service.js +51 -0
  188. package/dist/modules/shipping/calculator.d.ts +5 -0
  189. package/dist/modules/shipping/calculator.d.ts.map +1 -1
  190. package/dist/modules/shipping/calculator.js +10 -0
  191. package/dist/modules/shipping/module.d.ts +5 -1
  192. package/dist/modules/shipping/module.d.ts.map +1 -1
  193. package/dist/modules/shipping/module.js +4 -1
  194. package/dist/modules/shipping/repository/index.d.ts +27 -2
  195. package/dist/modules/shipping/repository/index.d.ts.map +1 -1
  196. package/dist/modules/shipping/repository/index.js +105 -1
  197. package/dist/modules/shipping/schema.d.ts +419 -0
  198. package/dist/modules/shipping/schema.d.ts.map +1 -0
  199. package/dist/modules/shipping/schema.js +56 -0
  200. package/dist/modules/shipping/service.d.ts +54 -0
  201. package/dist/modules/shipping/service.d.ts.map +1 -1
  202. package/dist/modules/shipping/service.js +161 -2
  203. package/dist/modules/tax/adapter.d.ts +13 -0
  204. package/dist/modules/tax/adapter.d.ts.map +1 -1
  205. package/dist/modules/tax/module.d.ts +5 -1
  206. package/dist/modules/tax/module.d.ts.map +1 -1
  207. package/dist/modules/tax/module.js +4 -1
  208. package/dist/modules/tax/repository/index.d.ts +38 -2
  209. package/dist/modules/tax/repository/index.d.ts.map +1 -1
  210. package/dist/modules/tax/repository/index.js +138 -1
  211. package/dist/modules/tax/schema.d.ts +355 -0
  212. package/dist/modules/tax/schema.d.ts.map +1 -0
  213. package/dist/modules/tax/schema.js +55 -0
  214. package/dist/modules/tax/service.d.ts +60 -1
  215. package/dist/modules/tax/service.d.ts.map +1 -1
  216. package/dist/modules/tax/service.js +227 -2
  217. package/dist/runtime/kernel-modules.d.ts +15 -2
  218. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  219. package/dist/runtime/kernel-modules.js +4 -0
  220. package/dist/runtime/kernel-types.d.ts +5 -1
  221. package/dist/runtime/kernel-types.d.ts.map +1 -1
  222. package/dist/runtime/kernel-types.js +2 -0
  223. package/dist/runtime/server.js +1 -1
  224. package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
  225. package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
  226. package/dist/test-utils/create-plugin-test-app.js +15 -3
  227. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  228. package/dist/test-utils/create-test-config.js +2 -0
  229. package/dist/test-utils/order-test-helpers.d.ts +12 -0
  230. package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
  231. package/dist/test-utils/order-test-helpers.js +18 -0
  232. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  233. package/dist/test-utils/rest-api-test-utils.js +7 -0
  234. package/dist/test-utils/test-actors.js +1 -1
  235. package/dist/testing.d.ts +1 -0
  236. package/dist/testing.d.ts.map +1 -1
  237. package/dist/testing.js +1 -0
  238. package/package.json +3 -3
  239. package/src/auth/middleware.ts +26 -7
  240. package/src/auth/setup.ts +11 -4
  241. package/src/config/defaults.ts +7 -0
  242. package/src/config/types.ts +26 -1
  243. package/src/generated/plugin-capabilities.d.ts +1 -0
  244. package/src/generated/plugin-manifest.ts +1 -0
  245. package/src/generated/plugin-repositories.d.ts +1 -0
  246. package/src/hooks/checkout-completion.ts +22 -16
  247. package/src/hooks/checkout.ts +36 -11
  248. package/src/index.ts +1 -0
  249. package/src/interfaces/rest/index.ts +12 -0
  250. package/src/interfaces/rest/routes/admin/staff.ts +260 -0
  251. package/src/interfaces/rest/routes/analytics.ts +33 -0
  252. package/src/interfaces/rest/routes/carts.ts +52 -2
  253. package/src/interfaces/rest/routes/catalog.ts +34 -0
  254. package/src/interfaces/rest/routes/checkout.ts +114 -27
  255. package/src/interfaces/rest/routes/documents.ts +61 -0
  256. package/src/interfaces/rest/routes/inventory.ts +14 -0
  257. package/src/interfaces/rest/routes/orders.ts +148 -1
  258. package/src/interfaces/rest/routes/pricing.ts +44 -1
  259. package/src/interfaces/rest/routes/settings.ts +61 -0
  260. package/src/interfaces/rest/routes/shipping.ts +92 -0
  261. package/src/interfaces/rest/routes/tax.ts +90 -0
  262. package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
  263. package/src/interfaces/rest/schemas/analytics.ts +39 -0
  264. package/src/interfaces/rest/schemas/carts.ts +40 -0
  265. package/src/interfaces/rest/schemas/catalog.ts +53 -0
  266. package/src/interfaces/rest/schemas/checkout.ts +4 -0
  267. package/src/interfaces/rest/schemas/documents.ts +72 -0
  268. package/src/interfaces/rest/schemas/orders.ts +258 -1
  269. package/src/interfaces/rest/schemas/pricing.ts +75 -0
  270. package/src/interfaces/rest/schemas/settings.ts +93 -0
  271. package/src/interfaces/rest/schemas/shipping.ts +164 -0
  272. package/src/interfaces/rest/schemas/tax.ts +148 -0
  273. package/src/kernel/database/schema.ts +12 -0
  274. package/src/kernel/database/scoped-db.ts +70 -1
  275. package/src/kernel/hooks/types.ts +1 -0
  276. package/src/kernel/plugin/manifest.ts +41 -2
  277. package/src/modules/analytics/drizzle-adapter.ts +19 -3
  278. package/src/modules/analytics/module.ts +10 -0
  279. package/src/modules/analytics/reports.ts +311 -0
  280. package/src/modules/analytics/service.ts +23 -0
  281. package/src/modules/cart/repository/index.ts +30 -1
  282. package/src/modules/cart/schema.ts +2 -0
  283. package/src/modules/cart/schemas.ts +1 -0
  284. package/src/modules/cart/service.ts +104 -2
  285. package/src/modules/catalog/entity-service.ts +196 -4
  286. package/src/modules/catalog/schema.ts +5 -0
  287. package/src/modules/catalog/schemas.ts +3 -0
  288. package/src/modules/catalog/service.ts +30 -0
  289. package/src/modules/customers/service.ts +6 -0
  290. package/src/modules/documents/module.ts +19 -0
  291. package/src/modules/documents/pdf.ts +101 -0
  292. package/src/modules/documents/render.ts +231 -0
  293. package/src/modules/documents/repository/index.ts +68 -0
  294. package/src/modules/documents/schema.ts +44 -0
  295. package/src/modules/documents/service.ts +176 -0
  296. package/src/modules/fulfillment/service.ts +139 -1
  297. package/src/modules/orders/repository/index.ts +127 -1
  298. package/src/modules/orders/schema.ts +58 -1
  299. package/src/modules/orders/service.ts +1041 -65
  300. package/src/modules/orders/stale-order-cleanup.ts +18 -4
  301. package/src/modules/pricing/repository/index.ts +34 -0
  302. package/src/modules/pricing/service.ts +76 -6
  303. package/src/modules/settings/module.ts +18 -0
  304. package/src/modules/settings/repository/index.ts +59 -0
  305. package/src/modules/settings/schema.ts +25 -0
  306. package/src/modules/settings/service.ts +82 -0
  307. package/src/modules/shipping/calculator.ts +27 -0
  308. package/src/modules/shipping/module.ts +6 -2
  309. package/src/modules/shipping/repository/index.ts +136 -2
  310. package/src/modules/shipping/schema.ts +58 -0
  311. package/src/modules/shipping/service.ts +260 -1
  312. package/src/modules/tax/adapter.ts +9 -0
  313. package/src/modules/tax/module.ts +6 -2
  314. package/src/modules/tax/repository/index.ts +169 -2
  315. package/src/modules/tax/schema.ts +57 -0
  316. package/src/modules/tax/service.ts +313 -2
  317. package/src/runtime/kernel-modules.ts +4 -0
  318. package/src/runtime/kernel-types.ts +6 -0
  319. package/src/runtime/server.ts +1 -1
  320. package/src/test-utils/create-plugin-test-app.ts +17 -4
  321. package/src/test-utils/create-test-config.ts +2 -0
  322. package/src/test-utils/order-test-helpers.ts +26 -0
  323. package/src/test-utils/rest-api-test-utils.ts +7 -0
  324. package/src/test-utils/test-actors.ts +1 -1
  325. package/src/testing.ts +1 -0
@@ -0,0 +1,206 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { eq, and } from "drizzle-orm";
3
+ import { member, user, invitation } from "../../../../auth/auth-schema.js";
4
+ import { resolveOrgId } from "../../../../auth/org.js";
5
+ import { makeId } from "../../../../utils/id.js";
6
+ import { listStaffRoute, createStaffRoute, inviteStaffRoute, listStaffInvitationsRoute, listStaffRolesRoute, updateStaffRoleRoute, revokeStaffRoute, } from "../../schemas/admin-staff.js";
7
+ import { requirePerm } from "../../utils.js";
8
+ /**
9
+ * Admin staff / RBAC surface (issue #46).
10
+ *
11
+ * Surfaces the Better Auth `member` table as first-class admin REST:
12
+ * list staff, add an existing user with a role, invite by email, change
13
+ * role, revoke — plus the role → permission mapping roles resolve to.
14
+ */
15
+ export function adminStaffRoutes(kernel) {
16
+ const router = new OpenAPIHono();
17
+ const db = kernel.database.db;
18
+ const config = kernel.config;
19
+ router.use("/staff", requirePerm("staff:manage"));
20
+ router.use("/staff/:id", requirePerm("staff:manage"));
21
+ router.use("/staff/invitations", requirePerm("staff:manage"));
22
+ 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
+ function invalidRole(c, role) {
32
+ return c.json({
33
+ error: {
34
+ code: "VALIDATION_FAILED",
35
+ message: `Unknown role "${role}". Valid roles: ${[...validRoles()].join(", ")}.`,
36
+ },
37
+ }, 422);
38
+ }
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;
46
+ }
47
+ function canGrantRole(actorRole, targetRole) {
48
+ return roleRank(actorRole) >= roleRank(targetRole);
49
+ }
50
+ function insufficientPrivilege(c, targetRole) {
51
+ return c.json({
52
+ error: {
53
+ code: "FORBIDDEN",
54
+ message: `Cannot assign role "${targetRole}": your role does not have sufficient privilege.`,
55
+ },
56
+ }, 403);
57
+ }
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;
64
+ }
65
+ router.openapi(listStaffRoute, async (c) => {
66
+ const orgId = resolveOrgId(c.get("actor"));
67
+ const rows = await db
68
+ .select({
69
+ id: member.id,
70
+ userId: member.userId,
71
+ role: member.role,
72
+ createdAt: member.createdAt,
73
+ email: user.email,
74
+ name: user.name,
75
+ })
76
+ .from(member)
77
+ .leftJoin(user, eq(member.userId, user.id))
78
+ .where(eq(member.organizationId, orgId));
79
+ return c.json({ data: rows });
80
+ });
81
+ // @ts-expect-error -- openapi handler union return type
82
+ router.openapi(createStaffRoute, async (c) => {
83
+ const body = c.req.valid("json");
84
+ const actor = c.get("actor");
85
+ const orgId = resolveOrgId(actor);
86
+ if (!validRoles().has(body.role))
87
+ return invalidRole(c, body.role);
88
+ if (!canGrantRole(actor.role, body.role))
89
+ return insufficientPrivilege(c, body.role);
90
+ const users = await db.select().from(user).where(eq(user.id, body.userId));
91
+ if (users.length === 0) {
92
+ return c.json({ error: { code: "NOT_FOUND", message: "User not found." } }, 404);
93
+ }
94
+ const existing = await db
95
+ .select({ id: member.id })
96
+ .from(member)
97
+ .where(and(eq(member.organizationId, orgId), eq(member.userId, body.userId)));
98
+ if (existing.length > 0) {
99
+ return c.json({ error: { code: "VALIDATION_FAILED", message: "User is already a member of this organization." } }, 422);
100
+ }
101
+ const rows = await db
102
+ .insert(member)
103
+ .values({
104
+ id: makeId(),
105
+ organizationId: orgId,
106
+ userId: body.userId,
107
+ role: body.role,
108
+ createdAt: new Date(),
109
+ })
110
+ .returning();
111
+ return c.json({ data: rows[0] }, 201);
112
+ });
113
+ // @ts-expect-error -- openapi handler union return type
114
+ router.openapi(inviteStaffRoute, async (c) => {
115
+ const body = c.req.valid("json");
116
+ const actor = c.get("actor");
117
+ const orgId = resolveOrgId(actor);
118
+ if (!validRoles().has(body.role))
119
+ return invalidRole(c, body.role);
120
+ const rows = await db
121
+ .insert(invitation)
122
+ .values({
123
+ id: makeId(),
124
+ organizationId: orgId,
125
+ email: body.email,
126
+ role: body.role,
127
+ status: "pending",
128
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
129
+ inviterId: actor.userId,
130
+ })
131
+ .returning();
132
+ return c.json({ data: rows[0] }, 201);
133
+ });
134
+ router.openapi(listStaffInvitationsRoute, async (c) => {
135
+ const orgId = resolveOrgId(c.get("actor"));
136
+ const rows = await db
137
+ .select()
138
+ .from(invitation)
139
+ .where(and(eq(invitation.organizationId, orgId), eq(invitation.status, "pending")));
140
+ return c.json({ data: rows });
141
+ });
142
+ router.openapi(listStaffRolesRoute, async (c) => {
143
+ const roles = config.auth?.roles ?? {};
144
+ const data = Object.entries(roles).map(([role, def]) => ({
145
+ role,
146
+ permissions: def.permissions,
147
+ }));
148
+ return c.json({ data });
149
+ });
150
+ // @ts-expect-error -- openapi handler union return type
151
+ router.openapi(updateStaffRoleRoute, async (c) => {
152
+ const body = c.req.valid("json");
153
+ const actor = c.get("actor");
154
+ const orgId = resolveOrgId(actor);
155
+ const id = c.req.param("id");
156
+ if (!validRoles().has(body.role))
157
+ return invalidRole(c, body.role);
158
+ if (!canGrantRole(actor.role, body.role))
159
+ 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) {
166
+ return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
167
+ }
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) {
173
+ return c.json({ error: { code: "VALIDATION_FAILED", message: "Cannot demote the organization's last owner." } }, 422);
174
+ }
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] });
181
+ });
182
+ // @ts-expect-error -- openapi handler union return type
183
+ router.openapi(revokeStaffRoute, async (c) => {
184
+ const actor = c.get("actor");
185
+ const orgId = resolveOrgId(actor);
186
+ 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) {
193
+ return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
194
+ }
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) {
200
+ return c.json({ error: { code: "VALIDATION_FAILED", message: "Cannot revoke the organization's last owner." } }, 422);
201
+ }
202
+ await db.delete(member).where(eq(member.id, id));
203
+ return c.json({ data: { deleted: true } });
204
+ });
205
+ return router;
206
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import { type AppEnv } from "../utils.js";
4
+ export declare function analyticsRoutes(kernel: Kernel): OpenAPIHono<AppEnv, {}, "/">;
5
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAE7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,gCA2B7C"}
@@ -0,0 +1,24 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { getReportRoute, listReportsRoute } from "../schemas/analytics.js";
3
+ import { mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
4
+ export function analyticsRoutes(kernel) {
5
+ const router = new OpenAPIHono();
6
+ router.use("/reports", requirePerm("analytics:read"));
7
+ router.use("/reports/:name", requirePerm("analytics:read"));
8
+ router.openapi(listReportsRoute, (c) => {
9
+ return c.json({ data: kernel.services.analytics.listReports() });
10
+ });
11
+ // @ts-expect-error -- openapi handler union return type
12
+ router.openapi(getReportRoute, async (c) => {
13
+ const { date, from, to } = c.req.valid("query");
14
+ const result = await kernel.services.analytics.getReport(c.req.param("name"), {
15
+ ...(date !== undefined ? { date } : {}),
16
+ ...(from !== undefined ? { from } : {}),
17
+ ...(to !== undefined ? { to } : {}),
18
+ }, c.get("actor"));
19
+ if (!result.ok)
20
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
21
+ return c.json({ data: result.value });
22
+ });
23
+ return router;
24
+ }
@@ -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,EAAwC,MAAM,aAAa,CAAC;AAEhF,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,gCA6FxC"}
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,6 +1,6 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
- import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute } from "../schemas/carts.js";
3
- import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
2
+ import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute, listCartsRoute, recoverCartRoute } from "../schemas/carts.js";
3
+ import { mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
4
4
  export function cartRoutes(kernel) {
5
5
  const router = new OpenAPIHono();
6
6
  // @ts-expect-error -- openapi() enforces strict response typing but our handler
@@ -14,6 +14,43 @@ export function cartRoutes(kernel) {
14
14
  return c.json({ data: result.value }, 201);
15
15
  });
16
16
  // @ts-expect-error -- openapi handler union return type
17
+ router.openapi(listCartsRoute, async (c) => {
18
+ const actor = c.get("actor");
19
+ const pagination = parsePagination(c.req.query());
20
+ const status = c.req.query("status");
21
+ const olderThanRaw = c.req.query("olderThan");
22
+ const hasCustomerRaw = c.req.query("hasCustomer");
23
+ let olderThan;
24
+ if (olderThanRaw) {
25
+ const d = new Date(olderThanRaw);
26
+ if (Number.isNaN(d.getTime())) {
27
+ return c.json({ error: { code: "VALIDATION_FAILED", message: "olderThan must be an ISO timestamp." } }, 422);
28
+ }
29
+ olderThan = d;
30
+ }
31
+ const result = await kernel.services.cart.list({
32
+ page: pagination.page,
33
+ limit: pagination.limit,
34
+ ...(status !== undefined ? { status } : {}),
35
+ ...(olderThan !== undefined ? { olderThan } : {}),
36
+ ...(hasCustomerRaw === "true" ? { hasCustomer: true } : {}),
37
+ ...(hasCustomerRaw === "false" ? { hasCustomer: false } : {}),
38
+ }, actor);
39
+ if (!result.ok)
40
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
41
+ return c.json({
42
+ data: result.value.items,
43
+ meta: { pagination: result.value.pagination },
44
+ });
45
+ });
46
+ // @ts-expect-error -- openapi handler union return type
47
+ router.openapi(recoverCartRoute, async (c) => {
48
+ const result = await kernel.services.cart.recover(c.req.param("id"), c.get("actor"));
49
+ if (!result.ok)
50
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
51
+ return c.json({ data: result.value });
52
+ });
53
+ // @ts-expect-error -- openapi handler union return type
17
54
  router.openapi(getCartRoute, async (c) => {
18
55
  const actor = c.get("actor");
19
56
  // Guest carts are gated by the cart secret; clients pass it as either
@@ -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;AA8CzD,OAAO,EACL,KAAK,MAAM,EAMZ,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,gCAud3C"}
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,5 +1,5 @@
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, } from "../schemas/catalog.js";
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
3
  import { mapErrorToResponse, mapErrorToStatus, parseInclude, parsePagination, parseSort, } from "../utils.js";
4
4
  export function catalogRoutes(kernel) {
5
5
  const router = new OpenAPIHono();
@@ -273,6 +273,22 @@ export function catalogRoutes(kernel) {
273
273
  return c.json({ data: result.value }, 201);
274
274
  });
275
275
  // @ts-expect-error -- openapi handler union return type
276
+ router.openapi(quickVariantRoute, async (c) => {
277
+ const body = c.req.valid("json");
278
+ const result = await kernel.services.catalog.quickCreateVariant(c.req.param("id"), body, c.get("actor"));
279
+ if (!result.ok)
280
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
281
+ return c.json({ data: result.value }, result.value.created ? 201 : 200);
282
+ });
283
+ // @ts-expect-error -- openapi handler union return type
284
+ router.openapi(bulkVariantsRoute, async (c) => {
285
+ const body = c.req.valid("json");
286
+ const result = await kernel.services.catalog.bulkCreateVariants(c.req.param("id"), body, c.get("actor"));
287
+ if (!result.ok)
288
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
289
+ return c.json({ data: result.value }, 201);
290
+ });
291
+ // @ts-expect-error -- openapi handler union return type
276
292
  router.openapi(generateVariantsRoute, async (c) => {
277
293
  const body = c.req.valid("json");
278
294
  const result = await kernel.services.catalog.generateVariants(c.req.param("id"), body, c.get("actor"));
@@ -1,5 +1,14 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { Kernel } from "../../../runtime/kernel.js";
3
3
  import { type AppEnv } from "../utils.js";
4
+ import type { Actor } from "../../../auth/types.js";
5
+ /**
6
+ * SEC-07 — resolve the customer profile a checkout order is attributed to.
7
+ * A self-service actor may ONLY attribute the order to its own customer
8
+ * profile; only actors with org-level `customers:read` (staff/clienteling) may
9
+ * name an arbitrary `customerId`. Returns undefined for guests / unresolved
10
+ * (guest checkout). Exported for unit testing.
11
+ */
12
+ export declare function resolveCheckoutCustomerUuid(customers: Kernel["services"]["customers"], actor: Actor | null, customerId: string | undefined): Promise<string | undefined>;
4
13
  export declare function checkoutRoutes(kernel: Kernel): OpenAPIHono<AppEnv, {}, "/">;
5
14
  //# sourceMappingURL=checkout.d.ts.map
@@ -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;AAKhF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCAsQ5C"}
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;AAKhF,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,gCA2S5C"}
@@ -5,7 +5,52 @@ import { runAfterHooks, runBeforeHooks } from "../../../kernel/hooks/executor.js
5
5
  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
+ import { assertPermission } from "../../../auth/permissions.js";
8
9
  import { makeId } from "../../../utils/id.js";
10
+ /**
11
+ * SEC-07 — resolve the customer profile a checkout order is attributed to.
12
+ * A self-service actor may ONLY attribute the order to its own customer
13
+ * profile; only actors with org-level `customers:read` (staff/clienteling) may
14
+ * name an arbitrary `customerId`. Returns undefined for guests / unresolved
15
+ * (guest checkout). Exported for unit testing.
16
+ */
17
+ export async function resolveCheckoutCustomerUuid(customers, actor, customerId) {
18
+ const actorUserId = actor?.userId;
19
+ let canActForOthers = false;
20
+ if (actorUserId) {
21
+ try {
22
+ assertPermission(actor, "customers:read");
23
+ canActForOthers = true;
24
+ }
25
+ catch {
26
+ canActForOthers = false;
27
+ }
28
+ }
29
+ if (customerId) {
30
+ const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
31
+ if (uuidRe.test(customerId)) {
32
+ const byId = await customers.getById(customerId, actor);
33
+ // Honor a profile UUID only if it is the actor's own, or the actor may
34
+ // act for other customers.
35
+ if (byId.ok && (canActForOthers || byId.value.userId === actorUserId)) {
36
+ return byId.value.id;
37
+ }
38
+ }
39
+ if (canActForOthers) {
40
+ // Staff may resolve/create a customer by a supplied user_id.
41
+ const byUser = await customers.getByUserId(customerId, actor);
42
+ if (byUser.ok)
43
+ return byUser.value.id;
44
+ }
45
+ }
46
+ // Self-service default: the authenticated actor's own customer profile.
47
+ if (actorUserId && !canActForOthers) {
48
+ const own = await customers.getByUserId(actorUserId, actor);
49
+ if (own.ok)
50
+ return own.value.id;
51
+ }
52
+ return undefined;
53
+ }
9
54
  export function checkoutRoutes(kernel) {
10
55
  const router = new OpenAPIHono();
11
56
  // @ts-expect-error -- openapi() enforces strict response typing but our handler
@@ -14,6 +59,40 @@ export function checkoutRoutes(kernel) {
14
59
  router.openapi(checkoutRoute, async (c) => {
15
60
  const body = c.req.valid("json");
16
61
  const actor = c.get("actor");
62
+ // Idempotent replay: a re-submitted checkout (offline POS queue, network
63
+ // retry) returns the already-created order BEFORE the pipeline runs — no
64
+ // double payment authorization, no duplicate order.
65
+ if (body.idempotencyKey) {
66
+ const replay = await kernel.services.orders.getByIdempotencyKey(body.idempotencyKey, actor);
67
+ if (replay.ok && replay.value) {
68
+ // IDOR guard: an idempotency key must only replay the requester's OWN
69
+ // order. Without this, a same-org customer who supplies/guesses another
70
+ // customer's key would receive that customer's order (PII, totals, lines).
71
+ let canActForOthers = false;
72
+ try {
73
+ assertPermission(actor, "customers:read");
74
+ canActForOthers = true;
75
+ }
76
+ catch {
77
+ canActForOthers = false;
78
+ }
79
+ const ownCustomer = actor
80
+ ? await resolveCheckoutCustomerUuid(kernel.services.customers, actor, undefined)
81
+ : undefined;
82
+ const replayCustomer = replay.value.customerId ?? null;
83
+ const ownsOrder = (replayCustomer !== null && replayCustomer === ownCustomer) ||
84
+ (replayCustomer === null && !ownCustomer);
85
+ if (canActForOthers || ownsOrder) {
86
+ return c.json({ data: replay.value }, 201);
87
+ }
88
+ return c.json({
89
+ error: {
90
+ code: "IDEMPOTENCY_CONFLICT",
91
+ message: "Idempotency key does not belong to this account.",
92
+ },
93
+ }, 409);
94
+ }
95
+ }
17
96
  const checkoutData = {
18
97
  checkoutId: makeId(),
19
98
  cartId: body.cartId,
@@ -84,27 +163,14 @@ export function checkoutRoutes(kernel) {
84
163
  // If Stripe takes 5s, the DB connection pool is not affected.
85
164
  context.tx = null;
86
165
  const processed = await runBeforeHooks(paymentHooks, validated, "create", context);
87
- // Resolve customer profile UUID from customerId (may be a profile UUID or a Better Auth user_id)
88
- let customerUuid = undefined;
89
- if (processed.customerId) {
90
- const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
91
- if (uuidRe.test(processed.customerId)) {
92
- // Looks like a profile UUID — try direct lookup (no auto-create)
93
- const byIdResult = await kernel.services.customers.getById(processed.customerId, actor);
94
- if (byIdResult.ok) {
95
- customerUuid = byIdResult.value.id;
96
- }
97
- }
98
- if (!customerUuid) {
99
- // Fall back to user_id lookup (auto-creates customer profile if needed)
100
- const byUserIdResult = await kernel.services.customers.getByUserId(processed.customerId, actor);
101
- if (byUserIdResult.ok) {
102
- customerUuid = byUserIdResult.value.id;
103
- }
104
- }
105
- // If both lookups fail, we still allow guest checkout (customerUuid remains undefined)
106
- }
166
+ // SEC-07: resolve the order's customer server-side. A self-service actor
167
+ // can only attribute the order to its own profile; a client-supplied
168
+ // foreign customerId is ignored unless the actor is staff.
169
+ const customerUuid = await resolveCheckoutCustomerUuid(kernel.services.customers, actor, processed.customerId);
107
170
  const orderPayload = {
171
+ ...(body.idempotencyKey !== undefined
172
+ ? { idempotencyKey: body.idempotencyKey }
173
+ : {}),
108
174
  currency: processed.currency,
109
175
  subtotal: processed.subtotal,
110
176
  taxTotal: processed.taxTotal,
@@ -133,6 +199,11 @@ export function checkoutRoutes(kernel) {
133
199
  quantity: lineItem.quantity,
134
200
  unitPrice: lineItem.resolvedUnitPrice ?? 0,
135
201
  totalPrice: lineItem.resolvedTotal ?? 0,
202
+ // Per-line tax from product tax classes (issue #57)
203
+ taxAmount: lineItem.taxAmount ?? 0,
204
+ ...(lineItem.discountAmount !== undefined
205
+ ? { discountAmount: lineItem.discountAmount }
206
+ : {}),
136
207
  };
137
208
  return lineItem.variantId !== undefined
138
209
  ? { ...payload, variantId: lineItem.variantId }
@@ -142,7 +213,12 @@ export function checkoutRoutes(kernel) {
142
213
  ? { customerId: customerUuid }
143
214
  : {}),
144
215
  };
145
- const order = await kernel.services.orders.create(orderPayload, actor);
216
+ // Checkout is a trusted, already-server-priced pipeline (resolveCurrentPrices
217
+ // + promotions/tax) and reserves stock in its own after-hooks — so it hands
218
+ // the order primitive precomputed totals rather than re-deriving them.
219
+ const order = await kernel.services.orders.create(orderPayload, actor, undefined, {
220
+ trustedPricing: true,
221
+ });
146
222
  if (!order.ok) {
147
223
  return c.json(mapErrorToResponse(order.error), mapErrorToStatus(order.error));
148
224
  }
@@ -0,0 +1,10 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import { type AppEnv } from "../utils.js";
4
+ /**
5
+ * Order document rendering (issue #47). Mounted under /orders. Access
6
+ * control rides on OrderService.getById (orders:read, or orders:read:own
7
+ * for the order's customer), so no extra scope is needed here.
8
+ */
9
+ export declare function documentRoutes(kernel: Kernel): OpenAPIHono<AppEnv, {}, "/">;
10
+ //# sourceMappingURL=documents.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,40 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { emailInvoiceRoute, invoiceHtmlRoute, invoicePdfRoute, receiptHtmlRoute, } from "../schemas/documents.js";
3
+ import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
4
+ /**
5
+ * Order document rendering (issue #47). Mounted under /orders. Access
6
+ * control rides on OrderService.getById (orders:read, or orders:read:own
7
+ * for the order's customer), so no extra scope is needed here.
8
+ */
9
+ export function documentRoutes(kernel) {
10
+ const router = new OpenAPIHono();
11
+ router.openapi(invoicePdfRoute, async (c) => {
12
+ const result = await kernel.services.documents.renderInvoicePdf(c.req.param("id"), c.get("actor"));
13
+ if (!result.ok)
14
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
15
+ return c.body(result.value.pdf.slice().buffer, 200, {
16
+ "content-type": "application/pdf",
17
+ "content-disposition": `inline; filename="invoice-${result.value.invoiceNumber}.pdf"`,
18
+ });
19
+ });
20
+ router.openapi(invoiceHtmlRoute, async (c) => {
21
+ const result = await kernel.services.documents.renderInvoiceHtml(c.req.param("id"), c.get("actor"));
22
+ if (!result.ok)
23
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
24
+ return c.html(result.value.html);
25
+ });
26
+ router.openapi(receiptHtmlRoute, async (c) => {
27
+ const result = await kernel.services.documents.renderReceiptHtml(c.req.param("id"), c.get("actor"));
28
+ if (!result.ok)
29
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
30
+ return c.html(result.value.html);
31
+ });
32
+ // @ts-expect-error -- openapi handler union return type
33
+ router.openapi(emailInvoiceRoute, async (c) => {
34
+ const result = await kernel.services.documents.emailInvoice(c.req.param("id"), c.req.valid("json").to, c.get("actor"));
35
+ if (!result.ok)
36
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
37
+ return c.json({ data: result.value });
38
+ });
39
+ return router;
40
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/inventory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAUzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAE7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,gCAiF7C"}
1
+ {"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/inventory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAUzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAE7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,gCA+F7C"}
@@ -5,6 +5,13 @@ export function inventoryRoutes(kernel) {
5
5
  const router = new OpenAPIHono();
6
6
  // Inventory levels listing requires inventory:read
7
7
  router.use("/levels", requirePerm("inventory:read"));
8
+ // Warehouse config + reservation are staff operations, never public. Gate them
9
+ // at the route (reserve/release are also called internally by checkout with the
10
+ // customer's actor, so the service itself must stay callable — the boundary is
11
+ // the HTTP route). Blocks anonymous (401) and customers lacking the perm (403).
12
+ router.use("/warehouses", requirePerm("inventory:read"));
13
+ router.use("/reserve", requirePerm("inventory:adjust"));
14
+ router.use("/release", requirePerm("inventory:adjust"));
8
15
  // @ts-expect-error -- openapi handler union return type
9
16
  router.openapi(listInventoryLevelsRoute, async (c) => {
10
17
  const actor = c.get("actor");
@@ -59,6 +66,12 @@ export function inventoryRoutes(kernel) {
59
66
  router.openapi(createWarehouseRoute, async (c) => {
60
67
  const body = c.req.valid("json");
61
68
  const actor = c.get("actor");
69
+ // Creating a warehouse (write) needs the higher inventory:adjust perm; the
70
+ // router.use above already requires inventory:read for the /warehouses path.
71
+ const perms = actor?.permissions ?? [];
72
+ if (!(perms.includes("inventory:adjust") || perms.includes("inventory:*") || perms.includes("*:*"))) {
73
+ return c.json({ error: { code: "FORBIDDEN", message: "Permission 'inventory:adjust' is required." } }, 403);
74
+ }
62
75
  const result = await kernel.services.inventory.createWarehouse(body, actor);
63
76
  if (!result.ok)
64
77
  return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
@@ -1 +1 @@
1
- {"version":3,"file":"orders.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/orders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,KAAK,MAAM,EAAiE,MAAM,aAAa,CAAC;AAGzG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,gCA8HzC"}
1
+ {"version":3,"file":"orders.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/orders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,KAAK,MAAM,EAAiE,MAAM,aAAa,CAAC;AAIzG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,gCAgRzC"}