@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.
Files changed (53) hide show
  1. package/LICENSE +115 -0
  2. package/dist/authorizers/index.d.ts +133 -0
  3. package/dist/authorizers/index.d.ts.map +1 -0
  4. package/dist/authorizers/index.js +188 -0
  5. package/dist/browser-guard.d.ts +2 -0
  6. package/dist/browser-guard.d.ts.map +1 -0
  7. package/dist/browser-guard.js +18 -0
  8. package/dist/handlers/bulkUpdate.d.ts +13 -0
  9. package/dist/handlers/bulkUpdate.d.ts.map +1 -0
  10. package/dist/handlers/bulkUpdate.js +168 -0
  11. package/dist/handlers/confirm.d.ts +13 -0
  12. package/dist/handlers/confirm.d.ts.map +1 -0
  13. package/dist/handlers/confirm.js +68 -0
  14. package/dist/handlers/getAlerts.d.ts +14 -0
  15. package/dist/handlers/getAlerts.d.ts.map +1 -0
  16. package/dist/handlers/getAlerts.js +52 -0
  17. package/dist/handlers/getMovements.d.ts +13 -0
  18. package/dist/handlers/getMovements.d.ts.map +1 -0
  19. package/dist/handlers/getMovements.js +62 -0
  20. package/dist/handlers/getStock.d.ts +13 -0
  21. package/dist/handlers/getStock.d.ts.map +1 -0
  22. package/dist/handlers/getStock.js +58 -0
  23. package/dist/handlers/index.d.ts +9 -0
  24. package/dist/handlers/index.d.ts.map +1 -0
  25. package/dist/handlers/index.js +8 -0
  26. package/dist/handlers/release.d.ts +13 -0
  27. package/dist/handlers/release.d.ts.map +1 -0
  28. package/dist/handlers/release.js +67 -0
  29. package/dist/handlers/reserve.d.ts +13 -0
  30. package/dist/handlers/reserve.d.ts.map +1 -0
  31. package/dist/handlers/reserve.js +112 -0
  32. package/dist/handlers/updateStock.d.ts +13 -0
  33. package/dist/handlers/updateStock.d.ts.map +1 -0
  34. package/dist/handlers/updateStock.js +126 -0
  35. package/dist/i18n.d.ts +14 -0
  36. package/dist/i18n.d.ts.map +1 -0
  37. package/dist/i18n.js +13 -0
  38. package/dist/index.d.ts +79 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +78 -0
  41. package/dist/requestHandlerMap.d.ts +34 -0
  42. package/dist/requestHandlerMap.d.ts.map +1 -0
  43. package/dist/requestHandlerMap.js +34 -0
  44. package/dist/routes.d.ts +61 -0
  45. package/dist/routes.d.ts.map +1 -0
  46. package/dist/routes.js +69 -0
  47. package/dist/types.d.ts +234 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +6 -0
  50. package/dist/utilities.d.ts +31 -0
  51. package/dist/utilities.d.ts.map +1 -0
  52. package/dist/utilities.js +79 -0
  53. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -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"}