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