@blamejs/blamejs-shop 0.4.112 → 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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/tax.js +49 -14
- 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.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
|
+
|
|
11
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.
|
|
12
14
|
|
|
13
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.
|
package/lib/asset-manifest.json
CHANGED
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
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
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
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
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(
|
|
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
|
-
|
|
455
|
-
|
|
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,
|
|
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,
|
|
469
|
-
buyer: { country:
|
|
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