@blamejs/blamejs-shop 0.1.37 → 0.2.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.
- package/CHANGELOG.md +6 -0
- package/lib/admin.js +205 -42
- package/lib/asset-manifest.json +5 -5
- package/lib/storefront.js +428 -51
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +12 -0
- package/lib/vendor/blamejs/README.md +2 -2
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
- package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
- package/lib/vendor/blamejs/lib/archive-read.js +101 -35
- package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
- package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
- package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
- package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
- package/lib/vendor/blamejs/lib/backup/index.js +2 -3
- package/lib/vendor/blamejs/lib/calendar.js +9 -2
- package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
- package/lib/vendor/blamejs/lib/cose.js +4 -3
- package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
- package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
- package/lib/vendor/blamejs/lib/crypto.js +2 -2
- package/lib/vendor/blamejs/lib/framework-error.js +2 -1
- package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
- package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
- package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
- package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +10 -10
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
- package/lib/vendor/blamejs/lib/mail-server-submission.js +6 -5
- package/lib/vendor/blamejs/lib/mail-store.js +2 -2
- package/lib/vendor/blamejs/lib/mail.js +2 -2
- package/lib/vendor/blamejs/lib/mdoc.js +14 -14
- package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
- package/lib/vendor/blamejs/lib/network-tls.js +10 -7
- package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
- package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
- package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
- package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
- package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
- package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
- package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +82 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +188 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
- 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.2.x
|
|
10
|
+
|
|
11
|
+
- v0.2.0 (2026-05-27) — **A new storefront visual identity — a dark, violet brand theme with self-hosted typefaces and a redesigned product buy-box.** The default storefront theme has been redesigned around the project's actual brand: a dark, near-black canvas with a violet-to-magenta accent system that matches the blamejs shield-and-terminal mark, replacing the previous light theme and its unrelated orange accent. The stylesheet is restructured onto a semantic design-token layer (a purpose tier — surface, text, border, accent, price — sitting over the raw palette), so colour, contrast, and surfaces are now consistent and themeable rather than hard-coded per component. Two distinctive open-licensed typefaces are self-hosted in place of Inter: Hanken Grotesk for display and body, Space Mono for the terminal-style chrome (navigation, labels, SKUs, prices). The product page's variant selector is rebuilt from a data table into a proper buy-box: a prominent price, the variant(s) as selectable chips, a full-width add-to-cart, and a post-quantum-secured-checkout trust line — the add-to-cart, variant, and currency-conversion behaviour is unchanged. Accessibility is preserved (contrast targets met on the dark surfaces, focus-visible, reduced-motion guards, the consent banner restyled as legible dark glass). Operators on the default theme get the new look on upgrade; a custom theme passed via the theme primitive is unaffected. **Changed:** *Dark violet brand theme replaces the light theme* — The default theme is now a dark, near-black storefront with a violet→magenta accent family (matching the logo's shield/terminal mark) carried across the chrome, hero, cards, forms, badges, switchers, and footer. The previous light canvas and its off-brand orange accent are gone. Depth comes from hard-edged and soft violet shadows and a faint circuit-grid texture; motion (a staggered page-load reveal, a glitch treatment on the hero accent word, a terminal cursor) is CSS-only and disabled under `prefers-reduced-motion`. · *Semantic design-token layer + self-hosted typefaces* — The stylesheet now has a primitive→semantic token structure (`--surface`/`--surface-raised`, `--text`/`--text-muted`, `--border`, `--color-accent`, `--color-price`, glow tokens) so components reference purpose, not raw values — fixing the previously undefined `--font-sans`/`--danger-l` tokens and a duplicated button definition along the way. Hanken Grotesk (display/body) and Space Mono (mono chrome) are self-hosted as OFL woff2, replacing Inter; nothing is fetched from a font CDN, consistent with the strict `font-src 'self'` policy. · *Product buy-box redesign* — The product page's variant table is replaced by a buy-box: a large monospace price, each variant as a selectable pill chip (with a clear selected state, fully operable with JavaScript off), a full-width add-to-cart, and a trust line stating the checkout is post-quantum secured. The underlying add-to-cart form, multi-variant selection, and multi-currency price formatting are unchanged; products with many variants keep a compact fallback. The PDP trust badge wording "Ships from origin" is replaced with a real delivery estimate.
|
|
12
|
+
|
|
9
13
|
## v0.1.x
|
|
10
14
|
|
|
15
|
+
- v0.1.38 (2026-05-26) — **Security hardening + accessibility and UI completeness across the storefront and admin.** This release closes two data-exposure gaps, tightens admin input validation, and brings a broad accessibility and visual-completeness pass to both the storefront and the operator console. A freshly issued gift-card code is no longer placed in a redirect URL where it could persist in browser history or server logs — it's now revealed once in the page itself. Signed-in order pages now enforce ownership so a customer can only view their own orders. On accessibility: admin tables carry proper header scope, every admin page has its own title, a skip-to-content link and current-page navigation state are in place, search-filter selection is exposed correctly to assistive technology, status changes are announced, and destructive actions (refunds, plan archival, endpoint deletion, subscription cancellation, address removal) now require a confirmation step. Several surfaces that shipped without styling — the faceted-search sidebar, the subscriptions page, loyalty and referral badges, and the locale switcher — are now fully styled; product cards gain depth, accent-colored text meets contrast requirements, touch targets meet the 44px minimum on mobile, and success messages and empty states are consistent. **Changed:** *Confirmation step on destructive actions* — Irreversible or money-moving actions now route through a server-rendered confirmation page that states the consequence before proceeding: order refunds and subscription-plan archival and webhook deletion in the admin, and subscription cancellation (including the immediate-cancel option, which now spells out the forfeited time) and saved-address removal (with an undo link) in the customer account. · *Success confirmations and consistent empty states* — Account actions — saving or removing an address, toggling the wishlist, moving a saved item, requesting a return (which now echoes the RMA code) — confirm success with an announced status message instead of redirecting silently. Empty states across the account area now share one consistent treatment. **Fixed:** *Accessibility across storefront and admin* — Admin data tables now use `scope` on column and row headers; every admin page renders its own document title; a skip-to-content link and `aria-current` navigation state are in place. On the storefront, search-facet links expose their selected state correctly to screen readers (replacing an invalid attribute), and account status changes are announced via live regions. Accent-colored text (prices, labels, links) now uses a darker shade that meets the 4.5:1 contrast minimum, and interactive controls meet the 44px touch-target minimum on small screens. · *Styling for surfaces that shipped unstyled* — The faceted-search sidebar, the subscriptions management page, loyalty and referral status badges, and the footer locale switcher now render with full styling instead of unstyled browser defaults. Product and collection cards gain a resting elevation, a duplicate button definition was consolidated to one consistent style, and disabled-button and placeholder text now meet contrast requirements. **Security:** *Gift-card one-time code is no longer exposed in a URL* — Issuing a gift card from the admin console previously redirected to a URL containing the plaintext code, which could persist in browser history, the redirect `Location` header, and access logs. The code is now shown once directly in the response body (the same pattern used for webhook signing secrets) and never placed in a URL. · *Order pages enforce ownership for signed-in customers* — `GET /orders/:id` now verifies that a signed-in customer owns the order before rendering it (returning 404 otherwise), closing a gap where an order's contents could be viewed by anyone with the order id. The guest post-purchase confirmation path is unchanged. · *Strict validation on admin money inputs* — Gift-card amounts and subscription-plan amounts/intervals/trials are now parsed with strict integer validation, so malformed input (e.g. `50abc`) is rejected rather than silently coerced.
|
|
16
|
+
|
|
11
17
|
- v0.1.37 (2026-05-26) — **Localized storefront — the UI chrome renders in the visitor's language, with a locale switcher and right-to-left support.** The storefront's interface chrome — the navigation, search controls, newsletter band, and footer — now renders in the visitor's language. The locale is resolved the same way at the edge and in the container: an explicit `?lang=` choice first, then a saved preference cookie, then the browser's Accept-Language header, then the operator's default. A locale switcher in the footer lets the visitor change it; the choice persists in a first-party cookie. Right-to-left languages (Arabic, Hebrew, Persian, …) flip the document direction automatically. Strings come from the operator's locale policy and translation rows, and any key without a translation falls back to the English baseline rather than showing a raw placeholder — so a partially translated catalog never renders broken text. The default locale renders at the edge for cookie-less visitors (so cached pages stay fast); a non-default choice is served by the container. Everything works with JavaScript off, and the localized markup is byte-identical across the edge and the container. **Added:** *Localized UI chrome + footer locale switcher* — Navigation, search labels, the newsletter band, and the footer render in the visitor's locale. A locale switcher in the footer lists the languages the operator has enabled (shown by their own names — "Deutsch", "Français"); selecting one persists the choice in a first-party cookie and returns the visitor to the page they were on. The locale resolves identically at the edge and in the container — `?lang=`, then the cookie, then `Accept-Language`, then the operator default — and a right-to-left language sets the document's `dir` so the layout mirrors correctly. The switcher and selection work without JavaScript. · *Operator locale policy with English-baseline fallback* — Enable languages by setting a locale policy (a default locale plus the supported list) and supplying translation rows for the chrome strings; the storefront resolves each string against the active policy, layered over the operator's translations, and falls back to the built-in English baseline for any key still missing — never a raw key or empty label. With no policy seeded the storefront renders the English baseline and shows no switcher, so the feature is opt-in and a fresh install is unaffected. `SHOP_DEFAULT_LOCALE` sets the default the edge serves to cookie-less visitors; set `SHOP_LOCALES` (the supported list) so the edge forwards a first-time visitor whose `Accept-Language` prefers a supported non-default locale to the container instead of caching the default. An explicit cookie or `?lang=` choice is always served by the container so edge caching stays simple.
|
|
12
18
|
|
|
13
19
|
- v0.1.36 (2026-05-26) — **Multi-currency display — shoppers browse prices in their own currency while you still charge in your base currency.** Shoppers can now pick a display currency from a switcher in the footer; every price on the storefront — product pages, the grid, search, and the cart — re-renders in that currency using the foreign-exchange rates you set, formatted with the right symbol, grouping, and decimal places for the locale. Conversion is display-only: the cart line and the eventual charge stay in your base currency, and a "Prices shown in EUR; you'll be charged in USD" disclosure makes that explicit whenever a conversion is in effect. The choice rides in a sealed first-party cookie and survives navigation; a per-currency rounding rule can snap converted prices to a tidy increment (for example Swiss 0.05 Rappenrundung). Every failure mode falls back to the base currency rather than showing a broken price: a currency with no rate on file, a rate that has expired, a currency you've removed from the allow-list, or a tampered cookie all render in your base currency. The switcher and all conversions render identically from the edge and the container, and the whole flow works with JavaScript off. **Added:** *Currency switcher + on-page price conversion* — A currency switcher in the footer lists the currencies you've enabled; choosing one `POST`s to `/currency`, stores the choice in a sealed cookie, and redirects back to the page the shopper was on. From then on product pages, the product grid, search results, and the cart display prices converted into that currency, each formatted for its locale (symbol, thousands separator, decimal places). When a conversion is active a "Prices shown in <display>; you'll be charged in <base>" note appears so the shopper always knows the charge currency. The switcher submits without JavaScript — the small enhancement script only auto-submits on change. · *Operator-set FX rates, display rounding, and a base-currency guarantee* — Set your base currency and the enabled list with `SHOP_BASE_CURRENCY` / `SHOP_CURRENCIES` (or the `shop.base_currency` / `shop.currencies` config keys), and supply foreign-exchange rates yourself from whatever source you trust — the framework never reaches out to a rate feed. Rates are stored as integer basis points and conversion uses banker's rounding; an optional per-currency rounding rule snaps the result to a display increment (e.g. nearest 0.05). Conversion is strictly for display: the cart line's currency and amount, and therefore the amount charged at checkout, always stay in the base currency. Any currency without a usable rate — missing, expired, de-listed, or a tampered cookie — falls back to base-currency display instead of a broken or `NaN` price, so a stale or absent rate feed never breaks a priced page.
|
package/lib/admin.js
CHANGED
|
@@ -117,6 +117,27 @@ function _htmlEscape(s) {
|
|
|
117
117
|
return b.template.escapeHtml(String(s));
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Strict integer coercion for money / count form fields. Refuses
|
|
121
|
+
// "50abc" / "" / floats, unlike parseInt's loose prefix match (which
|
|
122
|
+
// silently turns "50abc" into 50). Accepts an already-numeric value
|
|
123
|
+
// (must itself be a safe integer). Throws a TypeError whose message
|
|
124
|
+
// carries the caller's prefix so the browser form path surfaces it as
|
|
125
|
+
// a 400 notice. Same shape as the collections-rule _strictInt nested
|
|
126
|
+
// helper, hoisted to module scope so the gift-card + subscription-plan
|
|
127
|
+
// create paths share it.
|
|
128
|
+
function _strictMinorInt(value, prefix, label) {
|
|
129
|
+
if (typeof value === "number") {
|
|
130
|
+
if (!Number.isSafeInteger(value)) throw new TypeError(prefix + ": " + label + " out of range");
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value !== "string" || !/^-?\d+$/.test(value.trim())) {
|
|
134
|
+
throw new TypeError(prefix + ": " + label + " must be an integer");
|
|
135
|
+
}
|
|
136
|
+
var n = Number(value.trim());
|
|
137
|
+
if (!Number.isSafeInteger(n)) throw new TypeError(prefix + ": " + label + " out of range");
|
|
138
|
+
return n;
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
// ---- bearer auth --------------------------------------------------------
|
|
121
142
|
|
|
122
143
|
function _readBearer(req) {
|
|
@@ -163,6 +184,26 @@ function _setAdminCookie(res) {
|
|
|
163
184
|
function _clearAdminCookie(res) {
|
|
164
185
|
_adminJar().clear(res, ADMIN_COOKIE_NAME);
|
|
165
186
|
}
|
|
187
|
+
|
|
188
|
+
// One-time gift-card code reveal. The issue POST stashes the freshly-issued
|
|
189
|
+
// plaintext code in a sealed, HttpOnly, /admin-scoped cookie and 303-redirects
|
|
190
|
+
// (Post/Redirect/Get — so a refresh of the detail page can't re-issue a card),
|
|
191
|
+
// WITHOUT placing the code in the URL / Location header / browser history /
|
|
192
|
+
// access log. The detail GET reads it exactly once and clears it, so a reload
|
|
193
|
+
// after the reveal shows the normal card with no code.
|
|
194
|
+
var GC_REVEAL_COOKIE = "gc_reveal";
|
|
195
|
+
function _stashGiftCardReveal(res, id, code) {
|
|
196
|
+
_adminJar().writeSealed(res, GC_REVEAL_COOKIE, JSON.stringify({ id: id, code: code }),
|
|
197
|
+
{ expires: new Date(Date.now() + b.constants.TIME.hours(1)) });
|
|
198
|
+
}
|
|
199
|
+
function _takeGiftCardReveal(req, res, id) {
|
|
200
|
+
var raw = _adminJar().readSealed(req, GC_REVEAL_COOKIE);
|
|
201
|
+
if (raw === null) return null;
|
|
202
|
+
_adminJar().clear(res, GC_REVEAL_COOKIE);
|
|
203
|
+
var env;
|
|
204
|
+
try { env = JSON.parse(raw); } catch (_e) { return null; }
|
|
205
|
+
return (env && env.id === id && typeof env.code === "string") ? env.code : null;
|
|
206
|
+
}
|
|
166
207
|
function _adminCookieValid(req) {
|
|
167
208
|
var raw = _adminJar().readSealed(req, ADMIN_COOKIE_NAME);
|
|
168
209
|
if (raw === null) return false;
|
|
@@ -855,6 +896,35 @@ function mount(router, deps) {
|
|
|
855
896
|
return { refund: refund, order: await order.get(o.id) };
|
|
856
897
|
}
|
|
857
898
|
|
|
899
|
+
// Browser confirmation interstitial for the full refund — it moves
|
|
900
|
+
// money via the provider and advances the FSM, so the console makes
|
|
901
|
+
// the operator confirm (the CSP forbids a client confirm() dialog).
|
|
902
|
+
// Bearer clients never reach here; they keep POSTing /refund directly.
|
|
903
|
+
router.post("/admin/orders/:id/refund/confirm", _pageOrApi(false,
|
|
904
|
+
R(async function (_req, res) {
|
|
905
|
+
return _problem(res, 405, "use-canonical-endpoint", "POST /admin/orders/:id/refund directly for the JSON API");
|
|
906
|
+
}),
|
|
907
|
+
async function (req, res) {
|
|
908
|
+
var id = req.params.id;
|
|
909
|
+
var o;
|
|
910
|
+
try { o = await order.get(id); }
|
|
911
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; o = null; }
|
|
912
|
+
if (!o || !o.payment_intent_id) {
|
|
913
|
+
return _redirect(res, "/admin/orders/" + encodeURIComponent(id) + "?err=1");
|
|
914
|
+
}
|
|
915
|
+
var amount = pricing.format(o.grand_total_minor, o.currency);
|
|
916
|
+
_sendHtml(res, 200, renderAdminConfirm({
|
|
917
|
+
shop_name: deps.shop_name, nav_available: navAvailable, active: "orders",
|
|
918
|
+
heading: "Refund order " + o.id.slice(0, 8) + "?",
|
|
919
|
+
consequence: "This issues a full refund of " + amount + " through the payment provider and cannot be undone.",
|
|
920
|
+
detail: "The provider refund runs first; only if it succeeds does the order move to a refunded state.",
|
|
921
|
+
action: "/admin/orders/" + _htmlEscape(o.id) + "/refund",
|
|
922
|
+
confirm_label: "Refund " + amount,
|
|
923
|
+
cancel_href: "/admin/orders/" + encodeURIComponent(id),
|
|
924
|
+
}));
|
|
925
|
+
},
|
|
926
|
+
));
|
|
927
|
+
|
|
858
928
|
router.post("/admin/orders/:id/refund", _pageOrApi(false,
|
|
859
929
|
W("order.refund", async function (req, res) {
|
|
860
930
|
var o = await order.get(req.params.id);
|
|
@@ -1549,6 +1619,31 @@ function mount(router, deps) {
|
|
|
1549
1619
|
},
|
|
1550
1620
|
));
|
|
1551
1621
|
|
|
1622
|
+
// Browser confirmation interstitial for delete — removing an
|
|
1623
|
+
// endpoint is irreversible (its signing secret is gone for good and
|
|
1624
|
+
// deliveries stop), so the console confirms before the real DELETE.
|
|
1625
|
+
router.post("/admin/webhooks/:id/delete/confirm", _pageOrApi(false,
|
|
1626
|
+
R(async function (_req, res) {
|
|
1627
|
+
return _problem(res, 405, "use-canonical-endpoint", "DELETE /admin/webhooks/:id (or POST .../delete) directly for the JSON API");
|
|
1628
|
+
}),
|
|
1629
|
+
async function (req, res) {
|
|
1630
|
+
var id = req.params.id;
|
|
1631
|
+
var ep = null;
|
|
1632
|
+
try { ep = await webhooks.endpoints.get(id); }
|
|
1633
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; }
|
|
1634
|
+
if (!ep) return _redirect(res, "/admin/webhooks?err=1");
|
|
1635
|
+
_sendHtml(res, 200, renderAdminConfirm({
|
|
1636
|
+
shop_name: deps.shop_name, nav_available: navAvailable, active: "webhooks",
|
|
1637
|
+
heading: "Delete this endpoint?",
|
|
1638
|
+
consequence: "Deleting the endpoint is permanent — its signing secret is destroyed and no further deliveries are sent.",
|
|
1639
|
+
detail: ep.url ? "Endpoint: " + ep.url : "This endpoint will be removed.",
|
|
1640
|
+
action: "/admin/webhooks/" + _htmlEscape(id) + "/delete",
|
|
1641
|
+
confirm_label: "Delete endpoint",
|
|
1642
|
+
cancel_href: "/admin/webhooks",
|
|
1643
|
+
}));
|
|
1644
|
+
},
|
|
1645
|
+
));
|
|
1646
|
+
|
|
1552
1647
|
router.post("/admin/webhooks/:id/delete", _pageOrApi(false,
|
|
1553
1648
|
W("webhook.delete", async function (req, res) {
|
|
1554
1649
|
var ok = await webhooks.endpoints.delete(req.params.id);
|
|
@@ -2087,9 +2182,9 @@ function mount(router, deps) {
|
|
|
2087
2182
|
interval: body.interval,
|
|
2088
2183
|
currency: typeof body.currency === "string" ? body.currency.trim().toLowerCase() : body.currency,
|
|
2089
2184
|
};
|
|
2090
|
-
if (body.amount_minor != null && body.amount_minor !== "") input.amount_minor =
|
|
2091
|
-
if (body.interval_count != null && body.interval_count !== "") input.interval_count =
|
|
2092
|
-
if (body.trial_days != null && body.trial_days !== "") input.trial_days =
|
|
2185
|
+
if (body.amount_minor != null && body.amount_minor !== "") input.amount_minor = _strictMinorInt(body.amount_minor, "subscriptions", "amount_minor (minor units)");
|
|
2186
|
+
if (body.interval_count != null && body.interval_count !== "") input.interval_count = _strictMinorInt(body.interval_count, "subscriptions", "interval_count");
|
|
2187
|
+
if (body.trial_days != null && body.trial_days !== "") input.trial_days = _strictMinorInt(body.trial_days, "subscriptions", "trial_days");
|
|
2093
2188
|
if (body.variant_id) input.variant_id = String(body.variant_id).trim();
|
|
2094
2189
|
try {
|
|
2095
2190
|
await subscriptions.plans.create(input);
|
|
@@ -2147,6 +2242,31 @@ function mount(router, deps) {
|
|
|
2147
2242
|
return p;
|
|
2148
2243
|
}));
|
|
2149
2244
|
|
|
2245
|
+
// Browser confirmation interstitial for archive — terminal from the
|
|
2246
|
+
// console (a retired plan is replaced by creating a new one against a
|
|
2247
|
+
// fresh Stripe price id, never reactivated in place), so confirm it.
|
|
2248
|
+
router.post("/admin/subscription-plans/:id/archive/confirm", _pageOrApi(false,
|
|
2249
|
+
R(async function (_req, res) {
|
|
2250
|
+
return _problem(res, 405, "use-canonical-endpoint", "POST /admin/subscription-plans/:id/archive directly for the JSON API");
|
|
2251
|
+
}),
|
|
2252
|
+
async function (req, res) {
|
|
2253
|
+
var id = req.params.id;
|
|
2254
|
+
var p = null;
|
|
2255
|
+
try { p = await subscriptions.plans.get(id); }
|
|
2256
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; }
|
|
2257
|
+
if (!p) return _redirect(res, "/admin/subscription-plans?err=1");
|
|
2258
|
+
_sendHtml(res, 200, renderAdminConfirm({
|
|
2259
|
+
shop_name: deps.shop_name, nav_available: navAvailable, active: "subscriptions",
|
|
2260
|
+
heading: "Archive this plan?",
|
|
2261
|
+
consequence: "Archiving is terminal — this plan cannot be reactivated. To offer it again you create a new plan against a fresh Stripe price id.",
|
|
2262
|
+
detail: "Existing subscriptions on this plan are unaffected; new sign-ups can no longer select it.",
|
|
2263
|
+
action: "/admin/subscription-plans/" + _htmlEscape(id) + "/archive",
|
|
2264
|
+
confirm_label: "Archive plan",
|
|
2265
|
+
cancel_href: "/admin/subscription-plans",
|
|
2266
|
+
}));
|
|
2267
|
+
},
|
|
2268
|
+
));
|
|
2269
|
+
|
|
2150
2270
|
// Archive content-negotiates: bearer → JSON; browser form → archive,
|
|
2151
2271
|
// then PRG. An unknown / malformed id is a no-op notice (?err=1),
|
|
2152
2272
|
// never a 500.
|
|
@@ -2215,7 +2335,11 @@ function mount(router, deps) {
|
|
|
2215
2335
|
if (giftcards) {
|
|
2216
2336
|
// Issue a card. Bearer → JSON (the issued plaintext code is
|
|
2217
2337
|
// returned ONCE for the operator to deliver); browser form →
|
|
2218
|
-
// issue, then
|
|
2338
|
+
// issue, then render the detail page DIRECTLY from this POST 200
|
|
2339
|
+
// with the plaintext code passed in-body. We deliberately do NOT
|
|
2340
|
+
// redirect with the code on the query string — the one-time bearer
|
|
2341
|
+
// code must never land in a URL / Location header / browser
|
|
2342
|
+
// history / access log (mirrors renderAdminWebhookSecret).
|
|
2219
2343
|
router.post("/admin/gift-cards", _pageOrApi(false,
|
|
2220
2344
|
W("gift_card.issue", async function (req, res) {
|
|
2221
2345
|
var issued = await _issueGiftCard(req.body || {});
|
|
@@ -2235,7 +2359,11 @@ function mount(router, deps) {
|
|
|
2235
2359
|
}));
|
|
2236
2360
|
}
|
|
2237
2361
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".gift_card.issue", outcome: "success", metadata: { id: issued.id } });
|
|
2238
|
-
|
|
2362
|
+
// Stash the one-time code in a sealed cookie and 303-redirect to the
|
|
2363
|
+
// card's detail page (PRG: a refresh can't re-issue). The detail GET
|
|
2364
|
+
// reveals + clears the code; it never travels in the URL.
|
|
2365
|
+
_stashGiftCardReveal(res, issued.id, issued.code);
|
|
2366
|
+
_redirect(res, "/admin/gift-cards/" + encodeURIComponent(issued.id));
|
|
2239
2367
|
},
|
|
2240
2368
|
));
|
|
2241
2369
|
|
|
@@ -2271,13 +2399,16 @@ function mount(router, deps) {
|
|
|
2271
2399
|
}),
|
|
2272
2400
|
async function (req, res) {
|
|
2273
2401
|
var card = await giftcards.getById(req.params.id);
|
|
2274
|
-
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
2275
|
-
var issuedCode = url && url.searchParams.get("issued");
|
|
2276
2402
|
if (!card) {
|
|
2277
2403
|
return _sendHtml(res, 404, renderAdminGiftCard({
|
|
2278
2404
|
shop_name: deps.shop_name, nav_available: navAvailable, card: null,
|
|
2279
2405
|
}));
|
|
2280
2406
|
}
|
|
2407
|
+
// One-time reveal: if this GET follows a fresh issue (sealed
|
|
2408
|
+
// `gc_reveal` cookie present for this id), show the plaintext code
|
|
2409
|
+
// once and clear the cookie. A subsequent reload reveals nothing —
|
|
2410
|
+
// the code never lands in the URL / history / access log.
|
|
2411
|
+
var issuedCode = _takeGiftCardReveal(req, res, req.params.id);
|
|
2281
2412
|
var history = [];
|
|
2282
2413
|
if (giftCardLedger) {
|
|
2283
2414
|
try { history = (await giftCardLedger.history(req.params.id, { limit: 500 })).rows; }
|
|
@@ -2285,7 +2416,7 @@ function mount(router, deps) {
|
|
|
2285
2416
|
}
|
|
2286
2417
|
_sendHtml(res, 200, renderAdminGiftCard({
|
|
2287
2418
|
shop_name: deps.shop_name, nav_available: navAvailable, card: card,
|
|
2288
|
-
ledger: history, issued_code: issuedCode,
|
|
2419
|
+
ledger: history, issued_code: issuedCode || undefined,
|
|
2289
2420
|
}));
|
|
2290
2421
|
},
|
|
2291
2422
|
));
|
|
@@ -2298,15 +2429,11 @@ function mount(router, deps) {
|
|
|
2298
2429
|
async function _issueGiftCard(body) {
|
|
2299
2430
|
var input = { currency: typeof body.currency === "string" ? body.currency.trim().toUpperCase() : body.currency };
|
|
2300
2431
|
if (body.amount_minor != null && body.amount_minor !== "") {
|
|
2301
|
-
|
|
2302
|
-
if (!Number.isInteger(n)) throw new TypeError("giftcards: amount_minor must be an integer (minor units)");
|
|
2303
|
-
input.amount_minor = n;
|
|
2432
|
+
input.amount_minor = _strictMinorInt(body.amount_minor, "giftcards", "amount_minor (minor units)");
|
|
2304
2433
|
}
|
|
2305
2434
|
if (body.issued_to_email) input.issued_to_email = String(body.issued_to_email).trim();
|
|
2306
2435
|
if (body.expires_at != null && body.expires_at !== "") {
|
|
2307
|
-
|
|
2308
|
-
if (!Number.isInteger(ex)) throw new TypeError("giftcards: expires_at must be an integer epoch-ms");
|
|
2309
|
-
input.expires_at = ex;
|
|
2436
|
+
input.expires_at = _strictMinorInt(body.expires_at, "giftcards", "expires_at (epoch-ms)");
|
|
2310
2437
|
}
|
|
2311
2438
|
var issued = await giftcards.issue(input);
|
|
2312
2439
|
if (giftCardLedger) {
|
|
@@ -2470,10 +2597,11 @@ var DASHBOARD_LAYOUT =
|
|
|
2470
2597
|
" <meta charset=\"utf-8\">\n" +
|
|
2471
2598
|
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n" +
|
|
2472
2599
|
" <meta name=\"robots\" content=\"noindex,nofollow\">\n" +
|
|
2473
|
-
" <title>
|
|
2600
|
+
" <title>{{page_title}} — {{shop_name}}</title>\n" +
|
|
2474
2601
|
" RAW_ADMIN_CSS\n" +
|
|
2475
2602
|
"</head>\n" +
|
|
2476
2603
|
"<body>\n" +
|
|
2604
|
+
" <a class=\"skip-link\" href=\"#admin-main\">Skip to content</a>\n" +
|
|
2477
2605
|
" <header class=\"admin-header\">\n" +
|
|
2478
2606
|
" <div class=\"admin-header__inner\">\n" +
|
|
2479
2607
|
" <h1>{{shop_name}} <span class=\"brand-accent\">/ admin</span></h1>\n" +
|
|
@@ -2481,7 +2609,7 @@ var DASHBOARD_LAYOUT =
|
|
|
2481
2609
|
" </div>\n" +
|
|
2482
2610
|
" </header>\n" +
|
|
2483
2611
|
" {{nav}}\n" +
|
|
2484
|
-
" <main>{{body}}</main>\n" +
|
|
2612
|
+
" <main id=\"admin-main\">{{body}}</main>\n" +
|
|
2485
2613
|
"</body>\n" +
|
|
2486
2614
|
"</html>\n";
|
|
2487
2615
|
|
|
@@ -2565,7 +2693,7 @@ function renderDashboard(opts) {
|
|
|
2565
2693
|
}).join("");
|
|
2566
2694
|
otherCurrencies =
|
|
2567
2695
|
"<section><h2>Other currencies in window</h2><div class=\"panel\">" +
|
|
2568
|
-
"<table><thead><tr><th>Currency</th><th class=\"num\">Orders</th><th class=\"num\">Revenue</th></tr></thead><tbody>" + rows + "</tbody></table>" +
|
|
2696
|
+
"<table><thead><tr><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + rows + "</tbody></table>" +
|
|
2569
2697
|
"</div></section>";
|
|
2570
2698
|
}
|
|
2571
2699
|
|
|
@@ -2599,11 +2727,11 @@ function renderDashboard(opts) {
|
|
|
2599
2727
|
"<section><h2>Catalog + activity</h2><div class=\"two-col\">" +
|
|
2600
2728
|
" <div class=\"panel\">" +
|
|
2601
2729
|
" <h3 class=\"subhead\">Top SKUs by units sold</h3>" +
|
|
2602
|
-
" <table><thead><tr><th>SKU</th><th class=\"num\">Units</th><th class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>" +
|
|
2730
|
+
" <table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>" +
|
|
2603
2731
|
" </div>" +
|
|
2604
2732
|
" <div class=\"panel\">" +
|
|
2605
2733
|
" <h3 class=\"subhead\">Recent orders</h3>" +
|
|
2606
|
-
" <table><thead><tr><th>Order</th><th>Status</th><th class=\"num\">Total</th></tr></thead><tbody>" + recentRows + "</tbody></table>" +
|
|
2734
|
+
" <table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th></tr></thead><tbody>" + recentRows + "</tbody></table>" +
|
|
2607
2735
|
" </div>" +
|
|
2608
2736
|
"</div></section>";
|
|
2609
2737
|
|
|
@@ -2657,7 +2785,7 @@ function _adminNav(active, available) {
|
|
|
2657
2785
|
var links = ADMIN_NAV_ITEMS.filter(function (it) {
|
|
2658
2786
|
return !it.requires || !available || available[it.requires];
|
|
2659
2787
|
}).map(function (it) {
|
|
2660
|
-
return "<a href=\"" + it.href + "\"" + (it.key === active ? " class=\"active\"" : "") + ">" +
|
|
2788
|
+
return "<a href=\"" + it.href + "\"" + (it.key === active ? " class=\"active\" aria-current=\"page\"" : "") + ">" +
|
|
2661
2789
|
_htmlEscape(it.label) + "</a>";
|
|
2662
2790
|
}).join("");
|
|
2663
2791
|
return "<nav class=\"admin-nav\"><div class=\"admin-nav__inner\">" + links + "</div></nav>";
|
|
@@ -2666,6 +2794,7 @@ function _adminNav(active, available) {
|
|
|
2666
2794
|
function _renderAdminShell(shopName, subtitle, bodyHtml, active, available) {
|
|
2667
2795
|
return _renderTemplate(DASHBOARD_LAYOUT, {
|
|
2668
2796
|
shop_name: shopName || "blamejs.shop",
|
|
2797
|
+
page_title: subtitle || "Admin",
|
|
2669
2798
|
window_label: subtitle || "",
|
|
2670
2799
|
nav: "RAW_NAV",
|
|
2671
2800
|
body: "RAW_BODY",
|
|
@@ -2674,6 +2803,35 @@ function _renderAdminShell(shopName, subtitle, bodyHtml, active, available) {
|
|
|
2674
2803
|
.replace("RAW_BODY", bodyHtml);
|
|
2675
2804
|
}
|
|
2676
2805
|
|
|
2806
|
+
// Server-rendered confirmation interstitial for irreversible /
|
|
2807
|
+
// money-moving actions. The CSP forbids inline script, so a client
|
|
2808
|
+
// `confirm()` dialog is impossible — instead the action button POSTs
|
|
2809
|
+
// here, this page states the consequence, and its Confirm button POSTs
|
|
2810
|
+
// to the real endpoint (Cancel is a plain link back). `opts.fields` is
|
|
2811
|
+
// an optional map of hidden form inputs forwarded to the real endpoint.
|
|
2812
|
+
function renderAdminConfirm(opts) {
|
|
2813
|
+
opts = opts || {};
|
|
2814
|
+
var fields = opts.fields || {};
|
|
2815
|
+
var hidden = Object.keys(fields).map(function (k) {
|
|
2816
|
+
return "<input type=\"hidden\" name=\"" + _htmlEscape(k) + "\" value=\"" + _htmlEscape(fields[k]) + "\">";
|
|
2817
|
+
}).join("");
|
|
2818
|
+
var body =
|
|
2819
|
+
"<section class=\"mw-42\">" +
|
|
2820
|
+
"<h2>" + _htmlEscape(opts.heading || "Confirm action") + "</h2>" +
|
|
2821
|
+
"<div class=\"banner banner--warn\">" + _htmlEscape(opts.consequence || "This action cannot be undone.") + "</div>" +
|
|
2822
|
+
"<div class=\"panel\">" +
|
|
2823
|
+
"<p>" + _htmlEscape(opts.detail || "Are you sure you want to continue?") + "</p>" +
|
|
2824
|
+
"<div class=\"actions-row\">" +
|
|
2825
|
+
"<form method=\"post\" action=\"" + _htmlEscape(opts.action) + "\" class=\"form-inline\">" + hidden +
|
|
2826
|
+
"<button class=\"btn btn--danger\" type=\"submit\">" + _htmlEscape(opts.confirm_label || "Confirm") + "</button>" +
|
|
2827
|
+
"</form>" +
|
|
2828
|
+
"<a class=\"btn btn--ghost\" href=\"" + _htmlEscape(opts.cancel_href) + "\">Cancel</a>" +
|
|
2829
|
+
"</div>" +
|
|
2830
|
+
"</div>" +
|
|
2831
|
+
"</section>";
|
|
2832
|
+
return _renderAdminShell(opts.shop_name, opts.heading || "Confirm", body, opts.active, opts.nav_available);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2677
2835
|
function renderAdminLogin(opts) {
|
|
2678
2836
|
opts = opts || {};
|
|
2679
2837
|
var err = opts.error
|
|
@@ -2793,7 +2951,7 @@ function renderAdminIntegrations(opts) {
|
|
|
2793
2951
|
"<h2>Integrations</h2>" +
|
|
2794
2952
|
"<p class=\"meta\">Every integration is off until you supply its credentials — set them as deployment secrets, then redeploy. Nothing is enabled without your keys.</p>" +
|
|
2795
2953
|
"<div class=\"panel\"><table>" +
|
|
2796
|
-
"<thead><tr><th>Integration</th><th>Status</th><th>To enable</th></tr></thead>" +
|
|
2954
|
+
"<thead><tr><th scope=\"col\">Integration</th><th scope=\"col\">Status</th><th scope=\"col\">To enable</th></tr></thead>" +
|
|
2797
2955
|
"<tbody>" + rows + "</tbody>" +
|
|
2798
2956
|
"</table></div>" +
|
|
2799
2957
|
"<p class=\"meta mt-125\">Sign in with Apple and PayPal are planned. “Sign in with Shop” / Shop Pay isn't available to a self-hosted store. See the README “Optional integrations” section for full setup steps.</p>" +
|
|
@@ -2818,7 +2976,7 @@ function renderAdminProducts(opts) {
|
|
|
2818
2976
|
"<td>" + action + "</td></tr>";
|
|
2819
2977
|
}).join("");
|
|
2820
2978
|
var table = products.length
|
|
2821
|
-
? "<div class=\"panel\"><table><thead><tr><th>Title</th><th>Slug</th><th>Status</th><th>Action</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
2979
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
2822
2980
|
: "<p class=\"empty\">No products yet — create your first one below.</p>";
|
|
2823
2981
|
var body =
|
|
2824
2982
|
"<section><h2>Products</h2>" + created + notice + table +
|
|
@@ -2874,7 +3032,7 @@ function renderAdminOrders(opts) {
|
|
|
2874
3032
|
}).join("");
|
|
2875
3033
|
|
|
2876
3034
|
var table = orders.length
|
|
2877
|
-
? "<div class=\"panel\"><table><thead><tr><th>Order</th><th>Status</th><th class=\"num\">Items</th><th class=\"num\">Total</th><th>Placed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
3035
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
2878
3036
|
: "<p class=\"empty\">No orders" + (active ? " with status “" + _htmlEscape(active) + "”" : " yet") + ".</p>";
|
|
2879
3037
|
|
|
2880
3038
|
var body = "<section><h2>Orders</h2>" + notice + chips + table + "</section>";
|
|
@@ -2897,7 +3055,7 @@ function renderAdminOrder(opts) {
|
|
|
2897
3055
|
"</tr>";
|
|
2898
3056
|
}).join("");
|
|
2899
3057
|
var linesTable = (o.lines && o.lines.length)
|
|
2900
|
-
? "<table><thead><tr><th>SKU</th><th class=\"num\">Qty</th><th class=\"num\">Unit</th><th class=\"num\">Line</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
|
|
3058
|
+
? "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\" class=\"num\">Unit</th><th scope=\"col\" class=\"num\">Line</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
|
|
2901
3059
|
: "<p class=\"empty\">No line items recorded.</p>";
|
|
2902
3060
|
|
|
2903
3061
|
function _total(label, minor, strong) {
|
|
@@ -2927,7 +3085,9 @@ function renderAdminOrder(opts) {
|
|
|
2927
3085
|
var actionForms = transitions.map(function (t) {
|
|
2928
3086
|
if (t.on === "refund") {
|
|
2929
3087
|
if (!opts.can_refund) return ""; // no payment intent — nothing to refund here
|
|
2930
|
-
|
|
3088
|
+
// POSTs to the confirm interstitial (states the amount + warns
|
|
3089
|
+
// it's irreversible), whose Confirm button POSTs the real refund.
|
|
3090
|
+
return "<form method=\"post\" action=\"/admin/orders/" + _htmlEscape(o.id) + "/refund/confirm\" class=\"form-inline\">" +
|
|
2931
3091
|
"<button class=\"btn btn--danger\" type=\"submit\">" + _htmlEscape(t.label) + "</button>" +
|
|
2932
3092
|
"</form>";
|
|
2933
3093
|
}
|
|
@@ -3000,7 +3160,7 @@ function renderAdminCustomers(opts) {
|
|
|
3000
3160
|
}).join("");
|
|
3001
3161
|
|
|
3002
3162
|
var table = customers.length
|
|
3003
|
-
? "<div class=\"panel\"><table><thead><tr><th>Name</th><th>ID</th><th>Joined</th><th>Sign-in method</th><th class=\"num\">Orders</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
3163
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Name</th><th scope=\"col\">ID</th><th scope=\"col\">Joined</th><th scope=\"col\">Sign-in method</th><th scope=\"col\" class=\"num\">Orders</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
3004
3164
|
: "<p class=\"empty\">No customers yet.</p>";
|
|
3005
3165
|
|
|
3006
3166
|
// Cursor pager — a Next link when the page filled and more rows remain.
|
|
@@ -3056,7 +3216,7 @@ function renderAdminReturns(opts) {
|
|
|
3056
3216
|
}).join("");
|
|
3057
3217
|
|
|
3058
3218
|
var table = rmas.length
|
|
3059
|
-
? "<div class=\"panel\"><table><thead><tr><th>RMA</th><th>Order</th><th>Reason</th><th>Status</th><th class=\"num\">Items</th><th class=\"num\">Refund</th><th>Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
3219
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">RMA</th><th scope=\"col\">Order</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Refund</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
3060
3220
|
: "<p class=\"empty\">No “" + _htmlEscape(active) + "” returns.</p>";
|
|
3061
3221
|
|
|
3062
3222
|
var body = "<section><h2>Returns</h2>" + notice + chips + table + "</section>";
|
|
@@ -3076,7 +3236,7 @@ function renderAdminReturn(opts) {
|
|
|
3076
3236
|
"<td>" + _htmlEscape(l.reason || "—") + "</td></tr>";
|
|
3077
3237
|
}).join("");
|
|
3078
3238
|
var linesTable = (r.lines && r.lines.length)
|
|
3079
|
-
? "<table><thead><tr><th>SKU</th><th class=\"num\">Qty</th><th>Reason</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
|
|
3239
|
+
? "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\">Reason</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
|
|
3080
3240
|
: "<p class=\"empty\">No line items recorded.</p>";
|
|
3081
3241
|
|
|
3082
3242
|
function _field(label, value) {
|
|
@@ -3313,7 +3473,9 @@ function renderAdminQuestion(opts) {
|
|
|
3313
3473
|
|
|
3314
3474
|
var answerForm = "<div class=\"panel\"><h3>Post the seller answer</h3>" +
|
|
3315
3475
|
"<form method=\"post\" action=\"/admin/questions/" + _htmlEscape(q.id) + "/answer\">" +
|
|
3316
|
-
"<
|
|
3476
|
+
"<label class=\"form-field\"><span>Your answer</span>" +
|
|
3477
|
+
"<textarea name=\"body\" maxlength=\"4000\" rows=\"4\" required></textarea>" +
|
|
3478
|
+
"</label>" +
|
|
3317
3479
|
"<div class=\"order-actions\"><button class=\"btn\" type=\"submit\">Submit answer</button></div>" +
|
|
3318
3480
|
"</form>" +
|
|
3319
3481
|
"<p class=\"meta\">The answer lands pending — approve it above to publish it on the product page.</p>" +
|
|
@@ -3346,7 +3508,7 @@ function renderAdminInventory(opts) {
|
|
|
3346
3508
|
var isLow = th != null && available <= th;
|
|
3347
3509
|
var thVal = th == null ? "" : String(th);
|
|
3348
3510
|
return "<tr" + (isLow ? " class=\"row--low\"" : "") + ">" +
|
|
3349
|
-
"<
|
|
3511
|
+
"<th scope=\"row\"><code class=\"order-id\">" + _htmlEscape(r.sku) + "</code>" + (isLow ? " <span class=\"status-pill pending\">low</span>" : "") + "</th>" +
|
|
3350
3512
|
"<td class=\"num\">" + _htmlEscape(String(r.stock_on_hand)) + "</td>" +
|
|
3351
3513
|
"<td class=\"num\">" + _htmlEscape(String(r.stock_held)) + "</td>" +
|
|
3352
3514
|
"<td class=\"num\"><strong>" + _htmlEscape(String(available)) + "</strong></td>" +
|
|
@@ -3360,7 +3522,7 @@ function renderAdminInventory(opts) {
|
|
|
3360
3522
|
}).join("");
|
|
3361
3523
|
|
|
3362
3524
|
var table = rows.length
|
|
3363
|
-
? "<div class=\"panel\"><table><thead><tr><th>SKU</th><th class=\"num\">On hand</th><th class=\"num\">Held</th><th class=\"num\">Available</th><th>Restock / threshold</th></tr></thead><tbody>" + body + "</tbody></table></div>"
|
|
3525
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">On hand</th><th scope=\"col\" class=\"num\">Held</th><th scope=\"col\" class=\"num\">Available</th><th scope=\"col\">Restock / threshold</th></tr></thead><tbody>" + body + "</tbody></table></div>"
|
|
3364
3526
|
: "<p class=\"empty\">No inventory rows" + (opts.low ? " below threshold" : " yet") + ".</p>";
|
|
3365
3527
|
|
|
3366
3528
|
var createForm =
|
|
@@ -3402,7 +3564,7 @@ function renderAdminSubscriptionPlans(opts) {
|
|
|
3402
3564
|
var price = pricing.format(p.amount_minor, String(p.currency || "").toUpperCase()) + " / " + every;
|
|
3403
3565
|
var isActive = p.active === 1 || p.active === true;
|
|
3404
3566
|
var archiveCell = isActive
|
|
3405
|
-
? "<form method=\"post\" action=\"/admin/subscription-plans/" + _htmlEscape(p.id) + "/archive\" class=\"form-inline\">" +
|
|
3567
|
+
? "<form method=\"post\" action=\"/admin/subscription-plans/" + _htmlEscape(p.id) + "/archive/confirm\" class=\"form-inline\">" +
|
|
3406
3568
|
"<button class=\"btn btn--ghost\" type=\"submit\">Archive</button></form>"
|
|
3407
3569
|
: "<span class=\"meta\">—</span>";
|
|
3408
3570
|
return "<tr>" +
|
|
@@ -3416,7 +3578,7 @@ function renderAdminSubscriptionPlans(opts) {
|
|
|
3416
3578
|
}).join("");
|
|
3417
3579
|
|
|
3418
3580
|
var table = rows.length
|
|
3419
|
-
? "<div class=\"panel\"><table><thead><tr><th>Price / interval</th><th>Stripe price</th><th>Variant</th><th>Status</th><th>Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3581
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Price / interval</th><th scope=\"col\">Stripe price</th><th scope=\"col\">Variant</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3420
3582
|
: "<p class=\"empty\">No subscription plans" + (af === "0" ? " archived" : af === "1" ? " active" : " yet") + ".</p>";
|
|
3421
3583
|
|
|
3422
3584
|
var intervalOpts = ["month", "year", "week", "day"].map(function (iv) {
|
|
@@ -3474,7 +3636,7 @@ function renderAdminGiftCards(opts) {
|
|
|
3474
3636
|
var issued = pricing.format(gc.issued_minor, cur);
|
|
3475
3637
|
var remaining = pricing.format(gc.balance_minor, cur);
|
|
3476
3638
|
return "<tr>" +
|
|
3477
|
-
"<
|
|
3639
|
+
"<th scope=\"row\"><code class=\"order-id\">••••" + _htmlEscape(gc.code_hint) + "</code></th>" +
|
|
3478
3640
|
"<td class=\"num\">" + _htmlEscape(issued) + "</td>" +
|
|
3479
3641
|
"<td class=\"num\"><strong>" + _htmlEscape(remaining) + "</strong></td>" +
|
|
3480
3642
|
"<td>" + _pill(gc.status) + "</td>" +
|
|
@@ -3484,7 +3646,7 @@ function renderAdminGiftCards(opts) {
|
|
|
3484
3646
|
}).join("");
|
|
3485
3647
|
|
|
3486
3648
|
var table = rows.length
|
|
3487
|
-
? "<div class=\"panel\"><table><thead><tr><th>Code</th><th class=\"num\">Issued</th><th class=\"num\">Remaining</th><th>Status</th><th>Issued on</th><th>Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3649
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\" class=\"num\">Issued</th><th scope=\"col\" class=\"num\">Remaining</th><th scope=\"col\">Status</th><th scope=\"col\">Issued on</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3488
3650
|
: "<p class=\"empty\">No gift cards" + (sf ? " " + _htmlEscape(sf) : " yet") + ".</p>";
|
|
3489
3651
|
|
|
3490
3652
|
// The issue form composes the giftcards primitive's issue() — the
|
|
@@ -3549,7 +3711,7 @@ function renderAdminGiftCard(opts) {
|
|
|
3549
3711
|
"</tr>";
|
|
3550
3712
|
}).join("");
|
|
3551
3713
|
var ledgerTable = ledger.length
|
|
3552
|
-
? "<div class=\"panel\"><table><thead><tr><th>Type</th><th class=\"num\">Amount</th><th class=\"num\">Balance after</th><th>Detail</th><th>When</th></tr></thead><tbody>" + ledgerRows + "</tbody></table></div>"
|
|
3714
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Type</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Detail</th><th scope=\"col\">When</th></tr></thead><tbody>" + ledgerRows + "</tbody></table></div>"
|
|
3553
3715
|
: "<p class=\"empty\">No ledger transactions recorded for this card yet.</p>";
|
|
3554
3716
|
|
|
3555
3717
|
var body = "<section><h2>Gift card</h2>" + issuedBanner + summary +
|
|
@@ -3579,12 +3741,12 @@ function renderAdminWebhooks(opts) {
|
|
|
3579
3741
|
"<td>" +
|
|
3580
3742
|
"<a class=\"btn btn--ghost\" href=\"/admin/webhooks/" + _htmlEscape(e.id) + "/deliveries\">Deliveries</a> " +
|
|
3581
3743
|
"<form method=\"post\" action=\"/admin/webhooks/" + _htmlEscape(e.id) + "/toggle\" class=\"form-inline\"><button class=\"btn btn--ghost\" type=\"submit\">" + (isActive ? "Disable" : "Enable") + "</button></form> " +
|
|
3582
|
-
"<form method=\"post\" action=\"/admin/webhooks/" + _htmlEscape(e.id) + "/delete\" class=\"form-inline\"><button class=\"btn btn--danger\" type=\"submit\">Delete</button></form>" +
|
|
3744
|
+
"<form method=\"post\" action=\"/admin/webhooks/" + _htmlEscape(e.id) + "/delete/confirm\" class=\"form-inline\"><button class=\"btn btn--danger\" type=\"submit\">Delete</button></form>" +
|
|
3583
3745
|
"</td></tr>";
|
|
3584
3746
|
}).join("");
|
|
3585
3747
|
|
|
3586
3748
|
var table = rows.length
|
|
3587
|
-
? "<div class=\"panel\"><table><thead><tr><th>URL</th><th>Events</th><th>Status</th><th class=\"num\">Rate</th><th>Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3749
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">URL</th><th scope=\"col\">Events</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3588
3750
|
: "<p class=\"empty\">No webhook endpoints yet.</p>";
|
|
3589
3751
|
|
|
3590
3752
|
var eventChecks = known.map(function (ev) {
|
|
@@ -3661,7 +3823,7 @@ function renderAdminWebhookDeliveries(opts) {
|
|
|
3661
3823
|
}).join("");
|
|
3662
3824
|
|
|
3663
3825
|
var table = rows.length
|
|
3664
|
-
? "<div class=\"panel\"><table><thead><tr><th>Event</th><th>Status</th><th class=\"num\">Code</th><th class=\"num\">Attempts</th><th>Last error</th><th>Created</th><th></th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3826
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Event</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Code</th><th scope=\"col\" class=\"num\">Attempts</th><th scope=\"col\">Last error</th><th scope=\"col\">Created</th><th scope=\"col\"></th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3665
3827
|
: "<p class=\"empty\">No deliveries recorded for this endpoint yet.</p>";
|
|
3666
3828
|
|
|
3667
3829
|
var head = "<p class=\"meta\">Endpoint <code class=\"order-id\">" + _htmlEscape(e.url) + "</code></p>";
|
|
@@ -3705,7 +3867,7 @@ function renderAdminCollections(opts) {
|
|
|
3705
3867
|
}).join("");
|
|
3706
3868
|
|
|
3707
3869
|
var table = rows.length
|
|
3708
|
-
? "<div class=\"panel\"><table><thead><tr><th>Title</th><th>Slug</th><th>Type</th><th>Status</th><th class=\"num\">Products</th><th>Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3870
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Type</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Products</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
3709
3871
|
: "<p class=\"empty\">No collections" + (af === "0" ? " archived" : af === "1" ? " active" : " yet") + ".</p>";
|
|
3710
3872
|
|
|
3711
3873
|
// The create form toggles between a manual and a smart shape. Manual
|
|
@@ -3821,7 +3983,7 @@ function renderAdminCollection(opts) {
|
|
|
3821
3983
|
"</tr>";
|
|
3822
3984
|
}).join("");
|
|
3823
3985
|
var memberTable = members.length
|
|
3824
|
-
? "<div class=\"panel\"><table><thead><tr><th class=\"num\">#</th><th>Product id</th><th></th></tr></thead><tbody>" + memberRows + "</tbody></table></div>"
|
|
3986
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\" class=\"num\">#</th><th scope=\"col\">Product id</th><th scope=\"col\"></th></tr></thead><tbody>" + memberRows + "</tbody></table></div>"
|
|
3825
3987
|
: "<p class=\"empty\">No members yet — add a product below.</p>";
|
|
3826
3988
|
|
|
3827
3989
|
// Reorder: a single field of the current ids, comma-joined, that the
|
|
@@ -3861,7 +4023,7 @@ function renderAdminCollection(opts) {
|
|
|
3861
4023
|
"</tr>";
|
|
3862
4024
|
}).join("");
|
|
3863
4025
|
var previewTable = preview.length
|
|
3864
|
-
? "<div class=\"panel\"><table><thead><tr><th>Title</th><th>Slug</th><th>Status</th></tr></thead><tbody>" + previewCards + "</tbody></table></div>"
|
|
4026
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th></tr></thead><tbody>" + previewCards + "</tbody></table></div>"
|
|
3865
4027
|
: "<p class=\"empty\">No products match these rules yet.</p>";
|
|
3866
4028
|
detailBody = "<section class=\"mt\"><h3 class=\"fs-105\">Matched products (live preview)</h3>" +
|
|
3867
4029
|
"<p class=\"meta\">The first " + _htmlEscape(String(preview.length)) + " products the rules match right now.</p>" +
|
|
@@ -3901,4 +4063,5 @@ module.exports = {
|
|
|
3901
4063
|
renderAdminCollection: renderAdminCollection,
|
|
3902
4064
|
renderAdminGiftCards: renderAdminGiftCards,
|
|
3903
4065
|
renderAdminGiftCard: renderAdminGiftCard,
|
|
4066
|
+
renderAdminConfirm: renderAdminConfirm,
|
|
3904
4067
|
};
|
package/lib/asset-manifest.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.2.0",
|
|
3
3
|
"assets": {
|
|
4
4
|
"css/admin.css": {
|
|
5
|
-
"integrity": "sha384-
|
|
6
|
-
"fingerprinted": "css/admin.
|
|
5
|
+
"integrity": "sha384-oUi6NkXA0ULSy8+8+LG0FV6jsgr7Y11Xf3VdnwUESnvUPaN7anYEC+QOAUXwgsap",
|
|
6
|
+
"fingerprinted": "css/admin.7b692a8965624a0c.css"
|
|
7
7
|
},
|
|
8
8
|
"css/main.css": {
|
|
9
|
-
"integrity": "sha384-
|
|
10
|
-
"fingerprinted": "css/main.
|
|
9
|
+
"integrity": "sha384-4RH2PIC1FVxNFfphco1poLi1gMXa8MkvmOK8tXLSSwUKfhbGi0VYXlWtWBXu4Msd",
|
|
10
|
+
"fingerprinted": "css/main.f61829cdefb48dda.css"
|
|
11
11
|
},
|
|
12
12
|
"js/consent.js": {
|
|
13
13
|
"integrity": "sha384-KKMQ0og8HPOykRRPpUyxX7dMhTvKySfVtpGX/jGWzZwNaN/c4OykvRvXpqBHcQST",
|