@porulle/core 0.11.0 → 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.
- package/dist/auth/auth-schema-guard.d.ts +2444 -0
- package/dist/auth/auth-schema-guard.d.ts.map +1 -0
- package/dist/auth/auth-schema-guard.js +113 -0
- package/dist/auth/auth-schema.d.ts +228 -0
- package/dist/auth/auth-schema.d.ts.map +1 -1
- package/dist/auth/auth-schema.js +24 -2
- package/dist/auth/middleware.d.ts +1 -0
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +13 -5
- package/dist/auth/org.d.ts +8 -2
- package/dist/auth/org.d.ts.map +1 -1
- package/dist/auth/org.js +12 -4
- package/dist/auth/organization-guard.d.ts +5 -0
- package/dist/auth/organization-guard.d.ts.map +1 -0
- package/dist/auth/organization-guard.js +124 -0
- package/dist/auth/permissions.d.ts +10 -0
- package/dist/auth/permissions.d.ts.map +1 -1
- package/dist/auth/permissions.js +31 -7
- package/dist/auth/role-authority.d.ts +44 -0
- package/dist/auth/role-authority.d.ts.map +1 -0
- package/dist/auth/role-authority.js +85 -0
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +17 -0
- package/dist/auth/strict-org-resolution.d.ts.map +1 -1
- package/dist/auth/strict-org-resolution.js +9 -1
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/types.d.ts +30 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +10 -9
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/interfaces/rest/audit-middleware.d.ts.map +1 -1
- package/dist/interfaces/rest/audit-middleware.js +2 -0
- package/dist/interfaces/rest/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/customer-portal.js +9 -8
- package/dist/interfaces/rest/route-coverage.d.ts +14 -0
- package/dist/interfaces/rest/route-coverage.d.ts.map +1 -0
- package/dist/interfaces/rest/route-coverage.js +81 -0
- package/dist/interfaces/rest/router.d.ts +7 -2
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/router.js +8 -4
- package/dist/interfaces/rest/routes/admin/compensation-failures.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/compensation-failures.js +7 -5
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +2 -2
- package/dist/interfaces/rest/routes/admin/permissions.d.ts +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.js +2 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/staff.js +126 -66
- package/dist/interfaces/rest/routes/admin-jobs.js +3 -3
- package/dist/interfaces/rest/routes/audit.js +3 -3
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +13 -10
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +10 -2
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +30 -35
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/documents.js +10 -5
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +2 -0
- package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/media.js +7 -3
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +26 -8
- package/dist/interfaces/rest/routes/pricing.js +2 -2
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +13 -10
- package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/search.js +2 -1
- package/dist/interfaces/rest/schemas/carts.d.ts +7 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.d.ts +204 -0
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/responses.d.ts +71 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.d.ts +15 -0
- package/dist/interfaces/rest/utils.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.js +45 -2
- package/dist/kernel/compensation/executor.js +2 -2
- package/dist/kernel/database/tx-context.d.ts +8 -0
- package/dist/kernel/database/tx-context.d.ts.map +1 -1
- package/dist/kernel/database/tx-context.js +13 -0
- package/dist/kernel/hooks/create-context.d.ts +2 -0
- package/dist/kernel/hooks/create-context.d.ts.map +1 -1
- package/dist/kernel/hooks/create-context.js +1 -0
- package/dist/kernel/hooks/types.d.ts +2 -0
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +9 -5
- package/dist/modules/analytics/service.js +2 -2
- package/dist/modules/audit/service.js +3 -3
- package/dist/modules/cart/repository/index.d.ts +13 -7
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +16 -10
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +4 -0
- package/dist/modules/cart/service.d.ts +22 -9
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +57 -33
- package/dist/modules/catalog/brand-service.js +7 -7
- package/dist/modules/catalog/category-service.js +7 -7
- package/dist/modules/catalog/entity-service.d.ts +14 -7
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +109 -31
- package/dist/modules/catalog/read-policy.d.ts +9 -0
- package/dist/modules/catalog/read-policy.d.ts.map +1 -0
- package/dist/modules/catalog/read-policy.js +8 -0
- package/dist/modules/catalog/repository/index.d.ts +3 -2
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +14 -4
- package/dist/modules/catalog/service.d.ts +15 -13
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +25 -18
- package/dist/modules/customers/module.d.ts.map +1 -1
- package/dist/modules/customers/module.js +1 -0
- package/dist/modules/customers/service.d.ts +2 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +21 -20
- package/dist/modules/documents/module.d.ts.map +1 -1
- package/dist/modules/documents/module.js +1 -0
- package/dist/modules/documents/service.d.ts +18 -4
- package/dist/modules/documents/service.d.ts.map +1 -1
- package/dist/modules/documents/service.js +54 -16
- package/dist/modules/fulfillment/module.d.ts.map +1 -1
- package/dist/modules/fulfillment/module.js +1 -0
- package/dist/modules/fulfillment/service.d.ts +2 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +15 -6
- package/dist/modules/inventory/service.d.ts.map +1 -1
- package/dist/modules/inventory/service.js +15 -14
- package/dist/modules/media/service.js +6 -6
- package/dist/modules/orders/guest-access.d.ts +27 -0
- package/dist/modules/orders/guest-access.d.ts.map +1 -0
- package/dist/modules/orders/guest-access.js +46 -0
- package/dist/modules/orders/module.d.ts.map +1 -1
- package/dist/modules/orders/module.js +1 -0
- package/dist/modules/orders/quote.d.ts.map +1 -1
- package/dist/modules/orders/quote.js +1 -0
- package/dist/modules/orders/repository/index.d.ts +1 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +2 -2
- package/dist/modules/orders/schema.d.ts +17 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +5 -3
- package/dist/modules/orders/service.d.ts +16 -7
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +184 -55
- package/dist/modules/pricing/module.d.ts.map +1 -1
- package/dist/modules/pricing/module.js +1 -0
- package/dist/modules/pricing/service.d.ts +2 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +12 -11
- package/dist/modules/promotions/module.d.ts.map +1 -1
- package/dist/modules/promotions/module.js +1 -0
- package/dist/modules/promotions/service.d.ts +3 -0
- package/dist/modules/promotions/service.d.ts.map +1 -1
- package/dist/modules/promotions/service.js +19 -11
- package/dist/modules/search/adapter.d.ts +6 -0
- package/dist/modules/search/adapter.d.ts.map +1 -1
- package/dist/modules/search/module.d.ts.map +1 -1
- package/dist/modules/search/module.js +1 -0
- package/dist/modules/search/service.d.ts +1 -0
- package/dist/modules/search/service.d.ts.map +1 -1
- package/dist/modules/search/service.js +57 -6
- package/dist/modules/settings/module.d.ts.map +1 -1
- package/dist/modules/settings/module.js +1 -0
- package/dist/modules/settings/service.d.ts +3 -0
- package/dist/modules/settings/service.d.ts.map +1 -1
- package/dist/modules/settings/service.js +6 -4
- package/dist/modules/shipping/service.js +9 -9
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +1 -0
- package/dist/modules/tax/service.d.ts +3 -0
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +11 -9
- package/dist/modules/webhooks/hook.js +2 -2
- package/dist/modules/webhooks/module.d.ts.map +1 -1
- package/dist/modules/webhooks/module.js +1 -0
- package/dist/modules/webhooks/service.d.ts +2 -0
- package/dist/modules/webhooks/service.d.ts.map +1 -1
- package/dist/modules/webhooks/service.js +4 -4
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +10 -2
- package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
- package/dist/test-utils/create-pglite-adapter.js +35 -13
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +4 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +2 -0
- package/dist/test-utils/test-actors.js +2 -2
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +11 -0
- package/package.json +6 -5
- package/src/auth/auth-schema-guard.ts +143 -0
- package/src/auth/auth-schema.ts +50 -19
- package/src/auth/middleware.ts +17 -5
- package/src/auth/org.ts +15 -3
- package/src/auth/organization-guard.ts +158 -0
- package/src/auth/permissions.ts +31 -8
- package/src/auth/role-authority.ts +107 -0
- package/src/auth/setup.ts +17 -0
- package/src/auth/strict-org-resolution.ts +7 -1
- package/src/auth/types.ts +1 -1
- package/src/config/defaults.ts +2 -0
- package/src/config/types.ts +29 -4
- package/src/hooks/checkout.ts +19 -12
- package/src/index.ts +16 -6
- package/src/interfaces/rest/audit-middleware.ts +2 -0
- package/src/interfaces/rest/customer-portal.ts +31 -24
- package/src/interfaces/rest/route-coverage.ts +103 -0
- package/src/interfaces/rest/router.ts +16 -10
- package/src/interfaces/rest/routes/admin/compensation-failures.ts +8 -5
- package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +2 -2
- package/src/interfaces/rest/routes/admin/permissions.ts +3 -1
- package/src/interfaces/rest/routes/admin/staff.ts +161 -72
- package/src/interfaces/rest/routes/admin-jobs.ts +3 -3
- package/src/interfaces/rest/routes/audit.ts +3 -3
- package/src/interfaces/rest/routes/carts.ts +17 -7
- package/src/interfaces/rest/routes/catalog.ts +11 -0
- package/src/interfaces/rest/routes/checkout.ts +41 -38
- package/src/interfaces/rest/routes/documents.ts +15 -1
- package/src/interfaces/rest/routes/inventory.ts +2 -0
- package/src/interfaces/rest/routes/media.ts +8 -3
- package/src/interfaces/rest/routes/orders.ts +27 -8
- package/src/interfaces/rest/routes/pricing.ts +2 -2
- package/src/interfaces/rest/routes/promotions.ts +12 -9
- package/src/interfaces/rest/routes/search.ts +3 -1
- package/src/interfaces/rest/utils.ts +61 -2
- package/src/kernel/compensation/executor.ts +2 -2
- package/src/kernel/database/tx-context.ts +25 -0
- package/src/kernel/hooks/create-context.ts +3 -0
- package/src/kernel/hooks/types.ts +2 -0
- package/src/kernel/plugin/manifest.ts +9 -5
- package/src/modules/analytics/service.ts +2 -2
- package/src/modules/audit/service.ts +3 -3
- package/src/modules/cart/repository/index.ts +22 -8
- package/src/modules/cart/schema.ts +4 -0
- package/src/modules/cart/service.ts +62 -27
- package/src/modules/catalog/brand-service.ts +7 -7
- package/src/modules/catalog/category-service.ts +7 -7
- package/src/modules/catalog/entity-service.ts +117 -35
- package/src/modules/catalog/read-policy.ts +16 -0
- package/src/modules/catalog/repository/index.ts +19 -3
- package/src/modules/catalog/service.ts +46 -31
- package/src/modules/customers/module.ts +2 -0
- package/src/modules/customers/service.ts +23 -22
- package/src/modules/documents/module.ts +2 -0
- package/src/modules/documents/service.ts +74 -11
- package/src/modules/fulfillment/module.ts +2 -0
- package/src/modules/fulfillment/service.ts +17 -6
- package/src/modules/inventory/service.ts +15 -24
- package/src/modules/media/service.ts +6 -6
- package/src/modules/orders/guest-access.ts +55 -0
- package/src/modules/orders/module.ts +1 -0
- package/src/modules/orders/quote.ts +1 -0
- package/src/modules/orders/repository/index.ts +2 -0
- package/src/modules/orders/schema.ts +5 -3
- package/src/modules/orders/service.ts +302 -52
- package/src/modules/pricing/module.ts +2 -0
- package/src/modules/pricing/service.ts +14 -16
- package/src/modules/promotions/module.ts +2 -0
- package/src/modules/promotions/service.ts +25 -16
- package/src/modules/search/adapter.ts +6 -0
- package/src/modules/search/module.ts +1 -0
- package/src/modules/search/service.ts +55 -6
- package/src/modules/settings/module.ts +2 -0
- package/src/modules/settings/service.ts +8 -4
- package/src/modules/shipping/service.ts +9 -9
- package/src/modules/tax/module.ts +1 -0
- package/src/modules/tax/service.ts +13 -9
- package/src/modules/webhooks/hook.ts +2 -2
- package/src/modules/webhooks/module.ts +2 -0
- package/src/modules/webhooks/service.ts +6 -4
- package/src/runtime/server.ts +16 -2
- package/src/test-utils/create-pglite-adapter.ts +34 -12
- package/src/test-utils/create-test-config.ts +10 -6
- package/src/test-utils/rest-api-test-utils.ts +2 -0
- package/src/test-utils/test-actors.ts +2 -2
- package/src/utils/id.ts +20 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
invoicePdfRoute,
|
|
7
7
|
receiptHtmlRoute,
|
|
8
8
|
} from "../schemas/documents.js";
|
|
9
|
-
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
9
|
+
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requireAnyPerm } from "../utils.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Order document rendering (issue #47). Mounted under /orders. Access
|
|
@@ -16,10 +16,15 @@ import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
|
16
16
|
export function documentRoutes(kernel: Kernel) {
|
|
17
17
|
const router = new OpenAPIHono<AppEnv>();
|
|
18
18
|
|
|
19
|
+
router.use("/*", requireAnyPerm(["orders:read", "orders:read:own"]));
|
|
20
|
+
|
|
19
21
|
router.openapi(invoicePdfRoute, async (c) => {
|
|
22
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
20
23
|
const result = await kernel.services.documents.renderInvoicePdf(
|
|
21
24
|
c.req.param("id"),
|
|
22
25
|
c.get("actor"),
|
|
26
|
+
undefined,
|
|
27
|
+
guestCredential,
|
|
23
28
|
);
|
|
24
29
|
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
25
30
|
return c.body(result.value.pdf.slice().buffer as ArrayBuffer, 200, {
|
|
@@ -29,18 +34,24 @@ export function documentRoutes(kernel: Kernel) {
|
|
|
29
34
|
});
|
|
30
35
|
|
|
31
36
|
router.openapi(invoiceHtmlRoute, async (c) => {
|
|
37
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
32
38
|
const result = await kernel.services.documents.renderInvoiceHtml(
|
|
33
39
|
c.req.param("id"),
|
|
34
40
|
c.get("actor"),
|
|
41
|
+
undefined,
|
|
42
|
+
guestCredential,
|
|
35
43
|
);
|
|
36
44
|
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
37
45
|
return c.html(result.value.html);
|
|
38
46
|
});
|
|
39
47
|
|
|
40
48
|
router.openapi(receiptHtmlRoute, async (c) => {
|
|
49
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
41
50
|
const result = await kernel.services.documents.renderReceiptHtml(
|
|
42
51
|
c.req.param("id"),
|
|
43
52
|
c.get("actor"),
|
|
53
|
+
undefined,
|
|
54
|
+
guestCredential,
|
|
44
55
|
);
|
|
45
56
|
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
46
57
|
return c.html(result.value.html);
|
|
@@ -48,10 +59,13 @@ export function documentRoutes(kernel: Kernel) {
|
|
|
48
59
|
|
|
49
60
|
// @ts-expect-error -- openapi handler union return type
|
|
50
61
|
router.openapi(emailInvoiceRoute, async (c) => {
|
|
62
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
51
63
|
const result = await kernel.services.documents.emailInvoice(
|
|
52
64
|
c.req.param("id"),
|
|
53
65
|
c.req.valid("json").to,
|
|
54
66
|
c.get("actor"),
|
|
67
|
+
undefined,
|
|
68
|
+
guestCredential,
|
|
55
69
|
);
|
|
56
70
|
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
57
71
|
return c.json({ data: result.value });
|
|
@@ -24,6 +24,8 @@ export function inventoryRoutes(kernel: Kernel) {
|
|
|
24
24
|
router.use("/warehouses", requirePerm("inventory:read"));
|
|
25
25
|
router.use("/reserve", requirePerm("inventory:adjust"));
|
|
26
26
|
router.use("/release", requirePerm("inventory:adjust"));
|
|
27
|
+
router.use("/check", requirePerm("inventory:read"));
|
|
28
|
+
router.use("/adjust", requirePerm("inventory:adjust"));
|
|
27
29
|
|
|
28
30
|
// @ts-expect-error -- openapi handler union return type
|
|
29
31
|
router.openapi(listInventoryLevelsRoute, async (c) => {
|
|
@@ -2,13 +2,14 @@ import { OpenAPIHono } from "@hono/zod-openapi";
|
|
|
2
2
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
3
|
import type { AttachMediaInput } from "../../../modules/media/service.js";
|
|
4
4
|
import { attachMediaRoute, getMediaRoute, deleteMediaRoute } from "../schemas/media.js";
|
|
5
|
-
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
|
|
5
|
+
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requireMethodPerm, requirePerm } from "../utils.js";
|
|
6
6
|
|
|
7
7
|
export function mediaRoutes(kernel: Kernel) {
|
|
8
8
|
const router = new OpenAPIHono<AppEnv>();
|
|
9
9
|
|
|
10
10
|
// Upload requires authentication + media:write permission
|
|
11
11
|
router.use("/upload", requirePerm("media:write"));
|
|
12
|
+
router.use("/:id", requireMethodPerm(["DELETE"], "media:write"));
|
|
12
13
|
|
|
13
14
|
router.post("/upload", async (c) => {
|
|
14
15
|
// No storage configured (the default no-op adapter) → media is disabled.
|
|
@@ -43,16 +44,20 @@ export function mediaRoutes(kernel: Kernel) {
|
|
|
43
44
|
|
|
44
45
|
router.openapi(getMediaRoute, async (c) => {
|
|
45
46
|
const signed = c.req.query("signed") === "true";
|
|
47
|
+
const actor = c.get("actor");
|
|
48
|
+
|
|
49
|
+
if (!actor && !signed) {
|
|
50
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Media not found." } }, 404);
|
|
51
|
+
}
|
|
46
52
|
|
|
47
53
|
// Signed URLs require authentication
|
|
48
|
-
if (signed && !
|
|
54
|
+
if (signed && !actor) {
|
|
49
55
|
return c.json(
|
|
50
56
|
{ error: { code: "UNAUTHORIZED", message: "Authentication required for signed URLs." } },
|
|
51
57
|
401,
|
|
52
58
|
);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
const actor = c.get("actor");
|
|
56
61
|
const result = signed
|
|
57
62
|
? await kernel.services.media.getSignedUrl(c.req.param("id"), undefined, actor)
|
|
58
63
|
: await kernel.services.media.getUrl(c.req.param("id"), actor);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
3
|
import { changeOrderStatusRoute, listOrdersRoute, orderLookupRoute, getOrderRoute, getOrderFulfillmentsRoute, createOrderRoute, quoteOrderRoute, refundOrderRoute, captureOrderRoute, createOrderFulfillmentRoute, addOrderLineItemRoute, updateOrderLineItemRoute, removeOrderLineItemRoute, refundOrderLinesRoute, undoOrderRefundRoute, listOrderRefundsRoute, refundCapStatusRoute, createOrderNoteRoute, listOrderNotesRoute, deleteOrderNoteRoute, orderTimelineRoute } from "../schemas/orders.js";
|
|
4
|
-
import { type AppEnv, isUUID, mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
|
|
4
|
+
import { type AppEnv, isUUID, mapErrorToResponse, mapErrorToStatus, parsePagination, requireAnyPerm, requireMethodPerm, requirePerm } from "../utils.js";
|
|
5
5
|
import type { CreateOrderInput } from "../../../modules/orders/service.js";
|
|
6
6
|
import { computeOrderPricing } from "../../../modules/orders/quote.js";
|
|
7
7
|
import { assertPermission } from "../../../auth/permissions.js";
|
|
@@ -9,6 +9,13 @@ import { assertPermission } from "../../../auth/permissions.js";
|
|
|
9
9
|
export function orderRoutes(kernel: Kernel) {
|
|
10
10
|
const router = new OpenAPIHono<AppEnv>();
|
|
11
11
|
|
|
12
|
+
router.use("/", requireMethodPerm(["GET"], "orders:read"));
|
|
13
|
+
router.use("/", requireMethodPerm(["POST"], "orders:create"));
|
|
14
|
+
router.use("/lookup", requirePerm("orders:read"));
|
|
15
|
+
router.use("/:idOrNumber", requireAnyPerm(["orders:read", "orders:read:own"]));
|
|
16
|
+
router.use("/:id/*", requireAnyPerm(["orders:read", "orders:read:own", "orders:update", "orders:manage"]));
|
|
17
|
+
router.use("/refunds/cap", requirePerm("orders:read"));
|
|
18
|
+
|
|
12
19
|
// @ts-expect-error -- openapi handler union return type
|
|
13
20
|
router.openapi(listOrdersRoute, async (c) => {
|
|
14
21
|
const pagination = parsePagination(c.req.query());
|
|
@@ -54,9 +61,10 @@ export function orderRoutes(kernel: Kernel) {
|
|
|
54
61
|
// @ts-expect-error -- openapi handler union return type
|
|
55
62
|
router.openapi(getOrderRoute, async (c) => {
|
|
56
63
|
const idOrNumber = c.req.param("idOrNumber");
|
|
64
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
57
65
|
const result = isUUID(idOrNumber)
|
|
58
|
-
? await kernel.services.orders.getById(idOrNumber, c.get("actor"))
|
|
59
|
-
: await kernel.services.orders.getByNumber(idOrNumber, c.get("actor"));
|
|
66
|
+
? await kernel.services.orders.getById(idOrNumber, c.get("actor"), undefined, guestCredential)
|
|
67
|
+
: await kernel.services.orders.getByNumber(idOrNumber, c.get("actor"), undefined, guestCredential);
|
|
60
68
|
|
|
61
69
|
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
62
70
|
return c.json({ data: result.value });
|
|
@@ -65,12 +73,15 @@ export function orderRoutes(kernel: Kernel) {
|
|
|
65
73
|
// @ts-expect-error -- openapi handler union return type
|
|
66
74
|
router.openapi(createOrderRoute, async (c) => {
|
|
67
75
|
try {
|
|
68
|
-
assertPermission(c.get("actor"), "orders:
|
|
76
|
+
assertPermission(c.get("actor"), "orders:create");
|
|
69
77
|
} catch (error) {
|
|
70
78
|
return c.json(mapErrorToResponse(error), mapErrorToStatus(error));
|
|
71
79
|
}
|
|
72
80
|
const body = c.req.valid("json") as CreateOrderInput;
|
|
73
|
-
const
|
|
81
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
82
|
+
const result = await kernel.services.orders.create(body, c.get("actor"), undefined, {
|
|
83
|
+
...(guestCredential !== undefined ? { guestCredential } : {}),
|
|
84
|
+
});
|
|
74
85
|
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
75
86
|
return c.json({ data: result.value }, 201);
|
|
76
87
|
});
|
|
@@ -273,8 +284,15 @@ export function orderRoutes(kernel: Kernel) {
|
|
|
273
284
|
const actor = c.get("actor");
|
|
274
285
|
const orderId = c.req.param("id");
|
|
275
286
|
|
|
287
|
+
try {
|
|
288
|
+
assertPermission(actor, "orders:update");
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return c.json(mapErrorToResponse(error), mapErrorToStatus(error));
|
|
291
|
+
}
|
|
292
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
293
|
+
|
|
276
294
|
// Verify the order exists and the actor has access before recording
|
|
277
|
-
const orderResult = await kernel.services.orders.getById(orderId, actor);
|
|
295
|
+
const orderResult = await kernel.services.orders.getById(orderId, actor, undefined, guestCredential);
|
|
278
296
|
if (!orderResult.ok) return c.json(mapErrorToResponse(orderResult.error), mapErrorToStatus(orderResult.error));
|
|
279
297
|
|
|
280
298
|
const body = c.req.valid("json") as {
|
|
@@ -298,11 +316,12 @@ export function orderRoutes(kernel: Kernel) {
|
|
|
298
316
|
router.openapi(getOrderFulfillmentsRoute, async (c) => {
|
|
299
317
|
const orderId = c.req.param("id");
|
|
300
318
|
const actor = c.get("actor");
|
|
319
|
+
const guestCredential = c.req.header("x-cart-secret") ?? c.req.header("X-Cart-Secret") ?? undefined;
|
|
301
320
|
|
|
302
321
|
// Verify the order exists and the actor has access before returning fulfillments
|
|
303
322
|
const orderResult = isUUID(orderId)
|
|
304
|
-
? await kernel.services.orders.getById(orderId, actor)
|
|
305
|
-
: await kernel.services.orders.getByNumber(orderId, actor);
|
|
323
|
+
? await kernel.services.orders.getById(orderId, actor, undefined, guestCredential)
|
|
324
|
+
: await kernel.services.orders.getByNumber(orderId, actor, undefined, guestCredential);
|
|
306
325
|
|
|
307
326
|
if (!orderResult.ok) return c.json(mapErrorToResponse(orderResult.error), mapErrorToStatus(orderResult.error));
|
|
308
327
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveOrgIdForCommerce } from "../../../auth/org.js";
|
|
3
3
|
import { CommerceValidationError } from "../../../kernel/errors.js";
|
|
4
4
|
import type { DrizzleDatabase } from "../../../kernel/database/drizzle-db.js";
|
|
5
5
|
import { CatalogRepository } from "../../../modules/catalog/repository/index.js";
|
|
@@ -98,7 +98,7 @@ export function pricingRoutes(kernel: Kernel) {
|
|
|
98
98
|
|
|
99
99
|
if (input.entityId) {
|
|
100
100
|
const catalogRepo = new CatalogRepository(kernel.database.db as DrizzleDatabase);
|
|
101
|
-
const orgId =
|
|
101
|
+
const orgId = resolveOrgIdForCommerce(actor, kernel.config);
|
|
102
102
|
const entity = await catalogRepo.findEntityById(input.entityId);
|
|
103
103
|
if (!entity || entity.organizationId !== orgId) {
|
|
104
104
|
return c.json(
|
|
@@ -3,8 +3,9 @@ import { rateLimiter } from "hono-rate-limiter";
|
|
|
3
3
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
4
4
|
import { createPromotionRoute, updatePromotionRoute, validatePromotionRoute, deactivatePromotionRoute, listPromotionsRoute } from "../schemas/promotions.js";
|
|
5
5
|
import type { PromotionStatusFilter } from "../../../modules/promotions/service.js";
|
|
6
|
-
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
|
|
7
|
-
import {
|
|
6
|
+
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requireMethodPerm, requirePerm } from "../utils.js";
|
|
7
|
+
import { resolveOrgIdForCommerce } from "../../../auth/org.js";
|
|
8
|
+
import { assertPermission } from "../../../auth/permissions.js";
|
|
8
9
|
|
|
9
10
|
export function promotionRoutes(kernel: Kernel) {
|
|
10
11
|
const router = new OpenAPIHono<AppEnv>();
|
|
@@ -59,7 +60,7 @@ export function promotionRoutes(kernel: Kernel) {
|
|
|
59
60
|
router.openapi(validatePromotionRoute, async (c) => {
|
|
60
61
|
const payload = c.req.valid("json");
|
|
61
62
|
const actor = c.get("actor");
|
|
62
|
-
const orgId =
|
|
63
|
+
const orgId = resolveOrgIdForCommerce(actor, kernel.config);
|
|
63
64
|
|
|
64
65
|
// Apply (not just validate) so the response carries the authoritative
|
|
65
66
|
// discount the cart would receive — the same computation checkout runs.
|
|
@@ -84,10 +85,7 @@ export function promotionRoutes(kernel: Kernel) {
|
|
|
84
85
|
|
|
85
86
|
// Guard inline (not via router.use("/:id")) because a "/:id" middleware
|
|
86
87
|
// would also match single-segment routes like POST /validate.
|
|
87
|
-
router.use("/:id",
|
|
88
|
-
if (c.req.method !== "PATCH") return next();
|
|
89
|
-
return requirePerm("promotions:manage")(c, next);
|
|
90
|
-
});
|
|
88
|
+
router.use("/:id", requireMethodPerm(["PATCH"], "promotions:manage"));
|
|
91
89
|
|
|
92
90
|
// @ts-expect-error -- openapi() enforces strict response typing but our handler
|
|
93
91
|
// returns union responses (200 | 400 | 404 | 422). The route definition
|
|
@@ -95,7 +93,12 @@ export function promotionRoutes(kernel: Kernel) {
|
|
|
95
93
|
router.openapi(updatePromotionRoute, async (c) => {
|
|
96
94
|
const body = c.req.valid("json");
|
|
97
95
|
const actor = c.get("actor");
|
|
98
|
-
|
|
96
|
+
try {
|
|
97
|
+
assertPermission(actor, "promotions:manage");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return c.json(mapErrorToResponse(error), mapErrorToStatus(error));
|
|
100
|
+
}
|
|
101
|
+
const orgId = resolveOrgIdForCommerce(actor, kernel.config);
|
|
99
102
|
const result = await kernel.services.promotions.update(orgId, c.req.param("id"), body, actor);
|
|
100
103
|
if (!result.ok) {
|
|
101
104
|
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
@@ -110,7 +113,7 @@ export function promotionRoutes(kernel: Kernel) {
|
|
|
110
113
|
// contract; the handler returns dynamic status.
|
|
111
114
|
router.openapi(deactivatePromotionRoute, async (c) => {
|
|
112
115
|
const actor = c.get("actor");
|
|
113
|
-
const orgId =
|
|
116
|
+
const orgId = resolveOrgIdForCommerce(actor, kernel.config);
|
|
114
117
|
const result = await kernel.services.promotions.deactivate(orgId, c.req.param("id"));
|
|
115
118
|
if (!result.ok) {
|
|
116
119
|
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
3
|
import { searchRoute, suggestRoute } from "../schemas/search.js";
|
|
4
|
-
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
4
|
+
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
|
|
5
5
|
|
|
6
6
|
const SAFE_ATTRIBUTE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
7
7
|
|
|
8
8
|
export function searchRoutes(kernel: Kernel) {
|
|
9
9
|
const router = new OpenAPIHono<AppEnv>();
|
|
10
10
|
|
|
11
|
+
router.use("/*", requirePerm("catalog:read"));
|
|
12
|
+
|
|
11
13
|
// @ts-expect-error -- openapi handler union return type
|
|
12
14
|
router.openapi(searchRoute, async (c) => {
|
|
13
15
|
try {
|
|
@@ -3,6 +3,25 @@ import { mapErrorToStatus } from "../../kernel/error-mapper.js";
|
|
|
3
3
|
import { toCommerceError } from "../../kernel/errors.js";
|
|
4
4
|
import type { Actor } from "../../auth/types.js";
|
|
5
5
|
|
|
6
|
+
export const ROUTE_PERMISSION_GUARD = Symbol("porulle.routePermissionGuard");
|
|
7
|
+
|
|
8
|
+
type RouteGuardHandler = {
|
|
9
|
+
[ROUTE_PERMISSION_GUARD]?: { methods?: readonly string[] };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function markRoutePermissionGuard<T>(handler: T, methods?: readonly string[]): T {
|
|
13
|
+
if (typeof handler === "function") {
|
|
14
|
+
Object.defineProperty(handler, ROUTE_PERMISSION_GUARD, { value: { methods } });
|
|
15
|
+
}
|
|
16
|
+
return handler;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type PermissionContext = {
|
|
20
|
+
get(key: string): unknown;
|
|
21
|
+
json(data: unknown, status: number): unknown;
|
|
22
|
+
req: { method: string };
|
|
23
|
+
};
|
|
24
|
+
|
|
6
25
|
/**
|
|
7
26
|
* Shared Hono environment type for all sub-routers.
|
|
8
27
|
* Matches the Variables set by middleware in the top-level server app.
|
|
@@ -61,7 +80,7 @@ export { mapErrorToStatus };
|
|
|
61
80
|
* Usage: router.post("/", requirePerm("webhooks:manage"), handler);
|
|
62
81
|
*/
|
|
63
82
|
export function requirePerm(permission: string) {
|
|
64
|
-
|
|
83
|
+
const middleware = async (c: { get(key: string): unknown; json(data: unknown, status: number): unknown }, next: () => Promise<void>) => {
|
|
65
84
|
const actor = c.get("actor") as { permissions?: string[] } | null;
|
|
66
85
|
if (!actor) {
|
|
67
86
|
return c.json({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }, 401);
|
|
@@ -79,6 +98,46 @@ export function requirePerm(permission: string) {
|
|
|
79
98
|
}
|
|
80
99
|
return c.json({ error: { code: "FORBIDDEN", message: `Permission '${permission}' is required.` } }, 403);
|
|
81
100
|
};
|
|
101
|
+
return markRoutePermissionGuard(middleware);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function requireAnyPerm(permissions: readonly string[]) {
|
|
105
|
+
const middleware = async (c: { get(key: string): unknown; json(data: unknown, status: number): unknown }, next: () => Promise<void>) => {
|
|
106
|
+
const actor = c.get("actor") as { permissions?: string[] } | null;
|
|
107
|
+
const granted = actor?.permissions ?? [];
|
|
108
|
+
const allowed = permissions.some((permission) =>
|
|
109
|
+
granted.includes(permission) ||
|
|
110
|
+
granted.includes("*:*") ||
|
|
111
|
+
granted.includes(`${permission.split(":")[0]}:*`),
|
|
112
|
+
);
|
|
113
|
+
if (!actor) {
|
|
114
|
+
return c.json({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }, 401);
|
|
115
|
+
}
|
|
116
|
+
if (allowed) {
|
|
117
|
+
await next();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
return c.json({ error: { code: "FORBIDDEN", message: `One of these permissions is required: ${permissions.join(", ")}.` } }, 403);
|
|
121
|
+
};
|
|
122
|
+
return markRoutePermissionGuard(middleware);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function requireMethodPerm(methods: readonly string[], permission: string) {
|
|
126
|
+
const permissionGuard = requirePerm(permission);
|
|
127
|
+
const middleware = async (c: PermissionContext, next: () => Promise<void>) => {
|
|
128
|
+
if (!methods.includes(c.req.method)) {
|
|
129
|
+
await next();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
return permissionGuard(c, next);
|
|
133
|
+
};
|
|
134
|
+
return markRoutePermissionGuard(middleware, methods);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isRoutePermissionGuard(handler: unknown, method?: string): boolean {
|
|
138
|
+
if (typeof handler !== "function") return false;
|
|
139
|
+
const guard = (handler as unknown as RouteGuardHandler)[ROUTE_PERMISSION_GUARD];
|
|
140
|
+
return Boolean(guard && (!method || !guard.methods || guard.methods.includes(method)));
|
|
82
141
|
}
|
|
83
142
|
|
|
84
143
|
export function parseSort(
|
|
@@ -100,5 +159,5 @@ export function parseSort(
|
|
|
100
159
|
}
|
|
101
160
|
|
|
102
161
|
export function isUUID(value: string): boolean {
|
|
103
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[
|
|
162
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
104
163
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
2
2
|
import type { CompensationContext, Step } from "./types.js";
|
|
3
3
|
import type { Result } from "../result.js";
|
|
4
4
|
|
|
@@ -85,7 +85,7 @@ export async function runCompensationChain<TInput>(
|
|
|
85
85
|
);
|
|
86
86
|
if (ctx.failureRepository) {
|
|
87
87
|
try {
|
|
88
|
-
const orgId =
|
|
88
|
+
const orgId = resolveOrgIdForCommerce(ctx.hook.actor, ctx.hook.commerceConfig);
|
|
89
89
|
const recordResult = await ctx.failureRepository.record(
|
|
90
90
|
{
|
|
91
91
|
organizationId: orgId,
|
|
@@ -6,11 +6,19 @@ export interface TxContext<TTx = unknown> {
|
|
|
6
6
|
tx: TTx;
|
|
7
7
|
actor: Actor | null;
|
|
8
8
|
requestId: string;
|
|
9
|
+
hookContext?: Record<string, unknown>;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
export interface HookContextCarrier {
|
|
13
|
+
hookContext?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CatalogWriteContext<TTx = unknown> = TxContext<TTx> | HookContextCarrier;
|
|
17
|
+
|
|
11
18
|
export interface WithTransactionOptions {
|
|
12
19
|
actor: Actor | null;
|
|
13
20
|
requestId?: string;
|
|
21
|
+
hookContext?: Record<string, unknown>;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export function createTxContext<TTx>(
|
|
@@ -21,6 +29,7 @@ export function createTxContext<TTx>(
|
|
|
21
29
|
tx,
|
|
22
30
|
actor: options.actor,
|
|
23
31
|
requestId: options.requestId ?? randomUUID(),
|
|
32
|
+
...(options.hookContext ? { hookContext: options.hookContext } : {}),
|
|
24
33
|
};
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -44,3 +53,19 @@ export function reuseOrCreateTxContext<TTx>(
|
|
|
44
53
|
}
|
|
45
54
|
return createTxContext(tx, options);
|
|
46
55
|
}
|
|
56
|
+
|
|
57
|
+
function hookContextFromWriteContext(ctx?: CatalogWriteContext): Record<string, unknown> | undefined {
|
|
58
|
+
return ctx?.hookContext;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isTransactionalWriteContext<TTx>(ctx: CatalogWriteContext<TTx>): ctx is TxContext<TTx> {
|
|
62
|
+
return "tx" in ctx && ctx.tx != null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveWriteContextHookContext(ctx?: CatalogWriteContext): Record<string, unknown> | undefined {
|
|
66
|
+
return hookContextFromWriteContext(ctx);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isWriteContextTransactional<TTx>(ctx?: CatalogWriteContext<TTx>): ctx is TxContext<TTx> {
|
|
70
|
+
return ctx != null && isTransactionalWriteContext(ctx);
|
|
71
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { Actor } from "../../auth/types.js";
|
|
3
|
+
import type { CommerceConfig } from "../../config/types.js";
|
|
3
4
|
import type { JobsAdapter } from "../jobs/adapter.js";
|
|
4
5
|
import { NullJobsAdapter } from "../jobs/adapter.js";
|
|
5
6
|
import type { PluginDb } from "../database/plugin-types.js";
|
|
@@ -21,6 +22,7 @@ export interface CreateHookContextArgs {
|
|
|
21
22
|
* @deprecated Pass {@link CreateHookContextArgs.database} or {@link CreateHookContextArgs.db} instead.
|
|
22
23
|
*/
|
|
23
24
|
kernel?: { database: { db: PluginDb } };
|
|
25
|
+
commerceConfig?: CommerceConfig | null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const nullJobs = new NullJobsAdapter();
|
|
@@ -48,5 +50,6 @@ export function createHookContext(args: CreateHookContextArgs): HookContext {
|
|
|
48
50
|
origin: args.origin ?? "rest",
|
|
49
51
|
jobs: args.jobs ?? nullJobs,
|
|
50
52
|
db,
|
|
53
|
+
...(args.commerceConfig !== undefined ? { commerceConfig: args.commerceConfig } : {}),
|
|
51
54
|
};
|
|
52
55
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Actor } from "../../auth/types.js";
|
|
2
|
+
import type { CommerceConfig } from "../../config/types.js";
|
|
2
3
|
import type { JobsAdapter } from "../jobs/adapter.js";
|
|
3
4
|
import type { PluginDb } from "../database/plugin-types.js";
|
|
4
5
|
|
|
@@ -40,6 +41,7 @@ export interface HookContext {
|
|
|
40
41
|
* Populated by `createHookContext` when callers pass `db`, `database`, or `kernel`.
|
|
41
42
|
*/
|
|
42
43
|
db: PluginDb;
|
|
44
|
+
commerceConfig?: CommerceConfig | null;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export type BeforeHook<TData> = (args: {
|
|
@@ -2,7 +2,8 @@ import type { Hono } from "hono";
|
|
|
2
2
|
import type { OpenAPIHono, RouteConfig } from "@hono/zod-openapi";
|
|
3
3
|
import type { PluginDb } from "../database/plugin-types.js";
|
|
4
4
|
import type { DatabaseAdapter } from "../database/adapter.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
6
|
+
import { isRoutePermissionGuard, markRoutePermissionGuard } from "../../interfaces/rest/utils.js";
|
|
6
7
|
import { createScopedDb } from "../database/scoped-db.js";
|
|
7
8
|
import {
|
|
8
9
|
getPluginDatabaseScopeOrganizationId,
|
|
@@ -97,7 +98,7 @@ function buildPluginContextDatabase(
|
|
|
97
98
|
): PluginContext["database"] {
|
|
98
99
|
const orgGetter = () =>
|
|
99
100
|
getPluginDatabaseScopeOrganizationId() ??
|
|
100
|
-
|
|
101
|
+
resolveOrgIdForCommerce(null, commerceConfig);
|
|
101
102
|
|
|
102
103
|
const scopedDb = createScopedDb(rawDb, orgGetter);
|
|
103
104
|
const warnUnscoped = createRateLimitedUnscopedAccessWarning(logger);
|
|
@@ -288,7 +289,7 @@ export function defineCommercePlugin(
|
|
|
288
289
|
const wrappedHandler = async (...args: unknown[]) => {
|
|
289
290
|
const c = args[0] as { get?: (key: string) => unknown } | undefined;
|
|
290
291
|
const actor = c?.get?.("actor");
|
|
291
|
-
const orgId =
|
|
292
|
+
const orgId = resolveOrgIdForCommerce(actor, k.config);
|
|
292
293
|
try {
|
|
293
294
|
return await runWithPluginDatabaseScope(orgId, async () => {
|
|
294
295
|
const out = originalHandler(...args);
|
|
@@ -305,19 +306,22 @@ export function defineCommercePlugin(
|
|
|
305
306
|
throw err; // re-throw so global handler still catches
|
|
306
307
|
}
|
|
307
308
|
};
|
|
309
|
+
const registeredHandler = isRoutePermissionGuard(originalHandler)
|
|
310
|
+
? markRoutePermissionGuard(wrappedHandler)
|
|
311
|
+
: wrappedHandler;
|
|
308
312
|
|
|
309
313
|
if ("openapi" in route) {
|
|
310
314
|
// OpenAPI route: validated by Zod, appears in /api/doc
|
|
311
315
|
// Hono type interop — OpenAPIHono.openapi() expects strict RouteConfig+Handler
|
|
312
316
|
// generics that can't be statically resolved for dynamic plugin routes.
|
|
313
317
|
// @ts-expect-error -- dynamic plugin routes cannot satisfy Hono's strict handler generics
|
|
314
|
-
(app as OpenAPIHono).openapi(route.openapi,
|
|
318
|
+
(app as OpenAPIHono).openapi(route.openapi, registeredHandler);
|
|
315
319
|
} else {
|
|
316
320
|
// Legacy route: raw handler, invisible to OpenAPI spec.
|
|
317
321
|
// Explicit dispatch avoids casting Hono to a Record.
|
|
318
322
|
// Handler cast is a single `as any` (Hono's overloaded method
|
|
319
323
|
// signatures can't unify with our generic handler shape).
|
|
320
|
-
const h =
|
|
324
|
+
const h = registeredHandler as any;
|
|
321
325
|
switch (route.method.toLowerCase()) {
|
|
322
326
|
case "get": app.get(route.path, h); break;
|
|
323
327
|
case "post": app.post(route.path, h); break;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { CommerceConfig } from "../../config/types.js";
|
|
10
10
|
import { CommerceValidationError } from "../../kernel/errors.js";
|
|
11
11
|
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
13
13
|
import type { Actor } from "../../auth/types.js";
|
|
14
14
|
import type { ReportParams, RetailReportsEngine } from "./reports.js";
|
|
15
15
|
import { RETAIL_REPORTS } from "./reports.js";
|
|
@@ -135,7 +135,7 @@ export class AnalyticsService {
|
|
|
135
135
|
if (!this.deps.reports) {
|
|
136
136
|
return Err(new CommerceValidationError("Retail reports are not available."));
|
|
137
137
|
}
|
|
138
|
-
const orgId =
|
|
138
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
139
139
|
return this.deps.reports.run(name, params, orgId);
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -3,7 +3,7 @@ import type { InferSelectModel } from "drizzle-orm";
|
|
|
3
3
|
import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
|
|
4
4
|
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
5
5
|
import type { HookContext } from "../../kernel/hooks/types.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
7
7
|
import { auditLog } from "./schema.js";
|
|
8
8
|
|
|
9
9
|
export type AuditEntry = InferSelectModel<typeof auditLog>;
|
|
@@ -47,7 +47,7 @@ export function createNullAuditService(): AuditService {
|
|
|
47
47
|
async record(args) {
|
|
48
48
|
entries.push({
|
|
49
49
|
id: crypto.randomUUID(),
|
|
50
|
-
organizationId:
|
|
50
|
+
organizationId: resolveOrgIdForCommerce(args.ctx.actor, args.ctx.commerceConfig),
|
|
51
51
|
entityType: args.entityType,
|
|
52
52
|
entityId: args.entityId,
|
|
53
53
|
event: args.event,
|
|
@@ -94,7 +94,7 @@ export function createAuditService(db: DrizzleDatabase): AuditService {
|
|
|
94
94
|
: db;
|
|
95
95
|
|
|
96
96
|
await dbOrTx.insert(auditLog).values({
|
|
97
|
-
organizationId:
|
|
97
|
+
organizationId: resolveOrgIdForCommerce(ctx.actor, ctx.commerceConfig),
|
|
98
98
|
entityType,
|
|
99
99
|
entityId,
|
|
100
100
|
event,
|
|
@@ -130,34 +130,48 @@ export class CartRepository {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
* Atomically transitions a cart from "active" to "checking_out"
|
|
134
|
-
*
|
|
135
|
-
* the cart was not in "active" status
|
|
136
|
-
* claimed it). This prevents TOCTOU race
|
|
133
|
+
* Atomically transitions a cart from "active" to "checking_out" and records
|
|
134
|
+
* `claimToken` as the holder of that claim. Returns the updated cart if the
|
|
135
|
+
* transition succeeded, or undefined if the cart was not in "active" status
|
|
136
|
+
* (e.g., a concurrent checkout already claimed it). This prevents TOCTOU race
|
|
137
|
+
* conditions on double-checkout.
|
|
137
138
|
*/
|
|
138
139
|
async transitionToCheckingOut(
|
|
139
140
|
id: string,
|
|
141
|
+
claimToken: string,
|
|
140
142
|
ctx?: TxContext,
|
|
141
143
|
): Promise<Cart | undefined> {
|
|
142
144
|
const db = this.getDb(ctx);
|
|
143
145
|
const rows = await db
|
|
144
146
|
.update(carts)
|
|
145
|
-
.set({ status: "checking_out", updatedAt: new Date() })
|
|
147
|
+
.set({ status: "checking_out", checkoutClaimToken: claimToken, updatedAt: new Date() })
|
|
146
148
|
.where(and(eq(carts.id, id), eq(carts.status, "active")))
|
|
147
149
|
.returning();
|
|
148
150
|
return rows[0];
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Release an in-progress checkout claim held by `claimToken`. Terminal cart
|
|
155
|
+
* states are untouched, and so is a claim held by a different checkout
|
|
156
|
+
* attempt — a losing attempt's failure path must not reopen the cart the
|
|
157
|
+
* winner is still checking out.
|
|
158
|
+
*/
|
|
152
159
|
async releaseCheckoutClaim(
|
|
153
160
|
id: string,
|
|
161
|
+
claimToken: string,
|
|
154
162
|
ctx?: TxContext,
|
|
155
163
|
): Promise<Cart | undefined> {
|
|
156
164
|
const db = this.getDb(ctx);
|
|
157
165
|
const rows = await db
|
|
158
166
|
.update(carts)
|
|
159
|
-
.set({ status: "active", updatedAt: new Date() })
|
|
160
|
-
.where(
|
|
167
|
+
.set({ status: "active", checkoutClaimToken: null, updatedAt: new Date() })
|
|
168
|
+
.where(
|
|
169
|
+
and(
|
|
170
|
+
eq(carts.id, id),
|
|
171
|
+
eq(carts.status, "checking_out"),
|
|
172
|
+
eq(carts.checkoutClaimToken, claimToken),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
161
175
|
.returning();
|
|
162
176
|
return rows[0];
|
|
163
177
|
}
|
|
@@ -15,6 +15,10 @@ export const carts = pgTable("carts", {
|
|
|
15
15
|
// Shopper contact for guest carts — enables abandoned-checkout recovery
|
|
16
16
|
email: text("email"),
|
|
17
17
|
secret: text("secret"),
|
|
18
|
+
// Owner of an in-progress `checking_out` claim. Minted by the checkout
|
|
19
|
+
// attempt that wins the CAS; only that holder may release the claim, so a
|
|
20
|
+
// losing attempt's failure path cannot unwind the winner's claim.
|
|
21
|
+
checkoutClaimToken: text("checkout_claim_token"),
|
|
18
22
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
|
|
19
23
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
20
24
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|