@blamejs/blamejs-shop 0.4.8 → 0.4.9

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.9 (2026-06-05) — **Scheduled jobs actually run now, the cart's discount-code entry renders, and low-stock alerts reach the admin console.** Three shipped features were unreachable in a deployed store because the production composition never connected them. The worker's scheduled ticks and inventory events carry no browser headers, so the bot guard rejected every one of them before the shared-secret check ran — cart recovery, back-in-stock scans, wishlist alerts and digests, abandoned-checkout reaping, and customer-portal expiry never executed on a deployment fronted by the bundled worker. The cart page's discount-code entry and the low-stock alert pipeline were similarly disconnected: each renders or mounts only when the server hands the backing engine to the surface, and the server never did. All three are wired now, and the boot test gains a full-composition pass that fails the build when a gated surface stops reaching the wire. **Added:** *Boot test proves the full composition on the wire* — The integration boot test now boots `server.js` twice: once bare (the container health-check contract, unchanged) and once against an in-memory stand-in for the worker's SQL bridge, which mounts the full catalog + cart + storefront + admin composition exactly as a deployment runs it. The bridged pass adds to cart through the production middleware stack, applies a discount code end to end (the entry renders, the code resolves, the applied chip persists), proves the worker-shaped scheduled tick reaches its handler, and drives a real stock mutation — an admin restock on a SKU under its threshold — into a persisted low-stock alert row without ever touching the intake endpoint directly. A feature gated on a dependency the server forgets to hand over now fails this gate instead of shipping invisible — the failure mode behind all three fixes above. **Fixed:** *Internal scheduled and event endpoints are no longer rejected by the bot guard* — The worker calls the container's internal endpoints (`/_/cart-recovery-tick`, `/_/stock-alert-sweep`, `/_/wishlist-alerts-sweep`, `/_/wishlist-digest-sweep`, `/_/customer-portal-expire`, `/_/stale-order-reap`, `/_/low-stock-alert`) with no browser fingerprint, and the bot guard's missing-header heuristic returned 403 before each handler's constant-time shared-secret check ever ran. Because the worker fires these calls without reading the response, the failures were invisible: on a deployment fronted by the bundled worker, none of these jobs had ever executed — abandoned checkouts kept their stock holds, recovery and wishlist passes never ran, and expired portal grants were never cleaned. These paths now skip the browser-fingerprint heuristics only; every one of them still refuses callers without the shared bridge secret, and `/_/health` keeps its bot-guard coverage unchanged. · *The cart page's discount-code entry renders* — The "Have a discount code?" block and its `POST /cart/coupon` / `POST /cart/coupon/remove` routes mount only when the storefront receives the discount engine, and the server never passed it — so the entry introduced in 0.3.69 was invisible in a deployed store even though checkout already honoured codes submitted through its own field. The engine is now threaded to the storefront: shoppers can apply and remove codes on the cart, and the applied code carries through the same quote-and-confirm path checkout always used. · *Low-stock alerts flow end to end* — Crossing a SKU's low-stock threshold produced no alert, for two independent reasons. The catalog's stock-mutating operations — the checkout hold, a release, a decrement, an admin restock — expose an observer hook for exactly this, and the server never connected it, so the threshold check simply never ran. And the worker-side path (the inventory lock posting the alert to the container) had no `/_/low-stock-alert` endpoint to receive it, so even a directly-posted alert was dropped: no `inventory_alerts` row, no `inventory.low_stock` webhook, and a `/admin/inventory/alerts` screen that never mounted. Both halves are wired now: every stock mutation that leaves a SKU under its configured threshold writes the alert row, fans out the webhook through the shared dispatcher, and emits the warning log line; the intake endpoint exists (secret-gated, like the other internal endpoints); and the admin alert-history screen is reachable. · *Stray editor artifact removed from release-notes/* — A temporary file accidentally committed alongside the 0.3.74 notes is gone from the repository.
12
+
11
13
  - v0.4.8 (2026-06-05) — **Concurrent order status changes are race-safe: a cancelled order can't be revived and a freed stock hold can't be settled twice.** An order's status change read the current state, decided the next one, then wrote it back. Under two writers racing the same order — a payment confirmation arriving as the stale-checkout sweeper cancels it, or an operator marking it paid while a provider webhook lands — both could act on the same starting state, so the later write silently overwrote the earlier one. The write now carries the state it was decided from, so it only lands while the order is still in that state; the writer that loses the race applies nothing and reports the order as the winner left it. This closes a window where the sweeper could free an order's stock hold and the payment could then still confirm — overselling the last unit — and where a settled hold could be released or debited a second time. The common card path was already shielded by a pre-cancel check against the payment provider; this protects the paths that aren't (express wallets and PayPal, a store running without the card provider configured, an operator marking an order paid by hand, and a shopper cancelling as a webhook arrives). **Fixed:** *Order status transitions are serialized by a conditional claim* — A status transition now writes with the condition that the order is still in the state the transition was computed from, so concurrent transitions on one order resolve to exactly one winner. The loser matches no row and becomes a no-op: it writes no status change, records no transition entry, and runs no inventory settlement — preventing a stock hold from being released or debited twice and preventing a terminal order from being moved back to an active state. An uncontended transition behaves exactly as before. This mirrors the existing claim used when attaching a payment intent to a pending order. · *Stock holds can no longer be double-settled across the cancel and capture paths* — When the abandoned-checkout sweeper cancels a pending order at the same moment its payment confirms, only one edge now settles the hold: the cancel frees it or the capture debits it, never both. Previously the two could interleave so a hold was released and the same order still reached paid, overselling the unit. The fix covers the paths the provider-side pre-cancel check does not — express-wallet and PayPal orders, deployments without the card provider configured, an operator marking an order paid from the console, and a shopper cancelling while a webhook is in flight.
12
14
 
13
15
  - v0.4.7 (2026-06-05) — **Payment page: Trusted Types now permits Stripe's loader through a real allowlist, and the Amazon Pay button loads.** Two payment-page fixes. The site enforces Trusted Types, and Stripe's browser library assigns script URLs without registering a policy — the console logged a TrustedScriptURL violation on every payment page, the failure class that can break a 3-D Secure challenge mid-payment. Rather than relaxing enforcement, the page now registers the default Trusted Types policy before Stripe loads: it admits script URLs from Stripe's own origins and throws for anything else, so enforcement stays on with a genuine gate. Second, the express-wallet row offered an Amazon Pay button whose frame the payment page's policy refused to load; the page's frame allowance now includes the Stripe-operated CDN that serves wallet buttons, scoped to the payment page only. Every other page keeps the strict policy unchanged. **Fixed:** *Trusted Types policy for the Stripe loader* — The payment page registers the default Trusted Types policy before the Stripe script tag, synchronously, integrity-pinned like every other script. The policy admits script URLs whose origin is js.stripe.com or a subdomain of it and throws for any other host — lookalike domains, plain http, and non-Stripe CDNs are refused. Browsers without Trusted Types are unaffected. Pages outside the payment route keep require-trusted-types-for enforcement with no registered loader policy, exactly as before. · *Express wallet buttons can load their frames* — The payment page's frame allowance adds Stripe's wallet-button CDN (b.stripecdn.com) and Stripe's dynamic subdomains (*.js.stripe.com) alongside the existing js.stripe.com and hooks.stripe.com entries, restoring the Amazon Pay express button. The allowance is scoped to the payment page; the storefront-wide policy still permits no third-party frames at all.
package/README.md CHANGED
@@ -98,7 +98,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
98
98
  | **`lib/giftcards.js`** | Prepaid bearer gift cards. `issue({ amount_minor, currency })` generates a 16-char code (32-glyph alphabet, no ambiguous letters) via `b.crypto.generateBytes`, stores only its `namespaceHash` digest + a 4-char hint, and returns the plaintext code once. `balance(code)` / `lookup(code)` resolve a code to its live balance (constant-time hash compare); `redeem({ code, order_id, amount_minor })` decrements the balance with an atomic `balance >= amount` SQL guard so concurrent spends can't overdraw. Redeemed at checkout as a credit against the order grand total: the amount due drops by the applied balance (never below zero), the order still records the full total it owed, and the debit is recorded once per order — a card that fully covers the order is marked paid with no Stripe charge. Customers check a balance at `GET /gift-cards`; the page is not a code-existence oracle (unknown / malformed / expired all return the same generic not-found). |
99
99
  | **`lib/gift-card-ledger.js`** | Append-only credit / debit / expire history per gift card, with a denormalized `balance_after_minor` snapshot for O(1) balance reads. `credit` / `debit` / `expire` write one row each; `history(id)` paginates a card's transactions; `transactionsForOrder(id)` lists a card's movements for one order. The audit trail behind the admin gift-card ledger console; overdraft is refused at the primitive layer. |
100
100
  | **`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. |
101
- | **`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), with a persistent nav across every signed-in page. A guided **setup wizard** at `/admin/setup` writes shop identity to config; **Products** (`/admin/products`) browses the catalog and creates / archives / restores, and each product opens a management screen that edits its fields, adds / edits / removes variants, sets a variant's price and shows its price history, and attaches / uploads / removes images — the full path to a sellable product; **Inventory** (`/admin/inventory`) lists stock per SKU (on-hand / held / available) with a low-stock filter, restocks, sets per-SKU thresholds, and tracks new SKUs; **Orders** (`/admin/orders`) lists recent orders with status filters, opens an order's items, totals, and shipping address, and drives the lifecycle (mark paid → fulfil → ship → deliver, cancel — Refund goes through the payment provider) through the order FSM, with a rate-bounded resend of the order confirmation to an operator-supplied address (the buyer's email is stored only as a hash, so the operator types the recipient), and attaches a shipment (carrier + tracking number) with recorded shipment events that surface a public tracking link to the customer, plus a customer-service notes thread per order (internal or customer-visible, pinnable, resolvable); **Customers** (`/admin/customers`) is a read-only roster, newest first — display name, short id, join date, sign-in method (passkey count + linked OAuth providers), and order count, with the count and sign-in methods resolved by bounded aggregate queries so a page of customers costs no per-row trips (email addresses aren't stored in the clear, so they're not shown); each customer opens to an aggregated activity timeline (orders, loyalty, wishlist, reviews, support) read from the tables those primitives already populate; customer segments export their members as a streamed CSV — id, display name, join date, order count, deliberately no email column; **Returns** (`/admin/returns`) is the RMA moderation queue — filter by status, open a request's items and reason, and approve (with refund amount) → mark received → refund, or reject with a reason, over the return FSM; **Reviews** (`/admin/reviews`) is the review moderation queue — filter by status and publish, reject (with a reason), or take down each submission inline; **Q&A** (`/admin/questions`) is the question moderation queue — filter by status, open a question to its full answer thread, approve / reject the question, post the seller answer, and approve / reject / pin individual answers; **Subscriptions** (`/admin/subscription-plans`) is the recurring-offer catalog — filter active / archived, create a plan (Stripe price id, interval, amount, trial), and archive one, with archiving terminal because the mirrored Stripe price can go stale; **Collections** (`/admin/collections`) manages manual + smart product collections — filter active / archived, create a collection (manual or smart with a starter rule), and per collection edit title / description / sort strategy, manage manual members (add by product id, remove, reorder) or edit a smart collection's rule set with a live preview of the products the rules currently match, and archive; **Gift cards** (`/admin/gift-cards`) is the gift-card ledger — list issued cards (masked code, original + remaining balance, status, issued date) filtered by lifecycle status, issue a new card (the bearer code shown once, right after creation), open a card to see its full credit / debit / expire ledger, and void an active card through a confirmation step; **Webhooks** (`/admin/webhooks`) registers outbound endpoints (https:// only) with a one-time signing-secret reveal, enables / disables / deletes them, and opens an endpoint's delivery feed to retry a failed delivery — the signing secret is shown once on create and never in the list, and order transitions fan out signed deliveries to subscribed endpoints. **Tax** (`/admin/tax-rates`), **Shipping** (`/admin/shipping`), and **Discounts** (`/admin/discounts`) configure tax rates per jurisdiction, shipping zones + rates, and automatic-discount rules — including code-unlocked rules a shopper redeems with a discount code on the cart page — + coupon-stacking policies — create / edit / archive each. **Audit** (`/admin/audit`) is a read-only activity log of every privileged action — filtered by outcome (success / failure / denied) and paginated — composed on the framework's tamper-evident `b.audit` chain; opening it is itself recorded as an `audit.read` event. **Errors** (`/admin/errors`) lists captured server-error detail — time, status, route, and a truncated message for scrubbed 500-class failures (checkout confirm, public API, admin actions) — newest-first, with the same path answering a bearer-token request with JSON so the log is one `curl` away. **Carts** (`/admin/carts`) lists abandoned carts — active, has items, idle past a tunable window (24h default) — with line counts, value at risk, and guest/signed-in attribution; a per-cart action mints a single-use, code-gated discount the operator shares through their own channel (recovery email is impossible by design: buyer addresses are stored only as hashes, and the screen says so). **Analytics** (`/admin/analytics`) is the pre-purchase view the sales report can't see — browse-to-buy funnel with conversion rate, top search terms, most-viewed products, units-ranked top SKUs, and a revenue-by-day sparkline — cross-linked with the Reports screen, read-only, every aggregate window- and limit-bounded. The Customers, Returns, Reviews, Q&A, Subscriptions, Collections, Gift cards, Webhooks, Tax, Shipping, Discounts, Delivery estimates, Analytics, Carts, and Errors links appear only when those primitives are wired. Each console path content-negotiates: a bearer-token client still gets the JSON API unchanged, a signed-in browser gets HTML. Reachable by the cookie or the bearer token. The console's styling is an external, integrity-pinned stylesheet (`themes/default/assets/css/admin.css`) with the same self-hosted typeface — no inline styles and no third-party font host, so it renders correctly under the strict `style-src 'self'` / `font-src 'self'` CSP that governs the route. |
101
+ | **`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), with a persistent nav across every signed-in page. A guided **setup wizard** at `/admin/setup` writes shop identity to config; **Products** (`/admin/products`) browses the catalog and creates / archives / restores, and each product opens a management screen that edits its fields, adds / edits / removes variants, sets a variant's price and shows its price history, and attaches / uploads / removes images — the full path to a sellable product; **Inventory** (`/admin/inventory`) lists stock per SKU (on-hand / held / available) with a low-stock filter, restocks, sets per-SKU thresholds, tracks new SKUs, and opens the low-stock alert history (`/admin/inventory/alerts`) — each alert row is written when a checkout decrement crosses a SKU's threshold, alongside an `inventory.low_stock` webhook to subscribed endpoints; **Orders** (`/admin/orders`) lists recent orders with status filters, opens an order's items, totals, and shipping address, and drives the lifecycle (mark paid → fulfil → ship → deliver, cancel — Refund goes through the payment provider) through the order FSM, with a rate-bounded resend of the order confirmation to an operator-supplied address (the buyer's email is stored only as a hash, so the operator types the recipient), and attaches a shipment (carrier + tracking number) with recorded shipment events that surface a public tracking link to the customer, plus a customer-service notes thread per order (internal or customer-visible, pinnable, resolvable); **Customers** (`/admin/customers`) is a read-only roster, newest first — display name, short id, join date, sign-in method (passkey count + linked OAuth providers), and order count, with the count and sign-in methods resolved by bounded aggregate queries so a page of customers costs no per-row trips (email addresses aren't stored in the clear, so they're not shown); each customer opens to an aggregated activity timeline (orders, loyalty, wishlist, reviews, support) read from the tables those primitives already populate; customer segments export their members as a streamed CSV — id, display name, join date, order count, deliberately no email column; **Returns** (`/admin/returns`) is the RMA moderation queue — filter by status, open a request's items and reason, and approve (with refund amount) → mark received → refund, or reject with a reason, over the return FSM; **Reviews** (`/admin/reviews`) is the review moderation queue — filter by status and publish, reject (with a reason), or take down each submission inline; **Q&A** (`/admin/questions`) is the question moderation queue — filter by status, open a question to its full answer thread, approve / reject the question, post the seller answer, and approve / reject / pin individual answers; **Subscriptions** (`/admin/subscription-plans`) is the recurring-offer catalog — filter active / archived, create a plan (Stripe price id, interval, amount, trial), and archive one, with archiving terminal because the mirrored Stripe price can go stale; **Collections** (`/admin/collections`) manages manual + smart product collections — filter active / archived, create a collection (manual or smart with a starter rule), and per collection edit title / description / sort strategy, manage manual members (add by product id, remove, reorder) or edit a smart collection's rule set with a live preview of the products the rules currently match, and archive; **Gift cards** (`/admin/gift-cards`) is the gift-card ledger — list issued cards (masked code, original + remaining balance, status, issued date) filtered by lifecycle status, issue a new card (the bearer code shown once, right after creation), open a card to see its full credit / debit / expire ledger, and void an active card through a confirmation step; **Webhooks** (`/admin/webhooks`) registers outbound endpoints (https:// only) with a one-time signing-secret reveal, enables / disables / deletes them, and opens an endpoint's delivery feed to retry a failed delivery — the signing secret is shown once on create and never in the list, and order transitions fan out signed deliveries to subscribed endpoints. **Tax** (`/admin/tax-rates`), **Shipping** (`/admin/shipping`), and **Discounts** (`/admin/discounts`) configure tax rates per jurisdiction, shipping zones + rates, and automatic-discount rules — including code-unlocked rules a shopper redeems with a discount code on the cart page — + coupon-stacking policies — create / edit / archive each. **Audit** (`/admin/audit`) is a read-only activity log of every privileged action — filtered by outcome (success / failure / denied) and paginated — composed on the framework's tamper-evident `b.audit` chain; opening it is itself recorded as an `audit.read` event. **Errors** (`/admin/errors`) lists captured server-error detail — time, status, route, and a truncated message for scrubbed 500-class failures (checkout confirm, public API, admin actions) — newest-first, with the same path answering a bearer-token request with JSON so the log is one `curl` away. **Carts** (`/admin/carts`) lists abandoned carts — active, has items, idle past a tunable window (24h default) — with line counts, value at risk, and guest/signed-in attribution; a per-cart action mints a single-use, code-gated discount the operator shares through their own channel (recovery email is impossible by design: buyer addresses are stored only as hashes, and the screen says so). **Analytics** (`/admin/analytics`) is the pre-purchase view the sales report can't see — browse-to-buy funnel with conversion rate, top search terms, most-viewed products, units-ranked top SKUs, and a revenue-by-day sparkline — cross-linked with the Reports screen, read-only, every aggregate window- and limit-bounded. The Customers, Returns, Reviews, Q&A, Subscriptions, Collections, Gift cards, Webhooks, Tax, Shipping, Discounts, Delivery estimates, Analytics, Carts, and Errors links appear only when those primitives are wired. Each console path content-negotiates: a bearer-token client still gets the JSON API unchanged, a signed-in browser gets HTML. Reachable by the cookie or the bearer token. The console's styling is an external, integrity-pinned stylesheet (`themes/default/assets/css/admin.css`) with the same self-hosted typeface — no inline styles and no third-party font host, so it renders correctly under the strict `style-src 'self'` / `font-src 'self'` CSP that governs the route. |
102
102
  | **`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. |
103
103
  | **`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. |
104
104
 
package/SECURITY.md CHANGED
@@ -125,10 +125,16 @@ node -e "
125
125
  reaches D1 through a Worker service binding. The bridge is gated by
126
126
  a shared-secret header (`X-D1-Bridge-Secret`) so the route only
127
127
  accepts SQL from the bound container even if the binding is
128
- accidentally exposed. Generate 32 bytes of OS randomness and set
129
- the value identically on the Worker (`wrangler secret put
130
- D1_BRIDGE_SECRET`) and the container env. Rotate quarterly or after
131
- any Worker-credential compromise.
128
+ accidentally exposed. The same secret gates the container's internal
129
+ scheduled/event endpoints (`/_/cart-recovery-tick`,
130
+ `/_/stock-alert-sweep`, `/_/low-stock-alert`, and the other `/_/`
131
+ ticks): those paths skip the bot guard's browser-fingerprint
132
+ heuristics — the Worker's machine-to-machine calls carry no browser
133
+ headers — so the constant-time secret check is the deciding gate,
134
+ and an unauthenticated caller gets a 401. Generate ≥ 32 bytes of OS
135
+ randomness and set the value identically on the Worker (`wrangler
136
+ secret put D1_BRIDGE_SECRET`) and the container env. Rotate
137
+ quarterly or after any Worker-credential compromise.
132
138
  - **Referrer-Policy is load-bearing for CSRF.** The app sends
133
139
  `Referrer-Policy: same-origin` on both the edge and the container.
134
140
  Browsers serialize the `Origin` header as `null` on same-origin
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.8",
2
+ "version": "0.4.9",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
@@ -69,6 +69,31 @@ var WEBHOOK_PATHS = ["/api/webhooks/stripe", "/api/webhooks/paypal"];
69
69
  // could wedge the health signal.
70
70
  var HEALTH_PATH = "/_/health";
71
71
 
72
+ // Worker→container internal endpoints — machine-to-machine POSTs over
73
+ // the Cloudflare service binding (cron ticks + the InventoryLock DO's
74
+ // low-stock event), each authenticated FIRST thing in its handler by a
75
+ // constant-time check of the shared D1_BRIDGE_SECRET header. The
76
+ // worker's fetch() carries no browser fingerprint (no User-Agent, no
77
+ // Accept-Language), so bot-guard's missing-Accept-Language heuristic
78
+ // 403s every one of these calls before the handler's own — strictly
79
+ // stronger — secret gate ever runs, and it does so silently: the
80
+ // worker fires them under ctx.waitUntil / fire-and-forget and reads
81
+ // nothing back. Skipping bot-guard here does not weaken the surface
82
+ // (an unauthenticated caller still gets the handler's 401); it makes
83
+ // the shared-secret gate the deciding check, which is the design.
84
+ // HEALTH_PATH is deliberately NOT in this list — the Docker
85
+ // HEALTHCHECK probe is browser-shaped by contract and bot-guard stays
86
+ // on for it.
87
+ var INTERNAL_BRIDGE_PATHS = [
88
+ "/_/cart-recovery-tick",
89
+ "/_/stock-alert-sweep",
90
+ "/_/low-stock-alert",
91
+ "/_/wishlist-alerts-sweep",
92
+ "/_/wishlist-digest-sweep",
93
+ "/_/customer-portal-expire",
94
+ "/_/stale-order-reap",
95
+ ];
96
+
72
97
  // The abusable endpoints — POST / auth surfaces where a human does
73
98
  // well under five requests a minute. Each gets its own per-client-IP
74
99
  // budget so a spray against one can't borrow another's headroom, and a
@@ -528,6 +553,21 @@ function globalRateLimitOpts() {
528
553
  };
529
554
  }
530
555
 
556
+ /**
557
+ * createApp `middleware.botGuard` opts — the vendored defaults (block
558
+ * mode, the automation-UA deny list, the missing-Accept-Language
559
+ * heuristic) plus the internal-bridge skip list. Bot-guard is a
560
+ * heuristic for human-facing surfaces; the worker→container endpoints
561
+ * are machine-to-machine and authenticate with the shared
562
+ * D1_BRIDGE_SECRET, so on those paths the secret gate — not a browser
563
+ * fingerprint — is the deciding check (see INTERNAL_BRIDGE_PATHS).
564
+ */
565
+ function botGuardOpts() {
566
+ return {
567
+ skipPaths: INTERNAL_BRIDGE_PATHS.slice(),
568
+ };
569
+ }
570
+
531
571
  function _hasPrefix(pathname, prefixes) {
532
572
  for (var i = 0; i < prefixes.length; i += 1) {
533
573
  if (pathname.indexOf(prefixes[i]) === 0) return true;
@@ -648,6 +688,7 @@ module.exports = {
648
688
  clientKey: clientKey,
649
689
  securityHeadersOpts: securityHeadersOpts,
650
690
  globalRateLimitOpts: globalRateLimitOpts,
691
+ botGuardOpts: botGuardOpts,
651
692
  mountRouteGuards: mountRouteGuards,
652
693
  scopedCsp: scopedCsp,
653
694
  scopedPermissionsPolicy: scopedPermissionsPolicy,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {