@blamejs/blamejs-shop 0.0.129 → 0.1.1
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 +6 -0
- package/README.md +1 -1
- package/lib/admin.js +275 -9
- package/lib/affiliates.js +4 -3
- package/lib/analytics.js +3 -2
- package/lib/api-keys.js +1 -1
- package/lib/assembly-instructions.js +2 -1
- package/lib/auto-replenish.js +4 -3
- package/lib/backorder.js +2 -1
- package/lib/business-hours.js +8 -1
- package/lib/carrier-accounts.js +1 -1
- package/lib/carrier-rates.js +1 -1
- package/lib/cart-abandonment.js +3 -2
- package/lib/cart-bulk-ops.js +2 -1
- package/lib/cart-recovery.js +5 -4
- package/lib/cart.js +6 -2
- package/lib/catalog-drafts.js +1 -1
- package/lib/click-and-collect.js +3 -2
- package/lib/clickstream.js +4 -3
- package/lib/config.js +2 -1
- package/lib/cookie-consent.js +2 -1
- package/lib/credit-limits.js +2 -1
- package/lib/currency-display.js +2 -1
- package/lib/customer-activity.js +3 -2
- package/lib/customer-impersonation.js +3 -3
- package/lib/customer-merge.js +4 -3
- package/lib/customer-portal.js +4 -4
- package/lib/customer-risk-profile.js +2 -1
- package/lib/customer-segments.js +2 -1
- package/lib/customer-surveys.js +6 -3
- package/lib/delivery-estimate.js +2 -2
- package/lib/demand-forecast.js +2 -1
- package/lib/discount-analytics.js +2 -2
- package/lib/dunning.js +4 -1
- package/lib/email-warmup.js +6 -1
- package/lib/email.js +1 -8
- package/lib/error-log.js +3 -2
- package/lib/event-log.js +3 -2
- package/lib/fraud-screen.js +3 -1
- package/lib/fulfillment-sla.js +3 -1
- package/lib/index.js +11 -3
- package/lib/inventory-allocations.js +3 -0
- package/lib/inventory-snapshots.js +2 -1
- package/lib/invoice-renderer.js +2 -1
- package/lib/line-gift-wrap.js +6 -1
- package/lib/live-chat.js +2 -1
- package/lib/loyalty-redemption.js +2 -1
- package/lib/newsletter.js +6 -1
- package/lib/operator-activity-feed.js +4 -3
- package/lib/operator-sessions.js +7 -7
- package/lib/order-exchanges.js +1 -0
- package/lib/order-timeline.js +2 -1
- package/lib/payment-retries.js +2 -1
- package/lib/payment.js +5 -4
- package/lib/pixel-events.js +6 -5
- package/lib/preorder.js +2 -1
- package/lib/print-queue.js +2 -1
- package/lib/product-compare.js +2 -1
- package/lib/product-qa.js +2 -1
- package/lib/push-notifications.js +6 -5
- package/lib/recently-viewed.js +7 -2
- package/lib/recommendations.js +7 -2
- package/lib/referral-leaderboard.js +2 -1
- package/lib/refund-automation.js +1 -1
- package/lib/refund-policy.js +1 -1
- package/lib/reorder-reminders.js +2 -1
- package/lib/reorder-thresholds.js +2 -1
- package/lib/robots-config.js +1 -0
- package/lib/sales-reports.js +17 -14
- package/lib/sales-tax-filings.js +2 -1
- package/lib/save-for-later.js +2 -1
- package/lib/search-suggestions.js +1 -1
- package/lib/shipping-insurance.js +2 -1
- package/lib/shipping-labels.js +3 -2
- package/lib/shipping-zones.js +1 -0
- package/lib/shrinkage-report.js +9 -8
- package/lib/sms-dispatcher.js +6 -5
- package/lib/stock-alerts.js +1 -1
- package/lib/stock-receipts.js +2 -1
- package/lib/store-credit.js +2 -1
- package/lib/storefront-forms.js +1 -1
- package/lib/storefront.js +93 -112
- package/lib/subscription-analytics.js +7 -2
- package/lib/subscription-controls.js +9 -8
- package/lib/subscription-gifts.js +2 -1
- package/lib/subscriptions.js +2 -0
- package/lib/support-tickets.js +4 -4
- package/lib/tax-cert-renewals.js +2 -1
- package/lib/tax-remittance.js +2 -1
- package/lib/theme-assets.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +16 -0
- package/lib/vendor/blamejs/README.md +6 -4
- package/lib/vendor/blamejs/SECURITY.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +255 -2
- package/lib/vendor/blamejs/index.js +1 -0
- package/lib/vendor/blamejs/lib/cose.js +284 -10
- package/lib/vendor/blamejs/lib/crypto.js +119 -0
- package/lib/vendor/blamejs/lib/did.js +416 -0
- package/lib/vendor/blamejs/lib/mdoc.js +122 -0
- package/lib/vendor/blamejs/lib/network-dnssec.js +328 -0
- package/lib/vendor/blamejs/lib/network.js +1 -0
- package/lib/vendor/blamejs/lib/vc.js +231 -33
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.41.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.42.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.43.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.44.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.45.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.46.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.47.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.48.json +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +47 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +101 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-self-test.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +176 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +52 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +63 -0
- package/lib/vendor-invoices.js +1 -1
- package/lib/webhook-receiver.js +8 -2
- package/lib/webhook-subscriptions.js +1 -1
- package/lib/webhooks.js +6 -5
- package/lib/winback-campaigns.js +2 -1
- package/lib/wishlist-alerts.js +2 -1
- package/lib/wishlist-digest.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ 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.1 (2026-05-25) — **Admin setup wizard + a browser-accessible admin console.** The admin is now reachable from a browser, not just the bearer-token JSON API. Sign in once at /admin by pasting the ADMIN_API_KEY and a sealed, /admin-scoped session cookie carries you through the guided setup wizard (shop name, contact email, default currency, support URL — saved to shop config) and the analytics dashboard. The shop name set in the wizard drives the storefront header, page titles, and the admin header. **Added:** *Browser admin sign-in* — `/admin` renders a sign-in form; pasting the ADMIN_API_KEY sets a sealed `shop_admin` session cookie (SameSite=Strict, scoped to /admin) so the rendered admin pages are reachable from a browser. The JSON API stays bearer-only; the dashboard accepts either the cookie or the bearer token. · *Setup wizard* — `/admin/setup` is a guided form for the shop's core identity — name, contact email, default currency, support URL — validated (ISO-4217 currency, RFC-shaped email, http(s) support URL) and saved to shop config. The landing nags until setup is complete; the shop name then drives the storefront and admin headers.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
9
15
|
## v0.0.x
|
|
10
16
|
|
|
11
17
|
- 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.
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
|
|
|
72
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. |
|
|
73
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. |
|
|
74
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. |
|
|
75
|
-
| **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans + review moderation + return moderation. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. |
|
|
75
|
+
| **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans + review moderation + return moderation. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. Also serves a **browser admin console**: sign in at `/admin` by pasting the API key (sealed `shop_admin` session cookie, SameSite=Strict, /admin-scoped), a guided **setup wizard** at `/admin/setup` that writes shop identity to config, and the analytics dashboard — all reachable by the cookie or the bearer token. |
|
|
76
76
|
| **`lib/catalog-import.js`** | Bulk CSV import — `POST /admin/catalog/import` accepts a `text/csv` body, parses via `b.csv`, content-safety-filters every cell through `b.guardCsv` (formula-injection / bidi / control / dangerous-function denylist), validates exact header order, de-dupes rows by `product_slug`, returns per-row errors without aborting. Default 1 MiB / 10000 rows caps. |
|
|
77
77
|
| **`lib/theme.js`** | File-backed templates with fallback chain. Operators register a named theme under `<themesDir>/<name>/*.html` and the storefront dispatches every renderer through it. `assetUrl(path)` resolves to `/assets/themes/<name>/<path>`. The shipped `default` theme is the fallback. |
|
|
78
78
|
|
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 = { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" };
|
|
88
87
|
function _htmlEscape(s) {
|
|
89
88
|
if (s == null) return "";
|
|
90
|
-
return
|
|
89
|
+
return _b().template.escapeHtml(String(s));
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
// ---- bearer auth --------------------------------------------------------
|
|
@@ -106,6 +105,53 @@ function _authOk(token, expected) {
|
|
|
106
105
|
return _b().crypto.timingSafeEqual(token, expected);
|
|
107
106
|
}
|
|
108
107
|
|
|
108
|
+
// ---- admin browser session (sealed cookie) ------------------------------
|
|
109
|
+
//
|
|
110
|
+
// The JSON API (R/W wrappers) is bearer-token only — that's the contract
|
|
111
|
+
// for machine clients. The server-rendered admin pages (landing, setup
|
|
112
|
+
// wizard, dashboard) additionally accept a sealed `shop_admin` cookie so
|
|
113
|
+
// an operator can sign in from a browser by pasting the same token once.
|
|
114
|
+
// The cookie composes the framework cookie primitive (vault-sealed
|
|
115
|
+
// read/write) — scoped to /admin, SameSite=Strict, HttpOnly + Secure.
|
|
116
|
+
var ADMIN_COOKIE_NAME = "shop_admin";
|
|
117
|
+
|
|
118
|
+
var _adminJarMemo = null;
|
|
119
|
+
function _adminJar() {
|
|
120
|
+
if (!_adminJarMemo) {
|
|
121
|
+
_adminJarMemo = _b().cookies.create({
|
|
122
|
+
vault: _b().vault,
|
|
123
|
+
defaults: { httpOnly: true, secure: true, sameSite: "Strict", path: "/admin" },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return _adminJarMemo;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _setAdminCookie(res) {
|
|
130
|
+
_adminJar().writeSealed(res, ADMIN_COOKIE_NAME, JSON.stringify({
|
|
131
|
+
admin: true,
|
|
132
|
+
exp: Date.now() + _b().constants.TIME.hours(12),
|
|
133
|
+
}), { expires: new Date(Date.now() + _b().constants.TIME.hours(12)) });
|
|
134
|
+
}
|
|
135
|
+
function _clearAdminCookie(res) {
|
|
136
|
+
_adminJar().clear(res, ADMIN_COOKIE_NAME);
|
|
137
|
+
}
|
|
138
|
+
function _adminCookieValid(req) {
|
|
139
|
+
var raw = _adminJar().readSealed(req, ADMIN_COOKIE_NAME);
|
|
140
|
+
if (raw === null) return false;
|
|
141
|
+
var env;
|
|
142
|
+
try { env = JSON.parse(raw); } catch (_e) { return false; }
|
|
143
|
+
return !!(env && env.admin === true && env.exp && env.exp > Date.now());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// HTML-page auth: a valid admin cookie OR the bearer token (so existing
|
|
147
|
+
// tooling that sends the header still reaches the dashboard). Never
|
|
148
|
+
// throws — a missing vault surfaces as "not authed" so the caller can
|
|
149
|
+
// render the login form rather than 500.
|
|
150
|
+
function _htmlAuthed(req, expectedToken) {
|
|
151
|
+
if (_authOk(_readBearer(req), expectedToken)) return true;
|
|
152
|
+
try { return _adminCookieValid(req); } catch (_e) { return false; }
|
|
153
|
+
}
|
|
154
|
+
|
|
109
155
|
function _problem(res, status, code, detail) {
|
|
110
156
|
return _b().problemDetails.send(res, {
|
|
111
157
|
type: "/problems/" + code,
|
|
@@ -115,6 +161,21 @@ function _problem(res, status, code, detail) {
|
|
|
115
161
|
});
|
|
116
162
|
}
|
|
117
163
|
|
|
164
|
+
function _sendHtml(res, status, html) {
|
|
165
|
+
res.status(status);
|
|
166
|
+
if (res.setHeader) {
|
|
167
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
168
|
+
res.setHeader("x-robots-tag", "noindex, nofollow");
|
|
169
|
+
}
|
|
170
|
+
if (res.end) res.end(html); else res.send(html);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function _redirect(res, location) {
|
|
174
|
+
res.status(303);
|
|
175
|
+
if (res.setHeader) res.setHeader("location", location);
|
|
176
|
+
if (res.end) res.end(); else res.send("");
|
|
177
|
+
}
|
|
178
|
+
|
|
118
179
|
function _wrap(handler, opts) {
|
|
119
180
|
// Every admin handler routes through this wrapper: bearer-token
|
|
120
181
|
// gate, error-to-problem-details translation, audit write on the
|
|
@@ -814,24 +875,26 @@ function mount(router, deps) {
|
|
|
814
875
|
_json(res, 200, { rows: rows });
|
|
815
876
|
}));
|
|
816
877
|
|
|
817
|
-
|
|
878
|
+
// HTML page — accepts the admin browser cookie OR the bearer token,
|
|
879
|
+
// so it's reachable both from a signed-in browser and from tooling.
|
|
880
|
+
router.get("/admin/dashboard", async function (req, res) {
|
|
881
|
+
if (!_htmlAuthed(req, expectedToken)) {
|
|
882
|
+
return _sendHtml(res, 200, renderAdminLogin({ shop_name: deps.shop_name }));
|
|
883
|
+
}
|
|
818
884
|
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
819
885
|
var w = _parseWindow(url);
|
|
820
886
|
var summary = await analytics.summary(w);
|
|
821
887
|
var byDay = await analytics.revenueByDay(w);
|
|
822
888
|
var top = await analytics.topSKUs(Object.assign({}, w, { limit: 10 }));
|
|
823
889
|
var recent = await analytics.recentOrders({ limit: 20 });
|
|
824
|
-
|
|
890
|
+
_sendHtml(res, 200, renderDashboard({
|
|
825
891
|
summary: summary,
|
|
826
892
|
by_day: byDay,
|
|
827
893
|
top_skus: top,
|
|
828
894
|
recent: recent,
|
|
829
895
|
shop_name: (deps.shop_name || "blamejs.shop"),
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
if (res.setHeader) res.setHeader("content-type", "text/html; charset=utf-8");
|
|
833
|
-
if (res.end) res.end(html); else res.send(html);
|
|
834
|
-
}));
|
|
896
|
+
}));
|
|
897
|
+
});
|
|
835
898
|
}
|
|
836
899
|
|
|
837
900
|
// ---- subscriptions --------------------------------------------------
|
|
@@ -901,6 +964,112 @@ function mount(router, deps) {
|
|
|
901
964
|
}));
|
|
902
965
|
}
|
|
903
966
|
|
|
967
|
+
// ---- admin web pages (browser session + setup wizard) ---------------
|
|
968
|
+
//
|
|
969
|
+
// The operator signs in by pasting the ADMIN_API_KEY once; that sets a
|
|
970
|
+
// sealed, SameSite=Strict, /admin-scoped cookie so the rendered pages
|
|
971
|
+
// (landing, dashboard, setup) are reachable from a browser. The JSON
|
|
972
|
+
// API stays bearer-only. POST routes follow the storefront's
|
|
973
|
+
// form-POST pattern (SameSite cookie + the app-level origin /
|
|
974
|
+
// fetch-metadata guards), no separate CSRF token field.
|
|
975
|
+
|
|
976
|
+
async function _setupComplete() {
|
|
977
|
+
if (!config) return false;
|
|
978
|
+
try { return (await config.get("setup.completed", false)) === true; }
|
|
979
|
+
catch (_e) { return false; }
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
router.get("/admin", async function (req, res) {
|
|
983
|
+
if (!_htmlAuthed(req, expectedToken)) {
|
|
984
|
+
return _sendHtml(res, 200, renderAdminLogin({ shop_name: deps.shop_name }));
|
|
985
|
+
}
|
|
986
|
+
_sendHtml(res, 200, renderAdminLanding({
|
|
987
|
+
shop_name: deps.shop_name,
|
|
988
|
+
setup_complete: await _setupComplete(),
|
|
989
|
+
}));
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
router.post("/admin/login", async function (req, res) {
|
|
993
|
+
var body = req.body || {};
|
|
994
|
+
var token = typeof body.token === "string" ? body.token : "";
|
|
995
|
+
if (!_authOk(token, expectedToken)) {
|
|
996
|
+
return _sendHtml(res, 401, renderAdminLogin({ shop_name: deps.shop_name, error: true }));
|
|
997
|
+
}
|
|
998
|
+
try { _setAdminCookie(res); }
|
|
999
|
+
catch (e) {
|
|
1000
|
+
// Sealed cookies need an initialized vault — surface 503 rather
|
|
1001
|
+
// than 500 so the operator knows to configure VAULT_PASSPHRASE.
|
|
1002
|
+
if (e && e.code === "vault/not-initialized") {
|
|
1003
|
+
return _sendHtml(res, 503, renderAdminLogin({ shop_name: deps.shop_name }));
|
|
1004
|
+
}
|
|
1005
|
+
throw e;
|
|
1006
|
+
}
|
|
1007
|
+
_redirect(res, (await _setupComplete()) ? "/admin" : "/admin/setup");
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
router.post("/admin/logout", async function (_req, res) {
|
|
1011
|
+
_clearAdminCookie(res);
|
|
1012
|
+
_redirect(res, "/admin");
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (config) {
|
|
1016
|
+
router.get("/admin/setup", async function (req, res) {
|
|
1017
|
+
if (!_htmlAuthed(req, expectedToken)) return _redirect(res, "/admin");
|
|
1018
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
1019
|
+
var saved = !!(url && url.searchParams.get("saved"));
|
|
1020
|
+
var values = {};
|
|
1021
|
+
try {
|
|
1022
|
+
values.shop_name = await config.get("shop.name", deps.shop_name || "");
|
|
1023
|
+
values.contact_email = await config.get("shop.contact_email", "");
|
|
1024
|
+
values.currency = await config.get("shop.currency", "");
|
|
1025
|
+
values.support_url = await config.get("shop.support_url", "");
|
|
1026
|
+
} catch (_e) { /* unconfigured — render an empty form */ }
|
|
1027
|
+
_sendHtml(res, 200, renderAdminSetup({ shop_name: deps.shop_name, values: values, saved: saved }));
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
router.post("/admin/setup", async function (req, res) {
|
|
1031
|
+
if (!_htmlAuthed(req, expectedToken)) return _redirect(res, "/admin");
|
|
1032
|
+
var body = req.body || {};
|
|
1033
|
+
var values = {
|
|
1034
|
+
shop_name: (typeof body.shop_name === "string" ? body.shop_name : "").trim(),
|
|
1035
|
+
contact_email: (typeof body.contact_email === "string" ? body.contact_email : "").trim(),
|
|
1036
|
+
currency: (typeof body.currency === "string" ? body.currency : "").trim().toUpperCase(),
|
|
1037
|
+
support_url: (typeof body.support_url === "string" ? body.support_url : "").trim(),
|
|
1038
|
+
};
|
|
1039
|
+
// Defensive request-shape reader: bad input re-renders the form
|
|
1040
|
+
// with a notice (400), never a 500.
|
|
1041
|
+
var notice = null;
|
|
1042
|
+
if (!values.shop_name) notice = "Shop name is required.";
|
|
1043
|
+
else if (values.shop_name.length > 80) notice = "Shop name is too long (max 80 characters).";
|
|
1044
|
+
else if (values.currency && !/^[A-Z]{3}$/.test(values.currency)) notice = "Currency must be a 3-letter ISO 4217 code (e.g. USD).";
|
|
1045
|
+
else if (values.contact_email) {
|
|
1046
|
+
var emailReport = _b().guardEmail.validate(values.contact_email, { profile: "strict" });
|
|
1047
|
+
if (!emailReport || emailReport.ok === false) notice = "That contact email doesn't look valid.";
|
|
1048
|
+
}
|
|
1049
|
+
if (!notice && values.support_url) {
|
|
1050
|
+
var u = _b().safeUrl.parse(values.support_url);
|
|
1051
|
+
if (!u || (u.protocol !== "https:" && u.protocol !== "http:")) notice = "Support URL must be a valid http(s) URL.";
|
|
1052
|
+
}
|
|
1053
|
+
if (notice) {
|
|
1054
|
+
return _sendHtml(res, 400, renderAdminSetup({ shop_name: deps.shop_name, values: values, notice: notice }));
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
await config.put("shop.name", values.shop_name);
|
|
1058
|
+
if (values.contact_email) await config.put("shop.contact_email", values.contact_email);
|
|
1059
|
+
if (values.currency) await config.put("shop.currency", values.currency);
|
|
1060
|
+
if (values.support_url) await config.put("shop.support_url", values.support_url);
|
|
1061
|
+
await config.put("setup.completed", true);
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
return _sendHtml(res, 500, renderAdminSetup({
|
|
1064
|
+
shop_name: deps.shop_name, values: values,
|
|
1065
|
+
notice: "Couldn't save — " + ((e && e.message) || "please try again."),
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
_b().audit.safeEmit({ action: AUDIT_NAMESPACE + ".setup.save", outcome: "success", metadata: {} });
|
|
1069
|
+
_redirect(res, "/admin/setup?saved=1");
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
904
1073
|
// ---- ping (auth check) ----------------------------------------------
|
|
905
1074
|
|
|
906
1075
|
router.get("/admin/ping", R(async function (_req, res) {
|
|
@@ -963,6 +1132,25 @@ var DASHBOARD_LAYOUT =
|
|
|
963
1132
|
" .empty { color:var(--mute); font-style:italic; padding:1rem 0; text-align:center; }\n" +
|
|
964
1133
|
" .meta { color:var(--mute); font-size:.85rem; margin-bottom:1rem; }\n" +
|
|
965
1134
|
" .order-id { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.78rem; color:var(--ink-2); }\n" +
|
|
1135
|
+
" .form-field { display:block; margin-bottom:1.1rem; }\n" +
|
|
1136
|
+
" .form-field span { display:block; font-size:.78rem; text-transform:uppercase; letter-spacing:.05em; color:var(--mute); font-weight:600; margin-bottom:.35rem; }\n" +
|
|
1137
|
+
" .form-field input { width:100%; max-width:28rem; padding:.6rem .75rem; border:1px solid var(--hair); border-radius:6px; font:inherit; }\n" +
|
|
1138
|
+
" .form-field input:focus { outline:2px solid var(--accent); outline-offset:1px; border-color:var(--accent); }\n" +
|
|
1139
|
+
" .form-field small { display:block; color:var(--mute); font-size:.78rem; margin-top:.3rem; }\n" +
|
|
1140
|
+
" .btn { display:inline-flex; align-items:center; gap:.4rem; background:var(--accent); color:var(--paper); border:1px solid var(--accent); padding:.6rem 1.1rem; border-radius:6px; font-family:'Montserrat',sans-serif; font-weight:700; font-size:.82rem; letter-spacing:.04em; text-transform:uppercase; text-decoration:none; cursor:pointer; }\n" +
|
|
1141
|
+
" .btn:hover { background:var(--accent-d); border-color:var(--accent-d); }\n" +
|
|
1142
|
+
" .btn--ghost { background:transparent; color:var(--ink); border-color:var(--ink); }\n" +
|
|
1143
|
+
" .btn--ghost:hover { background:var(--ink); color:var(--paper); }\n" +
|
|
1144
|
+
" .nav-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(14rem,1fr)); gap:1rem; }\n" +
|
|
1145
|
+
" .nav-card { display:block; background:var(--paper); border:1px solid var(--hair); border-radius:8px; padding:1.4rem; text-decoration:none; color:var(--ink); }\n" +
|
|
1146
|
+
" .nav-card:hover { border-color:var(--accent); box-shadow:0 8px 20px -12px rgba(0,0,0,.25); }\n" +
|
|
1147
|
+
" .nav-card h3 { margin:0 0 .35rem; font-size:1.05rem; }\n" +
|
|
1148
|
+
" .nav-card p { margin:0; color:var(--mute); font-size:.88rem; }\n" +
|
|
1149
|
+
" .banner { padding:.9rem 1.1rem; border-radius:8px; margin-bottom:1.5rem; font-size:.92rem; }\n" +
|
|
1150
|
+
" .banner--warn { background:#fff8e1; border:1px solid #f1e1a8; color:#7a5d0f; }\n" +
|
|
1151
|
+
" .banner--ok { background:#e9f5ec; border:1px solid #bfe1c9; color:#1f6b3a; }\n" +
|
|
1152
|
+
" .banner--err { background:#fff1eb; border:1px solid #f6c5af; color:var(--accent-d); }\n" +
|
|
1153
|
+
" .actions-row { display:flex; gap:.75rem; flex-wrap:wrap; align-items:center; margin-top:1.5rem; }\n" +
|
|
966
1154
|
" </style>\n" +
|
|
967
1155
|
"</head>\n" +
|
|
968
1156
|
"<body>\n" +
|
|
@@ -1113,8 +1301,86 @@ function _statCard(label, value, accent) {
|
|
|
1113
1301
|
"<div class=\"value" + (accent ? " accent" : "") + "\">" + _htmlEscape(value) + "</div></div>";
|
|
1114
1302
|
}
|
|
1115
1303
|
|
|
1304
|
+
// ---- admin web pages (login / landing / setup wizard) -------------------
|
|
1305
|
+
|
|
1306
|
+
function _renderAdminShell(shopName, subtitle, bodyHtml) {
|
|
1307
|
+
return _renderTemplate(DASHBOARD_LAYOUT, {
|
|
1308
|
+
shop_name: shopName || "blamejs.shop",
|
|
1309
|
+
window_label: subtitle || "",
|
|
1310
|
+
body: "RAW_BODY",
|
|
1311
|
+
}).replace("RAW_BODY", bodyHtml);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function renderAdminLogin(opts) {
|
|
1315
|
+
opts = opts || {};
|
|
1316
|
+
var err = opts.error
|
|
1317
|
+
? "<div class=\"banner banner--err\">That key didn't match. Check the ADMIN_API_KEY this deployment was started with.</div>"
|
|
1318
|
+
: "";
|
|
1319
|
+
var body =
|
|
1320
|
+
"<section style=\"max-width:30rem;\">" +
|
|
1321
|
+
"<h2>Sign in</h2>" + err +
|
|
1322
|
+
"<form method=\"post\" action=\"/admin/login\">" +
|
|
1323
|
+
"<label class=\"form-field\"><span>Admin API key</span>" +
|
|
1324
|
+
"<input type=\"password\" name=\"token\" autocomplete=\"off\" autofocus required>" +
|
|
1325
|
+
"<small>Paste the ADMIN_API_KEY this deployment was started with.</small>" +
|
|
1326
|
+
"</label>" +
|
|
1327
|
+
"<button type=\"submit\" class=\"btn\">Sign in</button>" +
|
|
1328
|
+
"</form>" +
|
|
1329
|
+
"</section>";
|
|
1330
|
+
return _renderAdminShell(opts.shop_name, "Sign in", body);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function renderAdminLanding(opts) {
|
|
1334
|
+
opts = opts || {};
|
|
1335
|
+
var setupBanner = opts.setup_complete
|
|
1336
|
+
? ""
|
|
1337
|
+
: "<div class=\"banner banner--warn\">Your shop isn't set up yet. <a href=\"/admin/setup\">Finish setup →</a></div>";
|
|
1338
|
+
var body =
|
|
1339
|
+
"<section>" + setupBanner +
|
|
1340
|
+
"<h2>Admin</h2>" +
|
|
1341
|
+
"<div class=\"nav-cards\">" +
|
|
1342
|
+
"<a class=\"nav-card\" href=\"/admin/setup\"><h3>Setup wizard</h3><p>Shop identity, currency, and contact details.</p></a>" +
|
|
1343
|
+
"<a class=\"nav-card\" href=\"/admin/dashboard\"><h3>Dashboard</h3><p>Sales, revenue, and recent orders at a glance.</p></a>" +
|
|
1344
|
+
"</div>" +
|
|
1345
|
+
"<div class=\"actions-row\"><form method=\"post\" action=\"/admin/logout\"><button type=\"submit\" class=\"btn btn--ghost\">Sign out</button></form></div>" +
|
|
1346
|
+
"</section>";
|
|
1347
|
+
return _renderAdminShell(opts.shop_name, "", body);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function _setupField(label, name, value, type, hint, extra) {
|
|
1351
|
+
return "<label class=\"form-field\"><span>" + _htmlEscape(label) + "</span>" +
|
|
1352
|
+
"<input type=\"" + (type || "text") + "\" name=\"" + _htmlEscape(name) + "\" value=\"" + _htmlEscape(value || "") + "\"" + (extra || "") + ">" +
|
|
1353
|
+
(hint ? "<small>" + _htmlEscape(hint) + "</small>" : "") +
|
|
1354
|
+
"</label>";
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function renderAdminSetup(opts) {
|
|
1358
|
+
opts = opts || {};
|
|
1359
|
+
var v = opts.values || {};
|
|
1360
|
+
var saved = opts.saved ? "<div class=\"banner banner--ok\">Saved. Your shop details are live.</div>" : "";
|
|
1361
|
+
var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
1362
|
+
var body =
|
|
1363
|
+
"<section style=\"max-width:34rem;\">" +
|
|
1364
|
+
"<h2>Shop setup</h2>" +
|
|
1365
|
+
"<p class=\"meta\">Set the basics customers see across the storefront. You can change these any time.</p>" +
|
|
1366
|
+
saved + notice +
|
|
1367
|
+
"<form method=\"post\" action=\"/admin/setup\">" +
|
|
1368
|
+
_setupField("Shop name", "shop_name", v.shop_name, "text", "Shown in the header, page titles, and emails.", " maxlength=\"80\" required") +
|
|
1369
|
+
_setupField("Contact email", "contact_email", v.contact_email, "email", "Where customer replies and operational mail land.", " maxlength=\"160\"") +
|
|
1370
|
+
_setupField("Default currency", "currency", v.currency, "text", "3-letter ISO 4217 code (e.g. USD, EUR, GBP).", " maxlength=\"3\" style=\"text-transform:uppercase;max-width:8rem;\"") +
|
|
1371
|
+
_setupField("Support URL", "support_url", v.support_url, "url", "Linked from the storefront footer (help centre, contact page).", " maxlength=\"300\"") +
|
|
1372
|
+
"<div class=\"actions-row\"><button type=\"submit\" class=\"btn\">Save shop details</button>" +
|
|
1373
|
+
"<a class=\"btn btn--ghost\" href=\"/admin\">Back</a></div>" +
|
|
1374
|
+
"</form>" +
|
|
1375
|
+
"</section>";
|
|
1376
|
+
return _renderAdminShell(opts.shop_name, "Setup", body);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1116
1379
|
module.exports = {
|
|
1117
1380
|
mount: mount,
|
|
1118
1381
|
AUDIT_NAMESPACE: AUDIT_NAMESPACE,
|
|
1119
1382
|
renderDashboard: renderDashboard,
|
|
1383
|
+
renderAdminLogin: renderAdminLogin,
|
|
1384
|
+
renderAdminLanding: renderAdminLanding,
|
|
1385
|
+
renderAdminSetup: renderAdminSetup,
|
|
1120
1386
|
};
|
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
|
|
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 =
|
|
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) *
|
|
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
|
|
97
|
-
var DEFAULT_WINDOW_MS = 30
|
|
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 =
|
|
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 =
|
|
130
|
+
var DAY_MS = C.TIME.days(1);
|
|
130
131
|
|
|
131
132
|
// ---- monotonic clock ----------------------------------------------------
|
|
132
133
|
//
|
package/lib/auto-replenish.js
CHANGED
|
@@ -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:
|
|
114
|
-
daily:
|
|
115
|
-
weekly: 7
|
|
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
|
|
92
|
+
var WEEK_MS = C.TIME.days(7);
|
|
92
93
|
|
|
93
94
|
var BACKORDER_STATUSES = Object.freeze(["pending", "fulfilled", "cancelled"]);
|
|
94
95
|
|
package/lib/business-hours.js
CHANGED
|
@@ -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 =
|
|
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 ---------------------------------------------------------
|
package/lib/carrier-accounts.js
CHANGED
|
@@ -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 =
|
|
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";
|
package/lib/carrier-rates.js
CHANGED
|
@@ -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
|
package/lib/cart-abandonment.js
CHANGED
|
@@ -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 =
|
|
75
|
-
var DEFAULT_MAX_AGE_MS = 30
|
|
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
|
|
package/lib/cart-bulk-ops.js
CHANGED
|
@@ -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 +
|
|
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)",
|
package/lib/cart-recovery.js
CHANGED
|
@@ -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:
|
|
34
|
-
* { step_index: 1, offset_ms: 24
|
|
35
|
-
* { step_index: 2, offset_ms: 72
|
|
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 +
|
|
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
|
|
35
|
-
var MAX_QTY =
|
|
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
|
|
package/lib/catalog-drafts.js
CHANGED
|
@@ -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
|
|
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
|
package/lib/click-and-collect.js
CHANGED
|
@@ -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 =
|
|
111
|
-
var NO_SHOW_ESCALATE_MS = 7
|
|
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([
|