@blamejs/blamejs-shop 0.0.128 → 0.1.0

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -0
  3. package/lib/admin.js +1 -2
  4. package/lib/affiliates.js +4 -3
  5. package/lib/analytics.js +3 -2
  6. package/lib/api-keys.js +1 -1
  7. package/lib/assembly-instructions.js +2 -1
  8. package/lib/auto-replenish.js +4 -3
  9. package/lib/backorder.js +2 -1
  10. package/lib/business-hours.js +8 -1
  11. package/lib/carrier-accounts.js +1 -1
  12. package/lib/carrier-rates.js +1 -1
  13. package/lib/cart-abandonment.js +3 -2
  14. package/lib/cart-bulk-ops.js +2 -1
  15. package/lib/cart-recovery.js +5 -4
  16. package/lib/cart.js +6 -2
  17. package/lib/catalog-drafts.js +1 -1
  18. package/lib/click-and-collect.js +3 -2
  19. package/lib/clickstream.js +4 -3
  20. package/lib/config.js +2 -1
  21. package/lib/cookie-consent.js +2 -1
  22. package/lib/credit-limits.js +2 -1
  23. package/lib/currency-display.js +2 -1
  24. package/lib/customer-activity.js +3 -2
  25. package/lib/customer-impersonation.js +3 -3
  26. package/lib/customer-merge.js +4 -3
  27. package/lib/customer-portal.js +4 -4
  28. package/lib/customer-risk-profile.js +2 -1
  29. package/lib/customer-segments.js +2 -1
  30. package/lib/customer-surveys.js +6 -3
  31. package/lib/delivery-estimate.js +2 -2
  32. package/lib/demand-forecast.js +2 -1
  33. package/lib/discount-analytics.js +2 -2
  34. package/lib/dunning.js +4 -1
  35. package/lib/email-warmup.js +6 -1
  36. package/lib/email.js +1 -8
  37. package/lib/error-log.js +3 -2
  38. package/lib/event-log.js +3 -2
  39. package/lib/fraud-screen.js +3 -1
  40. package/lib/fulfillment-sla.js +3 -1
  41. package/lib/index.js +11 -3
  42. package/lib/inventory-allocations.js +3 -0
  43. package/lib/inventory-snapshots.js +2 -1
  44. package/lib/invoice-renderer.js +2 -1
  45. package/lib/line-gift-wrap.js +6 -1
  46. package/lib/live-chat.js +2 -1
  47. package/lib/loyalty-redemption.js +2 -1
  48. package/lib/newsletter.js +6 -1
  49. package/lib/operator-activity-feed.js +4 -3
  50. package/lib/operator-sessions.js +7 -7
  51. package/lib/order-exchanges.js +1 -0
  52. package/lib/order-timeline.js +2 -1
  53. package/lib/payment-retries.js +2 -1
  54. package/lib/payment.js +5 -4
  55. package/lib/pixel-events.js +6 -5
  56. package/lib/preorder.js +2 -1
  57. package/lib/print-queue.js +2 -1
  58. package/lib/product-compare.js +2 -1
  59. package/lib/product-qa.js +2 -1
  60. package/lib/push-notifications.js +6 -5
  61. package/lib/recently-viewed.js +7 -2
  62. package/lib/recommendations.js +7 -2
  63. package/lib/referral-leaderboard.js +2 -1
  64. package/lib/refund-automation.js +1 -1
  65. package/lib/refund-policy.js +1 -1
  66. package/lib/reorder-reminders.js +2 -1
  67. package/lib/reorder-thresholds.js +2 -1
  68. package/lib/robots-config.js +1 -0
  69. package/lib/sales-reports.js +17 -14
  70. package/lib/sales-tax-filings.js +2 -1
  71. package/lib/save-for-later.js +2 -1
  72. package/lib/search-suggestions.js +1 -1
  73. package/lib/shipping-insurance.js +2 -1
  74. package/lib/shipping-labels.js +3 -2
  75. package/lib/shipping-zones.js +1 -0
  76. package/lib/shrinkage-report.js +9 -8
  77. package/lib/sms-dispatcher.js +6 -5
  78. package/lib/stock-alerts.js +1 -1
  79. package/lib/stock-receipts.js +2 -1
  80. package/lib/store-credit.js +2 -1
  81. package/lib/storefront-forms.js +1 -1
  82. package/lib/storefront.js +223 -141
  83. package/lib/subscription-analytics.js +7 -2
  84. package/lib/subscription-controls.js +9 -8
  85. package/lib/subscription-gifts.js +2 -1
  86. package/lib/subscriptions.js +2 -0
  87. package/lib/support-tickets.js +4 -4
  88. package/lib/tax-cert-renewals.js +2 -1
  89. package/lib/tax-remittance.js +2 -1
  90. package/lib/theme-assets.js +1 -1
  91. package/lib/vendor/MANIFEST.json +2 -2
  92. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  93. package/lib/vendor/blamejs/README.md +2 -0
  94. package/lib/vendor/blamejs/api-snapshot.json +122 -2
  95. package/lib/vendor/blamejs/index.js +2 -0
  96. package/lib/vendor/blamejs/lib/did.js +367 -0
  97. package/lib/vendor/blamejs/lib/mdoc.js +305 -0
  98. package/lib/vendor/blamejs/package.json +1 -1
  99. package/lib/vendor/blamejs/release-notes/v0.12.40.json +18 -0
  100. package/lib/vendor/blamejs/release-notes/v0.12.41.json +18 -0
  101. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +27 -1
  102. package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +147 -0
  103. package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +230 -0
  104. package/lib/vendor-invoices.js +1 -1
  105. package/lib/webhook-receiver.js +8 -2
  106. package/lib/webhook-subscriptions.js +1 -1
  107. package/lib/webhooks.js +6 -5
  108. package/lib/winback-campaigns.js +2 -1
  109. package/lib/wishlist-alerts.js +2 -1
  110. package/lib/wishlist-digest.js +2 -1
  111. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,8 +6,14 @@ Pre-1.0 the surface is intentionally evolving — every release may
6
6
  change something operators depend on. Read each entry before
7
7
  upgrading across more than a few patches at a time.
8
8
 
9
+ ## v0.1.x
10
+
11
+ - v0.1.0 (2026-05-25) — **Responsive cart + storefront cookie handling moved onto the framework primitive.** A storefront polish pass. The cart, order-confirmation, and account-history tables now reflow into stacked, labelled cards on phones and fit their column on wider screens — no more inner horizontal scroll. The quantity field reads clearly and accepts up to 99,999. The line actions (Update / Save for later / Remove) are compact with a clear hierarchy. Under the hood, all storefront cookie handling now composes the framework's cookie primitive (RFC 6265 parse/serialize plus vault-sealed read/write) instead of hand-built headers, and a malformed session cookie can no longer turn the cart — or any page that shows the cart count — into a 500. **Changed:** *Quantity field is readable and accepts up to 99,999* — The cart quantity input is wide enough to read a five-digit quantity, and the per-line maximum is raised to 99,999 (enforced on the server). · *Compact, clearly-ranked line actions* — Update is a small accent button, Save for later a quiet secondary, Remove a danger-tinted secondary — so the primary checkout call stays dominant. · *Cookie handling composes the framework primitive* — Session, authentication, WebAuthn-challenge, and payment cookies are all parsed and written through the framework cookie primitive (sealed read/write for the authenticated-session cookie), replacing hand-built Set-Cookie strings and manual header parsing. Cookie lifetimes are expressed through the framework's duration constants. **Fixed:** *Cart tables no longer scroll sideways* — The cart, order-confirmation, and account order-history tables reflow into one labelled card per row below 48rem and are sized to fit their column on wider layouts, so content is never trapped behind an inner horizontal scrollbar. · *Malformed session cookie no longer 500s the storefront* — A `shop_sid` cookie carrying a value that isn't a well-formed session id now reads as "no session" instead of reaching the cart lookup (which rejected it and surfaced a 500 on every page that renders the cart count). · *Account dashboard controls wrap on narrow screens* — The row of account actions (Wishlist / Saved for later / Recently viewed / Addresses / Returns / Sign out) now wraps instead of overflowing the viewport on a phone.
12
+
9
13
  ## v0.0.x
10
14
 
15
+ - v0.0.129 (2026-05-25) — **Recently viewed — a signed-in customer's browse history at /account/recently-viewed.** Signed-in customers now have a browse history. Opening a product page records the view against the customer's account, and `/account/recently-viewed` lists those products newest-first as a grid, with a one-tap Clear history control. Recording is best-effort and never blocks the product page; archived products drop out of the grid; the page is login-gated like the rest of the account area. **Added:** *Recently viewed account page* — `GET /account/recently-viewed` renders the customer's most-recently-opened products newest-first, reusing the standard product card (image, title, price, link to the PDP). A `POST /account/recently-viewed/clear` control wipes the history. Linked from the account dashboard. · *Server-side view recording* — A signed-in customer's product-page visit records the view against their account — no client script. Recording is drop-silent: a write failure never breaks the product page. The history de-dupes per product and is capped per customer.
16
+
11
17
  - v0.0.128 (2026-05-25) — **Collections — browse curated and smart product lists at /collections.** The storefront now has real collection pages. `/collections` lists the shop's active collections, and `/collections/:slug` shows a collection's products as a grid — resolving both operator-curated manual collections (hand-picked members) and smart collections (rule-matched against the catalog). The footer links to it from every page, so collections are a first-class browse entry point rather than a search query. Unknown or malformed collection slugs return 404, never a 500. **Added:** *Collection browse pages* — `GET /collections` renders the active collections as cards (title, description, hero); `GET /collections/:slug` renders the collection's product grid, reusing the standard product card (image, title, price, link to the PDP). Public pages — no sign-in. · *Manual + smart resolution* — Manual collections list their hand-picked members; smart collections evaluate their stored rules against the active catalog and apply the collection's sort strategy. The page resolves each product fresh, so archived products drop out of the grid. · *Footer entry point* — The footer's Shop column links to `/collections` on every storefront page (edge- and container-rendered alike), making collections a real browse path. A bad or unknown slug is a 404.
12
18
 
13
19
  - v0.0.127 (2026-05-24) — **Self-serve returns — customers request RMAs on their orders, operators approve and refund.** Signed-in customers can request a return against one of their own orders — pick the items and a reason at the order, then track status at /account/returns. Operators work the queue at /admin/returns: approve (with a refund amount), mark received, refund, or reject with a reason, following the pending → approved → received → refunded lifecycle. The customer request route loads the order and confirms it belongs to the signed-in customer before showing it, and builds the return lines from the order's own records, so a foreign or guessed order id returns 404 and a client can't return items it never bought. **Added:** *Customer return requests + status* — `/account/orders/:order_id/return` shows the order's items with a reason picker; `/account/returns` lists the customer's RMAs with status (pending / approved / received / refunded / rejected) and any rejection reason. The account dashboard links to it. Empty selection or a bad reason re-renders the form with the message. · *Operator return queue* — `GET /admin/returns?status=pending` lists the queue across all orders; `GET /admin/returns/:id` reads one; `POST /admin/returns/:id/approve` (with refund amount), `/received`, `/refund`, and `/reject` (with reason) walk the lifecycle. Bearer-token-gated; an illegal transition is a 409 and a bad id a 404, never a 500. · *`returns.listByStatus(status, opts)`* — Lists return authorizations across all orders by status, newest first, with the same opaque cursor as `listForCustomer`. Backs the operator queue.
package/README.md CHANGED
@@ -68,6 +68,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
68
68
  | **`lib/save-for-later.js`** | Per-customer cart holding list. Each cart line gets a login-gated "Save for later" control (`POST /cart/lines/:id/save` → `moveFromCart`); `/account/saved` lists items with Move-to-cart / Remove. `moveToCart` reprices to the current catalog price and stock-gates (out-of-stock + non-backorderable is refused). Composes `catalog.inventory` + `catalog.prices` + `catalog.variants`. |
69
69
  | **`lib/addresses.js`** | Per-customer address book at `/account/addresses` — add / edit / set default shipping or billing / remove. One-default-per-role invariant (promoting clears the prior). Every by-id route confirms the address belongs to the signed-in customer before acting (a guessed id returns 404). `b.guardUuid` ids, 2-char ISO country. |
70
70
  | **`lib/returns.js`** | Self-serve RMAs. Customer requests a return against their own order at `/account/orders/:id/return` (items + reason, ownership-checked, lines built from the order's own records) and tracks status at `/account/returns`. Operators work `/admin/returns` — approve (refund amount) / mark received / refund / reject — over the pending → approved → received → refunded FSM; illegal transitions are 409, bad ids 404. |
71
+ | **`lib/recently-viewed.js`** | Signed-in customer browse history. A product-page visit records the view server-side against the customer's account (drop-silent — never blocks the page); `/account/recently-viewed` lists them newest-first as a grid with a Clear-history control. De-duped + capped per customer, archived products drop out, login-gated. Guest/session history is opt-in (a client beacon) and not shipped — the lib's `forSession` + `merge` support it. |
71
72
  | **`lib/collections.js`** | Curated + smart product groupings. `GET /collections` lists the shop's active collections; `GET /collections/:slug` renders the grid — manual collections list hand-picked members, smart collections evaluate stored rules against the active catalog and apply the collection's sort strategy. Each product resolves fresh, so archived products drop out. Public, no sign-in; a bad or unknown slug is a 404 (never a 500). Linked from the footer on every page. |
72
73
  | **`lib/subscriptions.js`** | Stripe-backed recurring billing — `subscription_plans` (interval / amount / trial) + `subscriptions` (mirrors Stripe's object byte-for-byte). `subscriptions.create` POSTs to Stripe via the payment dep, then persists the returned object locally. `handleStripeEvent` replays `customer.subscription.*` events into the local row so the shop has an authoritative view without round-tripping. |
73
74
  | **`lib/newsletter.js`** | Operator-collected email broadcast list — `signup({ email, source })` composes `b.guardEmail` for shape validation, `b.crypto.namespaceHash` for the dedup key, and `INSERT OR IGNORE` for idempotency. Storefront POST `/newsletter` route renders a designed thank-you card with separate copy for the `new` vs `dedup` branches. |
@@ -92,6 +93,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
92
93
  - `migrations-d1/0026_customer_addresses.sql` — per-customer address book (default shipping/billing flags)
93
94
  - `migrations-d1/0023_returns.sql` — return authorizations + lines (RMA lifecycle FSM)
94
95
  - `migrations-d1/0043_collections.sql` — manual + smart product collections (members + rules + sort strategy)
96
+ - `migrations-d1/0050_recently_viewed.sql` — per-customer / per-session product browse history (dedup + per-subject cap)
95
97
 
96
98
  ### Demo seed
97
99
 
package/lib/admin.js CHANGED
@@ -84,10 +84,9 @@ function _parseLimit(str, label, max, fallback) {
84
84
 
85
85
  // ---- HTML escape + dashboard layout ------------------------------------
86
86
 
87
- var HTML_ESCAPE_MAP = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" };
88
87
  function _htmlEscape(s) {
89
88
  if (s == null) return "";
90
- return String(s).replace(/[&<>"']/g, function (c) { return HTML_ESCAPE_MAP[c]; });
89
+ return _b().template.escapeHtml(String(s));
91
90
  }
92
91
 
93
92
  // ---- bearer auth --------------------------------------------------------
package/lib/affiliates.js CHANGED
@@ -135,6 +135,7 @@ function _b() {
135
135
  if (!bShop) bShop = require("./index");
136
136
  return bShop.framework;
137
137
  }
138
+ var C = _b().constants;
138
139
 
139
140
  // ---- validators ---------------------------------------------------------
140
141
 
@@ -677,10 +678,10 @@ function create(opts) {
677
678
  throw paused;
678
679
  }
679
680
 
680
- // Dedupe within one calendar minute (60000 ms). A refresh in
681
+ // Dedupe within one calendar minute. A refresh in
681
682
  // the same minute collapses to a single visit; later traffic
682
683
  // gets its own row so funnel-stats stay coherent.
683
- var dedupeWindow = 60000;
684
+ var dedupeWindow = C.TIME.minutes(1);
684
685
  var existing = await query(
685
686
  "SELECT id FROM affiliate_visits " +
686
687
  "WHERE visitor_session_id_hash = ?1 AND code = ?2 AND occurred_at >= ?3 " +
@@ -741,7 +742,7 @@ function create(opts) {
741
742
  );
742
743
  for (var i = 0; i < r.rows.length; i += 1) {
743
744
  var row = r.rows[i];
744
- var windowMs = Number(row.attribution_window_days) * 24 * 3600 * 1000;
745
+ var windowMs = Number(row.attribution_window_days) * C.TIME.days(1);
745
746
  if (now - Number(row.occurred_at) <= windowMs) {
746
747
  return {
747
748
  visit_id: row.visit_id,
package/lib/analytics.js CHANGED
@@ -92,9 +92,10 @@ function _b() {
92
92
  if (!bShop) bShop = require("./index");
93
93
  return bShop.framework;
94
94
  }
95
+ var C = _b().constants;
95
96
 
96
- var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
97
- var DEFAULT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
97
+ var ONE_YEAR_MS = C.TIME.days(365);
98
+ var DEFAULT_WINDOW_MS = C.TIME.days(30);
98
99
 
99
100
  // ---- validators ---------------------------------------------------------
100
101
 
package/lib/api-keys.js CHANGED
@@ -76,7 +76,7 @@ var TOKEN_BYTE_LEN = 32;
76
76
  var TOKEN_PLAINTEXT_LEN = 43;
77
77
  var TOKEN_PLAINTEXT_RE = /^[A-Za-z0-9_-]{43}$/;
78
78
 
79
- var ROTATION_GRACE_MS = 24 * 60 * 60 * 1000;
79
+ var ROTATION_GRACE_MS = _b().constants.TIME.days(1);
80
80
 
81
81
  var OWNER_TYPES = ["operator", "app", "affiliate", "tenant"];
82
82
  var STATUSES = ["active", "rotated", "revoked", "expired"];
@@ -110,6 +110,7 @@ function _b() {
110
110
  if (!bShop) bShop = require("./index");
111
111
  return bShop.framework;
112
112
  }
113
+ var C = _b().constants;
113
114
 
114
115
  // ---- constants ----------------------------------------------------------
115
116
 
@@ -126,7 +127,7 @@ var MAX_LIMIT = 500;
126
127
  var DEFAULT_LIMIT = 50;
127
128
  var MAX_DAYS = 3650;
128
129
  var SESSION_NAMESPACE = "assembly-instructions-session";
129
- var DAY_MS = 24 * 60 * 60 * 1000;
130
+ var DAY_MS = C.TIME.days(1);
130
131
 
131
132
  // ---- monotonic clock ----------------------------------------------------
132
133
  //
@@ -91,6 +91,7 @@ function _b() {
91
91
  if (!bShop) bShop = require("./index");
92
92
  return bShop.framework;
93
93
  }
94
+ var C = _b().constants;
94
95
 
95
96
  // ---- constants ----------------------------------------------------------
96
97
 
@@ -110,9 +111,9 @@ var MAX_LIMIT = 500;
110
111
  // again. Stored as constants so tests can read them when constructing
111
112
  // "tick just-after-last-run" scenarios.
112
113
  var SCHEDULE_INTERVAL_MS = Object.freeze({
113
- hourly: 60 * 60 * 1000,
114
- daily: 24 * 60 * 60 * 1000,
115
- weekly: 7 * 24 * 60 * 60 * 1000,
114
+ hourly: C.TIME.hours(1),
115
+ daily: C.TIME.days(1),
116
+ weekly: C.TIME.days(7),
116
117
  });
117
118
 
118
119
  // ---- monotonic clock ----------------------------------------------------
package/lib/backorder.js CHANGED
@@ -82,13 +82,14 @@ function _b() {
82
82
  if (!bShop) bShop = require("./index");
83
83
  return bShop.framework;
84
84
  }
85
+ var C = _b().constants;
85
86
 
86
87
  // ---- constants ----------------------------------------------------------
87
88
 
88
89
  var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
89
90
  var MAX_MESSAGE_LEN = 280;
90
91
  var MAX_REASON_LEN = 280;
91
- var WEEK_MS = 7 * 24 * 60 * 60 * 1000;
92
+ var WEEK_MS = C.TIME.days(7);
92
93
 
93
94
  var BACKORDER_STATUSES = Object.freeze(["pending", "fulfilled", "cancelled"]);
94
95
 
@@ -69,6 +69,13 @@
69
69
  * @related shop.deliveryEstimate
70
70
  */
71
71
 
72
+ var bShop;
73
+ function _b() {
74
+ if (!bShop) bShop = require("./index");
75
+ return bShop.framework;
76
+ }
77
+ var C = _b().constants;
78
+
72
79
  var MAX_SLUG_LEN = 64;
73
80
  var SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
74
81
 
@@ -81,7 +88,7 @@ var YMD_RE = /^\d{4}-\d{2}-\d{2}$/;
81
88
  var MAX_WEEKLY_HOURS = 64; // 7 days x 8 split-shifts each is more than any real schedule needs
82
89
  var MAX_NAME_LEN = 200;
83
90
 
84
- var DAY_MS = 24 * 60 * 60 * 1000;
91
+ var DAY_MS = C.TIME.days(1);
85
92
  var SEARCH_DAYS = 366 * 2; // worst-case "find next open" walk — 2 calendar years of closures
86
93
 
87
94
  // ---- validators ---------------------------------------------------------
@@ -99,7 +99,7 @@ var CARRIERS = Object.freeze([
99
99
 
100
100
  var STATUSES = Object.freeze(["active", "disabled", "rotating"]);
101
101
 
102
- var ROTATION_GRACE_MS = 24 * 60 * 60 * 1000;
102
+ var ROTATION_GRACE_MS = _b().constants.TIME.days(1);
103
103
 
104
104
  var NS_ACCOUNT_NUMBER = "carrier-account-account-number";
105
105
  var NS_API_KEY = "carrier-account-api-key";
@@ -70,7 +70,7 @@ var DEFAULT_RECENT_LIMIT = 25;
70
70
  var MAX_RECENT_LIMIT = 200;
71
71
 
72
72
  var MAX_DIMENSION_MM = 5000; // 5 m — generous, catches absurd input
73
- var MAX_WEIGHT_GRAMS = 1000 * 1000; // 1 t — generous, catches absurd input
73
+ var MAX_WEIGHT_GRAMS = 1000 * 1000; // allow:raw-time-literal — grams, not a duration (1 t — generous, catches absurd input)
74
74
 
75
75
  // Lazy framework handle — matches the pattern every other shop
76
76
  // primitive uses; avoids the require cycle that would arise from
@@ -53,6 +53,7 @@ function _b() {
53
53
  if (!bShop) bShop = require("./index");
54
54
  return bShop.framework;
55
55
  }
56
+ var C = _b().constants;
56
57
 
57
58
  // ---- constants ----------------------------------------------------------
58
59
 
@@ -71,8 +72,8 @@ var SKIP_REASONS = Object.freeze([
71
72
  "opted-out",
72
73
  ]);
73
74
 
74
- var DEFAULT_IDLE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24h
75
- var DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30d
75
+ var DEFAULT_IDLE_THRESHOLD_MS = C.TIME.days(1); // 24h
76
+ var DEFAULT_MAX_AGE_MS = C.TIME.days(30); // 30d
76
77
  var DEFAULT_MAX_CARTS = 500;
77
78
  var MAX_MAX_CARTS = 5000;
78
79
 
@@ -64,6 +64,7 @@ function _b() {
64
64
  if (!bShop) bShop = require("./index");
65
65
  return bShop.framework;
66
66
  }
67
+ var C = _b().constants;
67
68
 
68
69
  var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,198}[a-z0-9])?$/;
69
70
  var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
@@ -530,7 +531,7 @@ function create(opts) {
530
531
  var groupKey = groupOrder[gi];
531
532
  var childId = _b().uuid.v7();
532
533
  var childSession = "sess_" + _b().uuid.v7().replace(/-/g, "").slice(0, 24);
533
- var childExpires = ts + 24 * 60 * 60 * 1000;
534
+ var childExpires = ts + C.TIME.days(1);
534
535
  await query(
535
536
  "INSERT INTO carts (id, session_id, customer_id, currency, status, created_at, updated_at, expires_at) " +
536
537
  "VALUES (?1, ?2, ?3, ?4, 'active', ?5, ?5, ?6)",
@@ -30,9 +30,9 @@
30
30
  * slug: "default-recovery",
31
31
  * title: "Default 3-step recovery",
32
32
  * steps: [
33
- * { step_index: 0, offset_ms: 1 * 3600 * 1000, kind: "reminder" },
34
- * { step_index: 1, offset_ms: 24 * 3600 * 1000, kind: "discount" },
35
- * { step_index: 2, offset_ms: 72 * 3600 * 1000, kind: "last_chance" },
33
+ * { step_index: 0, offset_ms: C.TIME.hours(1), kind: "reminder" },
34
+ * { step_index: 1, offset_ms: C.TIME.hours(24), kind: "discount" },
35
+ * { step_index: 2, offset_ms: C.TIME.hours(72), kind: "last_chance" },
36
36
  * ],
37
37
  * });
38
38
  *
@@ -115,6 +115,7 @@ function _b() {
115
115
  if (!bShop) bShop = require("./index");
116
116
  return bShop.framework;
117
117
  }
118
+ var C = _b().constants;
118
119
 
119
120
  // ---- constants ----------------------------------------------------------
120
121
 
@@ -732,7 +733,7 @@ function create(opts) {
732
733
  "UPDATE cart_recovery_enrollments SET " +
733
734
  "next_step_at = ?1, updated_at = ?2 " +
734
735
  "WHERE id = ?3 AND status = 'enrolled'",
735
- [now + 3600 * 1000, now, enr.id],
736
+ [now + C.TIME.hours(1), now, enr.id],
736
737
  );
737
738
  } else {
738
739
  // Clean send. Advance.
package/lib/cart.js CHANGED
@@ -29,10 +29,14 @@ function _b() {
29
29
  if (!bShop) bShop = require("./index");
30
30
  return bShop.framework;
31
31
  }
32
+ // Framework constants (C.TIME / C.BYTES duration + byte helpers). The
33
+ // index entry point exposes `framework` before the require cascade, so
34
+ // resolving this at module-eval is safe.
35
+ var C = _b().constants;
32
36
 
33
37
  var CART_STATUSES = Object.freeze(["active", "abandoned", "converted"]);
34
- var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
35
- var MAX_QTY = 9999;
38
+ var DEFAULT_TTL_MS = C.TIME.days(30);
39
+ var MAX_QTY = 99999;
36
40
  var SESSION_ID_RE = /^[A-Za-z0-9_-]{16,64}$/; // shape-only; sealed-cookie origin
37
41
  var CURRENCY_RE = /^[A-Z]{3}$/;
38
42
 
@@ -104,7 +104,7 @@ var MAX_LIST_LIMIT = 200;
104
104
  var MAX_TAG_LEN = 64;
105
105
  var MAX_CHANGES_PER_DRAFT = 5000;
106
106
 
107
- var ROLLBACK_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
107
+ var ROLLBACK_WINDOW_MS = _b().constants.TIME.days(7);
108
108
 
109
109
  // Slug shape matches the project's recurring slug convention used
110
110
  // across coupon-stacking / refund-policy / customer-segments — alnum
@@ -97,6 +97,7 @@ function _b() {
97
97
  if (!bShop) bShop = require("./index");
98
98
  return bShop.framework;
99
99
  }
100
+ var C = _b().constants;
100
101
 
101
102
  // ---- constants ----------------------------------------------------------
102
103
 
@@ -107,8 +108,8 @@ var MAX_NAME_LEN = 200;
107
108
  var MAX_REASON_LEN = 280;
108
109
  var MAX_SIGNATURE_LEN = 8192;
109
110
  var MAX_LIST_LIMIT = 200;
110
- var HOUR_MS = 3600 * 1000;
111
- var NO_SHOW_ESCALATE_MS = 7 * 24 * 60 * 60 * 1000;
111
+ var HOUR_MS = C.TIME.hours(1);
112
+ var NO_SHOW_ESCALATE_MS = C.TIME.days(7);
112
113
  var SIGNATURE_NAMESPACE = "click-and-collect-signature";
113
114
 
114
115
  var PICKUP_STATUSES = Object.freeze([
@@ -98,6 +98,7 @@ function _b() {
98
98
  if (!bShop) bShop = require("./index");
99
99
  return bShop.framework;
100
100
  }
101
+ var C = _b().constants;
101
102
 
102
103
  // ---- constants ----------------------------------------------------------
103
104
 
@@ -124,8 +125,8 @@ var MAX_FUNNEL_STEPS = 16;
124
125
  var MAX_CLEANUP_DAYS = 365 * 5;
125
126
  var MIN_CLEANUP_DAYS = 1;
126
127
 
127
- var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
128
- var DEFAULT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
128
+ var ONE_YEAR_MS = C.TIME.days(365);
129
+ var DEFAULT_WINDOW_MS = C.TIME.days(30);
129
130
 
130
131
  // Raw-PII shapes refused at every write site (mirrors the analytics
131
132
  // guard). A hashed identifier is hex / base64url and never trips
@@ -679,7 +680,7 @@ function create(opts) {
679
680
  throw new TypeError("clickstream.cleanupOlderThan: days must be an integer in [" +
680
681
  MIN_CLEANUP_DAYS + ", " + MAX_CLEANUP_DAYS + "]");
681
682
  }
682
- var cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
683
+ var cutoff = Date.now() - (days * C.TIME.days(1));
683
684
  var pv = await query(
684
685
  "DELETE FROM clickstream_pageviews WHERE occurred_at < ?1",
685
686
  [cutoff],
package/lib/config.js CHANGED
@@ -31,9 +31,10 @@ function _b() {
31
31
  if (!bShop) bShop = require("./index");
32
32
  return bShop.framework;
33
33
  }
34
+ var C = _b().constants;
34
35
 
35
36
  var KEY_RE = /^[a-z][a-z0-9._]{0,63}$/;
36
- var CACHE_TTL_MS = 30 * 1000;
37
+ var CACHE_TTL_MS = C.TIME.seconds(30);
37
38
  var MAX_VALUE_LEN = 64 * 1024; // 64 KiB — fits even a long jurisdiction table
38
39
 
39
40
  function _validateKey(k) {
@@ -149,6 +149,7 @@ function _b() {
149
149
  if (!bShop) bShop = require("./index");
150
150
  return bShop.framework;
151
151
  }
152
+ var C = _b().constants;
152
153
 
153
154
  // ---- validators --------------------------------------------------------
154
155
 
@@ -572,7 +573,7 @@ function create(opts) {
572
573
  // out the same way an active one does.
573
574
  cleanupOlderThan: async function (days) {
574
575
  _positiveInt(days, "days");
575
- var cutoff = _now() - (days * 86400 * 1000);
576
+ var cutoff = _now() - (days * C.TIME.days(1));
576
577
  // The subquery picks the freshest row for each
577
578
  // `session_id_hash`; the outer DELETE removes every row older
578
579
  // than the cutoff that isn't on that survivor list.
@@ -95,6 +95,7 @@ function _b() {
95
95
  if (!bShop) bShop = require("./index");
96
96
  return bShop.framework;
97
97
  }
98
+ var C = _b().constants;
98
99
 
99
100
  var BILLING_CYCLES = ["weekly", "biweekly", "monthly"];
100
101
  var STATUSES = ["active", "suspended", "closed"];
@@ -106,7 +107,7 @@ var MAX_REF_LEN = 128;
106
107
  // injection cover has no legitimate place in a one-line column.
107
108
  var PRINTABLE_RE = /^[^\x00-\x1f\x7f]*$/;
108
109
 
109
- var MS_PER_DAY = 86400 * 1000;
110
+ var MS_PER_DAY = C.TIME.days(1);
110
111
 
111
112
  // Aging buckets. The boundary days are reported as part of the
112
113
  // agingReport return shape so downstream invoice rendering doesn't
@@ -56,9 +56,10 @@ function _b() {
56
56
  if (!bShop) bShop = require("./index");
57
57
  return bShop.framework;
58
58
  }
59
+ var C = _b().constants;
59
60
 
60
61
  var CURRENCY_RE = /^[A-Z]{3}$/;
61
- var DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
62
+ var DEFAULT_TTL_MS = C.TIME.days(1);
62
63
  var DEFAULT_SUPPORTED = [
63
64
  "USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF",
64
65
  "SEK", "NOK", "DKK", "NZD", "INR", "BRL", "MXN",
@@ -95,6 +95,7 @@ function _b() {
95
95
  if (!bShop) bShop = require("./index");
96
96
  return bShop.framework;
97
97
  }
98
+ var C = _b().constants;
98
99
 
99
100
  // ---- constants ----------------------------------------------------------
100
101
 
@@ -103,7 +104,7 @@ var DEFAULT_LIMIT = 50;
103
104
  var MAX_INACTIVE_LIMIT = 500;
104
105
  var DEFAULT_INACTIVE_LIMIT = 100;
105
106
 
106
- var MS_PER_DAY = 86400000;
107
+ var MS_PER_DAY = C.TIME.days(1);
107
108
  var WINDOW_30D = 30 * MS_PER_DAY;
108
109
  var WINDOW_90D = 90 * MS_PER_DAY;
109
110
  var WINDOW_365D = 365 * MS_PER_DAY;
@@ -111,7 +112,7 @@ var WINDOW_365D = 365 * MS_PER_DAY;
111
112
  // Cache freshness window. summarize() returns the cached row when
112
113
  // computed_at is within this window AND no source has a newer event
113
114
  // than last_activity_at. Same posture as order-timeline's cache.
114
- var CACHE_TTL_MS = 5 * 60 * 1000;
115
+ var CACHE_TTL_MS = C.TIME.minutes(5);
115
116
 
116
117
  // Order-FSM events → canonical English titles. Events not in this
117
118
  // map fall through with the raw event name as the title.
@@ -137,9 +137,9 @@
137
137
 
138
138
  var TOKEN_NAMESPACE = "customer-impersonation-token";
139
139
  var TOKEN_BYTES = 32;
140
- var DEFAULT_TTL_SECONDS = 60 * 60; // 60-minute default
140
+ var DEFAULT_TTL_SECONDS = 60 * 60; // allow:raw-time-literal — seconds value; C.TIME returns ms (60-minute default)
141
141
  var MIN_TTL_SECONDS = 60; // refuse sub-minute
142
- var MAX_TTL_SECONDS = 8 * 60 * 60; // hard ceiling — eight hours
142
+ var MAX_TTL_SECONDS = 8 * 60 * 60; // allow:raw-time-literal — seconds value; C.TIME returns ms (hard ceiling — eight hours)
143
143
  var MAX_REASON_LEN = 280;
144
144
  var MAX_END_REASON_LEN = 280;
145
145
  var MAX_ENDED_BY_LEN = 64;
@@ -348,7 +348,7 @@ function create(opts) {
348
348
  var tokenHash = _b().crypto.namespaceHash(TOKEN_NAMESPACE, plaintext);
349
349
  var id = _b().uuid.v7();
350
350
  var now = _now();
351
- var expiresAt = now + (ttl * 1000);
351
+ var expiresAt = now + (ttl * 1000); // allow:raw-time-literal — ttl is a runtime seconds value; *1000 converts to ms
352
352
 
353
353
  await query(
354
354
  "INSERT INTO impersonations " +
@@ -158,7 +158,7 @@ var DEFAULT_LIST_LIMIT = 50;
158
158
  var MAX_CANDIDATE_LIMIT = 200;
159
159
  var DEFAULT_CAND_LIMIT = 25;
160
160
  var MAX_REASON_LEN = 280;
161
- var ROLLBACK_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
161
+ var ROLLBACK_WINDOW_MS = _b().constants.TIME.days(7);
162
162
  var DEFAULT_SIMILARITY = 0.85;
163
163
  var MIN_SIMILARITY = 0.50;
164
164
  var MAX_SIMILARITY = 1.00;
@@ -187,6 +187,7 @@ function _b() {
187
187
  if (!bShop) bShop = require("./index");
188
188
  return bShop.framework;
189
189
  }
190
+ var C = _b().constants;
190
191
 
191
192
  // ---- monotonic clock ---------------------------------------------------
192
193
  //
@@ -747,8 +748,8 @@ function create(opts) {
747
748
  var now = _now();
748
749
  if (now - Number(row.executed_at) > ROLLBACK_WINDOW_MS) {
749
750
  var winErr = new Error("customerMerge.rollbackMerge: merge_id " + mergeId +
750
- " executed " + Math.floor((now - Number(row.executed_at)) / (24 * 60 * 60 * 1000)) +
751
- " days ago, past the " + (ROLLBACK_WINDOW_MS / (24 * 60 * 60 * 1000)) +
751
+ " executed " + Math.floor((now - Number(row.executed_at)) / C.TIME.days(1)) +
752
+ " days ago, past the " + (ROLLBACK_WINDOW_MS / C.TIME.days(1)) +
752
753
  "-day rollback window");
753
754
  winErr.code = "CUSTOMER_MERGE_ROLLBACK_WINDOW_EXPIRED";
754
755
  throw winErr;
@@ -75,8 +75,8 @@
75
75
 
76
76
  var TOKEN_NAMESPACE = "customer-portal-token";
77
77
  var TOKEN_BYTES = 32;
78
- var DEFAULT_TTL_SECONDS = 15 * 60;
79
- var MAX_TTL_SECONDS = 60 * 60 * 24; // hard ceiling — one day
78
+ var DEFAULT_TTL_SECONDS = 15 * 60; // allow:raw-time-literal — seconds value; C.TIME returns ms
79
+ var MAX_TTL_SECONDS = 60 * 60 * 24; // allow:raw-time-literal — seconds value; C.TIME returns ms (hard ceiling — one day)
80
80
  var MIN_TTL_SECONDS = 30; // refuse zero / negative / sub-30s
81
81
  var MAX_REASON_LEN = 64;
82
82
  var MAX_UA_CLASS_LEN = 64;
@@ -196,7 +196,7 @@ function create(opts) {
196
196
  var tokenHash = _b().crypto.namespaceHash(TOKEN_NAMESPACE, plaintext);
197
197
  var id = _b().uuid.v7();
198
198
  var now = _now();
199
- var expiresAt = now + (ttl * 1000);
199
+ var expiresAt = now + (ttl * 1000); // allow:raw-time-literal — ttl is a runtime seconds value; *1000 converts to ms
200
200
 
201
201
  await query(
202
202
  "INSERT INTO customer_portal_sessions " +
@@ -338,7 +338,7 @@ function create(opts) {
338
338
  // worker layer can emit a metric.
339
339
  expireOlderThan: async function (seconds) {
340
340
  _seconds(seconds, "seconds");
341
- var threshold = _now() - (seconds * 1000);
341
+ var threshold = _now() - (seconds * 1000); // allow:raw-time-literal — seconds is a runtime seconds value; *1000 converts to ms
342
342
  var r = await query(
343
343
  "UPDATE customer_portal_sessions " +
344
344
  "SET status = 'expired' " +
@@ -120,6 +120,7 @@ function _b() {
120
120
  }
121
121
  return bShop.framework;
122
122
  }
123
+ var C = _b().constants;
123
124
 
124
125
  // ---- constants ----------------------------------------------------------
125
126
 
@@ -149,7 +150,7 @@ var BANDS = Object.freeze({
149
150
  // score (their lifetime count still reflects them). 90 days matches
150
151
  // the typical card-network chargeback-evidence cycle: anything older
151
152
  // has already gone through dispute and is settled.
152
- var RECENT_WINDOW_MS = 90 * 86400 * 1000;
153
+ var RECENT_WINDOW_MS = C.TIME.days(90);
153
154
 
154
155
  // detail_json byte ceiling. Operators routinely embed order ids,
155
156
  // amounts, a few human-readable notes — 8 KiB after JSON-encoding
@@ -113,6 +113,7 @@ function _b() {
113
113
  if (!bShop) bShop = require("./index");
114
114
  return bShop.framework;
115
115
  }
116
+ var C = _b().constants;
116
117
 
117
118
  var DEFAULT_LIMIT = 100;
118
119
  var MAX_LIMIT = 1000;
@@ -398,7 +399,7 @@ function create(opts) {
398
399
  var lastAt = Number(row.last_order_at || 0);
399
400
  var aov = orderCount > 0 ? Math.floor(gross / orderCount) : 0;
400
401
  var refundBps = orderCount > 0 ? Math.floor((refunded * 10000) / orderCount) : 0;
401
- var recencyDays = lastAt > 0 ? Math.floor((nowTs - lastAt) / (24 * 60 * 60 * 1000)) : null;
402
+ var recencyDays = lastAt > 0 ? Math.floor((nowTs - lastAt) / C.TIME.days(1)) : null;
402
403
  out.push({
403
404
  customer_id: row.customer_id,
404
405
  order_count: orderCount,
@@ -73,6 +73,7 @@ function _b() {
73
73
  if (!bShop) bShop = require("./index");
74
74
  return bShop.framework;
75
75
  }
76
+ var C = _b().constants;
76
77
 
77
78
  // ---- constants ----------------------------------------------------------
78
79
 
@@ -579,7 +580,7 @@ function create(opts) {
579
580
  var plaintext = _generateToken();
580
581
  var tokenHash = _hashToken(plaintext);
581
582
  var issuedAt = _now();
582
- var expiresAt = issuedAt + (expiresHours * 60 * 60 * 1000);
583
+ var expiresAt = issuedAt + (expiresHours * C.TIME.hours(1));
583
584
 
584
585
  await query(
585
586
  "INSERT INTO survey_invitations " +
@@ -904,7 +905,8 @@ function create(opts) {
904
905
  out.csat = csatTotal === 0
905
906
  ? { positive_pct: 0, mean: 0, positives: 0, neutrals: 0, negatives: 0 }
906
907
  : {
907
- positive_pct: Math.round((pos / csatTotal) * 1000) / 10,
908
+ positive_pct: Math.round((pos / csatTotal) * 1000) / 10, // allow:raw-time-literal — percentage scaling factor, not a duration
909
+
908
910
  mean: Math.round(primary.mean * 100) / 100,
909
911
  positives: pos,
910
912
  neutrals: neu,
@@ -923,7 +925,8 @@ function create(opts) {
923
925
  ? { mean: 0, agree_pct: 0, agree: 0 }
924
926
  : {
925
927
  mean: Math.round(primary.mean * 100) / 100,
926
- agree_pct: Math.round((cesAgree / primary.count) * 1000) / 10,
928
+ agree_pct: Math.round((cesAgree / primary.count) * 1000) / 10, // allow:raw-time-literal — percentage scaling factor, not a duration
929
+
927
930
  agree: cesAgree,
928
931
  };
929
932
  }
@@ -124,9 +124,9 @@ var POSTAL_RE = /^[A-Za-z0-9][A-Za-z0-9 -]{0,15}$/;
124
124
 
125
125
  var COUNTRY_RE = /^[A-Z]{2}$/;
126
126
 
127
- var MAX_WEIGHT_GRAMS = 1000 * 1000;
127
+ var MAX_WEIGHT_GRAMS = 1000 * 1000; // allow:raw-time-literal — grams (1000 kg), not a duration
128
128
 
129
- var DAY_MS = 24 * 60 * 60 * 1000;
129
+ var DAY_MS = _b().constants.TIME.days(1);
130
130
 
131
131
  // Lazy framework handle — matches the convention every other shop
132
132
  // primitive uses; avoids the require cycle that would arise from
@@ -104,6 +104,7 @@ function _b() {
104
104
  if (!bShop) bShop = require("./index");
105
105
  return bShop.framework;
106
106
  }
107
+ var C = _b().constants;
107
108
 
108
109
  // ---- constants ----------------------------------------------------------
109
110
 
@@ -127,7 +128,7 @@ var MAX_UNITS_SOLD = 1000000000;
127
128
  var DEFAULT_WINDOW_DAYS = 30;
128
129
  var DEFAULT_ALPHA = 0.3;
129
130
 
130
- var DAY_MS = 24 * 60 * 60 * 1000;
131
+ var DAY_MS = C.TIME.days(1);
131
132
 
132
133
  // Weekly-seasonality needs at least two full weeks of history to
133
134
  // avoid claiming a pattern from one cycle. Monthly-seasonality needs
@@ -124,8 +124,8 @@ var MAX_SESSION_LEN = 256;
124
124
  var MAX_TIER_ID_LEN = 128;
125
125
  var MAX_LIMIT = 200;
126
126
 
127
- var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
128
- var DEFAULT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
127
+ var ONE_YEAR_MS = _b().constants.TIME.days(365);
128
+ var DEFAULT_WINDOW_MS = _b().constants.TIME.days(30);
129
129
 
130
130
  // Coupon-code shape — alnum + dot/dash/underscore plus a `:` to
131
131
  // admit the `tier:<id>` convention. Length-capped so a giant string