@blamejs/blamejs-shop 0.0.51 → 0.0.53

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,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.0.x
10
10
 
11
+ - v0.0.53 (2026-05-22) — **Docs refresh, sample catalog tripled, designed empty-cart card, worker hardening.** Four surfaces refresh in one release. `SECURITY.md` + `CONTRIBUTING.md` + `docs/deploy-cloudflare.md` cover the scoped package name + custom domain + the full 0001-0010 migration table. The sample catalog grows from 4 products to 12 (Operator Hoodie, Vault Stick, Signing Cable, Build Pass, Audit Log Kit, Self-Hosted Plan, Operator Mug, Sticker Pack — each with its own brand-coloured SVG hero). The empty-cart row gains a designed `cart-empty` card with the brand 🛒 icon, eyebrow + title + lede + dual CTAs (`Browse products` primary, `Find a specific product` ghost linking the header search). The Worker gets a `/robots.txt` edge fallback (so crawlers get a clean answer even during container cold-start), the warming-up page picks up `noindex`/`canonical`/`aria-live`/refresh tuned to `Sec-Fetch-Mode`, and a new `/_/version` probe surfaces deploy state. **Added:** *Sample catalog grows from 4 to 12 products with brand-coloured SVG heroes* — `scripts/sample-product-images/{operator-hoodie,vault-stick,signing-cable,build-pass,audit-log-kit,self-hosted-plan,operator-mug,sticker-pack}.svg` ship as the next round of reference imagery. Apparel SVGs use dark-ink gradients with accent-orange illustrations; hardware uses navy-to-blue gradients with grey/black device silhouettes + accent-orange chip highlights; digital uses purple-to-navy with credential-artifact motifs carrying a visible PQC / ML-DSA seal; bundles use deep-orange or green with stacked-object compositions. All 800×800 viewBox, system font stack, monospace SKU at bottom-right. `scripts/seed-sample-products.sql` + `scripts/seed-sample-product-media.sql` extend with the matching catalog + media rows (UUIDv7 ids continue the existing numbering 0005-000c). · *Designed empty-cart card* — `renderCart` emits a `.cart-empty` section instead of a bare `<td colspan>` row when there are no lines. The card carries the brand emoji in a dashed-border circle, `Cart` eyebrow, `Your cart is empty` headline, a lede that explains how the cart holds the add-time price, and dual CTAs — `Browse products →` (primary, → `/`) + `Find a specific product` (ghost, anchors `#site-search-q` for header-search focus without inline JS). Populated cart gains a `Shop / Cart` breadcrumb above the section head, matching the PDP's pattern. · *Worker `/robots.txt` edge fallback* — Even during container cold-start, the Worker now serves a minimal `User-agent: *\nAllow: /\nSitemap: https://blamejs.shop/sitemap.xml\n` directly at the edge with a 1h cache. Crawlers never see the warming page for robots probes. R2-uploaded `/assets/robots.txt` overrides still win for operators with a custom policy. · *Worker `/_/version` deploy probe* — Operator-friendly diagnostic endpoint returning `{ worker, container_image, time }`. No auth required (pure read-only probe); use it to verify a deploy reached the edge before sending traffic. · *`CONTRIBUTING.md`* — 210 lines covering: dev environment setup (clone → vendor-update → smoke), the release workflow (release-notes JSON → CHANGELOG rebuild → branch → PR → admin merge → tag → publish), code conventions (CommonJS, `var`, zero npm runtime deps, compose `b.*` primitives, vendored tree read-only, security defaults non-opt-in, PQC-first), how to run + write tests (smoke + layer-0/1/2, `waitUntil` over `setTimeout`), the explicit list of artifacts each publish produces, and a pointer to `SECURITY.md` for vuln reporting. **Changed:** *Warming-up page — `noindex`, canonical, ARIA-live, refresh tuned to navigation mode* — The cold-start fallback page picks up `<meta name="robots" content="noindex, nofollow">` + a canonical link so crawlers don't index the placeholder. The auto-refresh interval shifts based on `Sec-Fetch-Mode` — 5 seconds for `navigate` requests (real visitor in a browser tab), 8 seconds for non-navigation fetches (XHR/fetch/Stripe.js probes that shouldn't spam the container). A new `aria-live="polite"` region announces the warming state to screen readers. · *`SECURITY.md` — Stripe webhook clause updated to match shipped code* — The container's defense-in-depth verification clause used a placeholder phrase from before `lib/payment.js` shipped. Now describes the actual `b.webhook.verify` call (alg `hmac-sha256-stripe`) running inside `lib/payment.js` before any FSM transition. Every other audit point matched the workflow output as-is — SLSA L3 provenance, Sigstore-keyless SBOM signatures, SHA-256 + SHA3-512 digests, ML-DSA-65 PQC sidecar. · *`docs/deploy-cloudflare.md` — custom-domain section + 0001-0010 migration table + demo-seed step* — Adds a `Wire a custom domain` section (zone-add → Worker custom-domain bind → `wrangler.toml` route alternative → TLS verify). Replaces the bare-paragraph migration mention with the explicit 0001-0010 table (calling out the intentional `0007` gap from the abandoned discounts work). Adds a `Seed demo content` section that runs both seed SQL files via `wrangler d1 execute --remote`. Switches the existing migration-apply step to `--remote` for clarity.
12
+
13
+ - v0.0.52 (2026-05-22) — **Live deploy moves to the custom domain — README + wrangler config point at https://blamejs.shop.** The reference deploy now serves at `https://blamejs.shop` instead of the `*.workers.dev` subdomain. README header now reads 'Homepage: **https://blamejs.shop**' and the admin-API curl example uses a placeholder `your-shop.example.com` host since operators replace it with their own. `wrangler.toml#D1_BRIDGE_URL` switches to `https://blamejs.shop` so the container's externalDb adapter calls back through the canonical origin. **Changed:** *README header — 'Homepage' instead of 'Live demo', custom domain* — `Homepage: **https://blamejs.shop**` replaces the previous `Live demo: **https://blamejs-shop.coocoo.workers.dev/**`. Operators evaluating the framework now see the canonical address. The admin-API curl example in the operator quick-start drops the placeholder `<your-worker>.workers.dev` host in favour of a `your-shop.example.com` placeholder with a comment pointing at the reference deploy. · *`wrangler.toml#D1_BRIDGE_URL` → `https://blamejs.shop`* — The container's externalDb D1 adapter posts SQL to `<D1_BRIDGE_URL><D1_BRIDGE_PATH>` (the Worker's service-binding bridge endpoint). Updating the base URL routes those internal calls through the custom domain. Cloudflare resolves custom domains to the same Worker that serves `*.workers.dev`, so the bridge keeps working with no router change.
14
+
11
15
  - v0.0.51 (2026-05-22) — **SEO crawl surfaces — `/robots.txt` + `/sitemap.xml` listing every active product.** Crawlers landing on the live shop had no discovery surface — every product page was reachable only by being linked from the home grid, and there was no robots.txt to direct the crawl. This release wires `/robots.txt` (allow-everything-except-session-scoped, pointing at the sitemap) and `/sitemap.xml` (lists the home + /admin + every `status='active'` product with its `updated_at` lastmod, capped at 1000 rows). The XML is hand-rolled — no node:xml dep — with attribute-escaped values so a slug containing `&` or `<` can't break out of the document. **Added:** *`GET /robots.txt`* — `User-agent: *` + `Allow: /` for crawlable surfaces. Disallow paths cover the session-scoped + operator-only surfaces (`/admin`, `/cart`, `/checkout`, `/pay/`, `/orders/`, `/account`) — none of those have crawl value and exposing them risks crawlers tripping rate limits on the cart-bound DO. `Sitemap:` directive resolves from the request `Host` header so the same handler serves the right absolute URL for `localhost:8080` (dev), `blamejs-shop.coocoo.workers.dev` (live), or any custom domain. `Cache-Control: public, max-age=3600`. Operators with stricter requirements override by uploading a `robots.txt` key to R2 — the Worker's static-asset bridge serves R2 keys ahead of the storefront router, so the bucket file wins. · *`GET /sitemap.xml`* — Lists the home page (priority 1.0, daily changefreq), the `/admin` landing (priority 0.3, monthly changefreq), and every `status='active'` product (priority 0.8, weekly changefreq) with the product's `updated_at` or `created_at` as `lastmod` (ISO date). Capped at 1000 rows — the sitemap spec allows 50,000 but a shop at that scale should pre-segment into a sitemap index. The XML is hand-rolled, attribute-escape applies `&`, `<`, `>`, `"`, `'` so an operator-supplied slug can't break the document. `Cache-Control: public, max-age=300` so a catalog update propagates inside five minutes without an operator action.
12
16
 
13
17
  - v0.0.50 (2026-05-22) — **OpenGraph + Twitter Card meta tags — shared links unfurl with proper previews.** Pages rendered without `og:*` or `twitter:*` tags. Sharing the live URL in Slack / iMessage / Twitter / Discord landed a bare URL with no preview, no image, no description. This release wires OpenGraph + Twitter Card meta tags into the LAYOUT head with sensible site-level defaults (brand logo + shop-level lede). The PDP overrides them with product-specific values — `og:title` is the product title, `og:description` is the catalog `description`, `og:image` is the first attached media URL — so a product share renders with the actual hero image + product copy. **Added:** *OpenGraph + Twitter Card tags in `LAYOUT`* — `<meta property="og:type">`, `og:site_name`, `og:title`, `og:description`, `og:image`, `og:url` + the Twitter Card analogues (`twitter:card=summary_large_image`, `twitter:title`, `twitter:description`, `twitter:image`). All templated through `{{og_*}}` placeholders so the `_render` HTML-escape protects against operator-supplied content breaking out of the attribute boundary. `<meta name="description">` also lands so non-OG crawlers see the same description. · *Default OG values on `_wrap(opts)`* — Every page renders with sensible defaults: `og:type=website`, `og:title=<page title> — <shop name>`, `og:description=Open-source ecommerce framework built on blamejs. Server-rendered HTML, post-quantum crypto, zero npm runtime dependencies.`, `og:image=/assets/brand/logo.png`. Per-renderer overrides via `opts.og_*` work surgically — only the field passed in overrides; the rest stay on defaults. · *Product-specific OG on the PDP* — `renderProduct` overrides four OG fields: `og_type=product`, `og_title=<product.title> — <shop_name>`, `og_description=<product.description>` (or a generated fallback when the description is empty), `og_image=<assetPrefix><heroMedia.r2_key>` (the first attached media row). A PDP share now renders the SVG hero + the product copy, not the brand logo + a generic shop description.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  Open-source ecommerce framework built on [blamejs](https://github.com/blamejs/blamejs). Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.
10
10
 
11
- Live demo: **https://blamejs-shop.coocoo.workers.dev/**
11
+ Homepage: **https://blamejs.shop**
12
12
 
13
13
  ## Requirements
14
14
 
@@ -121,7 +121,9 @@ npx wrangler d1 migrations apply blamejs-shop --remote
121
121
  npx wrangler deploy
122
122
 
123
123
  # 5. Seed a product via the admin API
124
- curl -X POST https://<your-worker>.workers.dev/admin/products \
124
+ # (replace the host with your own worker route or custom domain;
125
+ # the reference deploy lives at https://blamejs.shop)
126
+ curl -X POST https://your-shop.example.com/admin/products \
125
127
  -H "Authorization: Bearer $ADMIN_API_KEY" \
126
128
  -H "Content-Type: application/json" \
127
129
  -d '{"slug":"first","title":"First product","status":"active"}'
package/SECURITY.md CHANGED
@@ -123,9 +123,11 @@ node -e "
123
123
  - **Stripe webhook signature.** Inbound `POST` to
124
124
  `/api/webhooks/stripe` is signature-verified at the Worker edge
125
125
  (HMAC-SHA256 over `<timestamp>.<body>`, 5-minute tolerance window)
126
- before forwarding to the container. The container also re-verifies
127
- defense-in-depth once the payment primitive lands. An unsigned or
128
- out-of-window delivery never touches origin resources.
126
+ before forwarding to the container. The container re-verifies the
127
+ same signature defense-in-depth via `b.webhook.verify` (alg
128
+ `hmac-sha256-stripe`) inside `lib/payment.js` before any FSM
129
+ transition runs. An unsigned or out-of-window delivery never
130
+ touches origin resources.
129
131
  - **Worker → Container trust boundary.** The Worker treats the
130
132
  container as untrusted-for-D1: it never proxies arbitrary headers
131
133
  into D1, only the explicit `sql` + `params` from the bridge body.
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.giftcards
4
+ * @title Gift cards primitive — issue and redeem prepaid balance
5
+ *
6
+ * @intro
7
+ * A gift card is a bearer credential. Whoever knows the plaintext
8
+ * code can spend the balance — so the shop never stores it. On
9
+ * `issue` we generate a 16-character alphanumeric code from
10
+ * `b.crypto.generateBytes`, store the
11
+ * `b.crypto.namespaceHash("giftcard-code", plaintext)` digest, and
12
+ * return the plaintext exactly once. The issuer is responsible for
13
+ * delivering it to the recipient (email, paper insert, etc.).
14
+ *
15
+ * The `code_hint` is the last 4 plaintext characters — useful when
16
+ * an operator triaging a support request needs to identify which
17
+ * row the customer is asking about. Four characters of a 32-letter
18
+ * alphabet is 2^20 ≈ one-in-a-million space, far too small to
19
+ * enable brute-force recovery of the remaining 12.
20
+ *
21
+ * Recipients without an account are addressed by an email-hash
22
+ * (`b.crypto.namespaceHash("giftcard-recipient", email)`) so a
23
+ * stolen D1 dump leaks no recipient addresses while still letting
24
+ * the storefront resolve "this address owns these cards" after the
25
+ * recipient registers.
26
+ *
27
+ * Composition:
28
+ * var gc = bShop.giftcards.create({ query: q });
29
+ * var { id, code, code_hint } = await gc.issue({
30
+ * amount_minor: 5000, currency: "USD", issued_to_email: "alice@example.com",
31
+ * });
32
+ * // deliver `code` to the recipient. Never readable again.
33
+ * var view = await gc.balance(code);
34
+ * var { remaining_balance_minor, redemption_id } =
35
+ * await gc.redeem({ code: code, order_id: orderId, amount_minor: 2500 });
36
+ *
37
+ * Display formatting (`XXXX-XXXX-XXXX-XXXX`) is purely cosmetic —
38
+ * redemption + balance + lookup all strip hyphens (and ASCII
39
+ * whitespace) before hashing, so a customer who types the dashes
40
+ * back in works without special handling.
41
+ */
42
+
43
+ var bShop;
44
+ function _b() {
45
+ if (!bShop) bShop = require("./index");
46
+ return bShop.framework;
47
+ }
48
+
49
+ var CODE_NAMESPACE = "giftcard-code";
50
+ var RECIPIENT_NAMESPACE = "giftcard-recipient";
51
+
52
+ // Alphabet excludes 0/O/I/1 so a code spoken aloud / read off a
53
+ // printed insert doesn't collapse into ambiguous characters. 32
54
+ // glyphs means each byte modulo-32 lands on a uniform draw (256 is a
55
+ // multiple of 32 — no modulo-bias correction needed).
56
+ var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
57
+ var CODE_LEN = 16;
58
+ var CODE_HINT_LEN = 4;
59
+ var CODE_ALPHABET_RE = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]+$/;
60
+
61
+ var CURRENCY_RE = /^[A-Z]{3}$/;
62
+ var STATUSES = ["active", "redeemed", "expired", "voided"];
63
+
64
+ // ---- validators ---------------------------------------------------------
65
+
66
+ function _uuid(s, label) {
67
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
68
+ catch (e) { throw new TypeError("giftcards: " + label + " — " + (e && e.message || "invalid UUID")); }
69
+ }
70
+
71
+ function _amountMinor(n, label) {
72
+ if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
73
+ throw new TypeError("giftcards: " + label + " must be a positive integer (minor units)");
74
+ }
75
+ return n;
76
+ }
77
+
78
+ function _currency(c) {
79
+ // ISO 4217 alpha-3, uppercase. Matches the storage CHECK
80
+ // (length(currency) = 3) and the operator-facing convention.
81
+ if (typeof c !== "string" || !CURRENCY_RE.test(c)) {
82
+ throw new TypeError("giftcards: currency must be 3-letter uppercase ISO 4217");
83
+ }
84
+ return c;
85
+ }
86
+
87
+ function _status(s) {
88
+ if (typeof s !== "string" || STATUSES.indexOf(s) === -1) {
89
+ throw new TypeError("giftcards: status must be one of " + STATUSES.join(", "));
90
+ }
91
+ return s;
92
+ }
93
+
94
+ function _expiresAt(ts) {
95
+ if (ts == null) return null;
96
+ if (typeof ts !== "number" || !Number.isInteger(ts) || ts <= 0) {
97
+ throw new TypeError("giftcards: expires_at must be a positive integer epoch-ms or null");
98
+ }
99
+ return ts;
100
+ }
101
+
102
+ function _now() { return Date.now(); }
103
+
104
+ // ---- code generation + canonicalization ---------------------------------
105
+
106
+ // Map random bytes to the 32-character alphabet. 256 % 32 === 0 so
107
+ // each modulo lands on a uniform alphabet index — no rejection
108
+ // sampling needed. Routes through `b.crypto.generateBytes` (SHAKE256
109
+ // over OS-RNG) for defense-in-depth over the bare OS RNG.
110
+ function _generateCode() {
111
+ var buf = _b().crypto.generateBytes(CODE_LEN);
112
+ var out = "";
113
+ for (var i = 0; i < CODE_LEN; i += 1) {
114
+ out += CODE_ALPHABET.charAt(buf[i] & 31);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ // Display form: XXXX-XXXX-XXXX-XXXX. Pure cosmetic — the hash
120
+ // derivation runs on the canonicalized (hyphen-stripped) form.
121
+ function _formatCode(plain) {
122
+ return plain.slice(0, 4) + "-" + plain.slice(4, 8) + "-" + plain.slice(8, 12) + "-" + plain.slice(12, 16);
123
+ }
124
+
125
+ // Strip hyphens + ASCII whitespace so a customer who types the
126
+ // dashes (or pastes with a trailing newline) works without special
127
+ // handling. Returns the canonical 16-character uppercase code, or
128
+ // throws if the result isn't a well-formed alphabet draw.
129
+ function _canonicalCode(input) {
130
+ if (typeof input !== "string" || !input.length) {
131
+ throw new TypeError("giftcards: code must be a non-empty string");
132
+ }
133
+ // Operator-facing affordance: tolerate hyphens + ASCII whitespace
134
+ // anywhere. Anything else (including unicode whitespace, control
135
+ // bytes, or out-of-alphabet glyphs) is a refusal — we don't want a
136
+ // sloppy normalizer to map two distinct codes to the same hash.
137
+ var stripped = input.replace(/[-\s]+/g, "").toUpperCase();
138
+ if (stripped.length !== CODE_LEN) {
139
+ throw new TypeError("giftcards: code must be " + CODE_LEN + " alphabet characters (hyphens optional)");
140
+ }
141
+ if (!CODE_ALPHABET_RE.test(stripped)) {
142
+ throw new TypeError("giftcards: code contains characters outside the gift-card alphabet");
143
+ }
144
+ return stripped;
145
+ }
146
+
147
+ function _hashCode(canonical) {
148
+ return _b().crypto.namespaceHash(CODE_NAMESPACE, canonical);
149
+ }
150
+
151
+ function _hashRecipient(email) {
152
+ // Recipient email is a free-form operator-supplied string at this
153
+ // tier; the storefront route layer is responsible for guardEmail
154
+ // validation before issuing the card. Here we only enforce
155
+ // shape-tier: non-empty, no control bytes (the namespaceHash
156
+ // primitive refuses CR/LF itself).
157
+ if (typeof email !== "string" || !email.length) {
158
+ throw new TypeError("giftcards: issued_to_email must be a non-empty string when provided");
159
+ }
160
+ // Lowercase the address before hashing so two casings of the same
161
+ // recipient collide on lookup. Local-part case sensitivity (RFC
162
+ // 5321) is operator-irrelevant for gift-card delivery — operators
163
+ // address the human, not the mailbox.
164
+ return _b().crypto.namespaceHash(RECIPIENT_NAMESPACE, email.toLowerCase());
165
+ }
166
+
167
+ // ---- factory ------------------------------------------------------------
168
+
169
+ function create(opts) {
170
+ opts = opts || {};
171
+ var query = opts.query;
172
+ if (!query) {
173
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
174
+ }
175
+
176
+ // `lookup` resolves a plaintext code to the live card row. Returns
177
+ // null on no match. The hash compare itself is constant-time
178
+ // because we go through `b.crypto.namespaceHash` (SHA3-512
179
+ // deterministic) and then SQL = comparison on the hex hash — an
180
+ // attacker who can time the query can't distinguish "no row" from
181
+ // "wrong hash" because both paths execute the same query.
182
+ // We additionally route the hex compare through
183
+ // `b.crypto.timingSafeEqual` for the returned row so a future
184
+ // refactor that adds non-constant-time matching can't slip in.
185
+ async function _lookup(plaintextCode) {
186
+ var canonical = _canonicalCode(plaintextCode);
187
+ var hash = _hashCode(canonical);
188
+ var r = await query(
189
+ "SELECT id, code_hash, balance_minor, currency, status, expires_at " +
190
+ "FROM giftcards WHERE code_hash = ?1",
191
+ [hash],
192
+ );
193
+ if (!r.rows.length) return null;
194
+ var row = r.rows[0];
195
+ // Belt-and-braces: the SQL = already matched, but route the hex
196
+ // strings through timingSafeEqual so the equality check leaves no
197
+ // micro-timing oracle in case a future schema change moves to a
198
+ // collection scan.
199
+ if (!_b().crypto.timingSafeEqual(row.code_hash, hash)) return null;
200
+ return {
201
+ id: row.id,
202
+ balance_minor: row.balance_minor,
203
+ currency: row.currency,
204
+ status: row.status,
205
+ expires_at: row.expires_at,
206
+ };
207
+ }
208
+
209
+ return {
210
+ CODE_NAMESPACE: CODE_NAMESPACE,
211
+ RECIPIENT_NAMESPACE: RECIPIENT_NAMESPACE,
212
+ CODE_ALPHABET: CODE_ALPHABET,
213
+ CODE_LEN: CODE_LEN,
214
+ STATUSES: STATUSES,
215
+
216
+ issue: async function (input) {
217
+ if (!input || typeof input !== "object") {
218
+ throw new TypeError("giftcards.issue: input object required");
219
+ }
220
+ _amountMinor(input.amount_minor, "amount_minor");
221
+ _currency(input.currency);
222
+ var expiresAt = _expiresAt(input.expires_at);
223
+
224
+ var issuedToCustomerId = null;
225
+ if (input.issued_to_customer_id != null) {
226
+ issuedToCustomerId = _uuid(input.issued_to_customer_id, "issued_to_customer_id");
227
+ }
228
+ var issuedToEmailHash = null;
229
+ if (input.issued_to_email != null) {
230
+ issuedToEmailHash = _hashRecipient(input.issued_to_email);
231
+ }
232
+
233
+ // Allow neither, either, or both. A purely operator-issued card
234
+ // (promotion / refund credit) has no recipient identity at all.
235
+
236
+ var id = _b().uuid.v7();
237
+ var code = _generateCode();
238
+ var hash = _hashCode(code);
239
+ var hint = code.slice(CODE_LEN - CODE_HINT_LEN);
240
+ var ts = _now();
241
+
242
+ await query(
243
+ "INSERT INTO giftcards (id, code_hash, code_hint, currency, issued_minor, balance_minor, " +
244
+ "issued_to_customer_id, issued_to_email_hash, expires_at, status, created_at, updated_at) " +
245
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?5, ?6, ?7, ?8, 'active', ?9, ?9)",
246
+ [
247
+ id, hash, hint, input.currency, input.amount_minor,
248
+ issuedToCustomerId, issuedToEmailHash, expiresAt, ts,
249
+ ],
250
+ );
251
+
252
+ // `code` is returned plaintext exactly ONCE. The issuer
253
+ // delivers it. Subsequent reads against this row only ever see
254
+ // the hash + hint.
255
+ return {
256
+ id: id,
257
+ code: _formatCode(code),
258
+ code_hint: hint,
259
+ };
260
+ },
261
+
262
+ // Public helper — returns null on no-match (constant-time at the
263
+ // hash layer; see `_lookup`).
264
+ lookup: function (plaintextCode) {
265
+ return _lookup(plaintextCode);
266
+ },
267
+
268
+ balance: async function (plaintextCode) {
269
+ var row = await _lookup(plaintextCode);
270
+ if (!row) return null;
271
+ return {
272
+ balance_minor: row.balance_minor,
273
+ currency: row.currency,
274
+ status: row.status,
275
+ expires_at: row.expires_at,
276
+ };
277
+ },
278
+
279
+ redeem: async function (input) {
280
+ if (!input || typeof input !== "object") {
281
+ throw new TypeError("giftcards.redeem: input object required");
282
+ }
283
+ _amountMinor(input.amount_minor, "amount_minor");
284
+ var orderId = null;
285
+ if (input.order_id != null) orderId = _uuid(input.order_id, "order_id");
286
+
287
+ var row = await _lookup(input.code);
288
+ if (!row) {
289
+ var miss = new Error("giftcards.redeem: code not recognized");
290
+ miss.code = "GIFTCARD_NOT_FOUND";
291
+ throw miss;
292
+ }
293
+ if (row.status !== "active") {
294
+ var inactive = new Error("giftcards.redeem: card is " + row.status);
295
+ inactive.code = "GIFTCARD_NOT_ACTIVE";
296
+ throw inactive;
297
+ }
298
+ var ts = _now();
299
+ if (row.expires_at != null && row.expires_at <= ts) {
300
+ // Expired-but-still-flagged-active: lazily transition the
301
+ // row so future reads reflect reality, then refuse this
302
+ // redemption. The transition itself is idempotent — every
303
+ // `redeem`/`balance` caller does the same check.
304
+ await query(
305
+ "UPDATE giftcards SET status = 'expired', updated_at = ?1 WHERE id = ?2 AND status = 'active'",
306
+ [ts, row.id],
307
+ );
308
+ var exp = new Error("giftcards.redeem: card is expired");
309
+ exp.code = "GIFTCARD_EXPIRED";
310
+ throw exp;
311
+ }
312
+ if (input.amount_minor > row.balance_minor) {
313
+ var ins = new Error("giftcards.redeem: amount exceeds remaining balance");
314
+ ins.code = "GIFTCARD_INSUFFICIENT_BALANCE";
315
+ throw ins;
316
+ }
317
+
318
+ // Atomic decrement guarded by a balance check at the SQL tier
319
+ // so two concurrent redemptions can't double-spend. The
320
+ // `balance_minor >= ?` predicate plus the row-level lock the
321
+ // UPDATE takes means whichever transaction lands second sees
322
+ // rowCount === 0 and we surface as insufficient.
323
+ var dec = await query(
324
+ "UPDATE giftcards SET balance_minor = balance_minor - ?1, " +
325
+ "status = CASE WHEN balance_minor - ?1 = 0 THEN 'redeemed' ELSE status END, " +
326
+ "updated_at = ?2 WHERE id = ?3 AND balance_minor >= ?1 AND status = 'active'",
327
+ [input.amount_minor, ts, row.id],
328
+ );
329
+ if (dec.rowCount === 0) {
330
+ // Race: another redemption beat us to the balance. Refuse
331
+ // with the same shape as the up-front insufficient check so
332
+ // the caller doesn't have to distinguish "checked then
333
+ // raced" from "always insufficient".
334
+ var raced = new Error("giftcards.redeem: amount exceeds remaining balance");
335
+ raced.code = "GIFTCARD_INSUFFICIENT_BALANCE";
336
+ throw raced;
337
+ }
338
+
339
+ var redemptionId = _b().uuid.v7();
340
+ await query(
341
+ "INSERT INTO giftcard_redemptions (id, giftcard_id, order_id, amount_minor, redeemed_at) " +
342
+ "VALUES (?1, ?2, ?3, ?4, ?5)",
343
+ [redemptionId, row.id, orderId, input.amount_minor, ts],
344
+ );
345
+
346
+ var remaining = row.balance_minor - input.amount_minor;
347
+ return {
348
+ remaining_balance_minor: remaining,
349
+ redemption_id: redemptionId,
350
+ };
351
+ },
352
+
353
+ "void": async function (id, opts2) {
354
+ opts2 = opts2 || {};
355
+ _uuid(id, "giftcard id");
356
+ var r = await query(
357
+ "SELECT id, status FROM giftcards WHERE id = ?1",
358
+ [id],
359
+ );
360
+ if (!r.rows.length) return null;
361
+ var row = r.rows[0];
362
+ if (row.status === "redeemed") {
363
+ var already = new Error("giftcards.void: card is fully redeemed");
364
+ already.code = "GIFTCARD_ALREADY_REDEEMED";
365
+ throw already;
366
+ }
367
+ if (row.status === "voided") {
368
+ // Idempotent — already voided; return the row as-is.
369
+ var existing = await query("SELECT * FROM giftcards WHERE id = ?1", [id]);
370
+ return existing.rows[0] || null;
371
+ }
372
+ var ts = _now();
373
+ await query(
374
+ "UPDATE giftcards SET status = 'voided', updated_at = ?1 WHERE id = ?2",
375
+ [ts, id],
376
+ );
377
+ // `opts2.reason` is operator-supplied free-form; it's not
378
+ // persisted on the row (no schema column) but accepted so a
379
+ // future audit-log primitive can ride alongside without a
380
+ // surface change.
381
+ void opts2.reason;
382
+ var after = await query("SELECT * FROM giftcards WHERE id = ?1", [id]);
383
+ return after.rows[0] || null;
384
+ },
385
+
386
+ listForCustomer: async function (customerId, opts3) {
387
+ _uuid(customerId, "customer_id");
388
+ opts3 = opts3 || {};
389
+ var sql = "SELECT * FROM giftcards WHERE issued_to_customer_id = ?1";
390
+ var params = [customerId];
391
+ if (opts3.status != null) {
392
+ _status(opts3.status);
393
+ sql += " AND status = ?2";
394
+ params.push(opts3.status);
395
+ }
396
+ sql += " ORDER BY created_at DESC";
397
+ var r = await query(sql, params);
398
+ return r.rows;
399
+ },
400
+ };
401
+ }
402
+
403
+ module.exports = {
404
+ create: create,
405
+ CODE_NAMESPACE: CODE_NAMESPACE,
406
+ RECIPIENT_NAMESPACE: RECIPIENT_NAMESPACE,
407
+ CODE_ALPHABET: CODE_ALPHABET,
408
+ CODE_LEN: CODE_LEN,
409
+ STATUSES: STATUSES,
410
+ };
package/lib/index.js CHANGED
@@ -54,4 +54,5 @@ module.exports = {
54
54
  webhooks: require("./webhooks"),
55
55
  analytics: require("./analytics"),
56
56
  inventoryAlerts: require("./inventory-alerts"),
57
+ giftcards: require("./giftcards"),
57
58
  };
package/lib/storefront.js CHANGED
@@ -1085,6 +1085,12 @@ function renderOrder(opts) {
1085
1085
 
1086
1086
  var CART_PAGE =
1087
1087
  "<section class=\"cart-page\">\n" +
1088
+ " <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n" +
1089
+ " <ol>\n" +
1090
+ " <li><a href=\"/\">Shop</a></li>\n" +
1091
+ " <li aria-current=\"page\">Cart</li>\n" +
1092
+ " </ol>\n" +
1093
+ " </nav>\n" +
1088
1094
  " <header class=\"section-head\">\n" +
1089
1095
  " <p class=\"eyebrow\">Your cart</p>\n" +
1090
1096
  " <h1 class=\"section-head__title\">Review your items</h1>\n" +
@@ -1110,6 +1116,28 @@ var CART_PAGE =
1110
1116
  " </div>\n" +
1111
1117
  "</section>\n";
1112
1118
 
1119
+ var CART_EMPTY_PAGE =
1120
+ "<section class=\"cart-page cart-page--empty\">\n" +
1121
+ " <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n" +
1122
+ " <ol>\n" +
1123
+ " <li><a href=\"/\">Shop</a></li>\n" +
1124
+ " <li aria-current=\"page\">Cart</li>\n" +
1125
+ " </ol>\n" +
1126
+ " </nav>\n" +
1127
+ " <div class=\"cart-empty\">\n" +
1128
+ " <div class=\"cart-empty__card\">\n" +
1129
+ " <p class=\"cart-empty__icon\" aria-hidden=\"true\">🛒</p>\n" +
1130
+ " <p class=\"eyebrow cart-empty__eyebrow\">Cart</p>\n" +
1131
+ " <h1 class=\"cart-empty__title\">Your cart is empty</h1>\n" +
1132
+ " <p class=\"cart-empty__lede\">Browse the catalog and the products you add show up here. Items hold their price at add-time, not at checkout.</p>\n" +
1133
+ " <div class=\"cart-empty__cta\">\n" +
1134
+ " <a href=\"/\" class=\"btn-primary\">Browse products <span aria-hidden=\"true\">→</span></a>\n" +
1135
+ " <a href=\"#site-search-q\" class=\"btn-ghost\">Find a specific product</a>\n" +
1136
+ " </div>\n" +
1137
+ " </div>\n" +
1138
+ " </div>\n" +
1139
+ "</section>\n";
1140
+
1113
1141
  function renderCart(opts) {
1114
1142
  if (!opts) throw new TypeError("storefront.renderCart: opts required");
1115
1143
  var lines = opts.lines || [];
@@ -1156,26 +1184,30 @@ function renderCart(opts) {
1156
1184
  return String(s == null ? "" : s)
1157
1185
  .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1158
1186
  }
1159
- var rows = rendered.map(function (l) {
1160
- var thumb = l.image_url
1161
- ? "<span class=\"cart-line__thumb\"><img src=\"" + _escAttr(l.image_url) + "\" alt=\"" + _escAttr(l.image_alt) + "\" loading=\"lazy\"></span>"
1162
- : "<span class=\"cart-line__thumb cart-line__thumb--empty\" aria-hidden=\"true\"></span>";
1163
- return _render(CART_LINE_EDITABLE, {
1164
- sku: l.sku,
1165
- qty: l.qty,
1166
- unit: l.unit,
1167
- total: l.total,
1168
- line_id: l.id,
1169
- product_title: l.product_title,
1170
- product_url: l.product_url,
1171
- }).replace("RAW_CART_LINE_THUMB", thumb);
1172
- }).join("");
1173
- if (!rows) rows = "<tr><td colspan=\"5\" class=\"empty\">Your cart is empty.</td></tr>";
1174
- var body = _render(CART_PAGE, {
1175
- line_rows: "RAW_LINES",
1176
- subtotal: subtotal,
1177
- total: total,
1178
- }).replace("RAW_LINES", rows);
1187
+ var body;
1188
+ if (rendered.length === 0) {
1189
+ body = CART_EMPTY_PAGE;
1190
+ } else {
1191
+ var rows = rendered.map(function (l) {
1192
+ var thumb = l.image_url
1193
+ ? "<span class=\"cart-line__thumb\"><img src=\"" + _escAttr(l.image_url) + "\" alt=\"" + _escAttr(l.image_alt) + "\" loading=\"lazy\"></span>"
1194
+ : "<span class=\"cart-line__thumb cart-line__thumb--empty\" aria-hidden=\"true\"></span>";
1195
+ return _render(CART_LINE_EDITABLE, {
1196
+ sku: l.sku,
1197
+ qty: l.qty,
1198
+ unit: l.unit,
1199
+ total: l.total,
1200
+ line_id: l.id,
1201
+ product_title: l.product_title,
1202
+ product_url: l.product_url,
1203
+ }).replace("RAW_CART_LINE_THUMB", thumb);
1204
+ }).join("");
1205
+ body = _render(CART_PAGE, {
1206
+ line_rows: "RAW_LINES",
1207
+ subtotal: subtotal,
1208
+ total: total,
1209
+ }).replace("RAW_LINES", rows);
1210
+ }
1179
1211
  return _wrap({
1180
1212
  title: "Cart",
1181
1213
  shop_name: shopName,
@@ -3,8 +3,8 @@
3
3
  "_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
4
4
  "packages": {
5
5
  "blamejs": {
6
- "version": "0.12.3",
7
- "tag": "v0.12.3",
6
+ "version": "0.12.4",
7
+ "tag": "v0.12.4",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.12.x
10
10
 
11
+ - v0.12.4 (2026-05-22) — **`SECURITY.md` Watch list — remove stale "framework doesn't ship CMS / S/MIME" entry.** The Watch list bullet claiming `framework does not ship a CMS / S/MIME / PKCS#7 surface today` has been wrong since v0.10.13 — `b.cms.encodeSignedData` / `decode` / `encodeEnvelopedData` / `parseSignedData` shipped then, and `b.mail.crypto.smime.sign` / `verify` / `verifyAll` / `checkCert` shipped under the mail-stack. The Watch list is for CVE classes the framework deliberately doesn't ship a primitive for; CMS no longer fits that shape. Entry removed. **Fixed:** *Watch list no longer claims CMS / S/MIME are unshipped* — `b.cms` exposes RFC 5652 ContentInfo / SignedData / EnvelopedData encode + decode with PQC signer support (ML-DSA-65 per RFC 9909 §5, ML-DSA-87 per RFC 9909 §6, SLH-DSA-SHAKE-256f per RFC 9881). `b.mail.crypto.smime` builds on it for RFC 8551 S/MIME signed + enveloped mail with `checkCert` for X.509 chain validation. The SECURITY.md Watch list entry that pointed operators to external CMS libraries is gone; operators on regulated mail interop reach for the in-framework primitives instead.
12
+
11
13
  - v0.12.3 (2026-05-22) — **README "What ships in the box" backfill — mail-stack listeners + JSCalendar + new postures.** The README's "Communication" + "Compliance regimes" bullets lagged behind the v0.11.24-v0.12.1 ship cadence. Backfilled: `b.mail.send.deliver` (turnkey outbound delivery chain), the four mail-server listeners (mx / submission / imap / jmap), the JMAP EmailSubmission/set reference handler, mail-crypto (CMS + PGP+WKD), the mail-stack agent, `b.calendar` (RFC 8984 JSCalendar substrate with full BY*+BYSETPOS+multi-rule expansion), and the 16 newly-promoted postures from v0.12.1 (`42-cfr-part-2` / `hti-1` / `uscdi-v4` / `irs-1075` / `nist-800-172-r3` / `tlp-2.0` / `soci-au` / `ffiec-cat-2` / `cri-profile-v2.0` / `m-22-09` / `m-22-18` / `nist-800-53-r5-privacy` / `nist-ai-600-1-genai` / `nist-csf-2.0` / `sb-53` / `nyc-ll144-2024`). **Changed:** *Communication section names every mail-stack listener + delivery chain + crypto primitive* — New entries: `b.mail.send.deliver` (MX → MTA-STS → DANE → REQUIRETLS → SMTP → DSN chain), four `b.mail.server.*` listeners, JMAP EmailSubmission reference handler, `b.mail.crypto.cms` + `b.mail.crypto.pgp`, `b.mail.agent` + `b.mailStore`, and `b.calendar` (JSCalendar / iCalendar bridge for JMAP Calendars interop). · *Compliance regimes section lists the 16 v0.12.1 backfilled postures* — New rows organise the additions under three sub-bullets: AI governance adds `nyc-ll144-2024` / `sb-53` / `nist-ai-rmf-1.0` / `nist-ai-600-1-genai` alongside the existing AI-act / NYC-LL144 / Colorado / Illinois entries; a new "Federal / sectoral" row covers `42-cfr-part-2` / `hti-1` / `uscdi-v4` / `irs-1075` / `nist-csf-2.0` / `nist-800-53-r5-privacy` / `nist-800-172-r3` / `m-22-09` / `m-22-18` / `ffiec-cat-2` / `cri-profile-v2.0`; a new "Critical infrastructure / info-sharing" row covers `soci-au` / `tlp-2.0`.
12
14
 
13
15
  - v0.12.2 (2026-05-22) — **Release-process docs point at `scripts/release.js` (the orchestrator shipped in v0.12.0).** `CONTRIBUTING.md` (maintainer section) and `examples/wiki/DEPLOY.md` ("Tag-driven releases") described the old multi-step manual release flow — version bump → commit → push → tag → push tag — without mentioning the v0.12.0 orchestrator. Both docs now point at `node scripts/release.js` as the canonical release mechanism, list the eight idempotent subcommands, and call out the two pre-requisites the script enforces (release-notes JSON + signed-commit config). **Added:** *`scripts/release.js regen` — re-run artifact regeneration mid-flow* — Edits to `release-notes/v<next>.json` after `prepare` (e.g. addressing a Codex finding, fixing a leak-vocabulary refusal) previously required running `node scripts/generate-changelog-entry.js --rebuild` + `scripts/refresh-api-snapshot.js` + `scripts/check-api-snapshot.js` + `scripts/check-changelog-extract.js` manually. The new `regen` subcommand wraps all four into a single idempotent step. Safe to run any time from any branch. The `prepare` phase calls the same shared helper internally so behaviour stays consistent. **Changed:** *`CONTRIBUTING.md` maintainer section names the orchestrator* — The release-process bullet now reads `node scripts/release.js — eight idempotent subcommands (prepare → smoke → commit → push → watch → merge → tag → publish) plus all for a one-shot`. The existing DEPLOY.md link stays as a pointer for the wiki-container side of the same flow. · *`examples/wiki/DEPLOY.md` Tag-driven releases section rewritten* — Replaces the four-bullet manual flow with the orchestrator surface, including the `all` / `all --minor` one-shot, the per-phase subcommands, and the pre-requisites the script enforces (release-notes JSON present, SSH signing config in place). The downstream wiki-image deploy step on the host (pin `docker-compose.prod.yml` + `docker compose pull && up -d`) is unchanged. **Fixed:** *`scripts/release.js` signature verification uses `git verify-commit` as the canonical truth* — The v0.12.0 orchestrator's commit-signature gate parsed `git log -1 --pretty=%h %G? %GS` looking for `G` in the second column. On some platforms the `%G?` format token's `?` character can be eaten by argument resolution, returning empty stdout even when the signature is Good. The fix runs `git verify-commit HEAD` (whose exit code is the canonical signal `required_signatures` GH ruleset enforces) as the primary check; the `%G?` parse stays as a human-readable confirmation but no longer gates the script. Surfaced via dogfooding the orchestrator on this very release. · *`scripts/release.js` Docker bind-mount path handles Windows host paths with spaces* — The `push` phase's gitleaks step bind-mounted the repo root via `-v <path>:/repo`. The previous path transform produced `/C:/Users/...` on Windows, which Docker's `-v src:dst[:mode]` parser interpreted as having three colon-separated fields. Fix: transform `C:\Users\...` to `//c/Users/...` (lowercased drive letter, double-slash prefix — matches Git Bash's `$(pwd)` form Docker Desktop accepts). POSIX hosts pass through unchanged. Operators with Windows paths containing spaces, parentheses, or special characters can now run `node scripts/release.js push` without manual mount fiddling.
@@ -412,7 +412,6 @@ CVE classes the framework tracks but does not currently ship a primitive for —
412
412
  - **AdonisJS multipart-filename → arbitrary-write class** — the framework's `b.fileUpload` routes every multipart filename through `b.guardFilename.gate({ profile: "strict" })` by default (path traversal / null-byte / NTFS ADS / UNC / overlong UTF-8 / Windows reserved names / RTLO bidi). Operators implementing a parallel multipart receiver outside the framework's primitive must wire the same gate.
413
413
  - **fs.realpath symlink-chain Permission Model bypass class** — see "Operator territory" entry above; the framework's symlink defenses live at the application layer (`b.vault` PEM-file read-side + `b.staticServe` realpath gate); operators using Node's experimental Permission Model add it as defense-in-depth, never as the primary symlink-resolution boundary.
414
414
  - **QUIC / HTTP/3 outbound (RFC 9000 / RFC 9001)** — the framework's `b.httpClient` is HTTP/1.1 + HTTP/2 only. QUIC + HTTP/3 are deferred-with-condition: re-open when Node's `--experimental-quic` flag graduates to stable and `node:http3` ships. Until then, operators wanting outbound h3 wire their own client outside the framework (see `lib/http-client.js` header note on the future `kind: "h3"` transport shape). The framework's TLS 1.3 + h2 anti-amplification + flow-control caps remain in force on every other transport. Inbound h3 is similarly deferred; operators terminating h3 at the edge route h2 / h1 to the framework's `b.router`.
415
- - **CMS (RFC 5083 / RFC 5652) + SHAKE-in-CMS (RFC 8702)** — the framework does not ship a CMS / S/MIME / PKCS#7 surface today. Operators integrating S/MIME-encoded mail or PKCS#7-signed payloads route through a separately-firewalled CMS library and pin the SHAKE-256 / SHA3-512 hash identifiers from RFC 8702 §3 / §4 when they set the signing algorithm. CMS support is deferred-with-condition: re-open when operator demand surfaces for S/MIME-encoded mail receivers OR when a regulatory regime mandates CMS-shaped envelope formats. The framework's existing `b.crypto` envelope (XChaCha20-Poly1305 + ML-KEM-1024 + SLH-DSA) covers the at-rest + in-transit shapes operators need today without the CMS legacy surface.
416
415
 
417
416
  ---
418
417
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.12.3",
4
- "createdAt": "2026-05-22T21:50:06.665Z",
3
+ "frameworkVersion": "0.12.4",
4
+ "createdAt": "2026-05-22T23:12:46.997Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.12.4",
4
+ "date": "2026-05-22",
5
+ "headline": "`SECURITY.md` Watch list — remove stale \"framework doesn't ship CMS / S/MIME\" entry",
6
+ "summary": "The Watch list bullet claiming `framework does not ship a CMS / S/MIME / PKCS#7 surface today` has been wrong since v0.10.13 — `b.cms.encodeSignedData` / `decode` / `encodeEnvelopedData` / `parseSignedData` shipped then, and `b.mail.crypto.smime.sign` / `verify` / `verifyAll` / `checkCert` shipped under the mail-stack. The Watch list is for CVE classes the framework deliberately doesn't ship a primitive for; CMS no longer fits that shape. Entry removed.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "Watch list no longer claims CMS / S/MIME are unshipped",
13
+ "body": "`b.cms` exposes RFC 5652 ContentInfo / SignedData / EnvelopedData encode + decode with PQC signer support (ML-DSA-65 per RFC 9909 §5, ML-DSA-87 per RFC 9909 §6, SLH-DSA-SHAKE-256f per RFC 9881). `b.mail.crypto.smime` builds on it for RFC 8551 S/MIME signed + enveloped mail with `checkCert` for X.509 chain validation. The SECURITY.md Watch list entry that pointed operators to external CMS libraries is gone; operators on regulated mail interop reach for the in-framework primitives instead."
14
+ }
15
+ ]
16
+ }
17
+ ],
18
+ "references": []
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
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": {