@blamejs/blamejs-shop 0.4.22 → 0.4.24

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.
@@ -606,6 +606,7 @@ var CHROME_DEFAULTS = Object.freeze({
606
606
  footer_operators_heading: "Your account",
607
607
  footer_operators_account: "Account",
608
608
  footer_operators_orders: "Orders",
609
+ footer_operators_suggestions: "Suggestion box",
609
610
  footer_operators_contact: "Contact",
610
611
 
611
612
  footer_copy_suffix: "built on blamejs · Apache 2.0 licensed.",
@@ -104,12 +104,12 @@ var SLUG_RE = /^[a-z0-9][a-z0-9_\-]{0,63}$/;
104
104
  var MAX_HEADER_NAME_LEN = 128;
105
105
  var HEADER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_\-]{0,127}$/;
106
106
 
107
- var MAX_IDEMPOTENCY_KEY_LEN = 256;
108
- // Idempotency keys are third-party-supplied opaque strings. Conservative
109
- // alphabet: printable ASCII excluding controls + quote characters that
110
- // could break a stored-comparison shape. Refuse zero-width / control
111
- // bytes to defeat downstream visual-spoofing in the dashboard.
112
- var IDEMPOTENCY_KEY_RE = /^[\x21-\x7e]{1,256}$/;
107
+ // Idempotency keys are third-party-supplied opaque strings. Validation is
108
+ // delegated to the framework's idempotency-key guard (strict profile:
109
+ // ≤256 bytes, ASCII-only) which additionally refuses path-traversal
110
+ // shapes `/`, `\`, and `..` runs that the earlier printable-ASCII
111
+ // regex let through. Strict allows a literal space (security-neutral for
112
+ // a stored-comparison key) and does NOT NFC-normalize the input.
113
113
 
114
114
  var MAX_OUTCOME_LEN = 280;
115
115
  var MAX_REASON_LEN = 1024;
@@ -228,20 +228,16 @@ function _retry(v) {
228
228
 
229
229
  function _idempotencyKey(s) {
230
230
  if (s == null) return null;
231
- if (typeof s !== "string") {
232
- throw new TypeError("webhookReceiver: idempotency_key must be a string when provided");
233
- }
234
- if (s.length === 0 || s.length > MAX_IDEMPOTENCY_KEY_LEN) {
235
- throw new TypeError(
236
- "webhookReceiver: idempotency_key must be 1.." + MAX_IDEMPOTENCY_KEY_LEN + " printable ASCII characters"
237
- );
231
+ // The framework guard owns the alphabet + length policy. It refuses
232
+ // empty keys, control bytes, non-ASCII, over-length, and the
233
+ // path-traversal shapes (`/`, `\`, `..`) the prior regex admitted.
234
+ // Translate its structured error to this primitive's TypeError shape
235
+ // so callers keep a uniform validation-failure surface.
236
+ try {
237
+ return b.guardIdempotencyKey.validate(s, { profile: "strict" });
238
+ } catch (e) {
239
+ throw new TypeError("webhookReceiver: idempotency_key — " + (e && e.message || "invalid idempotency key"));
238
240
  }
239
- if (!IDEMPOTENCY_KEY_RE.test(s)) {
240
- throw new TypeError(
241
- "webhookReceiver: idempotency_key must be printable ASCII (no control / non-ASCII bytes)"
242
- );
243
- }
244
- return s;
245
241
  }
246
242
 
247
243
  function _eventId(s) {
@@ -77,6 +77,13 @@
77
77
  * paths don't double-deliver the same restock signal. Without
78
78
  * it, every back_in_stock match fires regardless of stockAlerts
79
79
  * state.
80
+ * - emailSuppressions (optional) — when wired, the scanner
81
+ * short-circuits sends to addresses on the marketing-scope
82
+ * suppression list (hard bounce / spam complaint / unsubscribe)
83
+ * and accounts them as `suppressed` without consuming a
84
+ * dedupe / weekly-cap slot. These alerts are marketing mail and
85
+ * honour the same suppression list every other marketing path
86
+ * consults.
80
87
  *
81
88
  * Monotonic clock: a per-factory monotonic timestamp ensures that
82
89
  * two alerts written in the same wall-clock millisecond carry
@@ -269,6 +276,16 @@ function create(opts) {
269
276
  if (stockAlerts && typeof stockAlerts.isSubscribed !== "function") {
270
277
  throw new TypeError("wishlistAlerts.create: opts.stockAlerts must expose isSubscribed when wired");
271
278
  }
279
+ // Marketing suppression list. When wired the scanner short-circuits
280
+ // sends to addresses that hard-bounced, filed a spam complaint, or
281
+ // unsubscribed — these price-drop / back-in-stock notices are
282
+ // marketing-scope mail and must honour the same suppression list every
283
+ // other marketing path consults. Optional so an operator running only
284
+ // the cron without the suppression primitive still dispatches.
285
+ var emailSuppressions = opts.emailSuppressions || null;
286
+ if (emailSuppressions && typeof emailSuppressions.isSuppressed !== "function") {
287
+ throw new TypeError("wishlistAlerts.create: opts.emailSuppressions must expose isSuppressed when wired");
288
+ }
272
289
 
273
290
  // Pagination cursor secret — same posture as wishlist.create.
274
291
  if (typeof opts.cursorSecret !== "string" || !opts.cursorSecret.length) {
@@ -831,6 +848,7 @@ function create(opts) {
831
848
  weekly_cap_reached: 0,
832
849
  recent_dedupe: 0,
833
850
  no_email: 0,
851
+ suppressed: 0,
834
852
  email_dispatch_failed: 0,
835
853
  no_baseline_price: 0,
836
854
  no_current_price: 0,
@@ -892,6 +910,25 @@ function create(opts) {
892
910
  skipped += 1; skippedBy.no_email += 1; continue;
893
911
  }
894
912
 
913
+ // Marketing-suppression short-circuit. A hard-bounced /
914
+ // spam-complaint / unsubscribed address must not be mailed.
915
+ // Checked BEFORE the ledger claim so a suppressed customer never
916
+ // consumes a dedupe / weekly-cap slot for a send that can't
917
+ // happen — if they later resubscribe the next sweep can deliver.
918
+ if (emailSuppressions) {
919
+ var suppressed = false;
920
+ try {
921
+ var supView = await emailSuppressions.isSuppressed({
922
+ email: customerEmail,
923
+ scope: "marketing",
924
+ });
925
+ suppressed = !!(supView && supView.suppressed === true);
926
+ } catch (_eSup) { suppressed = false; }
927
+ if (suppressed) {
928
+ skipped += 1; skippedBy.suppressed += 1; continue;
929
+ }
930
+ }
931
+
895
932
  // Atomic dedupe + weekly-cap CLAIM. The conditional INSERT is the
896
933
  // serialization point: two concurrent sweeps can't both pass the
897
934
  // dedupe/cap guard and then both write — the loser's INSERT...
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
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": {