@blamejs/blamejs-shop 0.3.52 → 0.3.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/lib/admin.js +1135 -1
- package/lib/asset-manifest.json +5 -1
- package/lib/click-and-collect.js +35 -5
- package/lib/email.js +94 -0
- package/lib/payment.js +77 -0
- package/lib/security-middleware.js +9 -0
- package/lib/storefront.js +1173 -5
- package/package.json +1 -1
package/lib/asset-manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.3.
|
|
2
|
+
"version": "0.3.54",
|
|
3
3
|
"assets": {
|
|
4
4
|
"css/admin.css": {
|
|
5
5
|
"integrity": "sha384-/NizdENQkTc96romD0kmnl8220NAvW32QI0aSFuGiPbWE5lYbRxLXU5shsjlx5Ek",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"js/paypal-checkout.js": {
|
|
45
45
|
"integrity": "sha384-LI6y/1z0Y9F8Kx8RhW4EwY2WqJPXLwJozCXqnhDT+dTckLHyvhly0SsRpH0bsdui",
|
|
46
46
|
"fingerprinted": "js/paypal-checkout.b05ab5572cc3728f.js"
|
|
47
|
+
},
|
|
48
|
+
"js/saved-card.js": {
|
|
49
|
+
"integrity": "sha384-Kaj6n+Any4rwCH2lyREHoq30MrAZtEd/fTa+tDnIrMJ4zO01YWRhW5TTujcYyuVn",
|
|
50
|
+
"fingerprinted": "js/saved-card.d7fd750d746dbe4d.js"
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
}
|
package/lib/click-and-collect.js
CHANGED
|
@@ -631,6 +631,21 @@ function create(opts) {
|
|
|
631
631
|
return row;
|
|
632
632
|
},
|
|
633
633
|
|
|
634
|
+
// Soft-delete a location: clears `active` and stamps `archived_at` so
|
|
635
|
+
// `availableLocations` filters it out while past pickup schedules still
|
|
636
|
+
// resolve the FK. Re-defining the same code via definePickupLocation
|
|
637
|
+
// clears `archived_at` (resurrects it). Returns the updated row, or null
|
|
638
|
+
// when the code matched no location (the route maps null → 404).
|
|
639
|
+
archiveLocation: async function (code) {
|
|
640
|
+
_code(code, "code");
|
|
641
|
+
var now = _now();
|
|
642
|
+
await query(
|
|
643
|
+
"UPDATE pickup_locations SET active = 0, archived_at = ?1, updated_at = ?1 WHERE code = ?2",
|
|
644
|
+
[now, code],
|
|
645
|
+
);
|
|
646
|
+
return await this.getLocation(code);
|
|
647
|
+
},
|
|
648
|
+
|
|
634
649
|
// Read a hydrated schedule for an order. Returns null on miss.
|
|
635
650
|
getScheduleByOrder: async function (orderId) {
|
|
636
651
|
var id = _orderId(orderId);
|
|
@@ -678,18 +693,33 @@ function create(opts) {
|
|
|
678
693
|
throw new TypeError("click-and-collect.customerSchedules: opts.order must be wired for " +
|
|
679
694
|
"this read (the customer→order linkage lives on the order primitive)");
|
|
680
695
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
696
|
+
// Page through the customer's orders via the cursor. The order
|
|
697
|
+
// primitive caps a single page below MAX_LIST_LIMIT, so a single
|
|
698
|
+
// `limit: MAX_LIST_LIMIT` call would throw — request a conservative
|
|
699
|
+
// per-page size and walk pages until exhausted OR we've gathered
|
|
700
|
+
// MAX_LIST_LIMIT order ids (which bounds the IN-list below). A customer
|
|
701
|
+
// with more orders than that sees their most-recent MAX_LIST_LIMIT
|
|
702
|
+
// orders' pickups, which matches the order-list page model.
|
|
703
|
+
var orderIds = [];
|
|
704
|
+
var cursor = null;
|
|
705
|
+
do {
|
|
706
|
+
var orderPage = await orderPrim.listForCustomer(id, { limit: 100, cursor: cursor || undefined });
|
|
707
|
+
var rows0 = (orderPage && orderPage.rows) || [];
|
|
708
|
+
for (var oi = 0; oi < rows0.length && orderIds.length < MAX_LIST_LIMIT; oi += 1) {
|
|
709
|
+
orderIds.push(rows0[oi].id);
|
|
710
|
+
}
|
|
711
|
+
cursor = orderPage && orderPage.next_cursor;
|
|
712
|
+
} while (cursor && orderIds.length < MAX_LIST_LIMIT);
|
|
713
|
+
if (!orderIds.length) return [];
|
|
684
714
|
// SQL IN-list construction — every value is a UUID that came
|
|
685
715
|
// out of the order primitive (which itself validates UUIDs at
|
|
686
716
|
// write time), so the placeholder fan-out is safe. We still
|
|
687
717
|
// pass them as bound params rather than interpolating.
|
|
688
718
|
var placeholders = [];
|
|
689
719
|
var params = [];
|
|
690
|
-
for (var i = 0; i <
|
|
720
|
+
for (var i = 0; i < orderIds.length; i += 1) {
|
|
691
721
|
placeholders.push("?" + (i + 1));
|
|
692
|
-
params.push(
|
|
722
|
+
params.push(orderIds[i]);
|
|
693
723
|
}
|
|
694
724
|
var sql = "SELECT * FROM pickup_schedules WHERE order_id IN (" +
|
|
695
725
|
placeholders.join(", ") + ") ORDER BY scheduled_window_start DESC, id DESC";
|
package/lib/email.js
CHANGED
|
@@ -220,6 +220,49 @@ var REVIEW_REQUEST_TEXT_LINE =
|
|
|
220
220
|
var REVIEW_REQUEST_TEXT_AFTER_LINES =
|
|
221
221
|
"\nEach link takes about a minute. Thank you.\n";
|
|
222
222
|
|
|
223
|
+
// Back-in-stock — double-opt-in confirmation + the fired notification.
|
|
224
|
+
// Both auto-escape every field through the strict `{{var}}` renderer
|
|
225
|
+
// (product_title / sku are catalog data; escape anyway).
|
|
226
|
+
|
|
227
|
+
var STOCK_ALERT_CONFIRM_HTML =
|
|
228
|
+
"<!DOCTYPE html>\n" +
|
|
229
|
+
"<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Confirm your back-in-stock alert</title></head>" +
|
|
230
|
+
"<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
|
|
231
|
+
"<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
|
|
232
|
+
" <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">Confirm your back-in-stock alert</h1>\n" +
|
|
233
|
+
" <p style=\"margin:0 0 16px;\">You asked to be emailed when <strong>{{product_title}}</strong> (SKU {{sku}}) is back in stock. Confirm below and we'll email you once, the moment it returns.</p>\n" +
|
|
234
|
+
" <p style=\"margin:24px 0;\"><a href=\"{{confirm_url}}\" style=\"background:#fa4f09;color:#ffffff;padding:12px 20px;text-decoration:none;display:inline-block;font-weight:bold;\">Confirm my alert</a></p>\n" +
|
|
235
|
+
" <p style=\"margin:0;color:#0d0d0d;font-size:13px;\">If you didn't request this, ignore this email — no alert is set until you confirm. Link: {{confirm_url}}</p>\n" +
|
|
236
|
+
"</div>\n" +
|
|
237
|
+
"</body></html>\n";
|
|
238
|
+
|
|
239
|
+
var STOCK_ALERT_CONFIRM_TEXT =
|
|
240
|
+
"Confirm your back-in-stock alert\n\n" +
|
|
241
|
+
"You asked to be emailed when {{product_title}} (SKU {{sku}}) is back in stock.\n" +
|
|
242
|
+
"Confirm to set the alert — we'll email you once, the moment it returns.\n\n" +
|
|
243
|
+
"Confirm: {{confirm_url}}\n\n" +
|
|
244
|
+
"If you didn't request this, ignore this email — no alert is set until you confirm.\n";
|
|
245
|
+
|
|
246
|
+
var BACK_IN_STOCK_HTML =
|
|
247
|
+
"<!DOCTYPE html>\n" +
|
|
248
|
+
"<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Back in stock: {{product_title}}</title></head>" +
|
|
249
|
+
"<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
|
|
250
|
+
"<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
|
|
251
|
+
" <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">It's back in stock</h1>\n" +
|
|
252
|
+
" <p style=\"margin:0 0 16px;\"><strong>{{product_title}}</strong> (SKU {{sku}}) is available again. Stock can move fast — grab it while it's here.</p>\n" +
|
|
253
|
+
" <p style=\"margin:24px 0;\"><a href=\"{{product_url}}\" style=\"background:#fa4f09;color:#ffffff;padding:12px 20px;text-decoration:none;display:inline-block;font-weight:bold;\">View the product</a></p>\n" +
|
|
254
|
+
" <p style=\"margin:0;color:#0d0d0d;font-size:13px;\">Link: {{product_url}}</p>\n" +
|
|
255
|
+
" <p style=\"margin:16px 0 0;color:#0d0d0d;font-size:13px;\">Don't want these alerts? Unsubscribe: {{unsubscribe_url}}</p>\n" +
|
|
256
|
+
"</div>\n" +
|
|
257
|
+
"</body></html>\n";
|
|
258
|
+
|
|
259
|
+
var BACK_IN_STOCK_TEXT =
|
|
260
|
+
"It's back in stock\n\n" +
|
|
261
|
+
"{{product_title}} (SKU {{sku}}) is available again.\n" +
|
|
262
|
+
"Stock can move fast — grab it while it's here.\n\n" +
|
|
263
|
+
"View the product: {{product_url}}\n\n" +
|
|
264
|
+
"Don't want these alerts? Unsubscribe: {{unsubscribe_url}}\n";
|
|
265
|
+
|
|
223
266
|
// ---- factory ------------------------------------------------------------
|
|
224
267
|
|
|
225
268
|
function _email(s) {
|
|
@@ -460,6 +503,53 @@ function create(opts) {
|
|
|
460
503
|
html, text, input.replyTo
|
|
461
504
|
);
|
|
462
505
|
},
|
|
506
|
+
|
|
507
|
+
// Back-in-stock double-opt-in confirmation. `confirm_url` is built by
|
|
508
|
+
// the route from SHOP_ORIGIN + the plaintext token returned once by
|
|
509
|
+
// stockAlerts.subscribe. product_title / sku are catalog data; the
|
|
510
|
+
// strict renderer escapes every field anyway (escape-by-default).
|
|
511
|
+
sendStockAlertConfirmation: async function (input) {
|
|
512
|
+
if (!input) throw new TypeError("email.sendStockAlertConfirmation: input object required");
|
|
513
|
+
if (typeof input.to !== "string" || !input.to) {
|
|
514
|
+
throw new TypeError("email.sendStockAlertConfirmation: to required");
|
|
515
|
+
}
|
|
516
|
+
if (typeof input.confirm_url !== "string" || !input.confirm_url) {
|
|
517
|
+
throw new TypeError("email.sendStockAlertConfirmation: confirm_url required");
|
|
518
|
+
}
|
|
519
|
+
var vars = {
|
|
520
|
+
product_title: input.product_title == null ? input.sku || "this item" : String(input.product_title),
|
|
521
|
+
sku: input.sku == null ? "—" : String(input.sku),
|
|
522
|
+
confirm_url: input.confirm_url,
|
|
523
|
+
};
|
|
524
|
+
var html = _render(STOCK_ALERT_CONFIRM_HTML, vars);
|
|
525
|
+
var text = _render(STOCK_ALERT_CONFIRM_TEXT, vars);
|
|
526
|
+
return await _send(input.to, "Confirm your back-in-stock alert", html, text, input.replyTo);
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
// Back-in-stock fired notification. Subject is built from the same
|
|
530
|
+
// already-escaped var path as the body (the subject string itself is
|
|
531
|
+
// never HTML — it carries the raw title; the mailer transport escapes
|
|
532
|
+
// header-injection, and the body is rendered through the strict
|
|
533
|
+
// escaping renderer).
|
|
534
|
+
sendBackInStock: async function (input) {
|
|
535
|
+
if (!input) throw new TypeError("email.sendBackInStock: input object required");
|
|
536
|
+
if (typeof input.to !== "string" || !input.to) {
|
|
537
|
+
throw new TypeError("email.sendBackInStock: to required");
|
|
538
|
+
}
|
|
539
|
+
if (typeof input.product_url !== "string" || !input.product_url) {
|
|
540
|
+
throw new TypeError("email.sendBackInStock: product_url required");
|
|
541
|
+
}
|
|
542
|
+
var title = input.product_title == null ? (input.sku || "this item") : String(input.product_title);
|
|
543
|
+
var vars = {
|
|
544
|
+
product_title: title,
|
|
545
|
+
sku: input.sku == null ? "—" : String(input.sku),
|
|
546
|
+
product_url: input.product_url,
|
|
547
|
+
unsubscribe_url: input.unsubscribe_url == null ? "" : String(input.unsubscribe_url),
|
|
548
|
+
};
|
|
549
|
+
var html = _render(BACK_IN_STOCK_HTML, vars);
|
|
550
|
+
var text = _render(BACK_IN_STOCK_TEXT, vars);
|
|
551
|
+
return await _send(input.to, "Back in stock: " + title, html, text, input.replyTo);
|
|
552
|
+
},
|
|
463
553
|
};
|
|
464
554
|
}
|
|
465
555
|
|
|
@@ -491,5 +581,9 @@ module.exports = {
|
|
|
491
581
|
REVIEW_REQUEST_TEXT_BEFORE_LINES: REVIEW_REQUEST_TEXT_BEFORE_LINES,
|
|
492
582
|
REVIEW_REQUEST_TEXT_LINE: REVIEW_REQUEST_TEXT_LINE,
|
|
493
583
|
REVIEW_REQUEST_TEXT_AFTER_LINES: REVIEW_REQUEST_TEXT_AFTER_LINES,
|
|
584
|
+
STOCK_ALERT_CONFIRM_HTML: STOCK_ALERT_CONFIRM_HTML,
|
|
585
|
+
STOCK_ALERT_CONFIRM_TEXT: STOCK_ALERT_CONFIRM_TEXT,
|
|
586
|
+
BACK_IN_STOCK_HTML: BACK_IN_STOCK_HTML,
|
|
587
|
+
BACK_IN_STOCK_TEXT: BACK_IN_STOCK_TEXT,
|
|
494
588
|
},
|
|
495
589
|
};
|
package/lib/payment.js
CHANGED
|
@@ -43,6 +43,8 @@ var IDEMPOTENT_OPERATIONS = {
|
|
|
43
43
|
"subscription.create": true,
|
|
44
44
|
"subscription.update": true,
|
|
45
45
|
"subscription.cancel": true,
|
|
46
|
+
"customer.create": true,
|
|
47
|
+
"setup_intent.create": true,
|
|
46
48
|
// PayPal adapter mutating operations (Orders v2).
|
|
47
49
|
"paypal_order.create": true,
|
|
48
50
|
"paypal_capture.create": true,
|
|
@@ -348,6 +350,20 @@ function stripe(opts) {
|
|
|
348
350
|
if (input.metadata) params.metadata = input.metadata;
|
|
349
351
|
if (input.description) params.description = input.description;
|
|
350
352
|
if (input.receipt_email) params.receipt_email = input.receipt_email;
|
|
353
|
+
// Saved-card reuse: a stored payment_method drives an off-session
|
|
354
|
+
// (one-click) charge. Both are optional so the default Elements flow
|
|
355
|
+
// — no payment_method, automatic_payment_methods.enabled — is
|
|
356
|
+
// byte-identical. When a payment_method is supplied we confirm
|
|
357
|
+
// immediately (confirm:true) so the stored card is charged in the
|
|
358
|
+
// single create call rather than requiring a second client round trip.
|
|
359
|
+
if (input.payment_method) {
|
|
360
|
+
if (typeof input.payment_method !== "string" || !input.payment_method.length) {
|
|
361
|
+
throw new TypeError("payment.createPaymentIntent: payment_method must be a non-empty string");
|
|
362
|
+
}
|
|
363
|
+
params.payment_method = input.payment_method;
|
|
364
|
+
params.confirm = true;
|
|
365
|
+
if (input.off_session) params.off_session = true;
|
|
366
|
+
}
|
|
351
367
|
return _maybeIdempotent("payment_intent.create", idempotencyKey, params, function () {
|
|
352
368
|
return _stripeCall(opts, "POST", "/payment_intents", params, idempotencyKey);
|
|
353
369
|
});
|
|
@@ -370,6 +386,67 @@ function stripe(opts) {
|
|
|
370
386
|
{}, idempotencyKey);
|
|
371
387
|
},
|
|
372
388
|
|
|
389
|
+
// ---- Customer + SetupIntent (saved-card vaulting) ---------------------
|
|
390
|
+
//
|
|
391
|
+
// The card-on-file flow needs a Stripe Customer to attach the saved
|
|
392
|
+
// payment method to, plus a SetupIntent the browser confirms (off the
|
|
393
|
+
// Elements card form) to collect-without-charging. The shop persists
|
|
394
|
+
// only the resulting opaque `pm_…` token — never the PAN/CVV, which
|
|
395
|
+
// stay inside Stripe's PCI scope. `usage: "off_session"` so the saved
|
|
396
|
+
// card is reusable for a later one-click (off-session) charge.
|
|
397
|
+
|
|
398
|
+
// Create (or look up) a Stripe Customer for a shopper. `email` is the
|
|
399
|
+
// only field Stripe needs to dedupe; `metadata` carries the shop's own
|
|
400
|
+
// customer id so a Stripe-dashboard operator can correlate the two.
|
|
401
|
+
createCustomer: function (input, idempotencyKey) {
|
|
402
|
+
if (!input || typeof input !== "object") throw new TypeError("payment.createCustomer: input object required");
|
|
403
|
+
var params = {};
|
|
404
|
+
if (input.email != null) {
|
|
405
|
+
if (typeof input.email !== "string" || !input.email.length) {
|
|
406
|
+
throw new TypeError("payment.createCustomer: email must be a non-empty string when provided");
|
|
407
|
+
}
|
|
408
|
+
params.email = input.email;
|
|
409
|
+
}
|
|
410
|
+
if (input.name) params.name = input.name;
|
|
411
|
+
if (input.metadata) params.metadata = input.metadata;
|
|
412
|
+
return _maybeIdempotent("customer.create", idempotencyKey, params, function () {
|
|
413
|
+
return _stripeCall(opts, "POST", "/customers", params, idempotencyKey);
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// Open a SetupIntent for the given Stripe Customer. The browser's
|
|
418
|
+
// Elements confirms it against the entered card; on success Stripe
|
|
419
|
+
// returns a `pm_…` the shop reads back via retrieveSetupIntent →
|
|
420
|
+
// retrievePaymentMethod and stores through paymentMethods.add.
|
|
421
|
+
createSetupIntent: function (input, idempotencyKey) {
|
|
422
|
+
if (!input || typeof input !== "object") throw new TypeError("payment.createSetupIntent: input object required");
|
|
423
|
+
_assertSecret(input.customer, "createSetupIntent.customer");
|
|
424
|
+
var params = {
|
|
425
|
+
customer: input.customer,
|
|
426
|
+
automatic_payment_methods: { enabled: true },
|
|
427
|
+
usage: "off_session",
|
|
428
|
+
};
|
|
429
|
+
if (input.metadata) params.metadata = input.metadata;
|
|
430
|
+
return _maybeIdempotent("setup_intent.create", idempotencyKey, params, function () {
|
|
431
|
+
return _stripeCall(opts, "POST", "/setup_intents", params, idempotencyKey);
|
|
432
|
+
});
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
// Read a SetupIntent back (after the browser confirmed it) to learn the
|
|
436
|
+
// resulting payment_method id. Read-only.
|
|
437
|
+
retrieveSetupIntent: function (id) {
|
|
438
|
+
_assertSecret(id, "setup_intent id");
|
|
439
|
+
return _stripeCall(opts, "GET", "/setup_intents/" + encodeURIComponent(id), null, null);
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
// Read a saved payment method's display fields (brand / last4 /
|
|
443
|
+
// exp_month / exp_year) so the shop can render "ending in 4242". The
|
|
444
|
+
// PAN/CVV are never in this payload. Read-only.
|
|
445
|
+
retrievePaymentMethod: function (id) {
|
|
446
|
+
_assertSecret(id, "payment_method id");
|
|
447
|
+
return _stripeCall(opts, "GET", "/payment_methods/" + encodeURIComponent(id), null, null);
|
|
448
|
+
},
|
|
449
|
+
|
|
373
450
|
// Register a web domain so Stripe enables the wallet methods (Apple
|
|
374
451
|
// Pay / Google Pay / Link / PayPal) for the Express Checkout Element
|
|
375
452
|
// served from it. Stripe performs Apple merchant validation + hosts
|
|
@@ -126,6 +126,15 @@ var EDGE_POST_PATHS = [
|
|
|
126
126
|
// layer). Container-only — there's no edge copy to keep parity with.
|
|
127
127
|
"/unsubscribe",
|
|
128
128
|
"/announcements/",
|
|
129
|
+
// Back-in-stock "Notify me" — the subscribe form lives on the edge-cached,
|
|
130
|
+
// cookie-less PDP buy box (dual-rendered byte-identical with the worker
|
|
131
|
+
// twin), so it cannot carry a per-session `_csrf` token. A DISTINCT
|
|
132
|
+
// top-level action keeps `/products/` (which DOES require the token on its
|
|
133
|
+
// review + question POSTs) un-exempted. Keeps SameSite + fetch-metadata
|
|
134
|
+
// defense; the confirm/unsubscribe GETs need no CSRF. Kept in lockstep with
|
|
135
|
+
// the `edge-form-csrf-exempt` detector lookahead in
|
|
136
|
+
// test/layer-0-primitives/codebase-patterns.test.js.
|
|
137
|
+
"/stock-alert/subscribe",
|
|
129
138
|
];
|
|
130
139
|
|
|
131
140
|
// The TLS-terminated public origin(s) the storefront is served on. Behind
|