@blamejs/blamejs-shop 0.3.54 → 0.3.56

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.
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "0.3.54",
2
+ "version": "0.3.56",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
- "integrity": "sha384-/NizdENQkTc96romD0kmnl8220NAvW32QI0aSFuGiPbWE5lYbRxLXU5shsjlx5Ek",
6
- "fingerprinted": "css/admin.f028b2e410387546.css"
5
+ "integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
6
+ "fingerprinted": "css/admin.44eb97700c660798.css"
7
7
  },
8
8
  "css/main.css": {
9
- "integrity": "sha384-9rtEz2wKJbpfALccIipRdo/BWVQZi5O4IufqaLO/7jFyLK1xxCaDc/65Apjy8dbs",
10
- "fingerprinted": "css/main.1a1a379d6f58e2c0.css"
9
+ "integrity": "sha384-2nlE5YDlOjcnP/l1EY5kcYwvbV/TboHx+Hi6fAaDNyaau3nzQppJnNyXoLiFE8w1",
10
+ "fingerprinted": "css/main.59afbe779336867a.css"
11
11
  },
12
12
  "js/announcement.js": {
13
13
  "integrity": "sha384-z4zcEMn+tScoVnYRE4nEf8N/oyvpxdpaxTNrT4QO/jURChid4+qjAvWkzatCaAPq",
package/lib/email.js CHANGED
@@ -187,6 +187,75 @@ var ABANDONED_CART_TEXT_AFTER_LINES =
187
187
  "{{notes}}\n\n" +
188
188
  "Return to cart: {{cart_url}}\n";
189
189
 
190
+ // Wishlist digest — the periodic rollup of the customer's saved items
191
+ // (price + in/out-of-stock marker per line). The line set is rendered
192
+ // through the strict renderer once per line so each line's `title` /
193
+ // `price` / `stock` is HTML-escaped independently, then concatenated
194
+ // between the BEFORE/AFTER halves. The structured `lines[]` from
195
+ // composeDigest is the source — the pre-built `digest.html` is NEVER
196
+ // spliced in (that would double-handle escaping); rendering from the
197
+ // structured array keeps the escape-by-default path owning escaping
198
+ // end-to-end (the <script>-in-title XSS guard).
199
+
200
+ var WISHLIST_DIGEST_HTML_BEFORE_LINES =
201
+ "<!DOCTYPE html>\n" +
202
+ "<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Your wishlist update</title></head>" +
203
+ "<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
204
+ "<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
205
+ " <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">Your wishlist this period</h1>\n" +
206
+ " <p style=\"margin:0 0 16px;\">Here's where your saved items stand right now:</p>\n" +
207
+ " <ul style=\"padding-left:20px;margin:0 0 16px;color:#0d0d0d;\">\n";
208
+
209
+ var WISHLIST_DIGEST_HTML_LINE =
210
+ " <li><a href=\"{{product_url}}\" style=\"color:#fa4f09;\">{{title}}</a> — {{price}}{{stock}}</li>\n";
211
+
212
+ var WISHLIST_DIGEST_HTML_EMPTY =
213
+ " <li>No items in your wishlist right now.</li>\n";
214
+
215
+ var WISHLIST_DIGEST_HTML_AFTER_LINES =
216
+ " </ul>\n" +
217
+ " <p style=\"margin:16px 0;color:#0d0d0d;font-size:13px;\">You're receiving this because you opted into the wishlist digest.</p>\n" +
218
+ "</div>\n" +
219
+ "</body></html>\n";
220
+
221
+ var WISHLIST_DIGEST_TEXT_BEFORE_LINES =
222
+ "Your wishlist this period\n=========================\n\n" +
223
+ "Here's where your saved items stand right now:\n\n";
224
+
225
+ var WISHLIST_DIGEST_TEXT_LINE =
226
+ " {{title}} — {{price}}{{stock}}\n {{product_url}}\n";
227
+
228
+ var WISHLIST_DIGEST_TEXT_EMPTY =
229
+ " No items in your wishlist right now.\n";
230
+
231
+ var WISHLIST_DIGEST_TEXT_AFTER_LINES =
232
+ "\nYou're receiving this because you opted into the wishlist digest.\n";
233
+
234
+ // Magic-link sign-in — a single-use, time-limited link a customer
235
+ // follows to sign in without a passkey or password. The `link_url`
236
+ // carries the plaintext portal token; it's HTML-escaped through the
237
+ // strict renderer like every other value (the URL is built server-side
238
+ // but escape-by-default still applies).
239
+
240
+ var MAGIC_LINK_HTML =
241
+ "<!DOCTYPE html>\n" +
242
+ "<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Your sign-in link</title></head>" +
243
+ "<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
244
+ "<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
245
+ " <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">Sign in to your account</h1>\n" +
246
+ " <p style=\"margin:0 0 16px;\">Use the button below to sign in. This link works once and expires shortly.</p>\n" +
247
+ " <p style=\"margin:24px 0;\"><a href=\"{{link_url}}\" style=\"background:#fa4f09;color:#ffffff;padding:12px 20px;text-decoration:none;display:inline-block;font-weight:bold;\">Sign in</a></p>\n" +
248
+ " <p style=\"margin:0;color:#0d0d0d;font-size:13px;\">If the button doesn't work, paste this link into your browser: {{link_url}}</p>\n" +
249
+ " <p style=\"margin:16px 0 0;color:#0d0d0d;font-size:13px;\">If you didn't request this, you can ignore this email — no one can sign in without the link.</p>\n" +
250
+ "</div>\n" +
251
+ "</body></html>\n";
252
+
253
+ var MAGIC_LINK_TEXT =
254
+ "Sign in to your account\n\n" +
255
+ "Use this single-use link to sign in (it expires shortly):\n" +
256
+ "{{link_url}}\n\n" +
257
+ "If you didn't request this, you can ignore this email.\n";
258
+
190
259
  // Review request — sent ~7 days after ship. Per-product review
191
260
  // links use the same before/lines/after split as the abandoned
192
261
  // cart so the strict renderer keeps its escape-everything property.
@@ -443,6 +512,96 @@ function create(opts) {
443
512
  );
444
513
  },
445
514
 
515
+ // Wishlist digest — the periodic rollup of the customer's saved
516
+ // items. The caller (the wishlistDigest dispatcher) passes the
517
+ // structured `lines[]` composeDigest built; this renders each line
518
+ // through the strict renderer once so every `title` / `price` is
519
+ // HTML-escaped independently, then concatenates between the
520
+ // BEFORE/AFTER halves. The pre-built `digest.html` / `digest.text`
521
+ // are accepted by the caller's shape but NEVER spliced in raw —
522
+ // rendering from the structured array keeps the escape-by-default
523
+ // path owning escaping end-to-end (the <script>-in-title XSS guard).
524
+ // `lines` MAY be empty (the "no items" digest); validation is
525
+ // entry-point tier (throw TypeError so the cron caller's try/catch
526
+ // drops the row silently).
527
+ sendWishlistDigest: async function (input) {
528
+ if (!input) throw new TypeError("email.sendWishlistDigest: input object required");
529
+ if (typeof input.customer_email !== "string" || !input.customer_email) {
530
+ throw new TypeError("email.sendWishlistDigest: customer_email required");
531
+ }
532
+ if (!Array.isArray(input.lines)) {
533
+ throw new TypeError("email.sendWishlistDigest: lines array required (may be empty)");
534
+ }
535
+ var i;
536
+ for (i = 0; i < input.lines.length; i += 1) {
537
+ var ln = input.lines[i];
538
+ if (!ln || typeof ln !== "object") {
539
+ throw new TypeError("email.sendWishlistDigest: lines[" + i + "] must be an object");
540
+ }
541
+ if (typeof ln.title !== "string" || !ln.title) {
542
+ throw new TypeError("email.sendWishlistDigest: lines[" + i + "].title required");
543
+ }
544
+ }
545
+ var htmlLines = "";
546
+ var textLines = "";
547
+ if (input.lines.length === 0) {
548
+ htmlLines = WISHLIST_DIGEST_HTML_EMPTY;
549
+ textLines = WISHLIST_DIGEST_TEXT_EMPTY;
550
+ } else {
551
+ for (i = 0; i < input.lines.length; i += 1) {
552
+ var line = input.lines[i];
553
+ // The in/out-of-stock marker is derived here (not trusted from
554
+ // the caller as raw HTML) so it rides the strict renderer like
555
+ // every other field.
556
+ var stockMarker = line.in_stock === true ? " (in stock)"
557
+ : line.in_stock === false ? " (out of stock)"
558
+ : "";
559
+ var lineVars = {
560
+ title: line.title,
561
+ price: line.price == null ? "—" : String(line.price),
562
+ stock: stockMarker,
563
+ product_url: typeof line.product_url === "string" && line.product_url
564
+ ? line.product_url
565
+ : "#",
566
+ };
567
+ htmlLines += _render(WISHLIST_DIGEST_HTML_LINE, lineVars);
568
+ textLines += _render(WISHLIST_DIGEST_TEXT_LINE, lineVars);
569
+ }
570
+ }
571
+ var html =
572
+ WISHLIST_DIGEST_HTML_BEFORE_LINES +
573
+ htmlLines +
574
+ WISHLIST_DIGEST_HTML_AFTER_LINES;
575
+ var text =
576
+ WISHLIST_DIGEST_TEXT_BEFORE_LINES +
577
+ textLines +
578
+ WISHLIST_DIGEST_TEXT_AFTER_LINES;
579
+ return await _send(
580
+ input.customer_email,
581
+ "Your wishlist update",
582
+ html, text, input.replyTo
583
+ );
584
+ },
585
+
586
+ // Magic-link sign-in — emails a single-use portal link. The
587
+ // `link_url` carries the plaintext token; it's escaped through the
588
+ // strict renderer like every other value. Entry-point tier: throw
589
+ // TypeError on a bad shape so the caller's try/catch keeps the
590
+ // surface enumeration-safe (the generic confirmation shows either way).
591
+ sendMagicLink: async function (input) {
592
+ if (!input) throw new TypeError("email.sendMagicLink: input object required");
593
+ if (typeof input.customer_email !== "string" || !input.customer_email) {
594
+ throw new TypeError("email.sendMagicLink: customer_email required");
595
+ }
596
+ if (typeof input.link_url !== "string" || !input.link_url) {
597
+ throw new TypeError("email.sendMagicLink: link_url required");
598
+ }
599
+ var vars = { link_url: input.link_url };
600
+ var html = _render(MAGIC_LINK_HTML, vars);
601
+ var text = _render(MAGIC_LINK_TEXT, vars);
602
+ return await _send(input.customer_email, "Your sign-in link", html, text, input.replyTo);
603
+ },
604
+
446
605
  // Review request — sent ~7 days after ship. Per-product review
447
606
  // links are derived from review_base_url + "/" + slug + "/review",
448
607
  // then HTML-escaped through the strict renderer with the rest of
@@ -575,6 +734,16 @@ module.exports = {
575
734
  ABANDONED_CART_TEXT_BEFORE_LINES: ABANDONED_CART_TEXT_BEFORE_LINES,
576
735
  ABANDONED_CART_TEXT_LINE: ABANDONED_CART_TEXT_LINE,
577
736
  ABANDONED_CART_TEXT_AFTER_LINES: ABANDONED_CART_TEXT_AFTER_LINES,
737
+ WISHLIST_DIGEST_HTML_BEFORE_LINES: WISHLIST_DIGEST_HTML_BEFORE_LINES,
738
+ WISHLIST_DIGEST_HTML_LINE: WISHLIST_DIGEST_HTML_LINE,
739
+ WISHLIST_DIGEST_HTML_EMPTY: WISHLIST_DIGEST_HTML_EMPTY,
740
+ WISHLIST_DIGEST_HTML_AFTER_LINES: WISHLIST_DIGEST_HTML_AFTER_LINES,
741
+ WISHLIST_DIGEST_TEXT_BEFORE_LINES: WISHLIST_DIGEST_TEXT_BEFORE_LINES,
742
+ WISHLIST_DIGEST_TEXT_LINE: WISHLIST_DIGEST_TEXT_LINE,
743
+ WISHLIST_DIGEST_TEXT_EMPTY: WISHLIST_DIGEST_TEXT_EMPTY,
744
+ WISHLIST_DIGEST_TEXT_AFTER_LINES: WISHLIST_DIGEST_TEXT_AFTER_LINES,
745
+ MAGIC_LINK_HTML: MAGIC_LINK_HTML,
746
+ MAGIC_LINK_TEXT: MAGIC_LINK_TEXT,
578
747
  REVIEW_REQUEST_HTML_BEFORE_LINES: REVIEW_REQUEST_HTML_BEFORE_LINES,
579
748
  REVIEW_REQUEST_HTML_LINE: REVIEW_REQUEST_HTML_LINE,
580
749
  REVIEW_REQUEST_HTML_AFTER_LINES: REVIEW_REQUEST_HTML_AFTER_LINES,