@blamejs/blamejs-shop 0.0.52 → 0.0.54
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 +4 -0
- package/SECURITY.md +5 -3
- package/lib/analytics.js +400 -0
- package/lib/email.js +264 -0
- package/lib/giftcards.js +410 -0
- package/lib/index.js +4 -0
- package/lib/inventory-receive.js +494 -0
- package/lib/newsletter.js +176 -12
- package/lib/payment.js +193 -13
- package/lib/reviews.js +412 -0
- package/lib/storefront.js +52 -20
- package/lib/tax.js +391 -3
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/SECURITY.md +0 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.4.json +19 -0
- package/lib/webhooks.js +293 -16
- package/lib/wishlist.js +269 -0
- package/package.json +1 -1
package/lib/tax.js
CHANGED
|
@@ -154,8 +154,396 @@ function create(opts) {
|
|
|
154
154
|
return operatorTable(opts);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// ---- VAT / GST primitives ----------------------------------------------
|
|
158
|
+
//
|
|
159
|
+
// Sales tax (US-style) is *added* to a displayed price: $10 + 8.75% =
|
|
160
|
+
// $10.875. VAT (EU) and GST (AU / CA / IN / NZ / SG) are typically
|
|
161
|
+
// *extracted* from a displayed price: a €10 line at 20% VAT means the
|
|
162
|
+
// operator receives €8.33 net + €1.67 VAT, not €10 + €2. The B2B
|
|
163
|
+
// "reverse charge" mechanism on intra-EU trade moves the VAT
|
|
164
|
+
// obligation to the buyer (the seller invoices zero VAT and documents
|
|
165
|
+
// the buyer's VAT ID); the buyer self-assesses + remits in their own
|
|
166
|
+
// member state.
|
|
167
|
+
//
|
|
168
|
+
// These helpers are *pure functions* — they don't touch the network
|
|
169
|
+
// and they don't talk to the operator-table adapter. They're the
|
|
170
|
+
// math + format-validation surface the storefront / invoice
|
|
171
|
+
// primitives compose against. The operator-table adapter still owns
|
|
172
|
+
// jurisdiction-rule selection.
|
|
173
|
+
|
|
174
|
+
// Banker's rounding (round-half-to-even) for non-negative `raw`.
|
|
175
|
+
// JS Math.round is round-half-up, which biases tax totals upward
|
|
176
|
+
// across many small carts; this matches the rounding mode the
|
|
177
|
+
// existing operatorTable adapter uses.
|
|
178
|
+
function _bankersRound(raw) {
|
|
179
|
+
var floor = Math.floor(raw);
|
|
180
|
+
var frac = raw - floor;
|
|
181
|
+
if (frac < 0.5) return floor;
|
|
182
|
+
if (frac > 0.5) return floor + 1;
|
|
183
|
+
return (floor % 2 === 0) ? floor : floor + 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function _currency(c) {
|
|
187
|
+
if (typeof c !== "string" || !/^[A-Z]{3}$/.test(c)) {
|
|
188
|
+
throw new TypeError("tax: currency must be a 3-letter ISO 4217 code (uppercase), got " + JSON.stringify(c));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Extract VAT from a VAT-inclusive (gross) price.
|
|
194
|
+
*
|
|
195
|
+
* tax = round(gross * rate_bps / (10000 + rate_bps))
|
|
196
|
+
* net = gross - tax
|
|
197
|
+
*
|
|
198
|
+
* Returns `{ net_minor, tax_minor, gross_minor, rate_bps }`. The
|
|
199
|
+
* gross figure round-trips exactly (`net + tax === gross`) by
|
|
200
|
+
* construction — `net` is derived from `tax`, not rounded
|
|
201
|
+
* independently.
|
|
202
|
+
*
|
|
203
|
+
* Use when the storefront displays VAT-inclusive prices (the EU
|
|
204
|
+
* default for B2C). Banker's rounding to even.
|
|
205
|
+
*/
|
|
206
|
+
function calculateInclusive(args) {
|
|
207
|
+
if (!args || typeof args !== "object") throw new TypeError("tax.calculateInclusive: args object required");
|
|
208
|
+
_nonNegInt(args.amount_minor, "amount_minor");
|
|
209
|
+
_bps(args.rate_bps);
|
|
210
|
+
_currency(args.currency);
|
|
211
|
+
var gross = args.amount_minor;
|
|
212
|
+
var rate = args.rate_bps;
|
|
213
|
+
var tax = _bankersRound(gross * rate / (10000 + rate));
|
|
214
|
+
return {
|
|
215
|
+
net_minor: gross - tax,
|
|
216
|
+
tax_minor: tax,
|
|
217
|
+
gross_minor: gross,
|
|
218
|
+
rate_bps: rate,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Add VAT to a VAT-exclusive (net) price.
|
|
224
|
+
*
|
|
225
|
+
* tax = round(net * rate_bps / 10000)
|
|
226
|
+
* gross = net + tax
|
|
227
|
+
*
|
|
228
|
+
* Returns `{ net_minor, tax_minor, gross_minor, rate_bps }`. Use
|
|
229
|
+
* when the storefront displays VAT-exclusive prices (the standard
|
|
230
|
+
* B2B presentation, and the US-sales-tax shape). Banker's rounding
|
|
231
|
+
* to even — same convention as `operatorTable.calculate`.
|
|
232
|
+
*/
|
|
233
|
+
function calculateExclusive(args) {
|
|
234
|
+
if (!args || typeof args !== "object") throw new TypeError("tax.calculateExclusive: args object required");
|
|
235
|
+
_nonNegInt(args.amount_minor, "amount_minor");
|
|
236
|
+
_bps(args.rate_bps);
|
|
237
|
+
_currency(args.currency);
|
|
238
|
+
var net = args.amount_minor;
|
|
239
|
+
var rate = args.rate_bps;
|
|
240
|
+
var tax = _bankersRound(net * rate / 10000);
|
|
241
|
+
return {
|
|
242
|
+
net_minor: net,
|
|
243
|
+
tax_minor: tax,
|
|
244
|
+
gross_minor: net + tax,
|
|
245
|
+
rate_bps: rate,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---- VAT ID format validation -------------------------------------------
|
|
250
|
+
//
|
|
251
|
+
// VAT IDs have country-specific formats. The well-known patterns
|
|
252
|
+
// below cover the EU-27 + GB + Switzerland. These regexes validate
|
|
253
|
+
// the *format* only — they do NOT confirm the VAT ID is registered
|
|
254
|
+
// or active. That requires a live HTTPS call to the EU VIES service
|
|
255
|
+
// (or the equivalent national registry), which this primitive
|
|
256
|
+
// deliberately does NOT make:
|
|
257
|
+
//
|
|
258
|
+
// - Zero npm runtime deps (composing an HTTPS+XML client here is
|
|
259
|
+
// out of scope for a math primitive).
|
|
260
|
+
// - No SSRF surface in the tax math path.
|
|
261
|
+
// - VIES is a soft availability commitment; operators that depend
|
|
262
|
+
// on it need their own retry / cache / fallback policy, which
|
|
263
|
+
// belongs in the operator's checkout pipeline, not in lib/tax.js.
|
|
264
|
+
//
|
|
265
|
+
// Operators are responsible for VIES live-checking before storing
|
|
266
|
+
// any buyer VAT ID. This primitive only refuses obviously-malformed
|
|
267
|
+
// IDs (wrong country prefix, wrong length, wrong character class).
|
|
268
|
+
//
|
|
269
|
+
// The format table is operator-extensible — assign to
|
|
270
|
+
// `tax.VAT_FORMATS["XX"]` (a RegExp) to add or override a country.
|
|
271
|
+
//
|
|
272
|
+
// References: each pattern matches the published national format
|
|
273
|
+
// rule; mixed alphanumeric positions (FR, ES, IE) are preserved.
|
|
274
|
+
var VAT_FORMATS = {
|
|
275
|
+
AT: /^ATU[0-9]{8}$/, // Austria — U + 8 digits
|
|
276
|
+
BE: /^BE[01][0-9]{9}$/, // Belgium — 10 digits starting 0 or 1
|
|
277
|
+
BG: /^BG[0-9]{9,10}$/, // Bulgaria — 9 or 10 digits
|
|
278
|
+
CY: /^CY[0-9]{8}[A-Z]$/, // Cyprus — 8 digits + letter
|
|
279
|
+
CZ: /^CZ[0-9]{8,10}$/, // Czechia — 8-10 digits
|
|
280
|
+
DE: /^DE[0-9]{9}$/, // Germany — 9 digits
|
|
281
|
+
DK: /^DK[0-9]{8}$/, // Denmark — 8 digits
|
|
282
|
+
EE: /^EE[0-9]{9}$/, // Estonia — 9 digits
|
|
283
|
+
EL: /^EL[0-9]{9}$/, // Greece (EU-internal — Greece uses EL not GR)
|
|
284
|
+
GR: /^EL[0-9]{9}$/, // Greece alias — operators sometimes pass GR
|
|
285
|
+
ES: /^ES[A-Z0-9][0-9]{7}[A-Z0-9]$/, // Spain — alpha/digit, 7 digits, alpha/digit
|
|
286
|
+
FI: /^FI[0-9]{8}$/, // Finland — 8 digits
|
|
287
|
+
FR: /^FR[0-9A-HJ-NP-Z]{2}[0-9]{9}$/, // France — 2-char prefix (no I/O) + 9 digits
|
|
288
|
+
HR: /^HR[0-9]{11}$/, // Croatia — 11 digits
|
|
289
|
+
HU: /^HU[0-9]{8}$/, // Hungary — 8 digits
|
|
290
|
+
IE: /^IE[0-9][0-9A-Z*+][0-9]{5}[A-Z]{1,2}$/, // Ireland — 9-character mixed
|
|
291
|
+
IT: /^IT[0-9]{11}$/, // Italy — 11 digits
|
|
292
|
+
LT: /^LT(?:[0-9]{9}|[0-9]{12})$/, // Lithuania — 9 or 12 digits
|
|
293
|
+
LU: /^LU[0-9]{8}$/, // Luxembourg — 8 digits
|
|
294
|
+
LV: /^LV[0-9]{11}$/, // Latvia — 11 digits
|
|
295
|
+
MT: /^MT[0-9]{8}$/, // Malta — 8 digits
|
|
296
|
+
NL: /^NL[0-9]{9}B[0-9]{2}$/, // Netherlands — 9 digits + B + 2 digits
|
|
297
|
+
PL: /^PL[0-9]{10}$/, // Poland — 10 digits
|
|
298
|
+
PT: /^PT[0-9]{9}$/, // Portugal — 9 digits
|
|
299
|
+
RO: /^RO[0-9]{2,10}$/, // Romania — 2-10 digits
|
|
300
|
+
SE: /^SE[0-9]{12}$/, // Sweden — 12 digits
|
|
301
|
+
SI: /^SI[0-9]{8}$/, // Slovenia — 8 digits
|
|
302
|
+
SK: /^SK[0-9]{10}$/, // Slovakia — 10 digits
|
|
303
|
+
// Non-EU, retained for the EU-adjacent operator surface:
|
|
304
|
+
GB: /^GB(?:[0-9]{9}|[0-9]{12}|GD[0-9]{3}|HA[0-9]{3})$/, // United Kingdom — 9 / 12 digits or GD/HA + 3
|
|
305
|
+
CH: /^CHE[0-9]{9}(?:MWST|TVA|IVA)?$/, // Switzerland — CHE + 9 digits, optional suffix
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// EU-27 member-state country codes (for reverse-charge eligibility).
|
|
309
|
+
// GB is intentionally NOT included — Brexit removed the UK from the
|
|
310
|
+
// intra-EU reverse-charge mechanism. CH was never an EU member.
|
|
311
|
+
var EU_COUNTRY_CODES = [
|
|
312
|
+
"AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
|
|
313
|
+
"FR", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT",
|
|
314
|
+
"NL", "PL", "PT", "RO", "SE", "SI", "SK",
|
|
315
|
+
];
|
|
316
|
+
var _EU_SET = EU_COUNTRY_CODES.reduce(function (acc, c) { acc[c] = true; return acc; }, {});
|
|
317
|
+
|
|
318
|
+
function _isEu(cc) {
|
|
319
|
+
if (typeof cc !== "string") return false;
|
|
320
|
+
return _EU_SET[cc] === true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Validate a VAT ID's *format* against the country's published
|
|
325
|
+
* pattern. Returns `{ ok, country, vat_number, format }`:
|
|
326
|
+
*
|
|
327
|
+
* ok — true when the ID matches the country's regex
|
|
328
|
+
* country — the country code (uppercase, normalised)
|
|
329
|
+
* vat_number — the ID with the country prefix stripped
|
|
330
|
+
* format — the regex source string, for diagnostics
|
|
331
|
+
*
|
|
332
|
+
* On a mismatch returns `{ ok: false, country, vat_number: null,
|
|
333
|
+
* format }`. On an unknown country code throws — the caller asked
|
|
334
|
+
* for validation against a country we don't have a pattern for, and
|
|
335
|
+
* silently returning `{ ok: true }` would be unsafe.
|
|
336
|
+
*
|
|
337
|
+
* IMPORTANT: this is FORMAT validation only. It does NOT confirm
|
|
338
|
+
* the VAT ID is registered, active, or assigned to the named buyer.
|
|
339
|
+
* Operators MUST cross-check against VIES (https://ec.europa.eu/
|
|
340
|
+
* taxation_customs/vies/) or the equivalent national registry
|
|
341
|
+
* before relying on a VAT ID for reverse-charge invoicing. This
|
|
342
|
+
* primitive is the cheap pre-filter, not the legal record-of-truth.
|
|
343
|
+
*
|
|
344
|
+
* Extend `tax.VAT_FORMATS["XX"] = /^XX.../` to register additional
|
|
345
|
+
* country patterns at boot time.
|
|
346
|
+
*/
|
|
347
|
+
function validateVatId(vat_id, country_code) {
|
|
348
|
+
if (typeof vat_id !== "string") {
|
|
349
|
+
throw new TypeError("tax.validateVatId: vat_id must be a string, got " + JSON.stringify(vat_id));
|
|
350
|
+
}
|
|
351
|
+
if (typeof country_code !== "string" || !/^[A-Z]{2}$/.test(country_code)) {
|
|
352
|
+
throw new TypeError("tax.validateVatId: country_code must be a 2-letter uppercase ISO 3166-1 code, got " + JSON.stringify(country_code));
|
|
353
|
+
}
|
|
354
|
+
var pattern = VAT_FORMATS[country_code];
|
|
355
|
+
if (!pattern) {
|
|
356
|
+
throw new TypeError("tax.validateVatId: no VAT format registered for country " + JSON.stringify(country_code) + " — extend tax.VAT_FORMATS to add one");
|
|
357
|
+
}
|
|
358
|
+
// Normalise: strip whitespace; uppercase. Many invoicing UIs let
|
|
359
|
+
// operators paste "DE 123 456 789" or "de123456789" — accept both.
|
|
360
|
+
var normalised = vat_id.replace(/\s+/g, "").toUpperCase();
|
|
361
|
+
var matched = pattern.test(normalised);
|
|
362
|
+
if (!matched) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
country: country_code,
|
|
366
|
+
vat_number: null,
|
|
367
|
+
format: pattern.source,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// The country prefix on EU/GB/CH VAT IDs is always the first 2-3
|
|
371
|
+
// chars. We strip the matching country prefix from the front and
|
|
372
|
+
// return the remainder as the bare VAT number.
|
|
373
|
+
var prefix = normalised.indexOf(country_code) === 0 ? country_code : normalised.slice(0, 3);
|
|
374
|
+
return {
|
|
375
|
+
ok: true,
|
|
376
|
+
country: country_code,
|
|
377
|
+
vat_number: normalised.slice(prefix.length),
|
|
378
|
+
format: pattern.source,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Decide whether the intra-EU B2B reverse-charge mechanism applies
|
|
384
|
+
* to the current transaction. Returns one of:
|
|
385
|
+
*
|
|
386
|
+
* { rate_bps: 0, reverse_charge: true, reason: "eu-b2b",
|
|
387
|
+
* seller: { country, vat_number }, buyer: { country, vat_number } }
|
|
388
|
+
*
|
|
389
|
+
* { rate_bps: null, reverse_charge: false, reason: <why-not> }
|
|
390
|
+
*
|
|
391
|
+
* When the function returns `reverse_charge: true` the caller MUST
|
|
392
|
+
* invoice with VAT zero AND document both VAT IDs on the invoice;
|
|
393
|
+
* the buyer self-assesses + remits in their member state.
|
|
394
|
+
*
|
|
395
|
+
* Conditions (ALL must hold):
|
|
396
|
+
* 1. Seller has a VAT ID AND its country is in EU_COUNTRY_CODES.
|
|
397
|
+
* 2. Buyer has a VAT ID AND its country is in EU_COUNTRY_CODES.
|
|
398
|
+
* 3. Both VAT IDs pass format validation.
|
|
399
|
+
* 4. The buyer's billing country matches the buyer-VAT-ID country
|
|
400
|
+
* (a mismatch is a fraud red flag — the operator should refuse
|
|
401
|
+
* reverse charge and either charge the seller's rate or block
|
|
402
|
+
* checkout pending VIES confirmation).
|
|
403
|
+
*
|
|
404
|
+
* The `ctx` argument is reserved for future signals (e.g. a
|
|
405
|
+
* cached VIES-confirmation flag the operator's storefront sets
|
|
406
|
+
* after a successful live check). Today only the `vat_validated_at`
|
|
407
|
+
* timestamp is honoured as an advisory field on the seller side.
|
|
408
|
+
*
|
|
409
|
+
* IMPORTANT: this primitive does NOT call VIES. The operator must
|
|
410
|
+
* confirm the buyer's VAT ID against the EU VIES service (or
|
|
411
|
+
* equivalent national registry) BEFORE relying on the
|
|
412
|
+
* `reverse_charge: true` outcome to suppress VAT on an invoice.
|
|
413
|
+
* Format-pass + VIES-fail is a fraud vector; the operator's
|
|
414
|
+
* checkout pipeline owns the live check.
|
|
415
|
+
*/
|
|
416
|
+
function applyReverseCharge(args) {
|
|
417
|
+
if (!args || typeof args !== "object") throw new TypeError("tax.applyReverseCharge: args object required");
|
|
418
|
+
var ctx = args.ctx || {};
|
|
419
|
+
var buyerCountry = args.buyer_country;
|
|
420
|
+
if (typeof buyerCountry !== "string" || !/^[A-Z]{2}$/.test(buyerCountry)) {
|
|
421
|
+
throw new TypeError("tax.applyReverseCharge: buyer_country must be a 2-letter uppercase ISO 3166-1 code, got " + JSON.stringify(buyerCountry));
|
|
422
|
+
}
|
|
423
|
+
var sellerVatId = args.seller_vat_id;
|
|
424
|
+
var buyerVatId = args.buyer_vat_id;
|
|
425
|
+
|
|
426
|
+
// Both VAT IDs must be present. Missing either one means the
|
|
427
|
+
// buyer is treated as a B2C consumer — charge the seller's rate.
|
|
428
|
+
if (typeof sellerVatId !== "string" || !sellerVatId) {
|
|
429
|
+
return { rate_bps: null, reverse_charge: false, reason: "missing-seller-vat-id" };
|
|
430
|
+
}
|
|
431
|
+
if (typeof buyerVatId !== "string" || !buyerVatId) {
|
|
432
|
+
return { rate_bps: null, reverse_charge: false, reason: "missing-buyer-vat-id" };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// The seller's country prefix is the first 2 chars of the VAT ID.
|
|
436
|
+
// CH starts CHE — handle that out-of-band.
|
|
437
|
+
var sellerCountry = sellerVatId.slice(0, 2).toUpperCase();
|
|
438
|
+
if (sellerCountry === "CH") sellerCountry = "CH";
|
|
439
|
+
|
|
440
|
+
if (!_isEu(sellerCountry)) {
|
|
441
|
+
return { rate_bps: null, reverse_charge: false, reason: "seller-not-eu" };
|
|
442
|
+
}
|
|
443
|
+
if (!_isEu(buyerCountry)) {
|
|
444
|
+
return { rate_bps: null, reverse_charge: false, reason: "buyer-not-eu" };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Both VAT IDs must format-validate.
|
|
448
|
+
var sellerCheck;
|
|
449
|
+
var buyerCheck;
|
|
450
|
+
try { sellerCheck = validateVatId(sellerVatId, sellerCountry); }
|
|
451
|
+
catch (_e) { return { rate_bps: null, reverse_charge: false, reason: "seller-vat-id-unknown-country" }; }
|
|
452
|
+
if (!sellerCheck.ok) {
|
|
453
|
+
return { rate_bps: null, reverse_charge: false, reason: "seller-vat-id-bad-format" };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// The buyer's VAT ID country prefix must match the declared
|
|
457
|
+
// buyer_country. A mismatch is a red flag — the buyer may be
|
|
458
|
+
// trying to invoice into the wrong jurisdiction.
|
|
459
|
+
var buyerVatCountry = buyerVatId.slice(0, 2).toUpperCase();
|
|
460
|
+
if (buyerVatCountry !== buyerCountry) {
|
|
461
|
+
return { rate_bps: null, reverse_charge: false, reason: "buyer-vat-id-country-mismatch" };
|
|
462
|
+
}
|
|
463
|
+
try { buyerCheck = validateVatId(buyerVatId, buyerCountry); }
|
|
464
|
+
catch (_e) { return { rate_bps: null, reverse_charge: false, reason: "buyer-vat-id-unknown-country" }; }
|
|
465
|
+
if (!buyerCheck.ok) {
|
|
466
|
+
return { rate_bps: null, reverse_charge: false, reason: "buyer-vat-id-bad-format" };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
rate_bps: 0,
|
|
471
|
+
reverse_charge: true,
|
|
472
|
+
reason: "eu-b2b",
|
|
473
|
+
seller: { country: sellerCountry, vat_number: sellerCheck.vat_number },
|
|
474
|
+
buyer: { country: buyerCountry, vat_number: buyerCheck.vat_number },
|
|
475
|
+
// Advisory: timestamp at which the operator last live-checked
|
|
476
|
+
// VIES for the buyer (if supplied via ctx). The primitive does
|
|
477
|
+
// not consume this — it's surfaced so downstream invoice
|
|
478
|
+
// rendering can show "VIES-confirmed YYYY-MM-DD".
|
|
479
|
+
vies_validated_at: typeof ctx.vies_validated_at === "string" ? ctx.vies_validated_at : null,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Render a `rate_bps` value as a human-readable percentage string,
|
|
485
|
+
* locale-aware via `Intl.NumberFormat`. Returns:
|
|
486
|
+
*
|
|
487
|
+
* format({ rate_bps: 2000 }) -> "20.0%"
|
|
488
|
+
* format({ rate_bps: 550 }) -> "5.5%"
|
|
489
|
+
* format({ rate_bps: 0, reverse_charge: true }) -> "0% (reverse charge)"
|
|
490
|
+
* format({ rate_bps: 2000, locale: "de-DE" }) -> "20,0 %"
|
|
491
|
+
* format({ rate_bps: 2000, locale: "fr-FR" }) -> "20,0 %" (or NBSP variant)
|
|
492
|
+
*
|
|
493
|
+
* Locale defaults to `"en-US"`. The `reverse_charge` annotation is
|
|
494
|
+
* intentionally English-only in v1 — operator-facing accounting
|
|
495
|
+
* strings ship in English on every storefront the framework
|
|
496
|
+
* currently supports; localising the bracketed phrase lands when
|
|
497
|
+
* the storefront primitive grows a translation surface.
|
|
498
|
+
*/
|
|
499
|
+
function format(args) {
|
|
500
|
+
if (!args || typeof args !== "object") throw new TypeError("tax.format: args object required");
|
|
501
|
+
_bps(args.rate_bps);
|
|
502
|
+
if (args.locale !== undefined && args.locale !== null) {
|
|
503
|
+
if (typeof args.locale !== "string" || args.locale.length === 0) {
|
|
504
|
+
throw new TypeError("tax.format: locale must be a non-empty BCP 47 string");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
var locale = args.locale || "en-US";
|
|
508
|
+
var nf;
|
|
509
|
+
try {
|
|
510
|
+
nf = new Intl.NumberFormat(locale, {
|
|
511
|
+
style: "percent",
|
|
512
|
+
minimumFractionDigits: 1,
|
|
513
|
+
maximumFractionDigits: 2,
|
|
514
|
+
});
|
|
515
|
+
} catch (_e) {
|
|
516
|
+
throw new TypeError("tax.format: locale " + JSON.stringify(locale) + " rejected by Intl.NumberFormat");
|
|
517
|
+
}
|
|
518
|
+
// Intl percent formatter expects a decimal fraction (0.2 → "20%").
|
|
519
|
+
var rendered = nf.format(args.rate_bps / 10000);
|
|
520
|
+
// Strip a redundant ".0" / ",0" when the rate is a whole percent
|
|
521
|
+
// AND not the zero rate — "20.0%" reads naturally; "0.0%" reads
|
|
522
|
+
// as a precision claim we don't intend. For 0% with the reverse-
|
|
523
|
+
// charge flag we always render "0%" to keep the annotation
|
|
524
|
+
// grammatical.
|
|
525
|
+
if (args.reverse_charge === true && args.rate_bps === 0) {
|
|
526
|
+
// Intl renders 0 as "0%" already in en-US but as "0 %" in de/fr;
|
|
527
|
+
// we want a stable shape with the annotation.
|
|
528
|
+
var zero = new Intl.NumberFormat(locale, {
|
|
529
|
+
style: "percent",
|
|
530
|
+
minimumFractionDigits: 0,
|
|
531
|
+
maximumFractionDigits: 0,
|
|
532
|
+
}).format(0);
|
|
533
|
+
return zero + " (reverse charge)";
|
|
534
|
+
}
|
|
535
|
+
return rendered;
|
|
536
|
+
}
|
|
537
|
+
|
|
157
538
|
module.exports = {
|
|
158
|
-
create:
|
|
159
|
-
operatorTable:
|
|
160
|
-
MAX_BPS:
|
|
539
|
+
create: create,
|
|
540
|
+
operatorTable: operatorTable,
|
|
541
|
+
MAX_BPS: MAX_BPS,
|
|
542
|
+
calculateInclusive: calculateInclusive,
|
|
543
|
+
calculateExclusive: calculateExclusive,
|
|
544
|
+
validateVatId: validateVatId,
|
|
545
|
+
applyReverseCharge: applyReverseCharge,
|
|
546
|
+
format: format,
|
|
547
|
+
VAT_FORMATS: VAT_FORMATS,
|
|
548
|
+
EU_COUNTRY_CODES: EU_COUNTRY_CODES,
|
|
161
549
|
};
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.12.
|
|
7
|
-
"tag": "v0.12.
|
|
6
|
+
"version": "0.12.4",
|
|
7
|
+
"tag": "v0.12.4",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.4 (2026-05-22) — **`SECURITY.md` Watch list — remove stale "framework doesn't ship CMS / S/MIME" entry.** The Watch list bullet claiming `framework does not ship a CMS / S/MIME / PKCS#7 surface today` has been wrong since v0.10.13 — `b.cms.encodeSignedData` / `decode` / `encodeEnvelopedData` / `parseSignedData` shipped then, and `b.mail.crypto.smime.sign` / `verify` / `verifyAll` / `checkCert` shipped under the mail-stack. The Watch list is for CVE classes the framework deliberately doesn't ship a primitive for; CMS no longer fits that shape. Entry removed. **Fixed:** *Watch list no longer claims CMS / S/MIME are unshipped* — `b.cms` exposes RFC 5652 ContentInfo / SignedData / EnvelopedData encode + decode with PQC signer support (ML-DSA-65 per RFC 9909 §5, ML-DSA-87 per RFC 9909 §6, SLH-DSA-SHAKE-256f per RFC 9881). `b.mail.crypto.smime` builds on it for RFC 8551 S/MIME signed + enveloped mail with `checkCert` for X.509 chain validation. The SECURITY.md Watch list entry that pointed operators to external CMS libraries is gone; operators on regulated mail interop reach for the in-framework primitives instead.
|
|
12
|
+
|
|
11
13
|
- v0.12.3 (2026-05-22) — **README "What ships in the box" backfill — mail-stack listeners + JSCalendar + new postures.** The README's "Communication" + "Compliance regimes" bullets lagged behind the v0.11.24-v0.12.1 ship cadence. Backfilled: `b.mail.send.deliver` (turnkey outbound delivery chain), the four mail-server listeners (mx / submission / imap / jmap), the JMAP EmailSubmission/set reference handler, mail-crypto (CMS + PGP+WKD), the mail-stack agent, `b.calendar` (RFC 8984 JSCalendar substrate with full BY*+BYSETPOS+multi-rule expansion), and the 16 newly-promoted postures from v0.12.1 (`42-cfr-part-2` / `hti-1` / `uscdi-v4` / `irs-1075` / `nist-800-172-r3` / `tlp-2.0` / `soci-au` / `ffiec-cat-2` / `cri-profile-v2.0` / `m-22-09` / `m-22-18` / `nist-800-53-r5-privacy` / `nist-ai-600-1-genai` / `nist-csf-2.0` / `sb-53` / `nyc-ll144-2024`). **Changed:** *Communication section names every mail-stack listener + delivery chain + crypto primitive* — New entries: `b.mail.send.deliver` (MX → MTA-STS → DANE → REQUIRETLS → SMTP → DSN chain), four `b.mail.server.*` listeners, JMAP EmailSubmission reference handler, `b.mail.crypto.cms` + `b.mail.crypto.pgp`, `b.mail.agent` + `b.mailStore`, and `b.calendar` (JSCalendar / iCalendar bridge for JMAP Calendars interop). · *Compliance regimes section lists the 16 v0.12.1 backfilled postures* — New rows organise the additions under three sub-bullets: AI governance adds `nyc-ll144-2024` / `sb-53` / `nist-ai-rmf-1.0` / `nist-ai-600-1-genai` alongside the existing AI-act / NYC-LL144 / Colorado / Illinois entries; a new "Federal / sectoral" row covers `42-cfr-part-2` / `hti-1` / `uscdi-v4` / `irs-1075` / `nist-csf-2.0` / `nist-800-53-r5-privacy` / `nist-800-172-r3` / `m-22-09` / `m-22-18` / `ffiec-cat-2` / `cri-profile-v2.0`; a new "Critical infrastructure / info-sharing" row covers `soci-au` / `tlp-2.0`.
|
|
12
14
|
|
|
13
15
|
- v0.12.2 (2026-05-22) — **Release-process docs point at `scripts/release.js` (the orchestrator shipped in v0.12.0).** `CONTRIBUTING.md` (maintainer section) and `examples/wiki/DEPLOY.md` ("Tag-driven releases") described the old multi-step manual release flow — version bump → commit → push → tag → push tag — without mentioning the v0.12.0 orchestrator. Both docs now point at `node scripts/release.js` as the canonical release mechanism, list the eight idempotent subcommands, and call out the two pre-requisites the script enforces (release-notes JSON + signed-commit config). **Added:** *`scripts/release.js regen` — re-run artifact regeneration mid-flow* — Edits to `release-notes/v<next>.json` after `prepare` (e.g. addressing a Codex finding, fixing a leak-vocabulary refusal) previously required running `node scripts/generate-changelog-entry.js --rebuild` + `scripts/refresh-api-snapshot.js` + `scripts/check-api-snapshot.js` + `scripts/check-changelog-extract.js` manually. The new `regen` subcommand wraps all four into a single idempotent step. Safe to run any time from any branch. The `prepare` phase calls the same shared helper internally so behaviour stays consistent. **Changed:** *`CONTRIBUTING.md` maintainer section names the orchestrator* — The release-process bullet now reads `node scripts/release.js — eight idempotent subcommands (prepare → smoke → commit → push → watch → merge → tag → publish) plus all for a one-shot`. The existing DEPLOY.md link stays as a pointer for the wiki-container side of the same flow. · *`examples/wiki/DEPLOY.md` Tag-driven releases section rewritten* — Replaces the four-bullet manual flow with the orchestrator surface, including the `all` / `all --minor` one-shot, the per-phase subcommands, and the pre-requisites the script enforces (release-notes JSON present, SSH signing config in place). The downstream wiki-image deploy step on the host (pin `docker-compose.prod.yml` + `docker compose pull && up -d`) is unchanged. **Fixed:** *`scripts/release.js` signature verification uses `git verify-commit` as the canonical truth* — The v0.12.0 orchestrator's commit-signature gate parsed `git log -1 --pretty=%h %G? %GS` looking for `G` in the second column. On some platforms the `%G?` format token's `?` character can be eaten by argument resolution, returning empty stdout even when the signature is Good. The fix runs `git verify-commit HEAD` (whose exit code is the canonical signal `required_signatures` GH ruleset enforces) as the primary check; the `%G?` parse stays as a human-readable confirmation but no longer gates the script. Surfaced via dogfooding the orchestrator on this very release. · *`scripts/release.js` Docker bind-mount path handles Windows host paths with spaces* — The `push` phase's gitleaks step bind-mounted the repo root via `-v <path>:/repo`. The previous path transform produced `/C:/Users/...` on Windows, which Docker's `-v src:dst[:mode]` parser interpreted as having three colon-separated fields. Fix: transform `C:\Users\...` to `//c/Users/...` (lowercased drive letter, double-slash prefix — matches Git Bash's `$(pwd)` form Docker Desktop accepts). POSIX hosts pass through unchanged. Operators with Windows paths containing spaces, parentheses, or special characters can now run `node scripts/release.js push` without manual mount fiddling.
|
|
@@ -412,7 +412,6 @@ CVE classes the framework tracks but does not currently ship a primitive for —
|
|
|
412
412
|
- **AdonisJS multipart-filename → arbitrary-write class** — the framework's `b.fileUpload` routes every multipart filename through `b.guardFilename.gate({ profile: "strict" })` by default (path traversal / null-byte / NTFS ADS / UNC / overlong UTF-8 / Windows reserved names / RTLO bidi). Operators implementing a parallel multipart receiver outside the framework's primitive must wire the same gate.
|
|
413
413
|
- **fs.realpath symlink-chain Permission Model bypass class** — see "Operator territory" entry above; the framework's symlink defenses live at the application layer (`b.vault` PEM-file read-side + `b.staticServe` realpath gate); operators using Node's experimental Permission Model add it as defense-in-depth, never as the primary symlink-resolution boundary.
|
|
414
414
|
- **QUIC / HTTP/3 outbound (RFC 9000 / RFC 9001)** — the framework's `b.httpClient` is HTTP/1.1 + HTTP/2 only. QUIC + HTTP/3 are deferred-with-condition: re-open when Node's `--experimental-quic` flag graduates to stable and `node:http3` ships. Until then, operators wanting outbound h3 wire their own client outside the framework (see `lib/http-client.js` header note on the future `kind: "h3"` transport shape). The framework's TLS 1.3 + h2 anti-amplification + flow-control caps remain in force on every other transport. Inbound h3 is similarly deferred; operators terminating h3 at the edge route h2 / h1 to the framework's `b.router`.
|
|
415
|
-
- **CMS (RFC 5083 / RFC 5652) + SHAKE-in-CMS (RFC 8702)** — the framework does not ship a CMS / S/MIME / PKCS#7 surface today. Operators integrating S/MIME-encoded mail or PKCS#7-signed payloads route through a separately-firewalled CMS library and pin the SHAKE-256 / SHA3-512 hash identifiers from RFC 8702 §3 / §4 when they set the signing algorithm. CMS support is deferred-with-condition: re-open when operator demand surfaces for S/MIME-encoded mail receivers OR when a regulatory regime mandates CMS-shaped envelope formats. The framework's existing `b.crypto` envelope (XChaCha20-Poly1305 + ML-KEM-1024 + SLH-DSA) covers the at-rest + in-transit shapes operators need today without the CMS legacy surface.
|
|
416
415
|
|
|
417
416
|
---
|
|
418
417
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.4",
|
|
4
|
+
"date": "2026-05-22",
|
|
5
|
+
"headline": "`SECURITY.md` Watch list — remove stale \"framework doesn't ship CMS / S/MIME\" entry",
|
|
6
|
+
"summary": "The Watch list bullet claiming `framework does not ship a CMS / S/MIME / PKCS#7 surface today` has been wrong since v0.10.13 — `b.cms.encodeSignedData` / `decode` / `encodeEnvelopedData` / `parseSignedData` shipped then, and `b.mail.crypto.smime.sign` / `verify` / `verifyAll` / `checkCert` shipped under the mail-stack. The Watch list is for CVE classes the framework deliberately doesn't ship a primitive for; CMS no longer fits that shape. Entry removed.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Fixed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Watch list no longer claims CMS / S/MIME are unshipped",
|
|
13
|
+
"body": "`b.cms` exposes RFC 5652 ContentInfo / SignedData / EnvelopedData encode + decode with PQC signer support (ML-DSA-65 per RFC 9909 §5, ML-DSA-87 per RFC 9909 §6, SLH-DSA-SHAKE-256f per RFC 9881). `b.mail.crypto.smime` builds on it for RFC 8551 S/MIME signed + enveloped mail with `checkCert` for X.509 chain validation. The SECURITY.md Watch list entry that pointed operators to external CMS libraries is gone; operators on regulated mail interop reach for the in-framework primitives instead."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"references": []
|
|
19
|
+
}
|