[object Object]

← back to Dw Daily Catchup 20260909

Fail closed on malformed activation fields

c4f1747624c2d4fa9a27931a9b01e177358b60cf · 2026-08-28 21:40:54 -0700 · Steve Abrams

Files touched

Diff

commit c4f1747624c2d4fa9a27931a9b01e177358b60cf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 21:40:54 2026 -0700

    Fail closed on malformed activation fields
---
 lib/five-field-extra.js       | 16 +++++++++++++---
 test/five-field-extra.test.js | 25 ++++++++++++++++++++++++-
 verification/e2e-proof.json   |  8 +++++---
 3 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/lib/five-field-extra.js b/lib/five-field-extra.js
index 87548f2..6fd9940 100644
--- a/lib/five-field-extra.js
+++ b/lib/five-field-extra.js
@@ -1,13 +1,23 @@
 'use strict';
 
 function fiveFieldExtra(product) {
-  const variants = product?.variants?.nodes || [];
-  const tags = product?.tags || [];
+  const hasValidVariantsShape = Array.isArray(product?.variants?.nodes);
+  const hasValidTagsShape = Array.isArray(product?.tags);
+  const variants = hasValidVariantsShape ? product.variants.nodes : [];
+  const tags = hasValidTagsShape
+    ? product.tags.map((tag) => String(tag).trim().toLowerCase())
+    : [];
   const hasSample = variants.some((variant) => /-sample$/i.test(variant.sku || ''));
   const sellable = variants.filter((variant) => !/-sample$/i.test(variant.sku || ''));
-  const hasSellablePriced = sellable.some((variant) => Number.parseFloat(variant.price) > 0);
+  const hasSellablePriced = sellable.some((variant) => {
+    if (typeof variant.price !== 'string' || !/^\d+(?:\.\d+)?$/.test(variant.price)) return false;
+    const price = Number(variant.price);
+    return Number.isFinite(price) && price > 0;
+  });
   const isQuoteOnly = tags.includes('quotes');
   const reasons = [];
+  if (!hasValidVariantsShape) reasons.push('invalid-variants-shape');
+  if (!hasValidTagsShape) reasons.push('invalid-tags-shape');
   if (!hasSample) reasons.push('no-sample-variant');
   if (!sellable.length && !isQuoteOnly) reasons.push('no-sellable-variant');
   if (!hasSellablePriced && !isQuoteOnly) reasons.push('sellable-price-not-gt-0');
diff --git a/test/five-field-extra.test.js b/test/five-field-extra.test.js
index be1ee1b..8e68d11 100644
--- a/test/five-field-extra.test.js
+++ b/test/five-field-extra.test.js
@@ -18,7 +18,7 @@ test('fails closed without a sample', () => {
 });
 
 test('rejects missing, zero, malformed, and negative sellable prices', () => {
-  for (const price of [undefined, '', '0', 'not-a-price', '-1']) {
+  for (const price of [undefined, '', '0', 'not-a-price', '-1', '125 USD', '1e309']) {
     const result = fiveFieldExtra(product([
       { sku: 'DW-100-SAMPLE', price: '5.00' }, { sku: 'DW-100', price },
     ]));
@@ -27,6 +27,29 @@ test('rejects missing, zero, malformed, and negative sellable prices', () => {
   }
 });
 
+test('fails closed on non-array variants and tags with explicit shape reasons', () => {
+  const variants = [{ sku: 'DW-100-SAMPLE', price: '5.00' }, { sku: 'DW-100', price: '125.00' }];
+  const tagsString = fiveFieldExtra({ variants: { nodes: variants }, tags: 'wallpaper' });
+  assert.equal(tagsString.ok, false);
+  assert.ok(tagsString.reasons.includes('invalid-tags-shape'));
+
+  const quoteString = fiveFieldExtra({ variants: { nodes: [variants[0]] }, tags: 'quotes' });
+  assert.equal(quoteString.ok, false);
+  assert.ok(quoteString.reasons.includes('invalid-tags-shape'));
+  assert.ok(quoteString.reasons.includes('no-sellable-variant'));
+  assert.ok(quoteString.reasons.includes('sellable-price-not-gt-0'));
+
+  const variantsString = fiveFieldExtra({ variants: { nodes: 'not-an-array' }, tags: ['quotes', 'wallpaper'] });
+  assert.equal(variantsString.ok, false);
+  assert.ok(variantsString.reasons.includes('invalid-variants-shape'));
+});
+
+test('normalizes valid tag arrays before applying the quote exemption', () => {
+  assert.deepEqual(fiveFieldExtra(product([
+    { sku: 'DW-QUOTE-SAMPLE', price: '5.00' },
+  ], [' Quotes ', 'Wallpaper'])), { ok: true, reasons: [] });
+});
+
 test('requires at least two tags', () => {
   assert.deepEqual(fiveFieldExtra(product([
     { sku: 'DW-100-SAMPLE', price: '5.00' }, { sku: 'DW-100', price: '125.00' },
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 308cfff..94399bd 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -2,7 +2,7 @@
   "intent": "Use one fail-closed five-field invariant check in every local product activation lane before any ACTIVE mutation.",
   "risk_tier": "R1 isolated code; live Shopify activation is R4 and was intentionally not invoked",
   "environment": "local macOS workspace; zero-network tests",
-  "timestamp": "2026-08-29T04:37:05Z",
+  "timestamp": "2026-08-29T04:43:00Z",
   "ticket": "TK-10947-unify-five-field-activation-checks-acros",
   "precondition": "rotate-activate.js had the full extra invariant set; cmo-activate.js and mdc-activate.js duplicated only sample/tag checks.",
   "checks": [
@@ -10,7 +10,7 @@
       "verdict": "PASS",
       "boundary": "pure validation",
       "command": "node --test test/five-field-extra.test.js",
-      "assertions": "5/5 pass: normal pass, missing sample rejection, invalid price rejection, tag rejection, quote-only exemption boundaries"
+      "assertions": "7/7 pass: normal pass, missing sample rejection, strict finite-decimal prices, explicit invalid array-shape holds, tag rejection, normalized quote-only exemption boundaries"
     },
     {
       "verdict": "PASS",
@@ -33,7 +33,9 @@
   ],
   "negative_checks": [
     "missing sample",
-    "missing, blank, zero, malformed, and negative sellable price",
+    "missing, blank, zero, malformed, negative, suffixed, and non-finite sellable price",
+    "non-array variants.nodes and tags shapes",
+    "string tags cannot trigger count or quote-only exemptions",
     "fewer than two tags",
     "quote-only product without sample"
   ],

← ee136fc Unify pre-activation five-field checks  ·  back to Dw Daily Catchup 20260909  ·  rotation-activator: block $4.25 sample-leak price from passi d9e84e1 →