@porulle/core 0.7.0 → 0.9.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/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +26 -7
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +8 -5
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -0
- package/dist/config/types.d.ts +26 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.d.ts +3 -0
- package/dist/generated/plugin-manifest.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.js +1 -0
- package/dist/hooks/checkout-completion.d.ts.map +1 -1
- package/dist/hooks/checkout-completion.js +2 -2
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +17 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/staff.js +206 -0
- package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
- package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/analytics.js +24 -0
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +39 -2
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +17 -1
- package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +97 -21
- package/dist/interfaces/rest/routes/documents.d.ts +10 -0
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/documents.js +40 -0
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +13 -0
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +111 -1
- package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/pricing.js +36 -1
- package/dist/interfaces/rest/routes/settings.d.ts +5 -0
- package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/settings.js +50 -0
- package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
- package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/shipping.js +68 -0
- package/dist/interfaces/rest/routes/tax.d.ts +5 -0
- package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/tax.js +68 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/analytics.js +33 -0
- package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/carts.js +38 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +48 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/checkout.js +4 -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/documents.d.ts +509 -0
- package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/documents.js +65 -0
- package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.js +234 -1
- package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/pricing.js +70 -0
- package/dist/interfaces/rest/schemas/responses.d.ts +190 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +17 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
- package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/settings.js +79 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/shipping.js +146 -0
- package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
- package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/tax.js +132 -0
- package/dist/kernel/database/schema.d.ts +4 -0
- package/dist/kernel/database/schema.d.ts.map +1 -1
- package/dist/kernel/database/schema.js +8 -0
- package/dist/kernel/database/scoped-db.d.ts +2 -1
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +62 -1
- package/dist/kernel/hooks/types.d.ts +1 -1
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts +26 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +17 -2
- package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
- package/dist/modules/analytics/drizzle-adapter.js +16 -3
- package/dist/modules/analytics/module.d.ts.map +1 -1
- package/dist/modules/analytics/module.js +6 -0
- package/dist/modules/analytics/reports.d.ts +58 -0
- package/dist/modules/analytics/reports.d.ts.map +1 -0
- package/dist/modules/analytics/reports.js +249 -0
- package/dist/modules/analytics/service.d.ts +10 -0
- package/dist/modules/analytics/service.d.ts.map +1 -1
- package/dist/modules/analytics/service.js +14 -0
- package/dist/modules/cart/repository/index.d.ts +7 -0
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +25 -1
- 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 +2 -0
- package/dist/modules/cart/schemas.d.ts +1 -0
- package/dist/modules/cart/schemas.d.ts.map +1 -1
- package/dist/modules/cart/schemas.js +1 -0
- package/dist/modules/cart/service.d.ts +32 -0
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +61 -2
- package/dist/modules/catalog/entity-service.d.ts +21 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +166 -5
- package/dist/modules/catalog/schema.d.ts +34 -0
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +5 -0
- package/dist/modules/catalog/schemas.d.ts +3 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +3 -0
- package/dist/modules/catalog/service.d.ts +36 -0
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +6 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +6 -0
- package/dist/modules/documents/module.d.ts +7 -0
- package/dist/modules/documents/module.d.ts.map +1 -0
- package/dist/modules/documents/module.js +12 -0
- package/dist/modules/documents/pdf.d.ts +25 -0
- package/dist/modules/documents/pdf.d.ts.map +1 -0
- package/dist/modules/documents/pdf.js +80 -0
- package/dist/modules/documents/render.d.ts +32 -0
- package/dist/modules/documents/render.d.ts.map +1 -0
- package/dist/modules/documents/render.js +190 -0
- package/dist/modules/documents/repository/index.d.ts +25 -0
- package/dist/modules/documents/repository/index.d.ts.map +1 -0
- package/dist/modules/documents/repository/index.js +47 -0
- package/dist/modules/documents/schema.d.ts +214 -0
- package/dist/modules/documents/schema.d.ts.map +1 -0
- package/dist/modules/documents/schema.js +42 -0
- package/dist/modules/documents/service.d.ts +51 -0
- package/dist/modules/documents/service.d.ts.map +1 -0
- package/dist/modules/documents/service.js +119 -0
- package/dist/modules/fulfillment/service.d.ts +18 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +85 -1
- package/dist/modules/orders/repository/index.d.ts +18 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +86 -1
- package/dist/modules/orders/schema.d.ts +391 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +56 -1
- package/dist/modules/orders/service.d.ts +102 -3
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +653 -29
- package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
- package/dist/modules/orders/stale-order-cleanup.js +13 -1
- package/dist/modules/pricing/repository/index.d.ts +5 -0
- package/dist/modules/pricing/repository/index.d.ts.map +1 -1
- package/dist/modules/pricing/repository/index.js +19 -0
- package/dist/modules/pricing/service.d.ts +18 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +43 -5
- package/dist/modules/settings/module.d.ts +6 -0
- package/dist/modules/settings/module.d.ts.map +1 -0
- package/dist/modules/settings/module.js +11 -0
- package/dist/modules/settings/repository/index.d.ts +16 -0
- package/dist/modules/settings/repository/index.d.ts.map +1 -0
- package/dist/modules/settings/repository/index.js +41 -0
- package/dist/modules/settings/schema.d.ts +121 -0
- package/dist/modules/settings/schema.d.ts.map +1 -0
- package/dist/modules/settings/schema.js +24 -0
- package/dist/modules/settings/service.d.ts +26 -0
- package/dist/modules/settings/service.d.ts.map +1 -0
- package/dist/modules/settings/service.js +51 -0
- package/dist/modules/shipping/calculator.d.ts +5 -0
- package/dist/modules/shipping/calculator.d.ts.map +1 -1
- package/dist/modules/shipping/calculator.js +10 -0
- package/dist/modules/shipping/module.d.ts +5 -1
- package/dist/modules/shipping/module.d.ts.map +1 -1
- package/dist/modules/shipping/module.js +4 -1
- package/dist/modules/shipping/repository/index.d.ts +27 -2
- package/dist/modules/shipping/repository/index.d.ts.map +1 -1
- package/dist/modules/shipping/repository/index.js +105 -1
- package/dist/modules/shipping/schema.d.ts +419 -0
- package/dist/modules/shipping/schema.d.ts.map +1 -0
- package/dist/modules/shipping/schema.js +56 -0
- package/dist/modules/shipping/service.d.ts +54 -0
- package/dist/modules/shipping/service.d.ts.map +1 -1
- package/dist/modules/shipping/service.js +161 -2
- package/dist/modules/tax/adapter.d.ts +13 -0
- package/dist/modules/tax/adapter.d.ts.map +1 -1
- package/dist/modules/tax/module.d.ts +5 -1
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +4 -1
- package/dist/modules/tax/repository/index.d.ts +38 -2
- package/dist/modules/tax/repository/index.d.ts.map +1 -1
- package/dist/modules/tax/repository/index.js +138 -1
- package/dist/modules/tax/schema.d.ts +355 -0
- package/dist/modules/tax/schema.d.ts.map +1 -0
- package/dist/modules/tax/schema.js +55 -0
- package/dist/modules/tax/service.d.ts +60 -1
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +227 -2
- package/dist/runtime/kernel-modules.d.ts +15 -2
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/runtime/kernel-modules.js +4 -0
- package/dist/runtime/kernel-types.d.ts +5 -1
- package/dist/runtime/kernel-types.d.ts.map +1 -1
- package/dist/runtime/kernel-types.js +2 -0
- package/dist/runtime/server.js +1 -1
- package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
- package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
- package/dist/test-utils/create-plugin-test-app.js +15 -3
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +2 -0
- package/dist/test-utils/order-test-helpers.d.ts +12 -0
- package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
- package/dist/test-utils/order-test-helpers.js +18 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +7 -0
- package/dist/test-utils/test-actors.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/package.json +3 -3
- package/src/auth/middleware.ts +26 -7
- package/src/auth/setup.ts +11 -4
- package/src/config/defaults.ts +7 -0
- package/src/config/types.ts +26 -1
- package/src/generated/plugin-capabilities.d.ts +1 -0
- package/src/generated/plugin-manifest.ts +1 -0
- package/src/generated/plugin-repositories.d.ts +1 -0
- package/src/hooks/checkout-completion.ts +22 -16
- package/src/hooks/checkout.ts +36 -11
- package/src/index.ts +1 -0
- package/src/interfaces/rest/index.ts +12 -0
- package/src/interfaces/rest/routes/admin/staff.ts +260 -0
- package/src/interfaces/rest/routes/analytics.ts +33 -0
- package/src/interfaces/rest/routes/carts.ts +52 -2
- package/src/interfaces/rest/routes/catalog.ts +34 -0
- package/src/interfaces/rest/routes/checkout.ts +114 -27
- package/src/interfaces/rest/routes/documents.ts +61 -0
- package/src/interfaces/rest/routes/inventory.ts +14 -0
- package/src/interfaces/rest/routes/orders.ts +148 -1
- package/src/interfaces/rest/routes/pricing.ts +44 -1
- package/src/interfaces/rest/routes/settings.ts +61 -0
- package/src/interfaces/rest/routes/shipping.ts +92 -0
- package/src/interfaces/rest/routes/tax.ts +90 -0
- package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
- package/src/interfaces/rest/schemas/analytics.ts +39 -0
- package/src/interfaces/rest/schemas/carts.ts +40 -0
- package/src/interfaces/rest/schemas/catalog.ts +53 -0
- package/src/interfaces/rest/schemas/checkout.ts +4 -0
- package/src/interfaces/rest/schemas/documents.ts +72 -0
- package/src/interfaces/rest/schemas/orders.ts +258 -1
- package/src/interfaces/rest/schemas/pricing.ts +75 -0
- package/src/interfaces/rest/schemas/settings.ts +93 -0
- package/src/interfaces/rest/schemas/shipping.ts +164 -0
- package/src/interfaces/rest/schemas/tax.ts +148 -0
- package/src/kernel/database/schema.ts +12 -0
- package/src/kernel/database/scoped-db.ts +70 -1
- package/src/kernel/hooks/types.ts +1 -0
- package/src/kernel/plugin/manifest.ts +41 -2
- package/src/modules/analytics/drizzle-adapter.ts +19 -3
- package/src/modules/analytics/module.ts +10 -0
- package/src/modules/analytics/reports.ts +311 -0
- package/src/modules/analytics/service.ts +23 -0
- package/src/modules/cart/repository/index.ts +30 -1
- package/src/modules/cart/schema.ts +2 -0
- package/src/modules/cart/schemas.ts +1 -0
- package/src/modules/cart/service.ts +104 -2
- package/src/modules/catalog/entity-service.ts +196 -4
- package/src/modules/catalog/schema.ts +5 -0
- package/src/modules/catalog/schemas.ts +3 -0
- package/src/modules/catalog/service.ts +30 -0
- package/src/modules/customers/service.ts +6 -0
- package/src/modules/documents/module.ts +19 -0
- package/src/modules/documents/pdf.ts +101 -0
- package/src/modules/documents/render.ts +231 -0
- package/src/modules/documents/repository/index.ts +68 -0
- package/src/modules/documents/schema.ts +44 -0
- package/src/modules/documents/service.ts +176 -0
- package/src/modules/fulfillment/service.ts +139 -1
- package/src/modules/orders/repository/index.ts +127 -1
- package/src/modules/orders/schema.ts +58 -1
- package/src/modules/orders/service.ts +1041 -65
- package/src/modules/orders/stale-order-cleanup.ts +18 -4
- package/src/modules/pricing/repository/index.ts +34 -0
- package/src/modules/pricing/service.ts +76 -6
- package/src/modules/settings/module.ts +18 -0
- package/src/modules/settings/repository/index.ts +59 -0
- package/src/modules/settings/schema.ts +25 -0
- package/src/modules/settings/service.ts +82 -0
- package/src/modules/shipping/calculator.ts +27 -0
- package/src/modules/shipping/module.ts +6 -2
- package/src/modules/shipping/repository/index.ts +136 -2
- package/src/modules/shipping/schema.ts +58 -0
- package/src/modules/shipping/service.ts +260 -1
- package/src/modules/tax/adapter.ts +9 -0
- package/src/modules/tax/module.ts +6 -2
- package/src/modules/tax/repository/index.ts +169 -2
- package/src/modules/tax/schema.ts +57 -0
- package/src/modules/tax/service.ts +313 -2
- package/src/runtime/kernel-modules.ts +4 -0
- package/src/runtime/kernel-types.ts +6 -0
- package/src/runtime/server.ts +1 -1
- package/src/test-utils/create-plugin-test-app.ts +17 -4
- package/src/test-utils/create-test-config.ts +2 -0
- package/src/test-utils/order-test-helpers.ts +26 -0
- package/src/test-utils/rest-api-test-utils.ts +7 -0
- package/src/test-utils/test-actors.ts +1 -1
- package/src/testing.ts +1 -0
package/src/auth/middleware.ts
CHANGED
|
@@ -43,10 +43,10 @@ export function authMiddleware(
|
|
|
43
43
|
// Resolve the default org from config, falling back to deprecated constant
|
|
44
44
|
const defaultOrgId = config.auth?.defaultOrganizationId ?? DEFAULT_ORG_ID;
|
|
45
45
|
|
|
46
|
-
// Test-only actor injection: NODE_ENV === "test"
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
if (process.env.NODE_ENV === "test") {
|
|
46
|
+
// Test-only actor injection: requires NODE_ENV === "test" AND an explicit
|
|
47
|
+
// config opt-in so staging/preview deployments left as NODE_ENV=test do not
|
|
48
|
+
// silently become a full auth bypass.
|
|
49
|
+
if (process.env.NODE_ENV === "test" && config.auth?.allowTestActor) {
|
|
50
50
|
const testActorHeader = c.req.header("x-test-actor");
|
|
51
51
|
if (testActorHeader) {
|
|
52
52
|
try {
|
|
@@ -172,10 +172,29 @@ export function authMiddleware(
|
|
|
172
172
|
if (result?.valid && result.key) {
|
|
173
173
|
const apiKey = result.key as Record<string, unknown>;
|
|
174
174
|
|
|
175
|
-
// v1.5+ renamed userId → referenceId on the apikey table.
|
|
176
|
-
const userId = (apiKey.referenceId ?? "") as string;
|
|
177
175
|
const name = (apiKey.name ?? "API Key") as string;
|
|
178
|
-
|
|
176
|
+
// Read the org (and operator identity) from key metadata when present.
|
|
177
|
+
// POS shift keys carry { organizationId, operatorId } in metadata so the
|
|
178
|
+
// operator is scoped to their store WITHOUT any org membership/role
|
|
179
|
+
// (SEC-16 / R-01). Other keys fall back to the key's organizationId.
|
|
180
|
+
const rawMeta = apiKey.metadata;
|
|
181
|
+
const meta =
|
|
182
|
+
rawMeta && typeof rawMeta === "object"
|
|
183
|
+
? (rawMeta as Record<string, unknown>)
|
|
184
|
+
: typeof rawMeta === "string"
|
|
185
|
+
? (() => {
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(rawMeta) as Record<string, unknown>;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
})()
|
|
192
|
+
: null;
|
|
193
|
+
const metaOrg = typeof meta?.organizationId === "string" ? meta.organizationId : undefined;
|
|
194
|
+
const orgId = (metaOrg ?? apiKey.organizationId ?? defaultOrgId) as string;
|
|
195
|
+
const userId = ((typeof meta?.operatorId === "string" ? meta.operatorId : undefined)
|
|
196
|
+
?? apiKey.referenceId
|
|
197
|
+
?? "") as string;
|
|
179
198
|
|
|
180
199
|
// Better Auth stores permissions as Record<string, string[]>
|
|
181
200
|
// (e.g. {"catalog":["read","create"]}). Flatten to the
|
package/src/auth/setup.ts
CHANGED
|
@@ -7,13 +7,13 @@ import type { CommerceConfig } from "../config/types.js";
|
|
|
7
7
|
import type { DatabaseAdapter } from "../kernel/database/adapter.js";
|
|
8
8
|
import * as authSchema from "./auth-schema.js";
|
|
9
9
|
|
|
10
|
-
type BetterAuthDbProvider = "pg"
|
|
10
|
+
type BetterAuthDbProvider = "pg";
|
|
11
11
|
|
|
12
12
|
function resolveAuthDbProvider(provider: string): BetterAuthDbProvider {
|
|
13
13
|
if (provider === "postgres" || provider === "postgresql" || provider === "pg") return "pg";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Unsupported auth database provider "${provider}". Only PostgreSQL is supported.`,
|
|
16
|
+
);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface AuthEmailPayload {
|
|
@@ -90,6 +90,13 @@ export function createAuth(
|
|
|
90
90
|
},
|
|
91
91
|
}
|
|
92
92
|
: {}),
|
|
93
|
+
...(scope.keyExpiration ? { keyExpiration: scope.keyExpiration } : {}),
|
|
94
|
+
...("references" in scope && scope.references
|
|
95
|
+
? { references: scope.references as "user" | "organization" }
|
|
96
|
+
: {}),
|
|
97
|
+
...(scope.enableMetadata || scope.references === "organization"
|
|
98
|
+
? { enableMetadata: true }
|
|
99
|
+
: {}),
|
|
93
100
|
}));
|
|
94
101
|
plugins.push(apiKey(apiKeyConfigs));
|
|
95
102
|
} else if (config.auth?.apiKeys?.enabled) {
|
package/src/config/defaults.ts
CHANGED
|
@@ -27,9 +27,16 @@ export const defaultConfig: Partial<CommerceConfig> = {
|
|
|
27
27
|
"inventory:read",
|
|
28
28
|
"inventory:adjust",
|
|
29
29
|
"orders:read",
|
|
30
|
+
"orders:create",
|
|
30
31
|
"orders:update",
|
|
32
|
+
"orders:manage",
|
|
31
33
|
"cart:create",
|
|
32
34
|
"cart:update",
|
|
35
|
+
"cart:manage",
|
|
36
|
+
"shipping:manage",
|
|
37
|
+
"tax:manage",
|
|
38
|
+
"settings:manage",
|
|
39
|
+
"analytics:read",
|
|
33
40
|
"customers:read:self",
|
|
34
41
|
"customers:update:self",
|
|
35
42
|
],
|
package/src/config/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { BeforeHook, AfterHook } from "../kernel/hooks/types.js";
|
|
|
4
4
|
import type { PaymentAdapter } from "../modules/payments/adapter.js";
|
|
5
5
|
import type { StorageAdapter } from "../modules/media/adapter.js";
|
|
6
6
|
import type { Kernel } from "../runtime/kernel.js";
|
|
7
|
+
import type { AuthInstance } from "../auth/setup.js";
|
|
7
8
|
import type { DatabaseAdapter } from "../kernel/database/adapter.js";
|
|
8
9
|
import type { TaxAdapter } from "../modules/tax/adapter.js";
|
|
9
10
|
import type { SearchAdapter } from "../modules/search/adapter.js";
|
|
@@ -102,6 +103,20 @@ export interface ApiKeyScopeDefinition {
|
|
|
102
103
|
/** Time window in milliseconds. */
|
|
103
104
|
timeWindow: number;
|
|
104
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Expiry bounds for keys minted under this scope, in DAYS (Better Auth
|
|
108
|
+
* semantics; fractions allowed — 1/24 = one hour). Better Auth defaults
|
|
109
|
+
* minExpiresIn to 1 day, which blocks short-lived credentials like
|
|
110
|
+
* per-shift POS keys (issue #51).
|
|
111
|
+
*/
|
|
112
|
+
keyExpiration?: {
|
|
113
|
+
minExpiresIn?: number;
|
|
114
|
+
maxExpiresIn?: number;
|
|
115
|
+
};
|
|
116
|
+
/** Better Auth ownership model — POS shift keys bind to an organization. */
|
|
117
|
+
references?: "user" | "organization";
|
|
118
|
+
/** Allow metadata on keys minted under this scope (e.g. operatorId on POS shift keys). */
|
|
119
|
+
enableMetadata?: boolean;
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
export interface AuthConfig {
|
|
@@ -203,6 +218,11 @@ export interface AuthConfig {
|
|
|
203
218
|
* Appended to the core plugin list (organization, bearer, jwt, etc.).
|
|
204
219
|
*/
|
|
205
220
|
extraAuthPlugins?: unknown[];
|
|
221
|
+
/**
|
|
222
|
+
* When true (and NODE_ENV === "test"), the `x-test-actor` header may inject
|
|
223
|
+
* a trusted actor for integration tests. Default false.
|
|
224
|
+
*/
|
|
225
|
+
allowTestActor?: boolean;
|
|
206
226
|
}
|
|
207
227
|
|
|
208
228
|
export interface CartConfig {
|
|
@@ -374,7 +394,12 @@ export interface CommerceConfig {
|
|
|
374
394
|
hooks?: Record<string, Array<(...args: unknown[]) => unknown>>;
|
|
375
395
|
plugins?: CommercePlugin[];
|
|
376
396
|
middleware?: MiddlewareHandler[];
|
|
377
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Custom route registration. Receives the Better Auth instance as the
|
|
399
|
+
* third argument so integrations can mint/revoke API keys or call
|
|
400
|
+
* `auth.api.*` without module-global holder shims.
|
|
401
|
+
*/
|
|
402
|
+
routes?: (app: Hono<any>, kernel: Kernel, auth?: AuthInstance) => void;
|
|
378
403
|
/** Log level for structured logging. Default: "info". */
|
|
379
404
|
logLevel?: "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
|
380
405
|
/**
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export interface PluginCapabilityRegistryShape {
|
|
5
5
|
"appointments": Record<string, unknown>;
|
|
6
6
|
"gift-cards": Record<string, unknown>;
|
|
7
|
+
"layaway": Record<string, unknown>;
|
|
7
8
|
"loyalty": Record<string, unknown>;
|
|
8
9
|
"marketplace": Record<string, unknown>;
|
|
9
10
|
"notifications": Record<string, unknown>;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export const resolvedPluginManifest = [
|
|
6
6
|
{ id: "appointments", directory: "plugin-appointments" },
|
|
7
7
|
{ id: "gift-cards", directory: "plugin-gift-cards" },
|
|
8
|
+
{ id: "layaway", directory: "plugin-layaway" },
|
|
8
9
|
{ id: "loyalty", directory: "plugin-loyalty" },
|
|
9
10
|
{ id: "marketplace", directory: "plugin-marketplace" },
|
|
10
11
|
{ id: "notifications", directory: "plugin-notifications" },
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export interface PluginRepositoryRegistryShape {
|
|
5
5
|
"appointments": Record<string, unknown>;
|
|
6
6
|
"gift-cards": Record<string, unknown>;
|
|
7
|
+
"layaway": Record<string, unknown>;
|
|
7
8
|
"loyalty": Record<string, unknown>;
|
|
8
9
|
"marketplace": Record<string, unknown>;
|
|
9
10
|
"notifications": Record<string, unknown>;
|
|
@@ -13,14 +13,14 @@ interface InventoryServiceLike {
|
|
|
13
13
|
quantity: number;
|
|
14
14
|
orderId: string;
|
|
15
15
|
performedBy: string;
|
|
16
|
-
}): Promise<{ ok: boolean; error?: { message: string } }>;
|
|
16
|
+
}, actor?: unknown): Promise<{ ok: boolean; error?: { message: string } }>;
|
|
17
17
|
release(input: {
|
|
18
18
|
entityId: string;
|
|
19
19
|
variantId?: string;
|
|
20
20
|
quantity: number;
|
|
21
21
|
orderId: string;
|
|
22
22
|
performedBy: string;
|
|
23
|
-
}): Promise<unknown>;
|
|
23
|
+
}, actor?: unknown): Promise<unknown>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
interface PaymentsServiceLike {
|
|
@@ -80,13 +80,16 @@ export const reserveInventoryStep: Step<
|
|
|
80
80
|
const performedBy = ctx.hook.actor?.userId ?? "system";
|
|
81
81
|
|
|
82
82
|
for (const item of data.lineItems) {
|
|
83
|
-
const result = await inventory.reserve(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
const result = await inventory.reserve(
|
|
84
|
+
{
|
|
85
|
+
entityId: item.entityId,
|
|
86
|
+
...(item.variantId != null ? { variantId: item.variantId } : {}),
|
|
87
|
+
quantity: item.quantity,
|
|
88
|
+
orderId: data.checkoutId,
|
|
89
|
+
performedBy,
|
|
90
|
+
},
|
|
91
|
+
ctx.hook.actor,
|
|
92
|
+
);
|
|
90
93
|
|
|
91
94
|
if (!result.ok) {
|
|
92
95
|
return Err(
|
|
@@ -112,13 +115,16 @@ export const reserveInventoryStep: Step<
|
|
|
112
115
|
const performedBy = ctx.hook.actor?.userId ?? "system";
|
|
113
116
|
|
|
114
117
|
for (const r of reservations) {
|
|
115
|
-
await inventory.release(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
await inventory.release(
|
|
119
|
+
{
|
|
120
|
+
entityId: r.entityId,
|
|
121
|
+
...(r.variantId != null ? { variantId: r.variantId } : {}),
|
|
122
|
+
quantity: r.quantity,
|
|
123
|
+
orderId: r.orderId,
|
|
124
|
+
performedBy,
|
|
125
|
+
},
|
|
126
|
+
ctx.hook.actor,
|
|
127
|
+
);
|
|
122
128
|
}
|
|
123
129
|
},
|
|
124
130
|
};
|
package/src/hooks/checkout.ts
CHANGED
|
@@ -359,28 +359,40 @@ export const calculateTax: BeforeHook<CheckoutData> = async ({
|
|
|
359
359
|
currency: string;
|
|
360
360
|
customerId?: string;
|
|
361
361
|
shippingAmount: number;
|
|
362
|
+
orderDiscount?: number;
|
|
362
363
|
fromAddress?: ShippingAddress;
|
|
363
364
|
toAddress?: ShippingAddress;
|
|
364
365
|
lineItems: Array<{
|
|
365
366
|
id: string;
|
|
366
367
|
entityId: string;
|
|
368
|
+
variantId?: string;
|
|
367
369
|
description: string;
|
|
368
370
|
quantity: number;
|
|
369
371
|
unitPrice: number;
|
|
370
372
|
discount?: number;
|
|
371
373
|
}>;
|
|
372
|
-
}): Promise<
|
|
373
|
-
| { ok: true; value: { amountToCollect: number } }
|
|
374
|
+
}, orgId?: string): Promise<
|
|
375
|
+
| { ok: true; value: { amountToCollect: number; lines?: Array<{ id: string; taxClass: string | null; taxAmount: number }> } }
|
|
374
376
|
| { ok: false; error: Error }
|
|
375
377
|
>;
|
|
376
378
|
};
|
|
377
379
|
|
|
380
|
+
// Cart-level discount not already attributed to lines — pro-rated across
|
|
381
|
+
// lines before class-based tax (issue #57).
|
|
382
|
+
const lineDiscounts = data.lineItems.reduce(
|
|
383
|
+
(sum, lineItem) => sum + (lineItem.discountAmount ?? 0),
|
|
384
|
+
0,
|
|
385
|
+
);
|
|
386
|
+
const orderDiscount = Math.max(0, data.discountTotal - lineDiscounts);
|
|
387
|
+
|
|
378
388
|
const calculated = await tax.calculate({
|
|
379
389
|
currency: data.currency,
|
|
380
390
|
shippingAmount: data.shippingTotal,
|
|
391
|
+
orderDiscount,
|
|
381
392
|
lineItems: data.lineItems.map((lineItem) => ({
|
|
382
393
|
id: lineItem.id ?? `${lineItem.entityId}:${lineItem.variantId ?? "_"}`,
|
|
383
394
|
entityId: lineItem.entityId,
|
|
395
|
+
...(lineItem.variantId !== undefined ? { variantId: lineItem.variantId } : {}),
|
|
384
396
|
description: lineItem.title ?? lineItem.entityId,
|
|
385
397
|
quantity: lineItem.quantity,
|
|
386
398
|
unitPrice: lineItem.resolvedUnitPrice ?? 0,
|
|
@@ -392,7 +404,7 @@ export const calculateTax: BeforeHook<CheckoutData> = async ({
|
|
|
392
404
|
...(data.shippingAddress !== undefined
|
|
393
405
|
? { toAddress: data.shippingAddress }
|
|
394
406
|
: {}),
|
|
395
|
-
});
|
|
407
|
+
}, resolveOrgId(context.actor ?? null));
|
|
396
408
|
|
|
397
409
|
if (!calculated.ok) {
|
|
398
410
|
throw new CommerceValidationError(
|
|
@@ -400,6 +412,14 @@ export const calculateTax: BeforeHook<CheckoutData> = async ({
|
|
|
400
412
|
);
|
|
401
413
|
}
|
|
402
414
|
|
|
415
|
+
// Product tax classes return per-line results — store them so the order's
|
|
416
|
+
// line items carry taxAmount (issue #57). Lines arrive in request order.
|
|
417
|
+
if (calculated.value.lines) {
|
|
418
|
+
for (const [i, line] of calculated.value.lines.entries()) {
|
|
419
|
+
if (data.lineItems[i]) data.lineItems[i]!.taxAmount = line.taxAmount;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
403
423
|
data.taxTotal = Math.max(0, Math.round(calculated.value.amountToCollect));
|
|
404
424
|
recalculateTotals(data);
|
|
405
425
|
return data;
|
|
@@ -421,6 +441,7 @@ export const calculateShipping: BeforeHook<CheckoutData> = async ({
|
|
|
421
441
|
currency: string;
|
|
422
442
|
address?: ShippingAddress;
|
|
423
443
|
isFreeShipping?: boolean;
|
|
444
|
+
orgId?: string;
|
|
424
445
|
}): Promise<
|
|
425
446
|
| {
|
|
426
447
|
ok: true;
|
|
@@ -441,6 +462,7 @@ export const calculateShipping: BeforeHook<CheckoutData> = async ({
|
|
|
441
462
|
})),
|
|
442
463
|
subtotalAfterDiscount: Math.max(0, data.subtotal - data.discountTotal),
|
|
443
464
|
currency: data.currency,
|
|
465
|
+
orgId: resolveOrgId(context.actor ?? null),
|
|
444
466
|
...(data.shippingAddress !== undefined
|
|
445
467
|
? { address: data.shippingAddress }
|
|
446
468
|
: {}),
|
|
@@ -544,16 +566,19 @@ export const reserveInventory: AfterHook<OrderResult> = async ({ result, context
|
|
|
544
566
|
quantity: number;
|
|
545
567
|
orderId: string;
|
|
546
568
|
performedBy: string;
|
|
547
|
-
}): Promise<unknown>;
|
|
569
|
+
}, actor?: unknown): Promise<unknown>;
|
|
548
570
|
};
|
|
549
571
|
for (const lineItem of result.lineItems ?? []) {
|
|
550
|
-
await inventory.reserve(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
572
|
+
await inventory.reserve(
|
|
573
|
+
{
|
|
574
|
+
entityId: lineItem.entityId,
|
|
575
|
+
...(lineItem.variantId != null ? { variantId: lineItem.variantId } : {}),
|
|
576
|
+
quantity: lineItem.quantity,
|
|
577
|
+
orderId: result.id,
|
|
578
|
+
performedBy: context.actor?.userId ?? "system",
|
|
579
|
+
},
|
|
580
|
+
context.actor,
|
|
581
|
+
);
|
|
557
582
|
}
|
|
558
583
|
};
|
|
559
584
|
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type {
|
|
|
15
15
|
} from "./kernel/plugin/manifest.js";
|
|
16
16
|
|
|
17
17
|
export { router } from "./interfaces/rest/router.js";
|
|
18
|
+
export { requirePerm } from "./interfaces/rest/utils.js";
|
|
18
19
|
export { webhookRouter, type WebhookRouterResult } from "./interfaces/rest/webhook-router.js";
|
|
19
20
|
export { isPrivateUrl, isPrivateIp } from "./modules/webhooks/ssrf-guard.js";
|
|
20
21
|
export { createServer } from "./runtime/server.js";
|
|
@@ -4,6 +4,7 @@ import { sql } from "drizzle-orm";
|
|
|
4
4
|
import type { Kernel } from "../../runtime/kernel.js";
|
|
5
5
|
import type { AppEnv } from "./utils.js";
|
|
6
6
|
import { mapErrorToResponse } from "../../kernel/error-mapper.js";
|
|
7
|
+
import { analyticsRoutes } from "./routes/analytics.js";
|
|
7
8
|
import { catalogRoutes } from "./routes/catalog.js";
|
|
8
9
|
import { inventoryRoutes } from "./routes/inventory.js";
|
|
9
10
|
import { mediaRoutes } from "./routes/media.js";
|
|
@@ -19,7 +20,12 @@ import { auditRoutes } from "./routes/audit.js";
|
|
|
19
20
|
import { adminJobRoutes } from "./routes/admin-jobs.js";
|
|
20
21
|
import { compensationFailureAdminRoutes } from "./routes/admin/compensation-failures.js";
|
|
21
22
|
import { adminPermissionsRoutes } from "./routes/admin/permissions.js";
|
|
23
|
+
import { adminStaffRoutes } from "./routes/admin/staff.js";
|
|
22
24
|
import { customerRoutes } from "./routes/customers.js";
|
|
25
|
+
import { documentRoutes } from "./routes/documents.js";
|
|
26
|
+
import { settingsRoutes } from "./routes/settings.js";
|
|
27
|
+
import { shippingRoutes } from "./routes/shipping.js";
|
|
28
|
+
import { taxRoutes } from "./routes/tax.js";
|
|
23
29
|
export function createRestRoutes(kernel: Kernel) {
|
|
24
30
|
const router = new OpenAPIHono<AppEnv>({
|
|
25
31
|
// Standardize Zod validation error responses across all routes
|
|
@@ -72,9 +78,14 @@ export function createRestRoutes(kernel: Kernel) {
|
|
|
72
78
|
router.route("/carts", cartRoutes(kernel));
|
|
73
79
|
router.route("/checkout", checkoutRoutes(kernel));
|
|
74
80
|
router.route("/orders", orderRoutes(kernel));
|
|
81
|
+
router.route("/orders", documentRoutes(kernel));
|
|
75
82
|
router.route("/payments", paymentRoutes(kernel));
|
|
76
83
|
router.route("/webhooks", webhookRoutes(kernel));
|
|
77
84
|
router.route("/pricing", pricingRoutes(kernel));
|
|
85
|
+
router.route("/shipping", shippingRoutes(kernel));
|
|
86
|
+
router.route("/tax", taxRoutes(kernel));
|
|
87
|
+
router.route("/settings", settingsRoutes(kernel));
|
|
88
|
+
router.route("/analytics", analyticsRoutes(kernel));
|
|
78
89
|
router.route("/promotions", promotionRoutes(kernel));
|
|
79
90
|
router.route("/search", searchRoutes(kernel));
|
|
80
91
|
router.route("/customers", customerRoutes(kernel));
|
|
@@ -82,6 +93,7 @@ export function createRestRoutes(kernel: Kernel) {
|
|
|
82
93
|
router.route("/admin", adminJobRoutes(kernel));
|
|
83
94
|
router.route("/admin", compensationFailureAdminRoutes(kernel));
|
|
84
95
|
router.route("/admin", adminPermissionsRoutes(kernel));
|
|
96
|
+
router.route("/admin", adminStaffRoutes(kernel));
|
|
85
97
|
|
|
86
98
|
// API Reference (Scalar) — disabled in production unless config.exposeOpenApiSpec is true
|
|
87
99
|
const exposeSpec = kernel.config.exposeOpenApiSpec ?? (process.env.NODE_ENV !== "production");
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { eq, and } from "drizzle-orm";
|
|
3
|
+
import type { Kernel } from "../../../../runtime/kernel.js";
|
|
4
|
+
import type { CommerceConfig } from "../../../../config/types.js";
|
|
5
|
+
import type { DrizzleDatabase } from "../../../../kernel/database/drizzle-db.js";
|
|
6
|
+
import { member, user, invitation } from "../../../../auth/auth-schema.js";
|
|
7
|
+
import { resolveOrgId } from "../../../../auth/org.js";
|
|
8
|
+
import { makeId } from "../../../../utils/id.js";
|
|
9
|
+
import {
|
|
10
|
+
listStaffRoute,
|
|
11
|
+
createStaffRoute,
|
|
12
|
+
inviteStaffRoute,
|
|
13
|
+
listStaffInvitationsRoute,
|
|
14
|
+
listStaffRolesRoute,
|
|
15
|
+
updateStaffRoleRoute,
|
|
16
|
+
revokeStaffRoute,
|
|
17
|
+
} from "../../schemas/admin-staff.js";
|
|
18
|
+
import { type AppEnv, requirePerm } from "../../utils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Admin staff / RBAC surface (issue #46).
|
|
22
|
+
*
|
|
23
|
+
* Surfaces the Better Auth `member` table as first-class admin REST:
|
|
24
|
+
* list staff, add an existing user with a role, invite by email, change
|
|
25
|
+
* role, revoke — plus the role → permission mapping roles resolve to.
|
|
26
|
+
*/
|
|
27
|
+
export function adminStaffRoutes(kernel: Kernel) {
|
|
28
|
+
const router = new OpenAPIHono<AppEnv>();
|
|
29
|
+
const db = kernel.database.db as DrizzleDatabase;
|
|
30
|
+
const config = kernel.config as CommerceConfig;
|
|
31
|
+
|
|
32
|
+
router.use("/staff", requirePerm("staff:manage"));
|
|
33
|
+
router.use("/staff/:id", requirePerm("staff:manage"));
|
|
34
|
+
router.use("/staff/invitations", requirePerm("staff:manage"));
|
|
35
|
+
router.use("/staff/roles", requirePerm("staff:manage"));
|
|
36
|
+
|
|
37
|
+
function validRoles(): Set<string> {
|
|
38
|
+
return new Set([
|
|
39
|
+
...Object.keys(config.auth?.roles ?? {}),
|
|
40
|
+
"owner",
|
|
41
|
+
"admin",
|
|
42
|
+
"member",
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function invalidRole(c: { json: (d: unknown, s: number) => unknown }, role: string) {
|
|
47
|
+
return c.json(
|
|
48
|
+
{
|
|
49
|
+
error: {
|
|
50
|
+
code: "VALIDATION_FAILED",
|
|
51
|
+
message: `Unknown role "${role}". Valid roles: ${[...validRoles()].join(", ")}.`,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
422,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const BUILTIN_ROLE_RANK: Record<string, number> = {
|
|
59
|
+
owner: 3,
|
|
60
|
+
admin: 2,
|
|
61
|
+
};
|
|
62
|
+
const CUSTOM_ROLE_RANK = 1;
|
|
63
|
+
|
|
64
|
+
function roleRank(role: string): number {
|
|
65
|
+
return BUILTIN_ROLE_RANK[role] ?? CUSTOM_ROLE_RANK;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canGrantRole(actorRole: string, targetRole: string): boolean {
|
|
69
|
+
return roleRank(actorRole) >= roleRank(targetRole);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function insufficientPrivilege(
|
|
73
|
+
c: { json: (d: unknown, s: number) => unknown },
|
|
74
|
+
targetRole: string,
|
|
75
|
+
) {
|
|
76
|
+
return c.json(
|
|
77
|
+
{
|
|
78
|
+
error: {
|
|
79
|
+
code: "FORBIDDEN",
|
|
80
|
+
message: `Cannot assign role "${targetRole}": your role does not have sufficient privilege.`,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
403,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function countOwners(orgId: string): Promise<number> {
|
|
88
|
+
const rows = await db
|
|
89
|
+
.select({ id: member.id })
|
|
90
|
+
.from(member)
|
|
91
|
+
.where(and(eq(member.organizationId, orgId), eq(member.role, "owner")));
|
|
92
|
+
return rows.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
router.openapi(listStaffRoute, async (c) => {
|
|
96
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
97
|
+
const rows = await db
|
|
98
|
+
.select({
|
|
99
|
+
id: member.id,
|
|
100
|
+
userId: member.userId,
|
|
101
|
+
role: member.role,
|
|
102
|
+
createdAt: member.createdAt,
|
|
103
|
+
email: user.email,
|
|
104
|
+
name: user.name,
|
|
105
|
+
})
|
|
106
|
+
.from(member)
|
|
107
|
+
.leftJoin(user, eq(member.userId, user.id))
|
|
108
|
+
.where(eq(member.organizationId, orgId));
|
|
109
|
+
return c.json({ data: rows });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// @ts-expect-error -- openapi handler union return type
|
|
113
|
+
router.openapi(createStaffRoute, async (c) => {
|
|
114
|
+
const body = c.req.valid("json") as { userId: string; role: string };
|
|
115
|
+
const actor = c.get("actor");
|
|
116
|
+
const orgId = resolveOrgId(actor);
|
|
117
|
+
|
|
118
|
+
if (!validRoles().has(body.role)) return invalidRole(c, body.role);
|
|
119
|
+
if (!canGrantRole(actor!.role, body.role)) return insufficientPrivilege(c, body.role);
|
|
120
|
+
|
|
121
|
+
const users = await db.select().from(user).where(eq(user.id, body.userId));
|
|
122
|
+
if (users.length === 0) {
|
|
123
|
+
return c.json({ error: { code: "NOT_FOUND", message: "User not found." } }, 404);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const existing = await db
|
|
127
|
+
.select({ id: member.id })
|
|
128
|
+
.from(member)
|
|
129
|
+
.where(and(eq(member.organizationId, orgId), eq(member.userId, body.userId)));
|
|
130
|
+
if (existing.length > 0) {
|
|
131
|
+
return c.json(
|
|
132
|
+
{ error: { code: "VALIDATION_FAILED", message: "User is already a member of this organization." } },
|
|
133
|
+
422,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const rows = await db
|
|
138
|
+
.insert(member)
|
|
139
|
+
.values({
|
|
140
|
+
id: makeId(),
|
|
141
|
+
organizationId: orgId,
|
|
142
|
+
userId: body.userId,
|
|
143
|
+
role: body.role,
|
|
144
|
+
createdAt: new Date(),
|
|
145
|
+
})
|
|
146
|
+
.returning();
|
|
147
|
+
return c.json({ data: rows[0] }, 201);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// @ts-expect-error -- openapi handler union return type
|
|
151
|
+
router.openapi(inviteStaffRoute, async (c) => {
|
|
152
|
+
const body = c.req.valid("json") as { email: string; role: string };
|
|
153
|
+
const actor = c.get("actor");
|
|
154
|
+
const orgId = resolveOrgId(actor);
|
|
155
|
+
|
|
156
|
+
if (!validRoles().has(body.role)) return invalidRole(c, body.role);
|
|
157
|
+
|
|
158
|
+
const rows = await db
|
|
159
|
+
.insert(invitation)
|
|
160
|
+
.values({
|
|
161
|
+
id: makeId(),
|
|
162
|
+
organizationId: orgId,
|
|
163
|
+
email: body.email,
|
|
164
|
+
role: body.role,
|
|
165
|
+
status: "pending",
|
|
166
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
167
|
+
inviterId: actor!.userId,
|
|
168
|
+
})
|
|
169
|
+
.returning();
|
|
170
|
+
return c.json({ data: rows[0] }, 201);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
router.openapi(listStaffInvitationsRoute, async (c) => {
|
|
174
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
175
|
+
const rows = await db
|
|
176
|
+
.select()
|
|
177
|
+
.from(invitation)
|
|
178
|
+
.where(and(eq(invitation.organizationId, orgId), eq(invitation.status, "pending")));
|
|
179
|
+
return c.json({ data: rows });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
router.openapi(listStaffRolesRoute, async (c) => {
|
|
183
|
+
const roles = config.auth?.roles ?? {};
|
|
184
|
+
const data = Object.entries(roles).map(([role, def]) => ({
|
|
185
|
+
role,
|
|
186
|
+
permissions: def.permissions,
|
|
187
|
+
}));
|
|
188
|
+
return c.json({ data });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// @ts-expect-error -- openapi handler union return type
|
|
192
|
+
router.openapi(updateStaffRoleRoute, async (c) => {
|
|
193
|
+
const body = c.req.valid("json") as { role: string };
|
|
194
|
+
const actor = c.get("actor");
|
|
195
|
+
const orgId = resolveOrgId(actor);
|
|
196
|
+
const id = c.req.param("id");
|
|
197
|
+
|
|
198
|
+
if (!validRoles().has(body.role)) return invalidRole(c, body.role);
|
|
199
|
+
if (!canGrantRole(actor!.role, body.role)) return insufficientPrivilege(c, body.role);
|
|
200
|
+
|
|
201
|
+
const rows = await db
|
|
202
|
+
.select()
|
|
203
|
+
.from(member)
|
|
204
|
+
.where(and(eq(member.organizationId, orgId), eq(member.id, id)));
|
|
205
|
+
const target = rows[0];
|
|
206
|
+
if (!target) {
|
|
207
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// SEC-18/R-02: the actor must also outrank (or equal) the target's CURRENT
|
|
211
|
+
// role, else an admin could demote an owner they do not outrank.
|
|
212
|
+
if (!canGrantRole(actor!.role, target.role)) return insufficientPrivilege(c, target.role);
|
|
213
|
+
|
|
214
|
+
if (target.role === "owner" && body.role !== "owner" && (await countOwners(orgId)) <= 1) {
|
|
215
|
+
return c.json(
|
|
216
|
+
{ error: { code: "VALIDATION_FAILED", message: "Cannot demote the organization's last owner." } },
|
|
217
|
+
422,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const updated = await db
|
|
222
|
+
.update(member)
|
|
223
|
+
.set({ role: body.role })
|
|
224
|
+
.where(eq(member.id, id))
|
|
225
|
+
.returning();
|
|
226
|
+
return c.json({ data: updated[0] });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// @ts-expect-error -- openapi handler union return type
|
|
230
|
+
router.openapi(revokeStaffRoute, async (c) => {
|
|
231
|
+
const actor = c.get("actor");
|
|
232
|
+
const orgId = resolveOrgId(actor);
|
|
233
|
+
const id = c.req.param("id");
|
|
234
|
+
|
|
235
|
+
const rows = await db
|
|
236
|
+
.select()
|
|
237
|
+
.from(member)
|
|
238
|
+
.where(and(eq(member.organizationId, orgId), eq(member.id, id)));
|
|
239
|
+
const target = rows[0];
|
|
240
|
+
if (!target) {
|
|
241
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// SEC-18/R-02: the actor must outrank (or equal) the target's current role
|
|
245
|
+
// to revoke it — an admin cannot revoke an owner.
|
|
246
|
+
if (!canGrantRole(actor!.role, target.role)) return insufficientPrivilege(c, target.role);
|
|
247
|
+
|
|
248
|
+
if (target.role === "owner" && (await countOwners(orgId)) <= 1) {
|
|
249
|
+
return c.json(
|
|
250
|
+
{ error: { code: "VALIDATION_FAILED", message: "Cannot revoke the organization's last owner." } },
|
|
251
|
+
422,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await db.delete(member).where(eq(member.id, id));
|
|
256
|
+
return c.json({ data: { deleted: true } });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return router;
|
|
260
|
+
}
|