@blamejs/blamejs-shop 0.5.17 → 0.5.18
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/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/customer-impersonation.js +3 -3
- package/lib/operator-roles.js +12 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- v0.5.18 (2026-07-25) — **The operator role-permission check fails closed on an unknown capability instead of throwing, and customer impersonation's capability is grantable through the RBAC system.** Fixes an authorization-control defect in operatorRoles.hasPermission and the customer-impersonation capability gate. hasPermission — the runtime authorization check that operator features gate on — routed the requested permission through the same strict validator the config-time surfaces use, which throws on any token outside the closed permission allow-list. A feature that gated on a capability the allow-list did not carry therefore made the check CRASH rather than return allow or deny: with the roles peer wired, every such check threw and the gated action was inoperable; the correct posture for an authorization check is to fail closed. hasPermission now returns false (deny) for an unknown or garbage permission token; the config-time surfaces (defineRole / patchRole) still validate their permission lists strictly. Separately, the customer-impersonation capability was a token the allow-list did not carry, so it could never be granted to a role NOR checked — impersonation could not be gated through the RBAC system at all. It is now a first-class permission (customers.impersonate) that a role can grant and the check resolves, so an operator without it is denied and one with it is allowed. The same root fix turns the two-person-approval workflow's capability check from a crash into a fail-closed deny when a workflow names a capability outside the allow-list. **Security:** *The RBAC permission check fails closed on an unknown capability instead of throwing* — operatorRoles.hasPermission validated the requested permission with the strict allow-list check the configuration surfaces use, which throws on any token outside the closed allow-list. A feature gating on a capability the allow-list did not carry (customer impersonation's capability, or a two-person-approval workflow's required capability) therefore crashed the check instead of getting an allow/deny answer — inoperable with the roles peer wired, and never the intended fail-closed behavior. hasPermission now returns false for an unknown or garbage permission token, so a caller gating on an unrecognized capability is denied rather than crashed. defineRole / patchRole still reject an unknown permission at configuration time, where a strict error is correct. · *Customer-impersonation capability is a first-class, grantable permission* — The capability that customer impersonation checks defaulted to a token the permission allow-list did not carry, so it could neither be granted to an operator role nor resolved by the check — impersonation could not be gated through the RBAC system, and composing the real roles primitive made the gate throw. The capability is now customers.impersonate, a member of the closed allow-list: a role can grant it, an operator holding that role is allowed to impersonate, and an operator without it is refused with a typed error at the primitive layer.
|
|
12
|
+
|
|
11
13
|
- v0.5.17 (2026-07-25) — **Backorder capacity is enforced atomically under concurrent requests, and a stock-transfer over-receipt no longer mints net inventory.** Two inventory-quantity integrity fixes. recordBackorder checked the per-SKU supplier cap (max_backorder_quantity) against a snapshot read and then incremented the pending counter unconditionally, so two concurrent backorders of the same SKU could both pass the check on the same starting value and both apply their increment, pushing the pending total past the cap and over-committing to the supplier. The increment now folds the cap into a single guarded update — only an increment that keeps the pending total within the cap applies — and is gated on the row count: a losing claim removes the line it had inserted and refuses, so the counter can never exceed the cap regardless of interleaving. Separately, stock-transfer reconcile credited the destination location with the full received quantity, so a mis-scan that received MORE than was shipped credited the destination beyond what the origin was debited at ship time — minting net sellable inventory the transfer created nowhere. The destination credit is now clamped to the shipped (debited) quantity, and the over-count is recorded as a negative discrepancy for the operator; a normal or short receipt is unchanged. **Fixed:** *Backorder supplier cap holds under concurrent requests* — recordBackorder enforced max_backorder_quantity by reading the current pending total, checking pending + requested <= cap, and then unconditionally incrementing the counter. Two concurrent backorders of the same SKU could each read the same pending value, each pass the check, and each apply its increment, leaving the pending total above the cap — over-committing beyond supplier capacity. The counter increment now carries the cap in its WHERE clause so only an increment that keeps the total within the cap applies, and the call checks that exactly one row changed; a losing claim (a concurrent line took the remaining capacity, or the SKU was deactivated) removes the line it inserted and refuses with the current reason. The early snapshot check is kept as a fast-fail for the common case. · *Stock-transfer over-receipt no longer mints net inventory* — When a transfer was reconciled, the destination location was credited with the full received quantity. A mis-scan (or a genuinely found unit) that recorded more received than was shipped therefore credited the destination beyond what the origin had been debited when the transfer opened — creating net sellable stock the transfer never moved. The destination credit is now clamped to the shipped quantity (what the origin was debited), so an over-receipt can no longer mint inventory; the discrepancy column records the true shipped-minus-received delta (negative for an over-receipt) so the operator still sees the mis-count. Exact and short receipts are unaffected.
|
|
12
14
|
|
|
13
15
|
- v0.5.16 (2026-07-25) — **Vendored framework refreshed to 0.17.23: session refresh paths enforce the idle/absolute timeout floor and two verifier fail-closed contracts are hardened.** Refreshes the vendored framework from 0.17.22 to 0.17.23, a security patch to primitives the store composes; all of the following are inherited without shop code changes. The encrypted-session primitive now enforces its idle and absolute timeout floors on the refresh paths, not just on read: rotating a session or writing session data previously reset the activity clock without re-checking the idle floor, so a session already past its idle timeout — one a normal verify would expire and delete — could be revived with a fresh idle window; both refresh paths now fail closed on a breach. A strict device-fingerprint policy configured without a request to compute the fingerprint now fails closed instead of silently admitting the session from any device. The audit-signature verifier now returns false on a malformed public key instead of throwing, honoring its never-throw contract for callers that branch on the boolean. The bundled Public Suffix List is refreshed to the current snapshot. The minimum Node.js engine is unchanged at 24.18.0. **Changed:** *Vendored framework refreshed to 0.17.23* — Advances the vendored framework from 0.17.22 to 0.17.23. Beyond the session and verifier fixes above, the bundled Public Suffix List is refreshed to the current upstream snapshot so registrable-domain and public-suffix checks across URL, cookie, and SSRF handling reflect the latest registry delegations. The minimum Node.js engine is unchanged at 24.18.0. **Security:** *Session refresh paths can no longer resurrect an idle-expired session* — The encrypted-session primitive enforces two floors independent of the configured TTL — an idle timeout and an absolute timeout. Read paths already enforced them, but rotating a session (on a privilege change) or writing session data reset the activity clock without re-checking the idle floor, so a session already past its idle timeout — which a normal verify would expire and delete — could be revived with a fresh window by rotating or writing to it. Both refresh paths now route through the same floor check and fail closed on a breach, and the idle/absolute-expiry audit signal is emitted consistently. Inherited from the framework refresh; the store's sessions gain the tighter enforcement with no configuration change. · *A strict device-fingerprint policy with no request now fails closed; audit-signature verify no longer throws on a malformed key* — Two verifier fail-closed fixes ride in with the refresh. A strict device-binding policy (require-fingerprint-match / max-anomaly-score) that was configured without a request to compute the current fingerprint previously skipped the binding check and admitted the session from any device; it now fails closed. And the audit-signature verifier, documented never to throw so callers can branch on its boolean, threw on a malformed / non-PEM public key (an untrusted key can reach it from a signature block); it now catches that and returns false, so a verifier-only consumer cleanly rejects instead of crashing. Inherited from the framework refresh.
|
package/lib/asset-manifest.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* 1. The operator calls `startImpersonation({ operator_id,
|
|
23
23
|
* customer_id, reason })`. When the optional `operatorRoles`
|
|
24
24
|
* peer is wired, the call gates on `hasPermission(operator_id,
|
|
25
|
-
* "
|
|
25
|
+
* "customers.impersonate")` — operators without the
|
|
26
26
|
* capability are refused at the primitive layer, not in
|
|
27
27
|
* application code. The primitive mints a 32-byte base64url
|
|
28
28
|
* plaintext bearer via `b.crypto.generateBytes(32)` +
|
|
@@ -146,7 +146,7 @@ var MAX_ENDED_BY_LEN = 64;
|
|
|
146
146
|
var MAX_ACTION_LEN = 128;
|
|
147
147
|
var MAX_RESOURCE_KIND_LEN = 64;
|
|
148
148
|
var MAX_RESOURCE_ID_LEN = 256;
|
|
149
|
-
var DEFAULT_CAPABILITY = "
|
|
149
|
+
var DEFAULT_CAPABILITY = "customers.impersonate";
|
|
150
150
|
|
|
151
151
|
var STATUS_VALUES = Object.freeze([
|
|
152
152
|
"active", "ended", "expired", "revoked",
|
|
@@ -299,7 +299,7 @@ function create(opts) {
|
|
|
299
299
|
//
|
|
300
300
|
// Mints a fresh impersonation session. When `operatorRoles` is
|
|
301
301
|
// wired, gates on `hasPermission(operator_id,
|
|
302
|
-
// requires_capability ?? "
|
|
302
|
+
// requires_capability ?? "customers.impersonate")` — operators
|
|
303
303
|
// without the capability are refused with a typed error. The
|
|
304
304
|
// plaintext bearer is returned ONCE; the database stores only the
|
|
305
305
|
// namespaceHash. Default 60-minute TTL caps the blast radius if
|
package/lib/operator-roles.js
CHANGED
|
@@ -154,6 +154,7 @@ var PERMISSIONS = Object.freeze([
|
|
|
154
154
|
"orders.cancel",
|
|
155
155
|
"customers.read",
|
|
156
156
|
"customers.export",
|
|
157
|
+
"customers.impersonate",
|
|
157
158
|
"catalog.read",
|
|
158
159
|
"catalog.write",
|
|
159
160
|
"inventory.write",
|
|
@@ -640,7 +641,17 @@ function create(opts) {
|
|
|
640
641
|
throw new TypeError("operatorRoles.hasPermission: input object required");
|
|
641
642
|
}
|
|
642
643
|
var operatorId = _operatorId(input.operator_id, "operator_id");
|
|
643
|
-
|
|
644
|
+
// hasPermission is a hot-path authorization CHECK, not a config-time entry
|
|
645
|
+
// point: an unknown or garbage permission token is DENIED (return false),
|
|
646
|
+
// not thrown. Throwing here would crash a caller that gates on a capability
|
|
647
|
+
// the allow-list doesn't carry (e.g. a workflow's required_capability, or a
|
|
648
|
+
// feature capability) — the correct posture for an authorization check is
|
|
649
|
+
// fail-closed. Config-time surfaces (defineRole, patchRole) still validate
|
|
650
|
+
// permissions strictly via _permission.
|
|
651
|
+
if (typeof input.permission !== "string" || PERMISSIONS.indexOf(input.permission) === -1) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
var permission = input.permission;
|
|
644
655
|
|
|
645
656
|
var now = _now();
|
|
646
657
|
var rows = (await query(
|
package/package.json
CHANGED