← back to Designer Wallcoverings
TK-11422 + TK-11418: scope VCC's shared-catalog queries to the requesting vendor
cfff3faae0fe065182d54f208acbe7ac372fd480 · 2026-09-10 12:29:37 -0700 · Steve Abrams
vendor_registry.catalog_table resolves to the SHARED `vendor_catalog` for 29 vendors,
and that table holds rows for 157 distinct vendor_codes. Two endpoints queried it with no
vendor filter:
TK-11422 assign-skus (worse, and nothing was blocking it) — the SELECT of rows missing a
dw_sku was unfiltered, so one authenticated click enumerated every unassigned row in the
whole shared table and stamped each with the REQUESTING vendor's prefix. Measured
2026-09-10: 3,132 rows with an empty dw_sku spanning 31 different vendors. The
`UPDATE ... WHERE id = $2` after it looked scoped but was only ever as scoped as that
SELECT. Simulated after the fix: 3,132 -> each vendor's own rows only (7, 6, 2, 1, 1, 0...).
TK-11418 launch-test — `ORDER BY id DESC LIMIT 1` returned the newest row in the entire
shared table regardless of which vendor the operator selected (today the top 100 rows are
all `justindavid`), and the endpoint then created and activated it on live Shopify under
the selected vendor. Forensics found no evidence it ever fired; it is currently inert only
because Silas (127.0.0.1:9674) is down and STEP 2 short-circuits before the Shopify write.
That is safety by accident — reviving Silas would have armed it.
Both fixes mirror the pattern already in this file at ~:2255 (resolveSharedDiscriminator +
vendorFilter), added to fix the identical hazard in syncVendorCounts and never propagated.
Both FAIL CLOSED: a shared table with no vendor_code/brand/vendor column returns 409 /
skips the vendor rather than operating across vendors.
NOT fixed here (classifier-blocked, tracked on TK-11418): STEP 8 still stamps
`WHERE dw_sku = $2`, which is ambiguous — 1,148 dw_sku values are held by more than one
vendor_code. It needs `id` added to selectCols and the UPDATE scoped by primary key.
Code only. The running pm2 process still has the old code until it is restarted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M DW-Agents/vendor-command-center/server.js
Diff
commit cfff3faae0fe065182d54f208acbe7ac372fd480
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 12:29:37 2026 -0700
TK-11422 + TK-11418: scope VCC's shared-catalog queries to the requesting vendor
vendor_registry.catalog_table resolves to the SHARED `vendor_catalog` for 29 vendors,
and that table holds rows for 157 distinct vendor_codes. Two endpoints queried it with no
vendor filter:
TK-11422 assign-skus (worse, and nothing was blocking it) — the SELECT of rows missing a
dw_sku was unfiltered, so one authenticated click enumerated every unassigned row in the
whole shared table and stamped each with the REQUESTING vendor's prefix. Measured
2026-09-10: 3,132 rows with an empty dw_sku spanning 31 different vendors. The
`UPDATE ... WHERE id = $2` after it looked scoped but was only ever as scoped as that
SELECT. Simulated after the fix: 3,132 -> each vendor's own rows only (7, 6, 2, 1, 1, 0...).
TK-11418 launch-test — `ORDER BY id DESC LIMIT 1` returned the newest row in the entire
shared table regardless of which vendor the operator selected (today the top 100 rows are
all `justindavid`), and the endpoint then created and activated it on live Shopify under
the selected vendor. Forensics found no evidence it ever fired; it is currently inert only
because Silas (127.0.0.1:9674) is down and STEP 2 short-circuits before the Shopify write.
That is safety by accident — reviving Silas would have armed it.
Both fixes mirror the pattern already in this file at ~:2255 (resolveSharedDiscriminator +
vendorFilter), added to fix the identical hazard in syncVendorCounts and never propagated.
Both FAIL CLOSED: a shared table with no vendor_code/brand/vendor column returns 409 /
skips the vendor rather than operating across vendors.
NOT fixed here (classifier-blocked, tracked on TK-11418): STEP 8 still stamps
`WHERE dw_sku = $2`, which is ambiguous — 1,148 dw_sku values are held by more than one
vendor_code. It needs `id` added to selectCols and the UPDATE scoped by primary key.
Code only. The running pm2 process still has the old code until it is restarted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
DW-Agents/vendor-command-center/server.js | 69 ++++++++++++++++++++++++++++++-
1 file changed, 67 insertions(+), 2 deletions(-)
diff --git a/DW-Agents/vendor-command-center/server.js b/DW-Agents/vendor-command-center/server.js
index 5202e540..77933003 100644
--- a/DW-Agents/vendor-command-center/server.js
+++ b/DW-Agents/vendor-command-center/server.js
@@ -909,9 +909,46 @@ app.post('/api/vendors/:code/assign-skus', async (req, res) => {
let nextNum = Math.max(maxCat, maxShop, rangeStart);
if (nextNum < rangeStart) nextNum = rangeStart;
+ // TK-11422: this SELECT MUST be scoped to THIS vendor's rows. catalog_table is the
+ // SHARED `vendor_catalog` for 29 registered vendors, and that table holds rows for 157
+ // distinct vendor_codes. Unfiltered, one call to this endpoint enumerated every
+ // unassigned row in the whole shared table and stamped each with the REQUESTING
+ // vendor's prefix — measured 2026-09-10: 3,132 rows with an empty dw_sku spanning 31
+ // different vendors. The `UPDATE ... WHERE id = $2` below looks scoped but is only ever
+ // as scoped as this query. Same hazard, same fix as syncVendorCounts (~:2255
+ // resolveSharedDiscriminator + vendorFilter) — that fix was never propagated here.
+ let vendorAnd = '';
+ {
+ const { rows: sharers } = await pool.query(
+ 'SELECT COUNT(*)::int AS n FROM vendor_registry WHERE catalog_table = $1',
+ [v.catalog_table]
+ );
+ if ((sharers[0]?.n || 0) > 1) {
+ const { rows: discRows } = await pool.query(
+ "SELECT column_name FROM information_schema.columns WHERE table_name=$1 AND column_name IN ('vendor_code','brand','vendor')",
+ [catTable]
+ );
+ const have = new Set(discRows.map(r => r.column_name));
+ const discCol = have.has('vendor_code') ? 'vendor_code'
+ : have.has('brand') ? 'brand'
+ : have.has('vendor') ? 'vendor' : null;
+ // Fail CLOSED. A shared table with no discriminator cannot be scoped, and minting
+ // one vendor's prefix across other vendors' rows is worse than doing nothing.
+ if (!discCol) {
+ return res.status(409).json({
+ success: false,
+ error: `${catTable} is shared by ${sharers[0].n} vendors and has no vendor_code/brand/vendor column to scope by — refusing to assign SKUs across vendors.`
+ });
+ }
+ const discVal = String((discCol === 'vendor_code' ? v.vendor_code : v.vendor_name) || '').replace(/'/g, "''");
+ // Case-fold both sides: catalog rows carry mixed-case codes (dwdp vs DWDP, grd vs GRD).
+ vendorAnd = ` AND LOWER(${discCol}) = LOWER('${discVal}')`;
+ }
+ }
+
// Get products missing dw_sku, ordered by mfr_sku for consistency
const missing = await pool.query(
- `SELECT id, mfr_sku FROM ${catTable} WHERE dw_sku IS NULL OR dw_sku = '' ORDER BY mfr_sku ASC`
+ `SELECT id, mfr_sku FROM ${catTable} WHERE (dw_sku IS NULL OR dw_sku = '')${vendorAnd} ORDER BY mfr_sku ASC`
);
if (missing.rows.length === 0) {
@@ -1066,6 +1103,34 @@ app.post('/api/launch-test', async (req, res) => {
}
// ── STEP 1: Get newest eligible product ──
+ // TK-11418: scope to THIS vendor. catalog_table is the SHARED `vendor_catalog` for 29
+ // registered vendors, and that table holds rows for 157 distinct vendor_codes. Without
+ // this filter, `ORDER BY id DESC LIMIT 1` returned the newest row in the ENTIRE shared
+ // table regardless of which vendor the operator selected — measured 2026-09-10, the
+ // top 100 rows were all `justindavid` — and STEP 8 then published it to Shopify under
+ // the selected vendor. Fails CLOSED if a shared table has no discriminator column.
+ let vendorAnd = '';
+ {
+ const { rows: sharers } = await pool.query(
+ 'SELECT COUNT(*)::int AS n FROM vendor_registry WHERE catalog_table = $1', [v.catalog_table]
+ );
+ if ((sharers[0]?.n || 0) > 1) {
+ const discCol = colSet.has('brand') ? 'brand' : null;
+ const { rows: dRows } = await pool.query(
+ "SELECT column_name FROM information_schema.columns WHERE table_name=$1 AND column_name IN ('vendor_code','brand','vendor')", [catTable]
+ );
+ const have = new Set(dRows.map(r => r.column_name));
+ const useCol = have.has('vendor_code') ? 'vendor_code'
+ : have.has('brand') ? 'brand'
+ : have.has('vendor') ? 'vendor' : discCol;
+ if (!useCol) {
+ results.push({ vendor: v.vendor_name, status: 'error', reason: `${catTable} is shared by ${sharers[0].n} vendors with no discriminator column — refusing to publish another vendor's product` });
+ continue;
+ }
+ const discVal = String((useCol === 'vendor_code' ? v.vendor_code : v.vendor_name) || '').replace(/'/g, "''");
+ vendorAnd = ` AND LOWER(${useCol}) = LOWER('${discVal}')`;
+ }
+ }
const widthFilter = colSet.has('width') ? "AND width IS NOT NULL AND width <> ''" : '';
const imageFilter = colSet.has('image_url') ? "AND image_url IS NOT NULL AND image_url <> ''" : '';
const colorFilter = colSet.has('color_name') ? "AND color_name IS NOT NULL AND color_name <> ''" : '';
@@ -1096,7 +1161,7 @@ app.post('/api/launch-test', async (req, res) => {
const { rows: products } = await pool.query(
`SELECT ${selectCols.join(', ')}
FROM "${catTable}"
- WHERE dw_sku IS NOT NULL AND shopify_product_id IS NULL
+ WHERE dw_sku IS NOT NULL AND shopify_product_id IS NULL${vendorAnd}
${widthFilter} ${imageFilter} ${colorFilter}
ORDER BY id DESC LIMIT 1`
);
← a8005d1d auto-data-snapshot: 2026-09-10T11:08:25 (1 data files) — mai
·
back to Designer Wallcoverings
·
TK-11400: fix false-success in inventory-set-2026-newest swe e252ac31 →