@porulle/core 0.11.0 → 0.14.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/actor.d.ts +10 -0
- package/dist/auth/actor.d.ts.map +1 -0
- package/dist/auth/actor.js +91 -0
- 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 +16 -92
- 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 +19 -1
- 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 +32 -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 +7 -5
- 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/route-coverage.d.ts +14 -0
- package/dist/interfaces/rest/route-coverage.d.ts.map +1 -0
- package/dist/interfaces/rest/route-coverage.js +81 -0
- package/dist/interfaces/rest/router.d.ts +7 -2
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/router.js +8 -4
- package/dist/interfaces/rest/routes/admin/compensation-failures.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/compensation-failures.js +7 -5
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +2 -2
- package/dist/interfaces/rest/routes/admin/permissions.d.ts +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.js +2 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/staff.js +126 -66
- package/dist/interfaces/rest/routes/admin-jobs.js +3 -3
- package/dist/interfaces/rest/routes/audit.js +3 -3
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +13 -10
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +10 -2
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +30 -35
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/documents.js +10 -5
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +2 -0
- package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/media.js +7 -3
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +26 -8
- package/dist/interfaces/rest/routes/pricing.js +2 -2
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +13 -10
- package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/search.js +2 -1
- package/dist/interfaces/rest/schemas/carts.d.ts +7 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.d.ts +204 -0
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/responses.d.ts +71 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.d.ts +15 -0
- package/dist/interfaces/rest/utils.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.js +45 -2
- package/dist/kernel/compensation/executor.js +2 -2
- package/dist/kernel/database/plugin-types.d.ts +11 -0
- package/dist/kernel/database/plugin-types.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.d.ts +12 -0
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +12 -0
- package/dist/kernel/database/tx-context.d.ts +8 -0
- package/dist/kernel/database/tx-context.d.ts.map +1 -1
- package/dist/kernel/database/tx-context.js +13 -0
- package/dist/kernel/hooks/create-context.d.ts +2 -0
- package/dist/kernel/hooks/create-context.d.ts.map +1 -1
- package/dist/kernel/hooks/create-context.js +1 -0
- package/dist/kernel/hooks/types.d.ts +2 -0
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +9 -5
- package/dist/modules/analytics/service.js +2 -2
- package/dist/modules/audit/service.js +3 -3
- package/dist/modules/cart/repository/index.d.ts +13 -7
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +16 -10
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +4 -0
- package/dist/modules/cart/service.d.ts +22 -9
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +57 -33
- package/dist/modules/catalog/brand-service.js +7 -7
- package/dist/modules/catalog/category-service.js +7 -7
- package/dist/modules/catalog/entity-service.d.ts +14 -7
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +109 -31
- package/dist/modules/catalog/read-policy.d.ts +9 -0
- package/dist/modules/catalog/read-policy.d.ts.map +1 -0
- package/dist/modules/catalog/read-policy.js +8 -0
- package/dist/modules/catalog/repository/index.d.ts +3 -2
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +14 -4
- package/dist/modules/catalog/service.d.ts +15 -13
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +25 -18
- package/dist/modules/customers/module.d.ts.map +1 -1
- package/dist/modules/customers/module.js +1 -0
- package/dist/modules/customers/service.d.ts +2 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +21 -20
- package/dist/modules/documents/module.d.ts.map +1 -1
- package/dist/modules/documents/module.js +1 -0
- package/dist/modules/documents/service.d.ts +18 -4
- package/dist/modules/documents/service.d.ts.map +1 -1
- package/dist/modules/documents/service.js +54 -16
- package/dist/modules/fulfillment/module.d.ts.map +1 -1
- package/dist/modules/fulfillment/module.js +1 -0
- package/dist/modules/fulfillment/service.d.ts +2 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +15 -6
- package/dist/modules/inventory/service.d.ts.map +1 -1
- package/dist/modules/inventory/service.js +15 -14
- package/dist/modules/media/service.js +6 -6
- package/dist/modules/orders/guest-access.d.ts +27 -0
- package/dist/modules/orders/guest-access.d.ts.map +1 -0
- package/dist/modules/orders/guest-access.js +46 -0
- package/dist/modules/orders/module.d.ts.map +1 -1
- package/dist/modules/orders/module.js +1 -0
- package/dist/modules/orders/quote.d.ts.map +1 -1
- package/dist/modules/orders/quote.js +1 -0
- package/dist/modules/orders/repository/index.d.ts +1 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +2 -2
- package/dist/modules/orders/schema.d.ts +17 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +5 -3
- package/dist/modules/orders/service.d.ts +16 -7
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +184 -55
- package/dist/modules/pricing/module.d.ts.map +1 -1
- package/dist/modules/pricing/module.js +1 -0
- package/dist/modules/pricing/service.d.ts +2 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +12 -11
- package/dist/modules/promotions/module.d.ts.map +1 -1
- package/dist/modules/promotions/module.js +1 -0
- package/dist/modules/promotions/service.d.ts +3 -0
- package/dist/modules/promotions/service.d.ts.map +1 -1
- package/dist/modules/promotions/service.js +19 -11
- package/dist/modules/search/adapter.d.ts +11 -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 +4 -1
- package/dist/modules/search/service.d.ts +1 -0
- package/dist/modules/search/service.d.ts.map +1 -1
- package/dist/modules/search/service.js +57 -6
- package/dist/modules/settings/module.d.ts.map +1 -1
- package/dist/modules/settings/module.js +1 -0
- package/dist/modules/settings/service.d.ts +3 -0
- package/dist/modules/settings/service.d.ts.map +1 -1
- package/dist/modules/settings/service.js +6 -4
- package/dist/modules/shipping/service.js +9 -9
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +1 -0
- package/dist/modules/tax/service.d.ts +3 -0
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +11 -9
- package/dist/modules/webhooks/hook.js +2 -2
- package/dist/modules/webhooks/module.d.ts.map +1 -1
- package/dist/modules/webhooks/module.js +1 -0
- package/dist/modules/webhooks/service.d.ts +2 -0
- package/dist/modules/webhooks/service.d.ts.map +1 -1
- package/dist/modules/webhooks/service.js +4 -4
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +28 -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/actor.ts +110 -0
- package/src/auth/auth-schema-guard.ts +143 -0
- package/src/auth/auth-schema.ts +50 -19
- package/src/auth/middleware.ts +25 -101
- 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 +19 -1
- 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 +31 -4
- package/src/hooks/checkout.ts +19 -12
- package/src/index.ts +21 -6
- package/src/interfaces/rest/audit-middleware.ts +2 -0
- package/src/interfaces/rest/customer-portal.ts +31 -24
- package/src/interfaces/rest/route-coverage.ts +103 -0
- package/src/interfaces/rest/router.ts +16 -10
- package/src/interfaces/rest/routes/admin/compensation-failures.ts +8 -5
- package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +2 -2
- package/src/interfaces/rest/routes/admin/permissions.ts +3 -1
- package/src/interfaces/rest/routes/admin/staff.ts +161 -72
- package/src/interfaces/rest/routes/admin-jobs.ts +3 -3
- package/src/interfaces/rest/routes/audit.ts +3 -3
- package/src/interfaces/rest/routes/carts.ts +17 -7
- package/src/interfaces/rest/routes/catalog.ts +11 -0
- package/src/interfaces/rest/routes/checkout.ts +41 -38
- package/src/interfaces/rest/routes/documents.ts +15 -1
- package/src/interfaces/rest/routes/inventory.ts +2 -0
- package/src/interfaces/rest/routes/media.ts +8 -3
- package/src/interfaces/rest/routes/orders.ts +27 -8
- package/src/interfaces/rest/routes/pricing.ts +2 -2
- package/src/interfaces/rest/routes/promotions.ts +12 -9
- package/src/interfaces/rest/routes/search.ts +3 -1
- package/src/interfaces/rest/utils.ts +61 -2
- package/src/kernel/compensation/executor.ts +2 -2
- package/src/kernel/database/plugin-types.ts +11 -0
- package/src/kernel/database/scoped-db.ts +12 -0
- package/src/kernel/database/tx-context.ts +25 -0
- package/src/kernel/hooks/create-context.ts +3 -0
- package/src/kernel/hooks/types.ts +2 -0
- package/src/kernel/plugin/manifest.ts +9 -5
- package/src/modules/analytics/service.ts +2 -2
- package/src/modules/audit/service.ts +3 -3
- package/src/modules/cart/repository/index.ts +22 -8
- package/src/modules/cart/schema.ts +4 -0
- package/src/modules/cart/service.ts +62 -27
- package/src/modules/catalog/brand-service.ts +7 -7
- package/src/modules/catalog/category-service.ts +7 -7
- package/src/modules/catalog/entity-service.ts +117 -35
- package/src/modules/catalog/read-policy.ts +16 -0
- package/src/modules/catalog/repository/index.ts +19 -3
- package/src/modules/catalog/service.ts +46 -31
- package/src/modules/customers/module.ts +2 -0
- package/src/modules/customers/service.ts +23 -22
- package/src/modules/documents/module.ts +2 -0
- package/src/modules/documents/service.ts +74 -11
- package/src/modules/fulfillment/module.ts +2 -0
- package/src/modules/fulfillment/service.ts +17 -6
- package/src/modules/inventory/service.ts +15 -24
- package/src/modules/media/service.ts +6 -6
- package/src/modules/orders/guest-access.ts +55 -0
- package/src/modules/orders/module.ts +1 -0
- package/src/modules/orders/quote.ts +1 -0
- package/src/modules/orders/repository/index.ts +2 -0
- package/src/modules/orders/schema.ts +5 -3
- package/src/modules/orders/service.ts +302 -52
- package/src/modules/pricing/module.ts +2 -0
- package/src/modules/pricing/service.ts +14 -16
- package/src/modules/promotions/module.ts +2 -0
- package/src/modules/promotions/service.ts +25 -16
- package/src/modules/search/adapter.ts +9 -0
- package/src/modules/search/module.ts +5 -1
- package/src/modules/search/service.ts +55 -6
- package/src/modules/settings/module.ts +2 -0
- package/src/modules/settings/service.ts +8 -4
- package/src/modules/shipping/service.ts +9 -9
- package/src/modules/tax/module.ts +1 -0
- package/src/modules/tax/service.ts +13 -9
- package/src/modules/webhooks/hook.ts +2 -2
- package/src/modules/webhooks/module.ts +2 -0
- package/src/modules/webhooks/service.ts +6 -4
- package/src/runtime/server.ts +41 -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
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
|
@@ -200,10 +200,17 @@ export interface AuthConfig {
|
|
|
200
200
|
*/
|
|
201
201
|
storeResolver?: (request: Request) => string | null | Promise<string | null>;
|
|
202
202
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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.
|
|
207
214
|
*/
|
|
208
215
|
strictOrgResolution?: boolean;
|
|
209
216
|
/**
|
|
@@ -258,7 +265,25 @@ export interface CheckoutConfig {
|
|
|
258
265
|
};
|
|
259
266
|
}
|
|
260
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
|
+
|
|
261
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;
|
|
262
287
|
hooks?: {
|
|
263
288
|
beforeCreate?: BeforeHook<unknown>[];
|
|
264
289
|
afterCreate?: AfterHook<unknown>[];
|
|
@@ -428,6 +453,8 @@ export interface CommerceConfig {
|
|
|
428
453
|
api?: number;
|
|
429
454
|
/** Requests per minute for auth endpoints. Default: 10. */
|
|
430
455
|
auth?: number;
|
|
456
|
+
/** Requests per minute for session reads. Default: 120. */
|
|
457
|
+
session?: number;
|
|
431
458
|
/** Requests per minute for checkout. Default: 5. */
|
|
432
459
|
checkout?: number;
|
|
433
460
|
/** Attempts per 15 minutes for sign-in per email. Default: 10. */
|
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,17 @@ 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 {
|
|
35
|
+
export {
|
|
36
|
+
AUTH_COOKIE_PREFIX,
|
|
37
|
+
resolveActor,
|
|
38
|
+
SESSION_COOKIE_NAME,
|
|
39
|
+
} from "./auth/actor.js";
|
|
40
|
+
export { resolveOrgId, resolveOrgIdForCommerce, ensureDefaultOrg, DEFAULT_ORG_ID } from "./auth/org.js";
|
|
35
41
|
export { createSystemActor } from "./auth/system-actor.js";
|
|
36
42
|
export { OrganizationService } from "./modules/organization/service.js";
|
|
37
43
|
export { createScopedDb } from "./kernel/database/scoped-db.js";
|
|
38
44
|
export type { ScopedOrganizationId } from "./kernel/database/scoped-db.js";
|
|
39
|
-
export { assertOwnership, assertPermission } from "./auth/permissions.js";
|
|
45
|
+
export { assertOwnership, assertPermission, requireUserId } from "./auth/permissions.js";
|
|
40
46
|
export type { AccessResult, AccessContext, AccessFn, WhereClause } from "./auth/access.js";
|
|
41
47
|
export {
|
|
42
48
|
accessOR,
|
|
@@ -183,14 +189,18 @@ export type {
|
|
|
183
189
|
export type { DatabaseAdapter } from "./kernel/database/adapter.js";
|
|
184
190
|
export { isValidFieldPath, validateFieldPath } from "./modules/catalog/ownership.js";
|
|
185
191
|
export type { FieldPath, FieldOwner } from "./modules/catalog/ownership.js";
|
|
192
|
+
export type {
|
|
193
|
+
TxContext,
|
|
194
|
+
WithTransactionOptions,
|
|
195
|
+
CatalogWriteContext,
|
|
196
|
+
HookContextCarrier,
|
|
197
|
+
} from "./kernel/database/tx-context.js";
|
|
186
198
|
export {
|
|
187
199
|
createTxContext,
|
|
188
200
|
reuseOrCreateTxContext,
|
|
189
201
|
withTransaction,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
TxContext,
|
|
193
|
-
WithTransactionOptions,
|
|
202
|
+
isWriteContextTransactional,
|
|
203
|
+
resolveWriteContextHookContext,
|
|
194
204
|
} from "./kernel/database/tx-context.js";
|
|
195
205
|
export type {
|
|
196
206
|
TaxAdapter,
|
|
@@ -249,6 +259,11 @@ export type { QueryInput, QueryResult } from "./kernel/query/executor.js";
|
|
|
249
259
|
export type { CommerceModuleTypes } from "./types/commerce-types.js";
|
|
250
260
|
|
|
251
261
|
export { staleOrderCleanupTask } from "./modules/orders/stale-order-cleanup.js";
|
|
262
|
+
export {
|
|
263
|
+
windowedGuestOrderAccess,
|
|
264
|
+
defaultGuestOrderAccess,
|
|
265
|
+
parseAccessWindow,
|
|
266
|
+
} from "./modules/orders/guest-access.js";
|
|
252
267
|
export { COMMERCE_AGENT_SYSTEM_PROMPT } from "./utils/agent-prompt.js";
|
|
253
268
|
|
|
254
269
|
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,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { RouterRoute } from "hono/types";
|
|
2
|
+
import { isRoutePermissionGuard } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export type PublicRoute = {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
justification: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const PUBLIC_ROUTES: readonly PublicRoute[] = [
|
|
11
|
+
{ method: "GET", path: "/api/health", justification: "Load balancers need an unauthenticated liveness probe." },
|
|
12
|
+
{ method: "GET", path: "/api/reference", justification: "The development API explorer is intentionally public when enabled." },
|
|
13
|
+
{ method: "GET", path: "/api/doc", justification: "The generated OpenAPI document is public when documentation exposure is enabled." },
|
|
14
|
+
{ method: "GET", path: "/api/doc-ext", justification: "The enriched OpenAPI document is public when documentation exposure is enabled." },
|
|
15
|
+
{ method: "POST", path: "/api/carts", justification: "Anonymous storefront shoppers create secret-backed guest carts." },
|
|
16
|
+
{ method: "GET", path: "/api/carts/:id", justification: "Guest cart reads require the cart secret; customer carts require ownership in the service." },
|
|
17
|
+
{ method: "POST", path: "/api/carts/:id/items", justification: "Anonymous storefront shoppers add items to a secret-backed guest cart." },
|
|
18
|
+
{ method: "PATCH", path: "/api/carts/:id/items/:itemId", justification: "Anonymous storefront shoppers change quantities in a secret-backed guest cart." },
|
|
19
|
+
{ method: "DELETE", path: "/api/carts/:id/items/:itemId", justification: "Anonymous storefront shoppers remove items from a secret-backed guest cart." },
|
|
20
|
+
{ method: "POST", path: "/api/checkout", justification: "Anonymous storefront checkout is authorized by the customer actor and cart/payment pipeline." },
|
|
21
|
+
{ method: "GET", path: "/api/media/:id", justification: "Public media assets redirect to their configured public storage URL; signed URLs still require an actor in the handler." },
|
|
22
|
+
{ method: "POST", path: "/api/promotions/validate", justification: "Storefront shoppers validate a promotion code before checkout; the route is rate-limited." },
|
|
23
|
+
{ method: "POST", path: "/api/payments/webhook", justification: "The payment provider authenticates this endpoint with its signed webhook payload." },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
type RouteTableApp = {
|
|
27
|
+
routes: readonly RouterRoute[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ComposedRouteHandler = {
|
|
31
|
+
__COMPOSED_HANDLER?: unknown;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function hasPermissionGuard(handler: unknown, method?: string, seen = new Set<unknown>()): boolean {
|
|
35
|
+
if (typeof handler !== "function" || seen.has(handler)) return false;
|
|
36
|
+
seen.add(handler);
|
|
37
|
+
if (isRoutePermissionGuard(handler, method)) return true;
|
|
38
|
+
const composed = (handler as unknown as ComposedRouteHandler).__COMPOSED_HANDLER;
|
|
39
|
+
return hasPermissionGuard(composed, method, seen);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function splitPath(path: string): string[] {
|
|
43
|
+
return path.split("/").filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pathPatternCovers(pattern: string, target: string): boolean {
|
|
47
|
+
const patternParts = splitPath(pattern);
|
|
48
|
+
const targetParts = splitPath(target);
|
|
49
|
+
const wildcardIndex = patternParts.indexOf("*");
|
|
50
|
+
if (wildcardIndex >= 0) {
|
|
51
|
+
if (wildcardIndex !== patternParts.length - 1 || targetParts.length < wildcardIndex) return false;
|
|
52
|
+
} else if (patternParts.length !== targetParts.length) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return patternParts.every((part, index) => {
|
|
57
|
+
if (part === "*") return true;
|
|
58
|
+
const targetPart = targetParts[index];
|
|
59
|
+
return part.startsWith(":") || part === targetPart;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function publicRouteCovers(route: RouterRoute): boolean {
|
|
64
|
+
return PUBLIC_ROUTES.some((publicRoute) =>
|
|
65
|
+
publicRoute.method === route.method && pathPatternCovers(publicRoute.path, route.path),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function routeKey(route: RouterRoute): string {
|
|
70
|
+
return `${route.method} ${route.path}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function findUncoveredRoutes(app: RouteTableApp): string[] {
|
|
74
|
+
const routes = app.routes;
|
|
75
|
+
const guardedRoutes = routes
|
|
76
|
+
.map((route, index) => ({ route, index }))
|
|
77
|
+
.filter(({ route }) => route.method === "ALL" && hasPermissionGuard(route.handler));
|
|
78
|
+
const endpoints = new Map<string, { route: RouterRoute; index: number }>();
|
|
79
|
+
|
|
80
|
+
for (const [index, route] of routes.entries()) {
|
|
81
|
+
if (route.method === "ALL") continue;
|
|
82
|
+
const key = routeKey(route);
|
|
83
|
+
if (!endpoints.has(key)) endpoints.set(key, { route, index });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [...endpoints.values()]
|
|
87
|
+
.filter(({ route, index }) => {
|
|
88
|
+
if (publicRouteCovers(route)) return false;
|
|
89
|
+
const directlyGuarded = hasPermissionGuard(route.handler, route.method);
|
|
90
|
+
const coveredByMiddleware = guardedRoutes.some(({ route: guard, index: guardIndex }) =>
|
|
91
|
+
guardIndex <= index && pathPatternCovers(guard.path, route.path) && hasPermissionGuard(guard.handler, route.method),
|
|
92
|
+
);
|
|
93
|
+
return !directlyGuarded && !coveredByMiddleware;
|
|
94
|
+
})
|
|
95
|
+
.map(({ route }) => routeKey(route));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function assertRouteCoverage(app: RouteTableApp): void {
|
|
99
|
+
const uncovered = findUncoveredRoutes(app);
|
|
100
|
+
if (uncovered.length > 0) {
|
|
101
|
+
throw new Error(`Unclassified routes: ${uncovered.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
30
30
|
import { ErrorSchema } from "./schemas/shared.js";
|
|
31
|
-
import { mapErrorToResponse, mapErrorToStatus } from "./utils.js";
|
|
31
|
+
import { mapErrorToResponse, mapErrorToStatus, markRoutePermissionGuard } from "./utils.js";
|
|
32
|
+
import type { CommerceConfig } from "../../config/types.js";
|
|
32
33
|
import type { PluginRouteRegistration } from "../../kernel/plugin/manifest.js";
|
|
33
34
|
import { createScopedDb } from "../../kernel/database/scoped-db.js";
|
|
34
|
-
import {
|
|
35
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
35
36
|
|
|
36
37
|
// ─── Shared OpenAPI Error Responses ──────────────────────────────────────────
|
|
37
38
|
|
|
@@ -61,8 +62,8 @@ export interface RouteHandlerContext {
|
|
|
61
62
|
/** Path parameters, auto-extracted from {id} segments. */
|
|
62
63
|
params: Record<string, string>;
|
|
63
64
|
/** Authenticated actor. Guaranteed non-null if .auth() or .permission() was called. */
|
|
64
|
-
actor: { userId: string; role: string; permissions: string[]; vendorId?: string | null; [key: string]: unknown } | null;
|
|
65
|
-
/** Resolved organization ID. Derived from actor.organizationId
|
|
65
|
+
actor: { userId: string | null; role: string; permissions: string[]; vendorId?: string | null; [key: string]: unknown } | null;
|
|
66
|
+
/** Resolved organization ID. Derived from actor.organizationId or the deployment config. */
|
|
66
67
|
orgId: string;
|
|
67
68
|
/** Kernel services (orders, cart, inventory, etc.) */
|
|
68
69
|
services: Record<string, unknown>;
|
|
@@ -91,7 +92,7 @@ class RouteChain {
|
|
|
91
92
|
private fullPath: string,
|
|
92
93
|
private tag: string,
|
|
93
94
|
private routesList: PluginRouteRegistration[],
|
|
94
|
-
private pluginCtx?: { services: Record<string, unknown>; db: unknown },
|
|
95
|
+
private pluginCtx?: { config?: CommerceConfig; services: Record<string, unknown>; db: unknown },
|
|
95
96
|
) {
|
|
96
97
|
const paramNames = extractPathParams(fullPath);
|
|
97
98
|
if (paramNames.length > 0) {
|
|
@@ -214,6 +215,7 @@ class RouteChain {
|
|
|
214
215
|
|
|
215
216
|
// Use plugin context if provided (plugin routes), fallback to kernel on Hono context (core routes)
|
|
216
217
|
const kernel = pluginCtx ?? (ctx.get("kernel") as {
|
|
218
|
+
config?: CommerceConfig;
|
|
217
219
|
services?: Record<string, unknown>;
|
|
218
220
|
db?: unknown;
|
|
219
221
|
} | undefined);
|
|
@@ -222,7 +224,7 @@ class RouteChain {
|
|
|
222
224
|
// Plugin route handlers receive a db that auto-filters queries
|
|
223
225
|
// and auto-stamps inserts with the actor's organizationId.
|
|
224
226
|
const rawDb = kernel?.db;
|
|
225
|
-
const orgId =
|
|
227
|
+
const orgId = resolveOrgIdForCommerce(actor, kernel?.config);
|
|
226
228
|
const scopedDb = rawDb ? createScopedDb(rawDb as Record<string, unknown>, orgId) : rawDb;
|
|
227
229
|
|
|
228
230
|
const handlerCtx: RouteHandlerContext = {
|
|
@@ -262,7 +264,10 @@ class RouteChain {
|
|
|
262
264
|
}
|
|
263
265
|
};
|
|
264
266
|
|
|
265
|
-
|
|
267
|
+
const registeredHandler = requireAuth || requiredPermission
|
|
268
|
+
? markRoutePermissionGuard(honoHandler)
|
|
269
|
+
: honoHandler;
|
|
270
|
+
this.routesList.push({ openapi: routeConfig, handler: registeredHandler as (...args: unknown[]) => unknown });
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
|
|
@@ -271,12 +276,12 @@ class RouteChain {
|
|
|
271
276
|
class RouterImpl {
|
|
272
277
|
private _routes: PluginRouteRegistration[] = [];
|
|
273
278
|
private _prefix: string;
|
|
274
|
-
private _pluginCtx: { services: Record<string, unknown>; db: unknown } | undefined;
|
|
279
|
+
private _pluginCtx: { config?: CommerceConfig; services: Record<string, unknown>; db: unknown } | undefined;
|
|
275
280
|
|
|
276
281
|
constructor(
|
|
277
282
|
private tag: string,
|
|
278
283
|
prefix: string,
|
|
279
|
-
pluginCtx?: { services?: Record<string, unknown>; database?: { db: unknown } },
|
|
284
|
+
pluginCtx?: { config?: CommerceConfig; services?: Record<string, unknown>; database?: { db: unknown } },
|
|
280
285
|
) {
|
|
281
286
|
// Normalize: ensure /api prefix, strip trailing slash
|
|
282
287
|
let p = prefix.startsWith("/api") ? prefix : `/api${prefix}`;
|
|
@@ -286,6 +291,7 @@ class RouterImpl {
|
|
|
286
291
|
// If plugin context is provided, wire services + db for handler access
|
|
287
292
|
if (pluginCtx) {
|
|
288
293
|
this._pluginCtx = {
|
|
294
|
+
...(pluginCtx.config !== undefined ? { config: pluginCtx.config } : {}),
|
|
289
295
|
services: pluginCtx.services ?? {},
|
|
290
296
|
db: pluginCtx.database?.db,
|
|
291
297
|
};
|
|
@@ -330,6 +336,6 @@ class RouterImpl {
|
|
|
330
336
|
* }
|
|
331
337
|
* ```
|
|
332
338
|
*/
|
|
333
|
-
export function router(tag: string, prefix: string, ctx?: { services?: Record<string, unknown>; database?: { db: unknown } }): RouterImpl {
|
|
339
|
+
export function router(tag: string, prefix: string, ctx?: { config?: CommerceConfig; services?: Record<string, unknown>; database?: { db: unknown } }): RouterImpl {
|
|
334
340
|
return new RouterImpl(tag, prefix, ctx);
|
|
335
341
|
}
|