@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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { CommerceConfig } from "../config/types.js";
|
|
2
|
+
|
|
3
|
+
const BUILTIN_ROLE_RANK: Record<string, number> = {
|
|
4
|
+
owner: 3,
|
|
5
|
+
admin: 2,
|
|
6
|
+
};
|
|
7
|
+
const CUSTOM_ROLE_RANK = 1;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Who may grant which role.
|
|
11
|
+
*
|
|
12
|
+
* Every surface that mints or changes a membership answers this question, and
|
|
13
|
+
* they must all answer it the same way — the admin staff routes, and the guard
|
|
14
|
+
* in front of Better Auth's organization endpoints. Red-team round 6 found the
|
|
15
|
+
* takeover that follows from two surfaces disagreeing, so the arithmetic lives
|
|
16
|
+
* here and nowhere else.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Better Auth stores a member's role as a comma-separated list, so `"owner"`
|
|
21
|
+
* and `"owner,admin"` are both owners to it. Commerce role lookups are exact
|
|
22
|
+
* string matches, which is how a composite string used to read as an unknown
|
|
23
|
+
* custom role on one side and an owner on the other.
|
|
24
|
+
*/
|
|
25
|
+
export function roleParts(role: string | null | undefined): string[] {
|
|
26
|
+
return (role ?? "")
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((part) => part.trim())
|
|
29
|
+
.filter((part) => part.length > 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isCompositeRole(role: string | null | undefined): boolean {
|
|
33
|
+
return roleParts(role).length > 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when any part of the role string is `owner`, matching Better Auth. */
|
|
37
|
+
export function isOwnerRole(role: string | null | undefined): boolean {
|
|
38
|
+
return roleParts(role).includes("owner");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function permissionsForRole(config: CommerceConfig, role: string): string[] {
|
|
42
|
+
return config.auth?.roles?.[role]?.permissions ?? [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hasPermissionFromList(
|
|
46
|
+
actorPermissions: readonly string[],
|
|
47
|
+
required: string,
|
|
48
|
+
): boolean {
|
|
49
|
+
if (actorPermissions.includes("*:*")) return true;
|
|
50
|
+
|
|
51
|
+
const [resource] = required.split(":");
|
|
52
|
+
if (resource && actorPermissions.includes(`${resource}:*`)) return true;
|
|
53
|
+
return actorPermissions.includes(required);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The role's privilege rank. A composite role ranks as its strongest part, so
|
|
58
|
+
* `"owner,admin"` cannot slip past a rank floor by failing an exact lookup.
|
|
59
|
+
*/
|
|
60
|
+
export function roleRank(config: CommerceConfig, role: string | null | undefined): number {
|
|
61
|
+
const ranks = roleParts(role).map((part) => {
|
|
62
|
+
const builtin = BUILTIN_ROLE_RANK[part];
|
|
63
|
+
if (builtin !== undefined) return builtin;
|
|
64
|
+
if (permissionsForRole(config, part).includes("*:*")) return BUILTIN_ROLE_RANK["admin"]!;
|
|
65
|
+
return CUSTOM_ROLE_RANK;
|
|
66
|
+
});
|
|
67
|
+
return ranks.length > 0 ? Math.max(...ranks) : CUSTOM_ROLE_RANK;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface Granter {
|
|
71
|
+
role: string;
|
|
72
|
+
permissions: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A granter may assign `targetRole` only when it already holds every permission
|
|
77
|
+
* that role carries (containment) and outranks or equals it (rank floor).
|
|
78
|
+
*/
|
|
79
|
+
export function canGrantRole(
|
|
80
|
+
config: CommerceConfig,
|
|
81
|
+
granter: Granter | null,
|
|
82
|
+
targetRole: string,
|
|
83
|
+
): boolean {
|
|
84
|
+
if (!granter) return false;
|
|
85
|
+
const contains = permissionsForRole(config, targetRole).every((permission) =>
|
|
86
|
+
hasPermissionFromList(granter.permissions, permission),
|
|
87
|
+
);
|
|
88
|
+
return contains && roleRank(config, granter.role) >= roleRank(config, targetRole);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The granter a stored membership row represents. Permissions come from the
|
|
93
|
+
* role's current definition, which is the whole point of re-checking authority
|
|
94
|
+
* later: the row is re-read, not the authority the actor had when they acted.
|
|
95
|
+
*/
|
|
96
|
+
export function granterForRole(config: CommerceConfig, role: string): Granter {
|
|
97
|
+
return { role, permissions: permissionsForRole(config, role) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function validRoles(config: CommerceConfig): Set<string> {
|
|
101
|
+
return new Set([
|
|
102
|
+
...Object.keys(config.auth?.roles ?? {}),
|
|
103
|
+
"owner",
|
|
104
|
+
"admin",
|
|
105
|
+
"member",
|
|
106
|
+
]);
|
|
107
|
+
}
|
package/src/auth/setup.ts
CHANGED
|
@@ -65,6 +65,23 @@ export function createAuth(
|
|
|
65
65
|
| ReturnType<typeof jwt>
|
|
66
66
|
| ReturnType<typeof bearer>
|
|
67
67
|
> = [
|
|
68
|
+
// These are commerce permission arrays, not Better Auth access-control
|
|
69
|
+
// `Role` objects exposing `.authorize()`. Its organization plugin therefore
|
|
70
|
+
// cannot evaluate a permission with them, and every check reaching its
|
|
71
|
+
// authorize loop throws instead of deciding — fail-closed, but by type
|
|
72
|
+
// confusion rather than by intent.
|
|
73
|
+
//
|
|
74
|
+
// That is deliberate and stays. This repository does not use Better Auth's
|
|
75
|
+
// access-control model: `admin/staff.ts` is the governed membership
|
|
76
|
+
// surface, and `auth/role-authority.ts` holds the arithmetic. Handing the
|
|
77
|
+
// plugin real `Role` objects would stand up a second permission model to
|
|
78
|
+
// keep in agreement with the first, which is how the two layers disagreed
|
|
79
|
+
// about composite roles and owner counting in the first place.
|
|
80
|
+
//
|
|
81
|
+
// What must not be relied on is the throw. `auth/organization-guard.ts`
|
|
82
|
+
// refuses the plugin's membership writers outright, so repairing this
|
|
83
|
+
// wiring cannot turn an accidental closure into an open door. Pin that
|
|
84
|
+
// guard's test before changing anything here.
|
|
68
85
|
organization({
|
|
69
86
|
roles: (config.auth?.roles ?? {}) as unknown as Record<string, Role | undefined>,
|
|
70
87
|
}),
|
|
@@ -7,8 +7,14 @@ export function setBootStrictOrgResolution(strict: boolean): void {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function isStrictOrgResolution(config?: CommerceConfig | null): boolean {
|
|
10
|
+
// Fail closed by default. Without an actor there is no tenant, and silently
|
|
11
|
+
// resolving to a default organization served one merchant's data to
|
|
12
|
+
// unauthenticated callers on every unguarded or allowlisted read path.
|
|
13
|
+
// Single-tenant deployments that relied on that fallback opt out explicitly.
|
|
14
|
+
if (config?.auth?.strictOrgResolution === false) return false;
|
|
15
|
+
if (process.env.STRICT_ORG_RESOLUTION === "false") return false;
|
|
10
16
|
if (config?.auth?.strictOrgResolution === true) return true;
|
|
11
17
|
if (process.env.STRICT_ORG_RESOLUTION === "true") return true;
|
|
12
18
|
if ((config === undefined || config === null) && _bootStrictOrgResolution) return true;
|
|
13
|
-
return
|
|
19
|
+
return true;
|
|
14
20
|
}
|
package/src/auth/types.ts
CHANGED
package/src/config/defaults.ts
CHANGED
|
@@ -24,10 +24,12 @@ export const defaultConfig: Partial<CommerceConfig> = {
|
|
|
24
24
|
"catalog:update",
|
|
25
25
|
"catalog:delete",
|
|
26
26
|
"catalog:read",
|
|
27
|
+
"catalog:read:unpublished",
|
|
27
28
|
"inventory:read",
|
|
28
29
|
"inventory:adjust",
|
|
29
30
|
"orders:read",
|
|
30
31
|
"orders:create",
|
|
32
|
+
"orders:create:on-behalf",
|
|
31
33
|
"orders:update",
|
|
32
34
|
"orders:manage",
|
|
33
35
|
"cart:create",
|
package/src/config/types.ts
CHANGED
|
@@ -34,10 +34,13 @@ export type FieldType = "text" | "number" | "boolean" | "date" | "json" | "relat
|
|
|
34
34
|
export interface EntityFieldDefinition {
|
|
35
35
|
name: string;
|
|
36
36
|
type: FieldType;
|
|
37
|
+
filterable?: boolean;
|
|
38
|
+
localized?: boolean;
|
|
37
39
|
unit?: string;
|
|
38
40
|
schema?: unknown;
|
|
39
41
|
target?: string;
|
|
40
42
|
options?: string[];
|
|
43
|
+
sortOrder?: number;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export interface EntityVariantConfig {
|
|
@@ -197,10 +200,17 @@ export interface AuthConfig {
|
|
|
197
200
|
*/
|
|
198
201
|
storeResolver?: (request: Request) => string | null | Promise<string | null>;
|
|
199
202
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
203
|
+
* Governs two layers, and its default differs between them.
|
|
204
|
+
*
|
|
205
|
+
* In the store-resolver middleware, `true` makes a failing `storeResolver`
|
|
206
|
+
* return HTTP 503 rather than continuing with `actor = null`; that layer
|
|
207
|
+
* still defaults to the lenient behaviour.
|
|
208
|
+
*
|
|
209
|
+
* In organization resolution it **fails closed by default**: an actor-less
|
|
210
|
+
* request with no explicit organization and no `defaultOrganizationId` is
|
|
211
|
+
* refused rather than served a fallback tenant. Set this to `false`, or the
|
|
212
|
+
* env var `STRICT_ORG_RESOLUTION` to `"false"`, to restore the old fallback
|
|
213
|
+
* during a migration.
|
|
204
214
|
*/
|
|
205
215
|
strictOrgResolution?: boolean;
|
|
206
216
|
/**
|
|
@@ -255,7 +265,25 @@ export interface CheckoutConfig {
|
|
|
255
265
|
};
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Decides whether a guest bearer credential (a cart secret) may still read a
|
|
270
|
+
* placed order. The authenticated customer who owns an order is never subject
|
|
271
|
+
* to this — ownership grants permanent access.
|
|
272
|
+
*
|
|
273
|
+
* Modelled on Vendure's `OrderByCodeAccessStrategy`: the value of the seam is
|
|
274
|
+
* that the policy has one home, rather than being an emergent property of the
|
|
275
|
+
* read path.
|
|
276
|
+
*/
|
|
277
|
+
export interface GuestOrderAccessStrategy {
|
|
278
|
+
canAccessOrder(order: { placedAt: Date }, now: Date): boolean;
|
|
279
|
+
}
|
|
280
|
+
|
|
258
281
|
export interface OrdersConfig {
|
|
282
|
+
/**
|
|
283
|
+
* How long a guest cart secret keeps reading an order after placement.
|
|
284
|
+
* Defaults to `windowedGuestOrderAccess("7d")`.
|
|
285
|
+
*/
|
|
286
|
+
guestAccessStrategy?: GuestOrderAccessStrategy;
|
|
259
287
|
hooks?: {
|
|
260
288
|
beforeCreate?: BeforeHook<unknown>[];
|
|
261
289
|
afterCreate?: AfterHook<unknown>[];
|
package/src/hooks/checkout.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CommerceValidationError } from "../kernel/errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveOrgIdForCommerce } from "../auth/org.js";
|
|
3
3
|
import type { CompensationFailuresRepository } from "../kernel/compensation/repository.js";
|
|
4
4
|
import type { AfterHook, BeforeHook } from "../kernel/hooks/types.js";
|
|
5
5
|
import type { ShippingAddress } from "../modules/shipping/calculator.js";
|
|
@@ -97,7 +97,7 @@ export const validateCartNotEmpty: BeforeHook<CheckoutData> = async ({
|
|
|
97
97
|
context,
|
|
98
98
|
}) => {
|
|
99
99
|
const cartService = context.services.cart as {
|
|
100
|
-
|
|
100
|
+
getByIdForInternalUse(id: string, organizationId: string, ctx?: TxContext): Promise<
|
|
101
101
|
| {
|
|
102
102
|
ok: true;
|
|
103
103
|
value: {
|
|
@@ -114,7 +114,7 @@ export const validateCartNotEmpty: BeforeHook<CheckoutData> = async ({
|
|
|
114
114
|
}
|
|
115
115
|
| { ok: false }
|
|
116
116
|
>;
|
|
117
|
-
claimForCheckout(cartId: string, ctx?: unknown): Promise<
|
|
117
|
+
claimForCheckout(cartId: string, claimToken: string, ctx?: unknown): Promise<
|
|
118
118
|
| { ok: true; value: { id: string } }
|
|
119
119
|
| { ok: false; error: { message: string } }
|
|
120
120
|
>;
|
|
@@ -122,7 +122,9 @@ export const validateCartNotEmpty: BeforeHook<CheckoutData> = async ({
|
|
|
122
122
|
const catalogService = context.services.catalog as {
|
|
123
123
|
getById(
|
|
124
124
|
id: string,
|
|
125
|
-
options
|
|
125
|
+
options: { includeAttributes?: boolean } | undefined,
|
|
126
|
+
actor: unknown,
|
|
127
|
+
ctx?: TxContext,
|
|
126
128
|
): Promise<
|
|
127
129
|
| {
|
|
128
130
|
ok: true;
|
|
@@ -137,20 +139,25 @@ export const validateCartNotEmpty: BeforeHook<CheckoutData> = async ({
|
|
|
137
139
|
|
|
138
140
|
// M1 fix: Atomically claim the cart for checkout (active → checking_out).
|
|
139
141
|
// If a concurrent request already claimed it, this returns Err and we fail fast.
|
|
140
|
-
|
|
142
|
+
// The checkout id is the claim token, so only this attempt can release it.
|
|
143
|
+
const claimed = await cartService.claimForCheckout(data.cartId, data.checkoutId, context.tx);
|
|
141
144
|
if (!claimed.ok) {
|
|
142
145
|
throw new CommerceValidationError(
|
|
143
146
|
"Cart is not available for checkout. It may have already been checked out by a concurrent request.",
|
|
144
147
|
);
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
const cart = await cartService.
|
|
150
|
+
const cart = await cartService.getByIdForInternalUse(
|
|
151
|
+
data.cartId,
|
|
152
|
+
resolveOrgIdForCommerce(context.actor, context.commerceConfig),
|
|
153
|
+
context.tx as TxContext | undefined,
|
|
154
|
+
);
|
|
148
155
|
if (!cart.ok || cart.value.lineItems.length === 0) {
|
|
149
156
|
throw new CommerceValidationError("Cannot checkout an empty cart.");
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
// Cross-org guard: prevent org B from checking out org A's cart
|
|
153
|
-
const actorOrgId =
|
|
160
|
+
const actorOrgId = resolveOrgIdForCommerce(context.actor, context.commerceConfig);
|
|
154
161
|
if (cart.value.organizationId && cart.value.organizationId !== actorOrgId) {
|
|
155
162
|
throw new CommerceValidationError("Cart does not belong to this organization.");
|
|
156
163
|
}
|
|
@@ -174,7 +181,7 @@ export const validateCartNotEmpty: BeforeHook<CheckoutData> = async ({
|
|
|
174
181
|
cart.value.lineItems.map(async (item) => {
|
|
175
182
|
const entity = await catalogService.getById(item.entityId, {
|
|
176
183
|
includeAttributes: true,
|
|
177
|
-
});
|
|
184
|
+
}, context.actor, context.tx as TxContext | undefined);
|
|
178
185
|
const title = entity.ok
|
|
179
186
|
? (entity.value.attributes?.[0]?.title ?? item.entityId)
|
|
180
187
|
: item.entityId;
|
|
@@ -323,7 +330,7 @@ export const applyPromotionCodes: BeforeHook<CheckoutData> = async ({
|
|
|
323
330
|
};
|
|
324
331
|
|
|
325
332
|
const result = await promotions.applyPromotions({
|
|
326
|
-
orgId:
|
|
333
|
+
orgId: resolveOrgIdForCommerce(context.actor, context.commerceConfig),
|
|
327
334
|
cartId: data.cartId,
|
|
328
335
|
currency: data.currency,
|
|
329
336
|
subtotal: data.subtotal,
|
|
@@ -411,7 +418,7 @@ export const calculateTax: BeforeHook<CheckoutData> = async ({
|
|
|
411
418
|
...(data.shippingAddress !== undefined
|
|
412
419
|
? { toAddress: data.shippingAddress }
|
|
413
420
|
: {}),
|
|
414
|
-
},
|
|
421
|
+
}, resolveOrgIdForCommerce(context.actor ?? null, context.commerceConfig));
|
|
415
422
|
|
|
416
423
|
if (!calculated.ok) {
|
|
417
424
|
throw new CommerceValidationError(
|
|
@@ -469,7 +476,7 @@ export const calculateShipping: BeforeHook<CheckoutData> = async ({
|
|
|
469
476
|
})),
|
|
470
477
|
subtotalAfterDiscount: Math.max(0, data.subtotal - data.discountTotal),
|
|
471
478
|
currency: data.currency,
|
|
472
|
-
orgId:
|
|
479
|
+
orgId: resolveOrgIdForCommerce(context.actor ?? null, context.commerceConfig),
|
|
473
480
|
...(data.shippingAddress !== undefined
|
|
474
481
|
? { address: data.shippingAddress }
|
|
475
482
|
: {}),
|
|
@@ -546,7 +553,7 @@ export const authorizePayment: BeforeHook<CheckoutData> = async ({
|
|
|
546
553
|
: {}),
|
|
547
554
|
metadata: {
|
|
548
555
|
...(data.orderId !== undefined ? { orderId: data.orderId } : {}),
|
|
549
|
-
organizationId:
|
|
556
|
+
organizationId: resolveOrgIdForCommerce(context.actor, context.commerceConfig),
|
|
550
557
|
checkoutId: data.checkoutId,
|
|
551
558
|
cartId: data.cartId,
|
|
552
559
|
},
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type {
|
|
|
3
3
|
CommerceConfig,
|
|
4
4
|
CommercePlugin,
|
|
5
5
|
ApiKeyScopeDefinition,
|
|
6
|
+
GuestOrderAccessStrategy,
|
|
6
7
|
} from "./config/types.js";
|
|
7
8
|
export { defineCommercePlugin } from "./kernel/plugin/manifest.js";
|
|
8
9
|
export type {
|
|
@@ -31,12 +32,12 @@ export type { Kernel } from "./runtime/kernel.js";
|
|
|
31
32
|
// This avoids pulling drizzle-kit/tsx/esbuild into Turbopack's bundle graph.
|
|
32
33
|
|
|
33
34
|
export type { Actor } from "./auth/types.js";
|
|
34
|
-
export { resolveOrgId, ensureDefaultOrg, DEFAULT_ORG_ID } from "./auth/org.js";
|
|
35
|
+
export { resolveOrgId, resolveOrgIdForCommerce, ensureDefaultOrg, DEFAULT_ORG_ID } from "./auth/org.js";
|
|
35
36
|
export { createSystemActor } from "./auth/system-actor.js";
|
|
36
37
|
export { OrganizationService } from "./modules/organization/service.js";
|
|
37
38
|
export { createScopedDb } from "./kernel/database/scoped-db.js";
|
|
38
39
|
export type { ScopedOrganizationId } from "./kernel/database/scoped-db.js";
|
|
39
|
-
export { assertOwnership, assertPermission } from "./auth/permissions.js";
|
|
40
|
+
export { assertOwnership, assertPermission, requireUserId } from "./auth/permissions.js";
|
|
40
41
|
export type { AccessResult, AccessContext, AccessFn, WhereClause } from "./auth/access.js";
|
|
41
42
|
export {
|
|
42
43
|
accessOR,
|
|
@@ -141,8 +142,12 @@ export type {
|
|
|
141
142
|
} from "./modules/payments/adapter.js";
|
|
142
143
|
export { defineChannelConnector } from "./modules/channels/adapter.js";
|
|
143
144
|
export type {
|
|
145
|
+
ChannelCatalogImage,
|
|
144
146
|
ChannelCatalogItem,
|
|
147
|
+
ChannelCatalogLocalizedAttributes,
|
|
148
|
+
ChannelCatalogOptionType,
|
|
145
149
|
ChannelCatalogPage,
|
|
150
|
+
ChannelCatalogPrice,
|
|
146
151
|
ChannelCatalogVariant,
|
|
147
152
|
ChannelConnector,
|
|
148
153
|
ChannelConnectorCapabilities,
|
|
@@ -150,6 +155,14 @@ export type {
|
|
|
150
155
|
ChannelOrderLine,
|
|
151
156
|
ChannelOrderSlice,
|
|
152
157
|
ChannelConnectorError,
|
|
158
|
+
ChannelPushCatalogField,
|
|
159
|
+
ChannelPushCatalogImage,
|
|
160
|
+
ChannelPushCatalogIntent,
|
|
161
|
+
ChannelPushCatalogItem,
|
|
162
|
+
ChannelPushCatalogItemOutcome,
|
|
163
|
+
ChannelPushCatalogPreviousField,
|
|
164
|
+
ChannelPushCatalogResult,
|
|
165
|
+
ChannelPushCatalogVariant,
|
|
153
166
|
ChannelOrderStatus,
|
|
154
167
|
ChannelPushOrderResult,
|
|
155
168
|
ChannelRefundResult,
|
|
@@ -169,14 +182,20 @@ export type {
|
|
|
169
182
|
SearchSuggestParams,
|
|
170
183
|
} from "./modules/search/adapter.js";
|
|
171
184
|
export type { DatabaseAdapter } from "./kernel/database/adapter.js";
|
|
185
|
+
export { isValidFieldPath, validateFieldPath } from "./modules/catalog/ownership.js";
|
|
186
|
+
export type { FieldPath, FieldOwner } from "./modules/catalog/ownership.js";
|
|
187
|
+
export type {
|
|
188
|
+
TxContext,
|
|
189
|
+
WithTransactionOptions,
|
|
190
|
+
CatalogWriteContext,
|
|
191
|
+
HookContextCarrier,
|
|
192
|
+
} from "./kernel/database/tx-context.js";
|
|
172
193
|
export {
|
|
173
194
|
createTxContext,
|
|
174
195
|
reuseOrCreateTxContext,
|
|
175
196
|
withTransaction,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
TxContext,
|
|
179
|
-
WithTransactionOptions,
|
|
197
|
+
isWriteContextTransactional,
|
|
198
|
+
resolveWriteContextHookContext,
|
|
180
199
|
} from "./kernel/database/tx-context.js";
|
|
181
200
|
export type {
|
|
182
201
|
TaxAdapter,
|
|
@@ -235,6 +254,11 @@ export type { QueryInput, QueryResult } from "./kernel/query/executor.js";
|
|
|
235
254
|
export type { CommerceModuleTypes } from "./types/commerce-types.js";
|
|
236
255
|
|
|
237
256
|
export { staleOrderCleanupTask } from "./modules/orders/stale-order-cleanup.js";
|
|
257
|
+
export {
|
|
258
|
+
windowedGuestOrderAccess,
|
|
259
|
+
defaultGuestOrderAccess,
|
|
260
|
+
parseAccessWindow,
|
|
261
|
+
} from "./modules/orders/guest-access.js";
|
|
238
262
|
export { COMMERCE_AGENT_SYSTEM_PROMPT } from "./utils/agent-prompt.js";
|
|
239
263
|
|
|
240
264
|
export { DrizzleAnalyticsAdapter } from "./modules/analytics/drizzle-adapter.js";
|
|
@@ -95,12 +95,14 @@ export function auditMiddleware(kernel: Kernel): MiddlewareHandler {
|
|
|
95
95
|
try {
|
|
96
96
|
const requestId = c.get("requestId") as string | undefined;
|
|
97
97
|
const ctx = createHookContext({
|
|
98
|
+
// Anonymous audit records retain actor: null; organization comes from the deployment config.
|
|
98
99
|
actor: (c.get("actor") as Actor | null) ?? null,
|
|
99
100
|
...(requestId ? { requestId } : {}),
|
|
100
101
|
logger: kernel.logger,
|
|
101
102
|
services: kernel.services as unknown as ServiceContainer,
|
|
102
103
|
database: { db: kernel.database.db as unknown as PluginDb },
|
|
103
104
|
origin: "rest",
|
|
105
|
+
commerceConfig: kernel.config,
|
|
104
106
|
});
|
|
105
107
|
await kernel.services.audit.record({ entityType, entityId, event, payload, ctx });
|
|
106
108
|
} catch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { Kernel } from "../../runtime/kernel.js";
|
|
3
|
-
import { assertPermission } from "../../auth/permissions.js";
|
|
3
|
+
import { assertPermission, requireUserId } from "../../auth/permissions.js";
|
|
4
4
|
import type { Actor } from "../../auth/types.js";
|
|
5
5
|
import type { AppEnv } from "./utils.js";
|
|
6
6
|
import {
|
|
@@ -16,27 +16,30 @@ import {
|
|
|
16
16
|
updateProfileRoute,
|
|
17
17
|
createAddressRoute,
|
|
18
18
|
} from "./schemas/customer-portal.js";
|
|
19
|
-
import { isUUID, mapErrorToStatus } from "./utils.js";
|
|
19
|
+
import { isUUID, mapErrorToStatus, markRoutePermissionGuard } from "./utils.js";
|
|
20
|
+
|
|
21
|
+
type AuthenticatedActor = Actor & { userId: string };
|
|
20
22
|
|
|
21
23
|
export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
22
24
|
const router = new OpenAPIHono<AppEnv>();
|
|
23
25
|
|
|
24
|
-
router.use("*", async (c, next) => {
|
|
25
|
-
|
|
26
|
+
router.use("*", markRoutePermissionGuard(async (c, next) => {
|
|
27
|
+
const actor = c.get("actor") as Actor | null;
|
|
28
|
+
if (!actor?.userId) {
|
|
26
29
|
return c.json(
|
|
27
30
|
{ error: { code: "FORBIDDEN", message: "Authentication required." } },
|
|
28
31
|
401,
|
|
29
32
|
);
|
|
30
33
|
}
|
|
31
34
|
await next();
|
|
32
|
-
});
|
|
35
|
+
}));
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Resolves an actor whose userId is the customer profile UUID (not the Better Auth user ID).
|
|
36
39
|
* Required so that `assertOwnership(actor, order.customerId)` compares UUIDs correctly,
|
|
37
40
|
* since orders.customer_id stores the customer profile UUID, not the Better Auth string ID.
|
|
38
41
|
*/
|
|
39
|
-
async function resolveCustomerActor(actor:
|
|
42
|
+
async function resolveCustomerActor(actor: AuthenticatedActor): Promise<AuthenticatedActor | null> {
|
|
40
43
|
const customer = await kernel.services.customers.getByUserId(actor.userId, actor);
|
|
41
44
|
if (!customer.ok) return null;
|
|
42
45
|
return { ...actor, userId: customer.value.id };
|
|
@@ -44,7 +47,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
44
47
|
|
|
45
48
|
// @ts-expect-error -- openapi handler union return type
|
|
46
49
|
router.openapi(getProfileRoute, async (c) => {
|
|
47
|
-
const actor = c.get("actor") as
|
|
50
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
48
51
|
const customer = await kernel.services.customers.getByUserId(actor.userId, actor);
|
|
49
52
|
if (!customer.ok) return c.json({ error: customer.error }, 404);
|
|
50
53
|
return c.json({ data: customer.value });
|
|
@@ -52,7 +55,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
52
55
|
|
|
53
56
|
// @ts-expect-error -- openapi handler union return type
|
|
54
57
|
router.openapi(updateProfileRoute, async (c) => {
|
|
55
|
-
const actor = c.get("actor") as
|
|
58
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
56
59
|
assertPermission(actor, "customers:update:self");
|
|
57
60
|
const result = await kernel.services.customers.updateByUserId(
|
|
58
61
|
actor.userId,
|
|
@@ -64,7 +67,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
64
67
|
});
|
|
65
68
|
|
|
66
69
|
router.openapi(listAddressesRoute, async (c) => {
|
|
67
|
-
const actor = c.get("actor") as
|
|
70
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
68
71
|
const addresses = await kernel.services.customers.getAddresses(
|
|
69
72
|
actor.userId,
|
|
70
73
|
actor,
|
|
@@ -74,7 +77,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
74
77
|
|
|
75
78
|
// @ts-expect-error -- openapi handler union return type
|
|
76
79
|
router.openapi(createAddressRoute, async (c) => {
|
|
77
|
-
const actor = c.get("actor") as
|
|
80
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
78
81
|
const result = await kernel.services.customers.addAddress(
|
|
79
82
|
actor.userId,
|
|
80
83
|
c.req.valid("json") as Parameters<typeof kernel.services.customers.addAddress>[1],
|
|
@@ -86,7 +89,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
86
89
|
|
|
87
90
|
// @ts-expect-error -- openapi handler union return type
|
|
88
91
|
router.openapi(deleteAddressRoute, async (c) => {
|
|
89
|
-
const actor = c.get("actor") as
|
|
92
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
90
93
|
const result = await kernel.services.customers.deleteAddress(
|
|
91
94
|
actor.userId,
|
|
92
95
|
c.req.param("id"),
|
|
@@ -98,23 +101,27 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
98
101
|
|
|
99
102
|
// @ts-expect-error -- openapi handler union return type
|
|
100
103
|
router.openapi(listCustomerOrdersRoute, async (c) => {
|
|
101
|
-
const actor = c.get("actor") as
|
|
104
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
102
105
|
const status = c.req.query("status");
|
|
103
106
|
// Resolve customer profile UUID from Better Auth userId
|
|
104
107
|
const customerResult = await kernel.services.customers.getByUserId(actor.userId, actor);
|
|
105
108
|
if (!customerResult.ok) return c.json({ data: [], meta: { total: 0, page: 1, limit: 20, totalPages: 0 } });
|
|
106
|
-
const result = await kernel.services.orders.listByCustomer(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const result = await kernel.services.orders.listByCustomer(
|
|
110
|
+
customerResult.value.id,
|
|
111
|
+
{
|
|
112
|
+
page: Number.parseInt(c.req.query("page") ?? "1", 10),
|
|
113
|
+
limit: Number.parseInt(c.req.query("limit") ?? "20", 10),
|
|
114
|
+
...(status !== undefined ? { status } : {}),
|
|
115
|
+
},
|
|
116
|
+
actor,
|
|
117
|
+
);
|
|
118
|
+
if (!result.ok) return c.json({ error: result.error }, mapErrorToStatus(result.error));
|
|
112
119
|
return c.json({ data: result.value.items, meta: result.value.pagination });
|
|
113
120
|
});
|
|
114
121
|
|
|
115
122
|
// @ts-expect-error -- openapi handler union return type
|
|
116
123
|
router.openapi(getCustomerOrderRoute, async (c) => {
|
|
117
|
-
const actor = c.get("actor") as
|
|
124
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
118
125
|
const id = c.req.param("idOrNumber");
|
|
119
126
|
const customerActor = await resolveCustomerActor(actor);
|
|
120
127
|
if (!customerActor) return c.json({ error: { code: "NOT_FOUND", message: "Customer profile not found." } }, 404);
|
|
@@ -129,7 +136,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
129
136
|
|
|
130
137
|
// @ts-expect-error -- openapi handler union return type
|
|
131
138
|
router.openapi(getOrderTrackingRoute, async (c) => {
|
|
132
|
-
const actor = c.get("actor") as
|
|
139
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
133
140
|
const id = c.req.param("idOrNumber");
|
|
134
141
|
const customerActor = await resolveCustomerActor(actor);
|
|
135
142
|
if (!customerActor) return c.json({ error: { code: "NOT_FOUND", message: "Customer profile not found." } }, 404);
|
|
@@ -167,7 +174,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
167
174
|
|
|
168
175
|
// @ts-expect-error -- openapi handler union return type
|
|
169
176
|
router.openapi(getOrderDownloadsRoute, async (c) => {
|
|
170
|
-
const actor = c.get("actor") as
|
|
177
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
171
178
|
const customerActor = await resolveCustomerActor(actor);
|
|
172
179
|
if (!customerActor) return c.json({ error: { code: "NOT_FOUND", message: "Customer profile not found." } }, 404);
|
|
173
180
|
const orderResult = await kernel.services.orders.getById(
|
|
@@ -209,7 +216,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
209
216
|
|
|
210
217
|
// @ts-expect-error -- openapi handler union return type
|
|
211
218
|
router.openapi(listCoursesRoute, async (c) => {
|
|
212
|
-
const actor = c.get("actor") as
|
|
219
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
213
220
|
const result = await kernel.services.fulfillment.getDigitalAccess(
|
|
214
221
|
actor.userId,
|
|
215
222
|
"course",
|
|
@@ -230,7 +237,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
230
237
|
|
|
231
238
|
// @ts-expect-error -- openapi handler union return type
|
|
232
239
|
router.openapi(reorderRoute, async (c) => {
|
|
233
|
-
const actor = c.get("actor") as
|
|
240
|
+
const actor = c.get("actor") as AuthenticatedActor;
|
|
234
241
|
const customerActor = await resolveCustomerActor(actor);
|
|
235
242
|
if (!customerActor) return c.json({ error: { code: "NOT_FOUND", message: "Customer profile not found." } }, 404);
|
|
236
243
|
const orderResult = await kernel.services.orders.getById(
|
|
@@ -246,7 +253,7 @@ export function createCustomerPortalRoutes(kernel: Kernel) {
|
|
|
246
253
|
|
|
247
254
|
const cartResult = await kernel.services.cart.create(
|
|
248
255
|
{
|
|
249
|
-
customerId: actor
|
|
256
|
+
customerId: requireUserId(actor),
|
|
250
257
|
currency: orderResult.value.currency,
|
|
251
258
|
},
|
|
252
259
|
actor,
|
|
@@ -21,6 +21,8 @@ import { adminJobRoutes } from "./routes/admin-jobs.js";
|
|
|
21
21
|
import { compensationFailureAdminRoutes } from "./routes/admin/compensation-failures.js";
|
|
22
22
|
import { adminPermissionsRoutes } from "./routes/admin/permissions.js";
|
|
23
23
|
import { adminStaffRoutes } from "./routes/admin/staff.js";
|
|
24
|
+
import { adminEntityFieldDefinitionRoutes } from "./routes/admin/entity-field-definitions.js";
|
|
25
|
+
import { adminCustomFieldProposalRoutes } from "./routes/admin/custom-field-proposals.js";
|
|
24
26
|
import { customerRoutes } from "./routes/customers.js";
|
|
25
27
|
import { documentRoutes } from "./routes/documents.js";
|
|
26
28
|
import { settingsRoutes } from "./routes/settings.js";
|
|
@@ -94,6 +96,8 @@ export function createRestRoutes(kernel: Kernel) {
|
|
|
94
96
|
router.route("/admin", compensationFailureAdminRoutes(kernel));
|
|
95
97
|
router.route("/admin", adminPermissionsRoutes(kernel));
|
|
96
98
|
router.route("/admin", adminStaffRoutes(kernel));
|
|
99
|
+
router.route("/admin", adminEntityFieldDefinitionRoutes(kernel));
|
|
100
|
+
router.route("/admin", adminCustomFieldProposalRoutes(kernel));
|
|
97
101
|
|
|
98
102
|
// API Reference (Scalar) — disabled in production unless config.exposeOpenApiSpec is true
|
|
99
103
|
const exposeSpec = kernel.config.exposeOpenApiSpec ?? (process.env.NODE_ENV !== "production");
|