@blamejs/blamejs-shop 0.4.111 → 0.4.113

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.113 (2026-06-26) — **VAT-ID validation and EU reverse-charge now handle Greece's EL prefix and Switzerland's CHE prefix correctly.** Two related correctness fixes in the tax module's VAT handling. The bare VAT number returned by validateVatId was corrupted for Greek and Swiss IDs: it dropped the leading digit of a Greek number when the caller passed the ISO code "GR", and it left a stray "E" on a Swiss number — because the prefix was guessed by length instead of taken from the registry's actual prefix (Greece files its EU VAT under "EL", Switzerland under "CHE"). Separately, the intra-EU B2B reverse charge was always denied to Greek parties: the country was derived from the VAT-ID prefix ("EL") and checked against the EU member list, which is keyed by the ISO code ("GR"), with no alias normalization — so a Greek buyer or seller was treated as non-EU and charged standard VAT where EU law requires 0% with reverse charge. Both paths now canonicalize the EL/GR alias and strip the exact registry prefix. Every other country, and a genuine VAT-ID/country mismatch, behaves exactly as before. **Fixed:** *EU reverse-charge is no longer wrongly denied to Greek (EL) VAT IDs* — applyReverseCharge derives a party's country from the first characters of its VAT ID. Greek EU VAT IDs carry the prefix "EL" while Greece's ISO 3166-1 code is "GR", and the EU-membership check is keyed by ISO code — so a Greek seller, or a Greek buyer (whether the country was supplied as "GR" or "EL"), was classified as non-EU and refused the reverse charge, causing standard VAT to be invoiced on a transaction where EU law requires 0% with the buyer self-assessing. The derived prefix and the declared country are now canonicalized (EL is treated as GR) before the EU-membership and prefix-match checks. A genuine mismatch — a non-Greek VAT ID declared against a GR country, or vice versa — is still refused. · *validateVatId returns the exact bare number for Greek (EL) and Swiss (CHE) IDs* — The bare VAT number was produced by guessing the prefix length (2 vs 3 characters) rather than using the registry's actual prefix. For a Greek ID queried under the ISO code "GR" this dropped the first digit of the number; for a Swiss "CHE…" ID it left the "E" attached. The prefix is now taken from the registry — "EL" for Greece (ISO "GR"), "CHE" for Switzerland (ISO "CH"), and the two-letter ISO code for every other registry — so the returned number that prints on a reverse-charge invoice is exact. IDs whose registered number itself begins with letters (Austria's U, France's two-character lead, Spain's alphanumeric) are unaffected.
12
+
13
+ - v0.4.112 (2026-06-26) — **The edge Stripe-webhook signature check now uses the framework's hardened verifier instead of a hand-rolled parse.** The Cloudflare edge worker verifies the Stripe webhook signature before forwarding a delivery to the container. That edge check previously parsed the `t=`/`v1=` signature header and compared the HMAC by hand. It now composes the framework's Stripe-signature verifier (HMAC-SHA256, the scheme Stripe publishes), which adds a bound on the signature-header size and on each `v1` hex length (so a malformed or oversized header is rejected cheaply), refuses a tolerance window below 30 seconds, matches across every `v1` signature a header carries, and compares in constant time. The container still re-verifies the same signature authoritatively, so this is a stricter front-line filter, not the only line of defense. Valid Stripe deliveries verify exactly as before; an unsigned, tampered, replayed, or oversized delivery is now refused at the edge by the same hardened code path the container uses. No configuration changes and no operator action required. **Changed:** *Edge Stripe-webhook verification composes the framework's hardened signature verifier* — The edge worker's inbound Stripe-signature pre-check no longer hand-parses the `t=`/`v1=` header and hand-compares the HMAC. It now calls the framework's Stripe verifier (HMAC-SHA256 over `<timestamp>.<body>`, the format Stripe documents), which applies a signature-header size cap, a per-`v1` hex-length cap, a minimum 30-second tolerance floor, matching across all `v1` signatures present, and a constant-time digest comparison — then forwards to the container, which re-verifies authoritatively (defense in depth). Valid deliveries are unaffected; unsigned, tampered, replayed, or oversized deliveries are refused at the edge before any container resource is touched. The framework's webhook module resolves its outbound HTTP client and delivery dispatcher only on the send path, so importing it solely to verify an inbound signature pulls no networking into the edge bundle.
14
+
11
15
  - v0.4.111 (2026-06-26) — **Vendored framework refreshed to 0.15.30.** Updates the vendored blamejs framework to 0.15.30. Upstream makes the webhook module's inbound signature-verification path loadable in a Worker / edge runtime: the outbound HTTP client and the delivery dispatcher are now resolved lazily (only on the send path), so importing the webhook module directly to verify an inbound signature no longer pulls the Node networking stack. This is the capability needed to compose the framework's Stripe-signature verifier at the edge instead of a hand-rolled check; this release vendors it, and the edge adoption follows separately. No runtime API or behavior change for this storefront in this release; no operator action required. **Changed:** *Vendored framework refreshed to 0.15.30* — Refreshes the vendored blamejs framework to 0.15.30. Upstream resolves the webhook module's outbound HTTP client and delivery dispatcher lazily (on the send/dispatch path only), so the inbound signature-verification path loads no Node networking module and can run in a Worker / edge runtime when the webhook module is imported directly. No runtime API change and no operator action required in this release.
12
16
 
13
17
  - v0.4.110 (2026-06-26) — **Inbound webhook bodies and external API responses are parsed through the size-bounded, prototype-pollution-stripping JSON primitive instead of a raw parse.** The remaining places that parsed externally-supplied JSON with a raw JSON.parse — the Stripe and PayPal inbound webhook bodies, the captcha provider's verification response, and the Stripe / PayPal / R2 API responses — now route through the framework's safe-JSON parser, which strips prototype-pollution keys ("__proto__") and bounds parse depth, key count, and byte size. This matters most for the PayPal webhook body, which is parsed up front before its signature is verified, and for every inbound or provider response that previously had no size bound. An audit confirmed the rest of the app's JSON parsing already reads only values it wrote itself to the database (validated at write time) or already used the safe parser, so those are unchanged. Valid payloads parse identically; a malformed, prototype-pollution-laden, or oversized body is now defused. No configuration changes. **Changed:** *Externally-supplied JSON (webhook bodies, provider responses) is parsed with the safe-JSON primitive* — The inbound Stripe and PayPal webhook bodies, the captcha siteverify response, and the Stripe / PayPal / R2 API responses were parsed with a raw JSON.parse, which keeps a "__proto__" member as an own key and imposes no size bound. These now route through the framework's safe-JSON parser (prototype-pollution-key strip + depth / key / size caps), so an inbound or external payload can no longer carry a prototype-pollution gadget or force an unbounded parse — notably the PayPal webhook body, which is decoded before its signature is verified. The change is parse-only: valid payloads decode exactly as before, each call keeps its existing error fallback, and the rest of the app's JSON parsing (values it serialized to its own database, validated at write time) is left as-is.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.111",
2
+ "version": "0.4.113",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
package/lib/tax.js CHANGED
@@ -315,6 +315,15 @@ function _isEu(cc) {
315
315
  return _EU_SET[cc] === true;
316
316
  }
317
317
 
318
+ // Greece files its EU VAT IDs under the prefix "EL"; its ISO 3166-1
319
+ // code is "GR". The two name the same member state. Canonicalise a
320
+ // VAT-ID-derived or operator-declared "EL" to "GR" before the
321
+ // EU-membership and prefix-match checks so a Greek B2B party is not
322
+ // wrongly denied the reverse charge.
323
+ function _canonicalVatCountry(cc) {
324
+ return cc === "EL" ? "GR" : cc;
325
+ }
326
+
318
327
  /**
319
328
  * Validate a VAT ID's *format* against the country's published
320
329
  * pattern. Returns `{ ok, country, vat_number, format }`:
@@ -362,10 +371,32 @@ function validateVatId(vat_id, country_code) {
362
371
  format: pattern.source,
363
372
  };
364
373
  }
365
- // The country prefix on EU/GB/CH VAT IDs is always the first 2-3
366
- // chars. We strip the matching country prefix from the front and
367
- // return the remainder as the bare VAT number.
368
- var prefix = normalised.indexOf(country_code) === 0 ? country_code : normalised.slice(0, 3);
374
+ // Strip the registry prefix to return the bare VAT number. The
375
+ // prefix the number is FILED under is not always the 2-letter ISO
376
+ // code used as the table key:
377
+ // - Most registries file under the ISO code itself (DE, FR, …),
378
+ // and any letters after it are part of the registered number
379
+ // (Austria's "ATU…" → bare "U…", France's "FRAB…" → "AB…").
380
+ // When the ID starts with the ISO key, strip exactly the key.
381
+ // - Switzerland files under "CHE" — a superset of its ISO code
382
+ // "CH", so the starts-with-key test would wrongly keep the "E".
383
+ // Strip the full "CHE".
384
+ // - Greece files under "EL" (ISO "GR"), and an operator may
385
+ // register a custom VAT_FORMATS entry whose filed prefix is any
386
+ // alias of its key. For these the ID does not start with the ISO
387
+ // key, so strip the leading alphabetic run — the actual filed
388
+ // prefix — which resolves Greece AND custom aliases without
389
+ // hard-coding each one (and without the old length-guess that
390
+ // dropped the leading GR digit).
391
+ var prefix;
392
+ if (country_code === "CH") {
393
+ prefix = "CHE";
394
+ } else if (normalised.indexOf(country_code) === 0) {
395
+ prefix = country_code;
396
+ } else {
397
+ var leadAlpha = /^[A-Z]+/.exec(normalised);
398
+ prefix = leadAlpha ? leadAlpha[0] : country_code;
399
+ }
369
400
  return {
370
401
  ok: true,
371
402
  country: country_code,
@@ -428,14 +459,16 @@ function applyReverseCharge(args) {
428
459
  }
429
460
 
430
461
  // The seller's country prefix is the first 2 chars of the VAT ID.
431
- // CH starts CHEhandle that out-of-band.
432
- var sellerCountry = sellerVatId.slice(0, 2).toUpperCase();
433
- if (sellerCountry === "CH") sellerCountry = "CH";
462
+ // Greek IDs carry "EL" canonicalise to the ISO code "GR" so the
463
+ // EU-membership check (keyed by ISO code) recognises them. A Swiss
464
+ // "CHE…" ID yields "CH", which is correctly not in the EU set.
465
+ var sellerCountry = _canonicalVatCountry(sellerVatId.slice(0, 2).toUpperCase());
466
+ var buyerCountryCanon = _canonicalVatCountry(buyerCountry);
434
467
 
435
468
  if (!_isEu(sellerCountry)) {
436
469
  return { rate_bps: null, reverse_charge: false, reason: "seller-not-eu" };
437
470
  }
438
- if (!_isEu(buyerCountry)) {
471
+ if (!_isEu(buyerCountryCanon)) {
439
472
  return { rate_bps: null, reverse_charge: false, reason: "buyer-not-eu" };
440
473
  }
441
474
 
@@ -450,12 +483,14 @@ function applyReverseCharge(args) {
450
483
 
451
484
  // The buyer's VAT ID country prefix must match the declared
452
485
  // buyer_country. A mismatch is a red flag — the buyer may be
453
- // trying to invoice into the wrong jurisdiction.
454
- var buyerVatCountry = buyerVatId.slice(0, 2).toUpperCase();
455
- if (buyerVatCountry !== buyerCountry) {
486
+ // trying to invoice into the wrong jurisdiction. Both sides are
487
+ // canonicalised (EL→GR) so a Greek buyer declaring ISO "GR" with an
488
+ // "EL…" VAT ID is not falsely flagged as a mismatch.
489
+ var buyerVatCountry = _canonicalVatCountry(buyerVatId.slice(0, 2).toUpperCase());
490
+ if (buyerVatCountry !== buyerCountryCanon) {
456
491
  return { rate_bps: null, reverse_charge: false, reason: "buyer-vat-id-country-mismatch" };
457
492
  }
458
- try { buyerCheck = validateVatId(buyerVatId, buyerCountry); }
493
+ try { buyerCheck = validateVatId(buyerVatId, buyerCountryCanon); }
459
494
  catch (_e) { return { rate_bps: null, reverse_charge: false, reason: "buyer-vat-id-unknown-country" }; }
460
495
  if (!buyerCheck.ok) {
461
496
  return { rate_bps: null, reverse_charge: false, reason: "buyer-vat-id-bad-format" };
@@ -465,8 +500,8 @@ function applyReverseCharge(args) {
465
500
  rate_bps: 0,
466
501
  reverse_charge: true,
467
502
  reason: "eu-b2b",
468
- seller: { country: sellerCountry, vat_number: sellerCheck.vat_number },
469
- buyer: { country: buyerCountry, vat_number: buyerCheck.vat_number },
503
+ seller: { country: sellerCountry, vat_number: sellerCheck.vat_number },
504
+ buyer: { country: buyerCountryCanon, vat_number: buyerCheck.vat_number },
470
505
  // Advisory: timestamp at which the operator last live-checked
471
506
  // VIES for the buyer (if supplied via ctx). The primitive does
472
507
  // not consume this — it's surfaced so downstream invoice
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.111",
3
+ "version": "0.4.113",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {