@porulle/core 0.8.0 → 0.10.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 (162) 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 +7 -5
  5. package/dist/config/defaults.d.ts.map +1 -1
  6. package/dist/config/defaults.js +3 -1
  7. package/dist/config/types.d.ts +13 -3
  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 +1 -1
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/interfaces/rest/router.d.ts.map +1 -1
  20. package/dist/interfaces/rest/router.js +2 -0
  21. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
  22. package/dist/interfaces/rest/routes/admin/staff.js +37 -3
  23. package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
  24. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  25. package/dist/interfaces/rest/routes/checkout.js +82 -22
  26. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  27. package/dist/interfaces/rest/routes/inventory.js +13 -0
  28. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  29. package/dist/interfaces/rest/routes/orders.js +13 -0
  30. package/dist/interfaces/rest/schemas/catalog.d.ts +391 -0
  31. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  32. package/dist/interfaces/rest/schemas/orders.js +2 -2
  33. package/dist/interfaces/rest/schemas/responses.d.ts +85 -0
  34. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  35. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  36. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  37. package/dist/kernel/database/scoped-db.d.ts +2 -1
  38. package/dist/kernel/database/scoped-db.d.ts.map +1 -1
  39. package/dist/kernel/database/scoped-db.js +74 -15
  40. package/dist/kernel/errors.d.ts +12 -0
  41. package/dist/kernel/errors.d.ts.map +1 -1
  42. package/dist/kernel/errors.js +16 -0
  43. package/dist/kernel/jobs/adapter.d.ts +27 -8
  44. package/dist/kernel/jobs/adapter.d.ts.map +1 -1
  45. package/dist/kernel/jobs/adapter.js +1 -2
  46. package/dist/kernel/jobs/drizzle-adapter.d.ts +13 -3
  47. package/dist/kernel/jobs/drizzle-adapter.d.ts.map +1 -1
  48. package/dist/kernel/jobs/drizzle-adapter.js +46 -5
  49. package/dist/kernel/jobs/runner.d.ts +2 -1
  50. package/dist/kernel/jobs/runner.d.ts.map +1 -1
  51. package/dist/kernel/jobs/runner.js +100 -15
  52. package/dist/kernel/jobs/types.d.ts +13 -0
  53. package/dist/kernel/jobs/types.d.ts.map +1 -1
  54. package/dist/kernel/plugin/manifest.d.ts +5 -0
  55. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  56. package/dist/kernel/plugin/manifest.js +9 -0
  57. package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
  58. package/dist/modules/analytics/drizzle-adapter.js +16 -3
  59. package/dist/modules/cart/service.d.ts.map +1 -1
  60. package/dist/modules/cart/service.js +3 -2
  61. package/dist/modules/catalog/entity-service.d.ts +1 -0
  62. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  63. package/dist/modules/catalog/entity-service.js +49 -3
  64. package/dist/modules/catalog/repository/index.d.ts +2 -2
  65. package/dist/modules/catalog/repository/index.d.ts.map +1 -1
  66. package/dist/modules/catalog/repository/index.js +9 -1
  67. package/dist/modules/catalog/schema.d.ts +51 -0
  68. package/dist/modules/catalog/schema.d.ts.map +1 -1
  69. package/dist/modules/catalog/schema.js +12 -1
  70. package/dist/modules/catalog/schemas.d.ts +3 -1
  71. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  72. package/dist/modules/channels/adapter.d.ts +117 -0
  73. package/dist/modules/channels/adapter.d.ts.map +1 -0
  74. package/dist/modules/channels/adapter.js +3 -0
  75. package/dist/modules/customers/service.d.ts.map +1 -1
  76. package/dist/modules/customers/service.js +6 -0
  77. package/dist/modules/inventory/repository/index.js +4 -4
  78. package/dist/modules/inventory/service.d.ts.map +1 -1
  79. package/dist/modules/inventory/service.js +7 -3
  80. package/dist/modules/orders/schema.d.ts +17 -0
  81. package/dist/modules/orders/schema.d.ts.map +1 -1
  82. package/dist/modules/orders/schema.js +4 -0
  83. package/dist/modules/orders/service.d.ts +16 -2
  84. package/dist/modules/orders/service.d.ts.map +1 -1
  85. package/dist/modules/orders/service.js +226 -26
  86. package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
  87. package/dist/modules/orders/stale-order-cleanup.js +37 -16
  88. package/dist/modules/pricing/service.d.ts.map +1 -1
  89. package/dist/modules/pricing/service.js +5 -5
  90. package/dist/modules/promotions/repository/index.d.ts +1 -1
  91. package/dist/modules/promotions/repository/index.d.ts.map +1 -1
  92. package/dist/modules/promotions/repository/index.js +6 -3
  93. package/dist/modules/promotions/service.d.ts +1 -0
  94. package/dist/modules/promotions/service.d.ts.map +1 -1
  95. package/dist/modules/promotions/service.js +1 -1
  96. package/dist/modules/webhooks/repository/index.d.ts +1 -1
  97. package/dist/modules/webhooks/repository/index.d.ts.map +1 -1
  98. package/dist/modules/webhooks/repository/index.js +16 -10
  99. package/dist/runtime/kernel.d.ts.map +1 -1
  100. package/dist/runtime/kernel.js +14 -2
  101. package/dist/runtime/server.d.ts +1 -4
  102. package/dist/runtime/server.d.ts.map +1 -1
  103. package/dist/runtime/server.js +16 -34
  104. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  105. package/dist/test-utils/create-test-config.js +2 -0
  106. package/dist/test-utils/order-test-helpers.d.ts +12 -0
  107. package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
  108. package/dist/test-utils/order-test-helpers.js +18 -0
  109. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  110. package/dist/test-utils/rest-api-test-utils.js +1 -0
  111. package/dist/test-utils/test-actors.js +1 -1
  112. package/dist/testing.d.ts +1 -0
  113. package/dist/testing.d.ts.map +1 -1
  114. package/dist/testing.js +1 -0
  115. package/package.json +1 -1
  116. package/src/auth/middleware.ts +26 -7
  117. package/src/auth/setup.ts +10 -4
  118. package/src/config/defaults.ts +3 -1
  119. package/src/config/types.ts +16 -3
  120. package/src/generated/plugin-capabilities.d.ts +1 -0
  121. package/src/generated/plugin-manifest.ts +1 -0
  122. package/src/generated/plugin-repositories.d.ts +1 -0
  123. package/src/hooks/checkout-completion.ts +22 -16
  124. package/src/hooks/checkout.ts +11 -8
  125. package/src/index.ts +31 -1
  126. package/src/interfaces/rest/router.ts +2 -0
  127. package/src/interfaces/rest/routes/admin/staff.ts +45 -3
  128. package/src/interfaces/rest/routes/checkout.ts +94 -28
  129. package/src/interfaces/rest/routes/inventory.ts +14 -0
  130. package/src/interfaces/rest/routes/orders.ts +11 -0
  131. package/src/interfaces/rest/schemas/orders.ts +2 -2
  132. package/src/kernel/database/scoped-db.ts +83 -15
  133. package/src/kernel/errors.ts +20 -0
  134. package/src/kernel/jobs/adapter.ts +39 -8
  135. package/src/kernel/jobs/drizzle-adapter.ts +72 -9
  136. package/src/kernel/jobs/runner.ts +194 -72
  137. package/src/kernel/jobs/types.ts +25 -0
  138. package/src/kernel/plugin/manifest.ts +15 -0
  139. package/src/modules/analytics/drizzle-adapter.ts +19 -3
  140. package/src/modules/cart/service.ts +3 -2
  141. package/src/modules/catalog/entity-service.ts +37 -2
  142. package/src/modules/catalog/repository/index.ts +13 -3
  143. package/src/modules/catalog/schema.ts +33 -18
  144. package/src/modules/catalog/schemas.ts +3 -1
  145. package/src/modules/channels/adapter.ts +139 -0
  146. package/src/modules/customers/service.ts +6 -0
  147. package/src/modules/inventory/repository/index.ts +4 -4
  148. package/src/modules/inventory/service.ts +12 -2
  149. package/src/modules/orders/schema.ts +4 -0
  150. package/src/modules/orders/service.ts +404 -64
  151. package/src/modules/orders/stale-order-cleanup.ts +52 -27
  152. package/src/modules/pricing/service.ts +5 -6
  153. package/src/modules/promotions/repository/index.ts +6 -2
  154. package/src/modules/promotions/service.ts +2 -0
  155. package/src/modules/webhooks/repository/index.ts +16 -14
  156. package/src/runtime/kernel.ts +16 -2
  157. package/src/runtime/server.ts +22 -42
  158. package/src/test-utils/create-test-config.ts +2 -0
  159. package/src/test-utils/order-test-helpers.ts +26 -0
  160. package/src/test-utils/rest-api-test-utils.ts +1 -0
  161. package/src/test-utils/test-actors.ts +1 -1
  162. package/src/testing.ts +1 -0
@@ -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
@@ -20,7 +65,32 @@ export function checkoutRoutes(kernel) {
20
65
  if (body.idempotencyKey) {
21
66
  const replay = await kernel.services.orders.getByIdempotencyKey(body.idempotencyKey, actor);
22
67
  if (replay.ok && replay.value) {
23
- return c.json({ data: replay.value }, 201);
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);
24
94
  }
25
95
  }
26
96
  const checkoutData = {
@@ -93,26 +163,10 @@ export function checkoutRoutes(kernel) {
93
163
  // If Stripe takes 5s, the DB connection pool is not affected.
94
164
  context.tx = null;
95
165
  const processed = await runBeforeHooks(paymentHooks, validated, "create", context);
96
- // Resolve customer profile UUID from customerId (may be a profile UUID or a Better Auth user_id)
97
- let customerUuid = undefined;
98
- if (processed.customerId) {
99
- const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
100
- if (uuidRe.test(processed.customerId)) {
101
- // Looks like a profile UUID — try direct lookup (no auto-create)
102
- const byIdResult = await kernel.services.customers.getById(processed.customerId, actor);
103
- if (byIdResult.ok) {
104
- customerUuid = byIdResult.value.id;
105
- }
106
- }
107
- if (!customerUuid) {
108
- // Fall back to user_id lookup (auto-creates customer profile if needed)
109
- const byUserIdResult = await kernel.services.customers.getByUserId(processed.customerId, actor);
110
- if (byUserIdResult.ok) {
111
- customerUuid = byUserIdResult.value.id;
112
- }
113
- }
114
- // If both lookups fail, we still allow guest checkout (customerUuid remains undefined)
115
- }
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);
116
170
  const orderPayload = {
117
171
  ...(body.idempotencyKey !== undefined
118
172
  ? { idempotencyKey: body.idempotencyKey }
@@ -159,13 +213,19 @@ export function checkoutRoutes(kernel) {
159
213
  ? { customerId: customerUuid }
160
214
  : {}),
161
215
  };
162
- 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
+ });
163
222
  if (!order.ok) {
164
223
  return c.json(mapErrorToResponse(order.error), mapErrorToStatus(order.error));
165
224
  }
166
225
  if (order.ok && (processed.appliedPromotions?.length ?? 0) > 0) {
167
226
  await kernel.services.promotions.recordUsage({
168
227
  promotions: processed.appliedPromotions ?? [],
228
+ organizationId: order.value.organizationId,
169
229
  orderId: order.value.id,
170
230
  ...(customerUuid !== undefined
171
231
  ? { customerId: customerUuid }
@@ -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,gCAsQzC"}
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"}
@@ -1,6 +1,7 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import { changeOrderStatusRoute, listOrdersRoute, orderLookupRoute, getOrderRoute, getOrderFulfillmentsRoute, createOrderRoute, refundOrderRoute, captureOrderRoute, createOrderFulfillmentRoute, addOrderLineItemRoute, updateOrderLineItemRoute, removeOrderLineItemRoute, refundOrderLinesRoute, undoOrderRefundRoute, listOrderRefundsRoute, refundCapStatusRoute, createOrderNoteRoute, listOrderNotesRoute, deleteOrderNoteRoute, orderTimelineRoute } from "../schemas/orders.js";
3
3
  import { isUUID, mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
4
+ import { assertPermission } from "../../../auth/permissions.js";
4
5
  export function orderRoutes(kernel) {
5
6
  const router = new OpenAPIHono();
6
7
  // @ts-expect-error -- openapi handler union return type
@@ -55,6 +56,12 @@ export function orderRoutes(kernel) {
55
56
  });
56
57
  // @ts-expect-error -- openapi handler union return type
57
58
  router.openapi(createOrderRoute, async (c) => {
59
+ try {
60
+ assertPermission(c.get("actor"), "orders:manage");
61
+ }
62
+ catch (error) {
63
+ return c.json(mapErrorToResponse(error), mapErrorToStatus(error));
64
+ }
58
65
  const body = c.req.valid("json");
59
66
  const result = await kernel.services.orders.create(body, c.get("actor"));
60
67
  if (!result.ok)
@@ -139,6 +146,12 @@ export function orderRoutes(kernel) {
139
146
  });
140
147
  // @ts-expect-error -- openapi handler union return type
141
148
  router.openapi(addOrderLineItemRoute, async (c) => {
149
+ try {
150
+ assertPermission(c.get("actor"), "orders:manage");
151
+ }
152
+ catch (error) {
153
+ return c.json(mapErrorToResponse(error), mapErrorToStatus(error));
154
+ }
142
155
  const body = c.req.valid("json");
143
156
  const result = await kernel.services.orders.addLineItem(c.req.param("id"), body, c.get("actor"));
144
157
  if (!result.ok)