@molecule/api-resource-inventory 1.0.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/LICENSE +115 -0
- package/dist/authorizers/index.d.ts +133 -0
- package/dist/authorizers/index.d.ts.map +1 -0
- package/dist/authorizers/index.js +188 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/handlers/bulkUpdate.d.ts +13 -0
- package/dist/handlers/bulkUpdate.d.ts.map +1 -0
- package/dist/handlers/bulkUpdate.js +168 -0
- package/dist/handlers/confirm.d.ts +13 -0
- package/dist/handlers/confirm.d.ts.map +1 -0
- package/dist/handlers/confirm.js +68 -0
- package/dist/handlers/getAlerts.d.ts +14 -0
- package/dist/handlers/getAlerts.d.ts.map +1 -0
- package/dist/handlers/getAlerts.js +52 -0
- package/dist/handlers/getMovements.d.ts +13 -0
- package/dist/handlers/getMovements.d.ts.map +1 -0
- package/dist/handlers/getMovements.js +62 -0
- package/dist/handlers/getStock.d.ts +13 -0
- package/dist/handlers/getStock.d.ts.map +1 -0
- package/dist/handlers/getStock.js +58 -0
- package/dist/handlers/index.d.ts +9 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +8 -0
- package/dist/handlers/release.d.ts +13 -0
- package/dist/handlers/release.d.ts.map +1 -0
- package/dist/handlers/release.js +67 -0
- package/dist/handlers/reserve.d.ts +13 -0
- package/dist/handlers/reserve.d.ts.map +1 -0
- package/dist/handlers/reserve.js +112 -0
- package/dist/handlers/updateStock.d.ts +13 -0
- package/dist/handlers/updateStock.d.ts.map +1 -0
- package/dist/handlers/updateStock.js +126 -0
- package/dist/i18n.d.ts +14 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +13 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/requestHandlerMap.d.ts +34 -0
- package/dist/requestHandlerMap.d.ts.map +1 -0
- package/dist/requestHandlerMap.js +34 -0
- package/dist/routes.d.ts +61 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +69 -0
- package/dist/types.d.ts +234 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/utilities.d.ts +31 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +79 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship.
|
|
43
|
+
|
|
44
|
+
"Contribution" shall mean any work of authorship, including the
|
|
45
|
+
original version of the Work and any modifications or additions
|
|
46
|
+
to that Work, that is intentionally submitted to the Licensor for
|
|
47
|
+
inclusion in the Work by the copyright owner or by an individual or
|
|
48
|
+
Legal Entity authorized to submit on behalf of the copyright owner.
|
|
49
|
+
|
|
50
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
51
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
52
|
+
subsequently incorporated within the Work.
|
|
53
|
+
|
|
54
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
55
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
56
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
57
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
58
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
59
|
+
Work and such Derivative Works in Source or Object form.
|
|
60
|
+
|
|
61
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
62
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
63
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
64
|
+
patent license to make, have made, use, offer to sell, sell, import,
|
|
65
|
+
and otherwise transfer the Work.
|
|
66
|
+
|
|
67
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
68
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
69
|
+
modifications, and in Source or Object form, provided that You
|
|
70
|
+
meet the following conditions:
|
|
71
|
+
|
|
72
|
+
(a) You must give any other recipients of the Work or
|
|
73
|
+
Derivative Works a copy of this License; and
|
|
74
|
+
|
|
75
|
+
(b) You must cause any modified files to carry prominent notices
|
|
76
|
+
stating that You changed the files; and
|
|
77
|
+
|
|
78
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
79
|
+
that You distribute, all copyright, patent, trademark, and
|
|
80
|
+
attribution notices from the Source form of the Work,
|
|
81
|
+
excluding those notices that do not pertain to any part of
|
|
82
|
+
the Derivative Works; and
|
|
83
|
+
|
|
84
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
85
|
+
distribution, then any Derivative Works that You distribute must
|
|
86
|
+
include a readable copy of the attribution notices contained
|
|
87
|
+
within such NOTICE file.
|
|
88
|
+
|
|
89
|
+
5. Submission of Contributions.
|
|
90
|
+
|
|
91
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
92
|
+
names, trademarks, service marks, or product names of the Licensor.
|
|
93
|
+
|
|
94
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
95
|
+
agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
|
|
96
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
|
|
97
|
+
|
|
98
|
+
8. Limitation of Liability. In no event and under no legal theory shall
|
|
99
|
+
any Contributor be liable to You for damages.
|
|
100
|
+
|
|
101
|
+
9. Accepting Warranty or Additional Liability.
|
|
102
|
+
|
|
103
|
+
Copyright 2026 Molecule Dev, Inc.
|
|
104
|
+
|
|
105
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
106
|
+
you may not use this file except in compliance with the License.
|
|
107
|
+
You may obtain a copy of the License at
|
|
108
|
+
|
|
109
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
110
|
+
|
|
111
|
+
Unless required by applicable law or agreed to in writing, software
|
|
112
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
113
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
114
|
+
See the License for the specific language governing permissions and
|
|
115
|
+
limitations under the License.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inventory authorizers.
|
|
3
|
+
*
|
|
4
|
+
* Inventory is store/admin-scoped: stock is a shared catalog resource with no
|
|
5
|
+
* per-user owner. The destructive admin-side mutations — `updateStock` and
|
|
6
|
+
* `bulkUpdate` — directly rewrite stock levels and are therefore **admin-only**.
|
|
7
|
+
* That intent is enforced in two independent layers:
|
|
8
|
+
*
|
|
9
|
+
* 1. {@link requireInventoryAdmin} — a route middleware referenced from
|
|
10
|
+
* {@link routes}. It is a key in `requestHandlerMap`, so the mlcl injector's
|
|
11
|
+
* route scanner keeps it (a bare `'authenticate'` string is silently dropped
|
|
12
|
+
* because it isn't a handler-map key — which is exactly why the old gate was
|
|
13
|
+
* inert and any authenticated user could rewrite stock).
|
|
14
|
+
* 2. {@link isInventoryAdmin} — called at the top of every admin handler, so
|
|
15
|
+
* protection holds even when the resource is wired via the middleware-less
|
|
16
|
+
* module `@example` (`requestHandlerMap[route.handler]` only). Secure by
|
|
17
|
+
* default: the handler fails closed regardless of whether the route
|
|
18
|
+
* middleware survives codegen.
|
|
19
|
+
*
|
|
20
|
+
* Reservation lifecycle endpoints (`reserve`/`release`/`confirm`) are tied to a
|
|
21
|
+
* cart/order rather than the catalog: any authenticated user may reserve stock,
|
|
22
|
+
* the reservation is bound to the caller's `userId`, and `release`/`confirm`
|
|
23
|
+
* verify ownership (owner **or** admin) in-handler.
|
|
24
|
+
*
|
|
25
|
+
* "Admin" is resolved fail-closed from session claims only, keeping this resource
|
|
26
|
+
* dependency-light: an explicit `isAdmin`/`role`/`roles`/`permissions` claim on
|
|
27
|
+
* `res.locals.session`. With none, access is denied — the app developer must
|
|
28
|
+
* define who may administer inventory (or compose a richer authz middleware,
|
|
29
|
+
* e.g. `@molecule/api-permissions`, that sets an admin claim before this runs).
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
import type { MoleculeRequestHandler, MoleculeResponse } from '@molecule/api-resource';
|
|
34
|
+
/**
|
|
35
|
+
* Permission action describing inventory administration, e.g. for an app's own
|
|
36
|
+
* `@molecule/api-permissions` wiring.
|
|
37
|
+
*/
|
|
38
|
+
export declare const INVENTORY_PERMISSION_ACTION = "manage";
|
|
39
|
+
/**
|
|
40
|
+
* Permission resource describing inventory administration, e.g. for an app's own
|
|
41
|
+
* `@molecule/api-permissions` wiring.
|
|
42
|
+
*/
|
|
43
|
+
export declare const INVENTORY_PERMISSION_RESOURCE = "inventory";
|
|
44
|
+
/**
|
|
45
|
+
* Session-claim permission string (`'inventory:manage'`) that, when present in a
|
|
46
|
+
* session's `permissions` array, grants inventory administration.
|
|
47
|
+
*/
|
|
48
|
+
export declare const INVENTORY_ADMIN_PERMISSION = "inventory:manage";
|
|
49
|
+
/**
|
|
50
|
+
* Structural view of the fields on `res.locals.session` this resource inspects to
|
|
51
|
+
* decide authorization. All fields are optional — a standard molecule session
|
|
52
|
+
* carries only `userId`; apps that model roles may also set `isAdmin`/`role`/
|
|
53
|
+
* `roles`/`permissions` claims, which are honored here.
|
|
54
|
+
*/
|
|
55
|
+
export interface InventorySession {
|
|
56
|
+
/** Authenticated user id (set by the global auth middleware). */
|
|
57
|
+
userId?: string;
|
|
58
|
+
/** Optional boolean admin claim. */
|
|
59
|
+
isAdmin?: boolean;
|
|
60
|
+
/** Optional single-role claim. */
|
|
61
|
+
role?: string;
|
|
62
|
+
/** Optional multi-role claim. */
|
|
63
|
+
roles?: string[];
|
|
64
|
+
/** Optional permission strings claim. */
|
|
65
|
+
permissions?: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reads the structural session off `res.locals`.
|
|
69
|
+
*
|
|
70
|
+
* @param res - The response whose `locals.session` is inspected.
|
|
71
|
+
* @returns The session, or `undefined` when unauthenticated.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getInventorySession(res: MoleculeResponse): InventorySession | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Resolves whether the current request's session belongs to an actor authorized
|
|
76
|
+
* to administer inventory (rewrite/bulk-update stock, release/confirm any
|
|
77
|
+
* reservation). Fail-closed: returns `false` when there is no authenticated
|
|
78
|
+
* session, and otherwise only `true` when the session carries an admin claim —
|
|
79
|
+
* `isAdmin === true`, `role === 'admin'`, `roles` containing `'admin'`, or
|
|
80
|
+
* `permissions` containing `'admin'` / `'inventory:manage'`.
|
|
81
|
+
*
|
|
82
|
+
* Use this for in-handler defense-in-depth (it does not depend on the route
|
|
83
|
+
* middleware being preserved by the injector).
|
|
84
|
+
*
|
|
85
|
+
* @param res - The response whose `locals.session` is inspected.
|
|
86
|
+
* @returns `true` when the session is an authorized inventory admin.
|
|
87
|
+
*/
|
|
88
|
+
export declare function isInventoryAdmin(res: MoleculeResponse): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* In-handler admin guard for the admin-only inventory mutations. Writes the
|
|
91
|
+
* appropriate JSON error and returns `false` when the caller is not an
|
|
92
|
+
* authorized admin — `401` when unauthenticated, `403` when authenticated but
|
|
93
|
+
* not an admin. Returns `true` (and writes nothing) when the caller is an admin.
|
|
94
|
+
*
|
|
95
|
+
* Call this at the top of every admin handler so protection holds independently
|
|
96
|
+
* of the route middleware (defense-in-depth, fail-closed).
|
|
97
|
+
*
|
|
98
|
+
* @param res - The response, whose `locals.session` is inspected and onto which
|
|
99
|
+
* an error is written when access is denied.
|
|
100
|
+
* @returns `true` when the caller is an authorized admin, otherwise `false`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function assertInventoryAdmin(res: MoleculeResponse): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* In-handler guard for reservation-lifecycle mutations (`release`, `confirm`).
|
|
105
|
+
* A reservation is owned by the user who created it; only that owner — or an
|
|
106
|
+
* inventory admin — may release or confirm it. Writes the appropriate JSON
|
|
107
|
+
* error and returns `false` when access is denied (`401` when unauthenticated,
|
|
108
|
+
* `403` when authenticated but neither the owner nor an admin); returns `true`
|
|
109
|
+
* (writing nothing) when the caller may act.
|
|
110
|
+
*
|
|
111
|
+
* Fail-closed: a reservation with no recorded owner (`null` — e.g. a legacy row
|
|
112
|
+
* created before ownership binding) is accessible only to admins.
|
|
113
|
+
*
|
|
114
|
+
* @param res - The response, whose `locals.session` is inspected and onto which
|
|
115
|
+
* an error is written when access is denied.
|
|
116
|
+
* @param reservationUserId - The `userId` recorded on the reservation, or `null`.
|
|
117
|
+
* @returns `true` when the caller is the owner or an admin, otherwise `false`.
|
|
118
|
+
*/
|
|
119
|
+
export declare function assertReservationActor(res: MoleculeResponse, reservationUserId: string | null): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Route middleware that gates the admin-only inventory routes (`updateStock`,
|
|
122
|
+
* `bulkUpdate`). Calls `next()` only for an authenticated admin; otherwise
|
|
123
|
+
* forwards an error to the framework error handler — `Unauthorized` when no
|
|
124
|
+
* session is present, `Forbidden` when the session is authenticated but not an
|
|
125
|
+
* admin.
|
|
126
|
+
*
|
|
127
|
+
* Exposed as a `requestHandlerMap` key so the injector's route scanner keeps it
|
|
128
|
+
* (unlike the inert global `'authenticate'` string, which is dropped).
|
|
129
|
+
*
|
|
130
|
+
* @returns An Express-compatible middleware function.
|
|
131
|
+
*/
|
|
132
|
+
export declare const requireInventoryAdmin: () => MoleculeRequestHandler;
|
|
133
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/authorizers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEtF;;;GAGG;AACH,eAAO,MAAM,2BAA2B,WAAW,CAAA;AAEnD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,cAAc,CAAA;AAExD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,qBAAoE,CAAA;AAE3G;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,SAAS,CAEvF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAsB/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAmBnE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,gBAAgB,EACrB,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B,OAAO,CAsBT;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,QAC5B,sBAcH,CAAA"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inventory authorizers.
|
|
3
|
+
*
|
|
4
|
+
* Inventory is store/admin-scoped: stock is a shared catalog resource with no
|
|
5
|
+
* per-user owner. The destructive admin-side mutations — `updateStock` and
|
|
6
|
+
* `bulkUpdate` — directly rewrite stock levels and are therefore **admin-only**.
|
|
7
|
+
* That intent is enforced in two independent layers:
|
|
8
|
+
*
|
|
9
|
+
* 1. {@link requireInventoryAdmin} — a route middleware referenced from
|
|
10
|
+
* {@link routes}. It is a key in `requestHandlerMap`, so the mlcl injector's
|
|
11
|
+
* route scanner keeps it (a bare `'authenticate'` string is silently dropped
|
|
12
|
+
* because it isn't a handler-map key — which is exactly why the old gate was
|
|
13
|
+
* inert and any authenticated user could rewrite stock).
|
|
14
|
+
* 2. {@link isInventoryAdmin} — called at the top of every admin handler, so
|
|
15
|
+
* protection holds even when the resource is wired via the middleware-less
|
|
16
|
+
* module `@example` (`requestHandlerMap[route.handler]` only). Secure by
|
|
17
|
+
* default: the handler fails closed regardless of whether the route
|
|
18
|
+
* middleware survives codegen.
|
|
19
|
+
*
|
|
20
|
+
* Reservation lifecycle endpoints (`reserve`/`release`/`confirm`) are tied to a
|
|
21
|
+
* cart/order rather than the catalog: any authenticated user may reserve stock,
|
|
22
|
+
* the reservation is bound to the caller's `userId`, and `release`/`confirm`
|
|
23
|
+
* verify ownership (owner **or** admin) in-handler.
|
|
24
|
+
*
|
|
25
|
+
* "Admin" is resolved fail-closed from session claims only, keeping this resource
|
|
26
|
+
* dependency-light: an explicit `isAdmin`/`role`/`roles`/`permissions` claim on
|
|
27
|
+
* `res.locals.session`. With none, access is denied — the app developer must
|
|
28
|
+
* define who may administer inventory (or compose a richer authz middleware,
|
|
29
|
+
* e.g. `@molecule/api-permissions`, that sets an admin claim before this runs).
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
import { t } from '@molecule/api-i18n';
|
|
34
|
+
/**
|
|
35
|
+
* Permission action describing inventory administration, e.g. for an app's own
|
|
36
|
+
* `@molecule/api-permissions` wiring.
|
|
37
|
+
*/
|
|
38
|
+
export const INVENTORY_PERMISSION_ACTION = 'manage';
|
|
39
|
+
/**
|
|
40
|
+
* Permission resource describing inventory administration, e.g. for an app's own
|
|
41
|
+
* `@molecule/api-permissions` wiring.
|
|
42
|
+
*/
|
|
43
|
+
export const INVENTORY_PERMISSION_RESOURCE = 'inventory';
|
|
44
|
+
/**
|
|
45
|
+
* Session-claim permission string (`'inventory:manage'`) that, when present in a
|
|
46
|
+
* session's `permissions` array, grants inventory administration.
|
|
47
|
+
*/
|
|
48
|
+
export const INVENTORY_ADMIN_PERMISSION = `${INVENTORY_PERMISSION_RESOURCE}:${INVENTORY_PERMISSION_ACTION}`;
|
|
49
|
+
/**
|
|
50
|
+
* Reads the structural session off `res.locals`.
|
|
51
|
+
*
|
|
52
|
+
* @param res - The response whose `locals.session` is inspected.
|
|
53
|
+
* @returns The session, or `undefined` when unauthenticated.
|
|
54
|
+
*/
|
|
55
|
+
export function getInventorySession(res) {
|
|
56
|
+
return res.locals.session;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolves whether the current request's session belongs to an actor authorized
|
|
60
|
+
* to administer inventory (rewrite/bulk-update stock, release/confirm any
|
|
61
|
+
* reservation). Fail-closed: returns `false` when there is no authenticated
|
|
62
|
+
* session, and otherwise only `true` when the session carries an admin claim —
|
|
63
|
+
* `isAdmin === true`, `role === 'admin'`, `roles` containing `'admin'`, or
|
|
64
|
+
* `permissions` containing `'admin'` / `'inventory:manage'`.
|
|
65
|
+
*
|
|
66
|
+
* Use this for in-handler defense-in-depth (it does not depend on the route
|
|
67
|
+
* middleware being preserved by the injector).
|
|
68
|
+
*
|
|
69
|
+
* @param res - The response whose `locals.session` is inspected.
|
|
70
|
+
* @returns `true` when the session is an authorized inventory admin.
|
|
71
|
+
*/
|
|
72
|
+
export function isInventoryAdmin(res) {
|
|
73
|
+
const session = getInventorySession(res);
|
|
74
|
+
if (!session?.userId) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (session.isAdmin === true) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (session.role === 'admin') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(session.roles) && session.roles.includes('admin')) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(session.permissions) &&
|
|
87
|
+
(session.permissions.includes('admin') ||
|
|
88
|
+
session.permissions.includes(INVENTORY_ADMIN_PERMISSION))) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* In-handler admin guard for the admin-only inventory mutations. Writes the
|
|
95
|
+
* appropriate JSON error and returns `false` when the caller is not an
|
|
96
|
+
* authorized admin — `401` when unauthenticated, `403` when authenticated but
|
|
97
|
+
* not an admin. Returns `true` (and writes nothing) when the caller is an admin.
|
|
98
|
+
*
|
|
99
|
+
* Call this at the top of every admin handler so protection holds independently
|
|
100
|
+
* of the route middleware (defense-in-depth, fail-closed).
|
|
101
|
+
*
|
|
102
|
+
* @param res - The response, whose `locals.session` is inspected and onto which
|
|
103
|
+
* an error is written when access is denied.
|
|
104
|
+
* @returns `true` when the caller is an authorized admin, otherwise `false`.
|
|
105
|
+
*/
|
|
106
|
+
export function assertInventoryAdmin(res) {
|
|
107
|
+
const session = getInventorySession(res);
|
|
108
|
+
if (!session?.userId) {
|
|
109
|
+
res.status(401).json({
|
|
110
|
+
error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
|
|
111
|
+
errorKey: 'resource.error.unauthorized',
|
|
112
|
+
});
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (!isInventoryAdmin(res)) {
|
|
116
|
+
res.status(403).json({
|
|
117
|
+
error: t('inventory.error.forbidden', undefined, {
|
|
118
|
+
defaultValue: 'Admin access required to manage inventory',
|
|
119
|
+
}),
|
|
120
|
+
errorKey: 'inventory.error.forbidden',
|
|
121
|
+
});
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* In-handler guard for reservation-lifecycle mutations (`release`, `confirm`).
|
|
128
|
+
* A reservation is owned by the user who created it; only that owner — or an
|
|
129
|
+
* inventory admin — may release or confirm it. Writes the appropriate JSON
|
|
130
|
+
* error and returns `false` when access is denied (`401` when unauthenticated,
|
|
131
|
+
* `403` when authenticated but neither the owner nor an admin); returns `true`
|
|
132
|
+
* (writing nothing) when the caller may act.
|
|
133
|
+
*
|
|
134
|
+
* Fail-closed: a reservation with no recorded owner (`null` — e.g. a legacy row
|
|
135
|
+
* created before ownership binding) is accessible only to admins.
|
|
136
|
+
*
|
|
137
|
+
* @param res - The response, whose `locals.session` is inspected and onto which
|
|
138
|
+
* an error is written when access is denied.
|
|
139
|
+
* @param reservationUserId - The `userId` recorded on the reservation, or `null`.
|
|
140
|
+
* @returns `true` when the caller is the owner or an admin, otherwise `false`.
|
|
141
|
+
*/
|
|
142
|
+
export function assertReservationActor(res, reservationUserId) {
|
|
143
|
+
const userId = getInventorySession(res)?.userId;
|
|
144
|
+
if (!userId) {
|
|
145
|
+
res.status(401).json({
|
|
146
|
+
error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
|
|
147
|
+
errorKey: 'resource.error.unauthorized',
|
|
148
|
+
});
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (isInventoryAdmin(res)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (reservationUserId !== null && reservationUserId === userId) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
res.status(403).json({
|
|
158
|
+
error: t('inventory.error.reservationForbidden', undefined, {
|
|
159
|
+
defaultValue: 'You do not have permission to modify this reservation',
|
|
160
|
+
}),
|
|
161
|
+
errorKey: 'inventory.error.reservationForbidden',
|
|
162
|
+
});
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Route middleware that gates the admin-only inventory routes (`updateStock`,
|
|
167
|
+
* `bulkUpdate`). Calls `next()` only for an authenticated admin; otherwise
|
|
168
|
+
* forwards an error to the framework error handler — `Unauthorized` when no
|
|
169
|
+
* session is present, `Forbidden` when the session is authenticated but not an
|
|
170
|
+
* admin.
|
|
171
|
+
*
|
|
172
|
+
* Exposed as a `requestHandlerMap` key so the injector's route scanner keeps it
|
|
173
|
+
* (unlike the inert global `'authenticate'` string, which is dropped).
|
|
174
|
+
*
|
|
175
|
+
* @returns An Express-compatible middleware function.
|
|
176
|
+
*/
|
|
177
|
+
export const requireInventoryAdmin = () => (req, res, next) => {
|
|
178
|
+
const session = getInventorySession(res);
|
|
179
|
+
if (!session?.userId) {
|
|
180
|
+
return next(t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }));
|
|
181
|
+
}
|
|
182
|
+
if (isInventoryAdmin(res)) {
|
|
183
|
+
return next();
|
|
184
|
+
}
|
|
185
|
+
return next(t('inventory.error.forbidden', undefined, {
|
|
186
|
+
defaultValue: 'Admin access required to manage inventory',
|
|
187
|
+
}));
|
|
188
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser guard — `@molecule/api-resource-inventory` is SERVER-ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
|
|
5
|
+
* Evaluating a server package in a browser bundle is always an import-graph mistake
|
|
6
|
+
* (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
|
|
7
|
+
* ("Buffer is not defined") far from the culprit. Throwing here names the package and
|
|
8
|
+
* the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
|
|
9
|
+
* unaffected: the throw requires browser globals AND the absence of a node runtime.
|
|
10
|
+
*/
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
|
|
13
|
+
throw new Error('@molecule/api-resource-inventory is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
|
|
14
|
+
'from server code (a server route/function or your API), or dynamic-import it inside ' +
|
|
15
|
+
'the server handler — never from components or shared client modules, and never ' +
|
|
16
|
+
'polyfill Buffer/process to silence this.');
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for bulk stock updates.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
|
|
7
|
+
/**
|
|
8
|
+
* Processes multiple stock adjustments in a single request.
|
|
9
|
+
* @param req - The request with `adjustments` array body.
|
|
10
|
+
* @param res - The response object.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bulkUpdate(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=bulkUpdate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulkUpdate.d.ts","sourceRoot":"","sources":["../../src/handlers/bulkUpdate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAc/E;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoK3F"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for bulk stock updates.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { create, findOne, updateById } from '@molecule/api-database';
|
|
7
|
+
import { t } from '@molecule/api-i18n';
|
|
8
|
+
import { logger } from '@molecule/api-logger';
|
|
9
|
+
import { assertInventoryAdmin } from '../authorizers/index.js';
|
|
10
|
+
import { toStockInfo } from '../utilities.js';
|
|
11
|
+
/** Valid adjustment types. */
|
|
12
|
+
const VALID_TYPES = ['add', 'remove', 'set'];
|
|
13
|
+
/**
|
|
14
|
+
* Processes multiple stock adjustments in a single request.
|
|
15
|
+
* @param req - The request with `adjustments` array body.
|
|
16
|
+
* @param res - The response object.
|
|
17
|
+
*/
|
|
18
|
+
export async function bulkUpdate(req, res) {
|
|
19
|
+
// Admin-only: bulk-rewriting stock is a catalog mutation. Fail closed
|
|
20
|
+
// in-handler so the gate holds even if the route middleware is dropped.
|
|
21
|
+
if (!assertInventoryAdmin(res)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { adjustments } = req.body;
|
|
25
|
+
if (!adjustments || !Array.isArray(adjustments) || adjustments.length === 0) {
|
|
26
|
+
res.status(400).json({
|
|
27
|
+
error: t('inventory.error.adjustmentsRequired', undefined, {
|
|
28
|
+
defaultValue: 'At least one adjustment is required',
|
|
29
|
+
}),
|
|
30
|
+
errorKey: 'inventory.error.adjustmentsRequired',
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const results = [];
|
|
36
|
+
let succeeded = 0;
|
|
37
|
+
let failed = 0;
|
|
38
|
+
for (const adjustment of adjustments) {
|
|
39
|
+
try {
|
|
40
|
+
if (!adjustment.productId ||
|
|
41
|
+
adjustment.quantity == null ||
|
|
42
|
+
adjustment.quantity < 0 ||
|
|
43
|
+
!VALID_TYPES.includes(adjustment.type)) {
|
|
44
|
+
results.push({
|
|
45
|
+
productId: adjustment.productId ?? 'unknown',
|
|
46
|
+
variantId: adjustment.variantId,
|
|
47
|
+
success: false,
|
|
48
|
+
error: 'Invalid adjustment parameters',
|
|
49
|
+
});
|
|
50
|
+
failed++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const variantId = adjustment.variantId ?? null;
|
|
54
|
+
const where = [
|
|
55
|
+
{ field: 'productId', operator: '=', value: adjustment.productId },
|
|
56
|
+
{ field: 'variantId', operator: '=', value: variantId },
|
|
57
|
+
];
|
|
58
|
+
let row = await findOne('inventory_stock', where);
|
|
59
|
+
let newTotal;
|
|
60
|
+
if (!row) {
|
|
61
|
+
newTotal =
|
|
62
|
+
adjustment.type === 'set'
|
|
63
|
+
? adjustment.quantity
|
|
64
|
+
: adjustment.type === 'add'
|
|
65
|
+
? adjustment.quantity
|
|
66
|
+
: 0;
|
|
67
|
+
const createResult = await create('inventory_stock', {
|
|
68
|
+
productId: adjustment.productId,
|
|
69
|
+
variantId,
|
|
70
|
+
total: newTotal,
|
|
71
|
+
reserved: 0,
|
|
72
|
+
lowStockThreshold: 10,
|
|
73
|
+
});
|
|
74
|
+
row = createResult.data;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
if (adjustment.type === 'add') {
|
|
78
|
+
newTotal = row.total + adjustment.quantity;
|
|
79
|
+
}
|
|
80
|
+
else if (adjustment.type === 'remove') {
|
|
81
|
+
newTotal = row.total - adjustment.quantity;
|
|
82
|
+
if (newTotal < row.reserved) {
|
|
83
|
+
results.push({
|
|
84
|
+
productId: adjustment.productId,
|
|
85
|
+
variantId: adjustment.variantId,
|
|
86
|
+
success: false,
|
|
87
|
+
error: 'Would drop below reserved quantity',
|
|
88
|
+
});
|
|
89
|
+
failed++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
newTotal = adjustment.quantity;
|
|
95
|
+
if (newTotal < row.reserved) {
|
|
96
|
+
results.push({
|
|
97
|
+
productId: adjustment.productId,
|
|
98
|
+
variantId: adjustment.variantId,
|
|
99
|
+
success: false,
|
|
100
|
+
error: 'Value is below reserved quantity',
|
|
101
|
+
});
|
|
102
|
+
failed++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const updateResult = await updateById('inventory_stock', row.id, {
|
|
107
|
+
total: newTotal,
|
|
108
|
+
});
|
|
109
|
+
row = updateResult.data ?? { ...row, total: newTotal };
|
|
110
|
+
}
|
|
111
|
+
// Record movement
|
|
112
|
+
const movementQuantity = adjustment.type === 'add'
|
|
113
|
+
? adjustment.quantity
|
|
114
|
+
: adjustment.type === 'remove'
|
|
115
|
+
? -adjustment.quantity
|
|
116
|
+
: newTotal;
|
|
117
|
+
await create('inventory_movements', {
|
|
118
|
+
productId: adjustment.productId,
|
|
119
|
+
variantId,
|
|
120
|
+
type: 'adjustment',
|
|
121
|
+
quantity: movementQuantity,
|
|
122
|
+
reason: adjustment.reason ?? null,
|
|
123
|
+
referenceId: null,
|
|
124
|
+
});
|
|
125
|
+
results.push({
|
|
126
|
+
productId: adjustment.productId,
|
|
127
|
+
variantId: adjustment.variantId,
|
|
128
|
+
success: true,
|
|
129
|
+
stock: toStockInfo(row),
|
|
130
|
+
});
|
|
131
|
+
succeeded++;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
logger.warn('Failed to process stock adjustment', {
|
|
135
|
+
error,
|
|
136
|
+
productId: adjustment.productId,
|
|
137
|
+
});
|
|
138
|
+
results.push({
|
|
139
|
+
productId: adjustment.productId ?? 'unknown',
|
|
140
|
+
variantId: adjustment.variantId,
|
|
141
|
+
success: false,
|
|
142
|
+
error: 'Internal error processing adjustment',
|
|
143
|
+
});
|
|
144
|
+
failed++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
logger.debug('Bulk stock update completed', {
|
|
148
|
+
total: adjustments.length,
|
|
149
|
+
succeeded,
|
|
150
|
+
failed,
|
|
151
|
+
});
|
|
152
|
+
res.json({
|
|
153
|
+
total: adjustments.length,
|
|
154
|
+
succeeded,
|
|
155
|
+
failed,
|
|
156
|
+
results,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger.error('Failed to process bulk stock update', { error });
|
|
161
|
+
res.status(500).json({
|
|
162
|
+
error: t('inventory.error.bulkUpdateFailed', undefined, {
|
|
163
|
+
defaultValue: 'Failed to process bulk stock update',
|
|
164
|
+
}),
|
|
165
|
+
errorKey: 'inventory.error.bulkUpdateFailed',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for confirming a stock reservation (permanently removing from stock).
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
|
|
7
|
+
/**
|
|
8
|
+
* Confirms a reservation, permanently removing the reserved quantity from total stock.
|
|
9
|
+
* @param req - The request with `reservationId` param.
|
|
10
|
+
* @param res - The response object.
|
|
11
|
+
*/
|
|
12
|
+
export declare function confirm(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=confirm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm.d.ts","sourceRoot":"","sources":["../../src/handlers/confirm.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAK/E;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DxF"}
|