From 1a95cf109878201e7d7ec3d7059325cebaf890b0 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 16:04:31 +0000 Subject: [PATCH 1/8] TASK22028: phase 15 hash guard consistency hardening slice --- memory-bank/activeContext.md | 42 +++ memory-bank/change-log.md | 49 ++++ memory-bank/progress.md | 62 +++++ memory-bank/refactor-plan-actions-index.md | 38 +++ .../file/createappealcompletemessage_api.js | 4 +- pages/api/file/setupcontainer.js | 20 +- pages/api/file/upload.js | 4 + pages/api/file/uploadsinglefile.js | 5 + tests/phase15/service-behaviour.test.cjs | 245 ++++++++++++++++++ 9 files changed, 458 insertions(+), 11 deletions(-) create mode 100644 tests/phase15/service-behaviour.test.cjs diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 5ae5f9f9..a53b2808 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -458,6 +458,48 @@ - Response-shape contract stability preserved for touched handlers/services. - Remaining risk is primarily external dependency behavior on valid-hash happy paths in local environments (relay/storage), not hash-bypass behavior. +## Phase 15 hardening status (2026-03-13) + +- Working branch created from `origin/SIPS-Development`: + - `TASK22028-phase15-hardening-slice` +- Delivered consistency-only hardening across 4 additional sensitive file handlers: + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/file/setupcontainer.js` +- Hardening posture updates: + - standardized explicit early 400 on missing hash where validation was already required + - standardized early hash-mismatch return path style in `setupcontainer` + - preserved response-shape/signature contracts (no intended contract changes) +- Added focused phase-15 tests: + - `tests/phase15/service-behaviour.test.cjs` + - negative-path checks for missing/invalid hash across all selected handlers + - required-input negative-path checks where applicable + - one valid-hash mocked happy-path check (`setupcontainer` response shape) + +### Phase 15 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings) + +### Phase 15 manual HTTP snapshot + +- Dev server run on `http://localhost:3002`. +- Missing/invalid hash negative-path checks for selected handlers -> **400**. +- Missing required params where applicable -> **400**. +- Valid-hash spot-check: + - `GET /api/file/setupcontainer?ident=c1&hash=` -> **200**. + ## Likely next steps 1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index d5f61e80..95dc12a5 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -929,3 +929,52 @@ Follow-ups: - Keep signer allow-list expansion minimal and task-driven. - Continue replacing remaining browser-side direct hash assumptions only where flows require it. - Confirm work-item/branch naming alignment for any subsequent phase slices if strict tracker continuity is required. + +--- + +### CL-026: Phase 15 hardening — consistency guards for additional hash-sensitive file handlers + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{upload,uploadsinglefile,createappealcompletemessage_api,setupcontainer}.js`, `tests/phase15/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Deliver a small reversible consistency-only hardening slice by standardizing missing/invalid hash negative paths and required input handling in additional sensitive file handlers. +impact: Consistent early 400 behavior for missing/invalid hash and missing required params while preserving existing response shapes/signatures. +status: completed + +Summary: + +- Started new branch from `origin/SIPS-Development`: `TASK22028-phase15-hardening-slice`. +- Applied minimal hardening updates: + - `upload.js`: explicit missing-hash guard before hash comparison. + - `uploadsinglefile.js`: explicit missing-hash guard before hash comparison. + - `createappealcompletemessage_api.js`: added missing-hash guard to existing required-input guard block. + - `setupcontainer.js`: standardized to early-return on hash mismatch; contract and status behavior preserved. +- Added focused tests: + - `tests/phase15/service-behaviour.test.cjs` + - covers missing-hash negative paths for all selected handlers + - includes required-input negative paths and one valid-hash happy-path shape check. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) +- Manual checks on `localhost:3002`: + - invalid/missing hash for each selected handler -> 400 + - missing required params where applicable -> 400 + - valid-hash spot-check (`setupcontainer`) -> 200 + +Follow-ups: + +- Continue small-batch consistency hardening for remaining sensitive handlers where guard patterns are still inconsistent. +- Keep manual happy-path checks constrained to feasible local dependencies; document downstream/environment-caused failures separately. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 8ed83923..02938dbb 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -588,6 +588,68 @@ 3. Remove `tests/phase8/service-behaviour.test.cjs` if full slice rollback required. 4. Re-run phase6/7 baseline tests + lint after rollback. +## Latest update (2026-03-13 — Phase 15 hardening slice) + +- New branch created from `origin/SIPS-Development` with confirmed work item prefix: + - `TASK22028-phase15-hardening-slice` +- Delivered a small consistency-only hardening slice across 4 additional sensitive file handlers: + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/file/setupcontainer.js` +- Scope delivered: + - standardized explicit early 400 handling for missing hash in handlers where hash validation was present but missing-hash guard was inconsistent + - standardized guard style in `setupcontainer` to early-return on hash mismatch (same status/contract) + - standardized missing required hash handling in `createappealcompletemessage_api` + - preserved response shapes and signatures (no contract changes) +- Added focused phase-15 tests: + - `tests/phase15/service-behaviour.test.cjs` + - negative-path coverage for missing hash across all 4 handlers + - additional required-input checks where applicable + - one valid-hash happy-path contract check (`setupcontainer` -> 200 with existing response shape) + +### Validation snapshot (Phase 15) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> **pass** (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase15/service-behaviour.test.cjs` -> **pass** (5/5) +- `npm run lint` -> **warnings only** (pre-existing `react-hooks/exhaustive-deps` warnings) + +### Manual HTTP snapshot (Phase 15) + +- Dev server run on `http://localhost:3002`. +- Negative-path checks: + - `POST /api/file/upload` missing hash -> **400** + - `POST /api/file/upload?hash=wrong` -> **400** + - `POST /api/file/uploadsinglefile` missing hash -> **400** + - `POST /api/file/uploadsinglefile?hash=wrong` -> **400** + - `GET /api/file/createappealcompletemessage_api?container=c1&tempcaseref=t1` missing hash -> **400** + - `GET /api/file/createappealcompletemessage_api?container=c1&tempcaseref=t1&hash=wrong` -> **400** + - `GET /api/file/createappealcompletemessage_api?container=c1` missing required params/hash -> **400** + - `GET /api/file/setupcontainer?ident=c1` missing hash -> **400** + - `GET /api/file/setupcontainer?ident=c1&hash=wrong` -> **400** + - `GET /api/file/setupcontainer?hash=` missing required `ident` -> **400** +- Valid-hash spot-check: + - `GET /api/file/setupcontainer?ident=c1&hash=` -> **200** + +### Rollback plan (Phase 15) + +1. Revert these files: + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/file/setupcontainer.js` + - `tests/phase15/service-behaviour.test.cjs` +2. Re-run phase6–phase15 tests and lint to confirm parity. + ## Outstanding risks / gaps - Navigation regressions across EN/CY + route query combinations. diff --git a/memory-bank/refactor-plan-actions-index.md b/memory-bank/refactor-plan-actions-index.md index bc73a5bc..33b4d679 100644 --- a/memory-bank/refactor-plan-actions-index.md +++ b/memory-bank/refactor-plan-actions-index.md @@ -467,6 +467,44 @@ Status key: `[x] done`, `[ ] pending` - Revert `b8fa514` to remove getrepsblob contract-alignment + Quill import fix. - Re-run phase6–phase14 tests and lint after rollback. +### Phase 15 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Create new branch from `origin/SIPS-Development` with required work item prefix + - branch: `TASK22028-phase15-hardening-slice` +2. `[x]` Apply minimal consistency-only hardening in 4 additional sensitive handlers + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/file/setupcontainer.js` +3. `[x]` Standardize negative-path behavior + - explicit early 400 for missing/invalid hash in selected handlers + - explicit missing required-input checks where route contract requires them + - preserved response-shape contracts and function signatures +4. `[x]` Add focused Phase 15 tests + - added `tests/phase15/service-behaviour.test.cjs` + - includes negative-path tests for missing/invalid hash + required-input checks + - includes one valid-hash mocked happy-path response-shape check +5. `[x]` Execute required validation bundle + - phase6 parity + phase6–phase14 behaviour tests -> pass + - phase15 behaviour tests -> pass + - lint -> warnings only (pre-existing) +6. `[x]` Execute targeted manual checks + - missing/invalid hash across selected handlers -> 400 + - missing required params where applicable -> 400 + - valid-hash `setupcontainer` spot-check -> 200 + +### Phase 15 rollback notes + +- Revert these files to rollback the full hardening slice: + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/file/setupcontainer.js` + - `tests/phase15/service-behaviour.test.cjs` +- Re-run phase6–phase15 baseline tests and lint after rollback to confirm parity. + ## Safe execution mode for migration chunks (required) To reduce terminal hangs during bulk migration work, run refactor chunks in **safe stepwise mode** instead of long chained commands. diff --git a/pages/api/file/createappealcompletemessage_api.js b/pages/api/file/createappealcompletemessage_api.js index 83fcb8a9..2c96b1a3 100644 --- a/pages/api/file/createappealcompletemessage_api.js +++ b/pages/api/file/createappealcompletemessage_api.js @@ -26,7 +26,9 @@ ApiProxy.get(async (req, res) => { typeof containerName === "undefined" || containerName.length === 0 || typeof tempCaseRef === "undefined" || - tempCaseRef.length === 0 + tempCaseRef.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 ) { return res.status(400).json(); } diff --git a/pages/api/file/setupcontainer.js b/pages/api/file/setupcontainer.js index 1d251c2f..f02ba816 100644 --- a/pages/api/file/setupcontainer.js +++ b/pages/api/file/setupcontainer.js @@ -29,18 +29,18 @@ ApiProxy.get(async (req, res) => { var checkquerypath = "/api/file/setupcontainer?ident=" + containerName; - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await createContainer(containerName) - .then((data) => { - return res.status(200).json({ data: "success", output: data }); - }) - .catch((error) => { - consoleLogger(error); - res.status(400).json(error); - }); - } else { + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { return res.status(400).json(); } + + await createContainer(containerName) + .then((data) => { + return res.status(200).json({ data: "success", output: data }); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }); }); export const config = { diff --git a/pages/api/file/upload.js b/pages/api/file/upload.js index 2d8afbd2..1185f174 100644 --- a/pages/api/file/upload.js +++ b/pages/api/file/upload.js @@ -14,6 +14,10 @@ ApiProxy.use(middleware); ApiProxy.post(async (req, res) => { var checkHash = req.query.hash; + if (typeof checkHash === "undefined" || checkHash.length === 0) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/upload"; if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { diff --git a/pages/api/file/uploadsinglefile.js b/pages/api/file/uploadsinglefile.js index e0ebc854..ffbf222e 100644 --- a/pages/api/file/uploadsinglefile.js +++ b/pages/api/file/uploadsinglefile.js @@ -37,6 +37,11 @@ ApiProxy.use(middleware); ApiProxy.post(async (req, res) => { var checkHash = req.query.hash; + + if (typeof checkHash === "undefined" || checkHash.length === 0) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/uploadsinglefile"; if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { diff --git a/tests/phase15/service-behaviour.test.cjs b/tests/phase15/service-behaviour.test.cjs new file mode 100644 index 00000000..207c55eb --- /dev/null +++ b/tests/phase15/service-behaviour.test.cjs @@ -0,0 +1,245 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + }, + post(fn) { + this.handler = fn; + } + }; + + return () => router; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined + }; + + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("upload rejects missing hash with 400", async () => { + const calls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/upload.js", { + hashAPIPath: () => "?hash=expected", + createBlob: async (...args) => { + calls.push(args); + return { ok: true }; + }, + createRepBlob: async (...args) => { + calls.push(args); + return { ok: true }; + }, + uploadFile: async () => {}, + nextConnect, + middleware: () => {} + }); + + const req = { + query: {}, + body: { appealData: {}, containerID: ["c1"], casefolderID: ["case-1"] }, + files: {} + }; + const res = createRes(); + + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("uploadsinglefile rejects missing hash with 400", async () => { + const uploadCalls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/uploadsinglefile.js", { + hashAPIPath: () => "?hash=expected", + uploadSingleFile: async (...args) => { + uploadCalls.push(args); + return { ok: true }; + }, + consoleLogger: () => {}, + fileTypeFromBuffer: async () => ({ mime: "application/pdf" }), + fs: { readFileSync: () => Buffer.from("file") }, + path: { basename: (value) => value }, + nextConnect, + middleware: () => {} + }); + + const req = { + query: {}, + body: { containerID: ["c1"], casefolderID: ["case-1"] }, + files: {} + }; + const res = createRes(); + + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(uploadCalls.length, 0); +}); + +test("createappealcompletemessage_api rejects missing hash with 400", async () => { + const calls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule( + "pages/api/file/createappealcompletemessage_api.js", + { + hashAPIPath: () => "&hash=expected", + getProgressBlobs: async (...args) => { + calls.push(args); + return { path: "p1" }; + }, + downloadProgressFile: async () => ({}), + createBlob: async () => ({}), + getCaseBlob: async () => ({}), + createCaseCompleteMessage: async () => ({}), + updateAccount: async () => ({}), + consoleLogger: () => {}, + _: { isEmpty: (value) => !value }, + nextConnect, + middleware: () => {} + } + ); + + const req = { + query: { container: "c1", tempcaseref: "tmp-1", inv: 846040001 } + }; + const res = createRes(); + + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("setupcontainer rejects missing hash with 400", async () => { + const createCalls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/setupcontainer.js", { + hashAPIPath: () => "&hash=expected", + createContainer: async (...args) => { + createCalls.push(args); + return { ok: true }; + }, + createContainerSas: async () => {}, + getContainers: async () => {}, + getBlobs: async () => {}, + uploadFile: async () => {}, + consoleLogger: () => {}, + nextConnect, + middleware: () => {} + }); + + const req = { query: { ident: "container-1" } }; + const res = createRes(); + + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(createCalls.length, 0); +}); + +test("setupcontainer valid hash returns 200 with expected response shape", async () => { + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/setupcontainer.js", { + hashAPIPath: () => "&hash=expected", + createContainer: async () => ({ container: "container-1" }), + createContainerSas: async () => {}, + getContainers: async () => {}, + getBlobs: async () => {}, + uploadFile: async () => {}, + consoleLogger: () => {}, + nextConnect, + middleware: () => {} + }); + + const req = { query: { ident: "container-1", hash: "expected" } }; + const res = createRes(); + + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + data: "success", + output: { container: "container-1" } + }); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 15 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); From c196edb4572979a9290f54e391555a3f9f14ddd3 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 16:23:34 +0000 Subject: [PATCH 2/8] TASK22028: phase 16 file-proxy guard consistency slice --- .../getawaitingsubmissionfromblobproxy.js | 20 +-- pages/api/file/getbloblistproxy.js | 25 +-- pages/api/file/getrepsblobproxy.js | 20 +-- tests/phase16/service-behaviour.test.cjs | 147 ++++++++++++++++++ 4 files changed, 159 insertions(+), 53 deletions(-) create mode 100644 tests/phase16/service-behaviour.test.cjs diff --git a/pages/api/file/getawaitingsubmissionfromblobproxy.js b/pages/api/file/getawaitingsubmissionfromblobproxy.js index 2e05852a..6f8e3ef7 100644 --- a/pages/api/file/getawaitingsubmissionfromblobproxy.js +++ b/pages/api/file/getawaitingsubmissionfromblobproxy.js @@ -1,30 +1,18 @@ -import { - downloadAllProgressFiles, - getAllProgressBlobs -} from "../../../actions/azurestorage"; -import _ from "lodash"; -import nextConnect from "next-connect"; -import middleware from "../middleware/middleware"; import { getToken } from "../../../actions/core/token"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import axios from "axios"; -import CryptoJS from "crypto-js"; - -const WORDKEY = process.env.HASHKEY; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; +const hasValue = (value) => + typeof value === "string" && value.trim().length > 0; + export default async function ApiProxy(req, res) { var containerName = req.query.container; - var checkHash = req.query.hash; - if (typeof containerName === "undefined" || containerName.length === 0) { + if (!hasValue(containerName)) { return res.status(400).json(); } diff --git a/pages/api/file/getbloblistproxy.js b/pages/api/file/getbloblistproxy.js index 8032d1c8..10a7b958 100644 --- a/pages/api/file/getbloblistproxy.js +++ b/pages/api/file/getbloblistproxy.js @@ -1,36 +1,19 @@ -import { - downloadAllProgressFiles, - getAllProgressBlobs -} from "../../../actions/azurestorage"; -import _ from "lodash"; -import nextConnect from "next-connect"; -import middleware from "../middleware/middleware"; import { getToken } from "../../../actions/core/token"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import axios from "axios"; -import CryptoJS from "crypto-js"; - -const WORDKEY = process.env.HASHKEY; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; +const hasValue = (value) => + typeof value === "string" && value.trim().length > 0; + export default async function ApiProxy(req, res) { var containerName = req.query.container; var casefolderID = req.query.casefolderID; - var checkHash = req.query.hash; - if ( - typeof containerName === "undefined" || - containerName.length === 0 || - typeof casefolderID === "undefined" || - casefolderID.length === 0 - ) { + if (!hasValue(containerName) || !hasValue(casefolderID)) { return res.status(400).json(); } diff --git a/pages/api/file/getrepsblobproxy.js b/pages/api/file/getrepsblobproxy.js index 7b377b2d..3a0bb850 100644 --- a/pages/api/file/getrepsblobproxy.js +++ b/pages/api/file/getrepsblobproxy.js @@ -1,30 +1,18 @@ -import { - downloadAllProgressFiles, - getAllProgressBlobs -} from "../../../actions/azurestorage"; -import _ from "lodash"; -import nextConnect from "next-connect"; -import middleware from "../middleware/middleware"; import { getToken } from "../../../actions/core/token"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import axios from "axios"; -import CryptoJS from "crypto-js"; - -const WORDKEY = process.env.HASHKEY; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; +const hasValue = (value) => + typeof value === "string" && value.trim().length > 0; + export default async function ApiProxy(req, res) { var containerName = req.query.container; - var checkHash = req.query.hash; - if (typeof containerName === "undefined" || containerName.length === 0) { + if (!hasValue(containerName)) { return res.status(400).json(); } diff --git a/tests/phase16/service-behaviour.test.cjs b/tests/phase16/service-behaviour.test.cjs new file mode 100644 index 00000000..467efa1f --- /dev/null +++ b/tests/phase16/service-behaviour.test.cjs @@ -0,0 +1,147 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { log: () => {}, error: () => {} }, + port: 3000, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined, + sentBody: undefined, + headers: {} + }; + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + }, + send(payload) { + state.sentBody = payload; + return payload; + }, + setHeader(name, value) { + state.headers[name] = value; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("getbloblistproxy rejects missing container with 400", async () => { + const mod = loadModule("pages/api/file/getbloblistproxy.js", { + getToken: async () => ({ access_token: "t" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: { ok: true } }) }, + consoleLogger: () => {} + }); + + const req = { query: { casefolderID: "case-1" } }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getrepsblobproxy rejects missing container with 400", async () => { + const mod = loadModule("pages/api/file/getrepsblobproxy.js", { + getToken: async () => ({ access_token: "t" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: { ok: true } }) }, + consoleLogger: () => {} + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getawaitingsubmissionfromblobproxy rejects missing container with 400", async () => { + const mod = loadModule( + "pages/api/file/getawaitingsubmissionfromblobproxy.js", + { + getToken: async () => ({ access_token: "t" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: { ok: true } }) }, + consoleLogger: () => {} + } + ); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getbloblistproxy valid input returns 200 with existing response shape", async () => { + const mod = loadModule("pages/api/file/getbloblistproxy.js", { + getToken: async () => ({ access_token: "t" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: [{ name: "a" }] }) }, + consoleLogger: () => {} + }); + + const req = { query: { container: "c1", casefolderID: "case-1" } }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(res.state.jsonBody, [{ name: "a" }]); +}); + +const run = async () => { + let passed = 0; + for (const t of tests) { + await t.fn(); + passed += 1; + } + console.log( + `Phase 16 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); From 6a9a257ae8ed53f0fca0c09ef34802c0aa61415e Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 16:24:55 +0000 Subject: [PATCH 3/8] TASK22028: phase 16 memory-bank updates --- memory-bank/activeContext.md | 21 +++++++++ memory-bank/change-log.md | 40 ++++++++++++++++ memory-bank/progress.md | 53 ++++++++++++++++++++++ memory-bank/refactor-plan-actions-index.md | 31 +++++++++++++ 4 files changed, 145 insertions(+) diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index a53b2808..ed984a4f 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -500,6 +500,27 @@ - Valid-hash spot-check: - `GET /api/file/setupcontainer?ident=c1&hash=` -> **200**. +## Phase 16 hardening status (2026-03-13) + +- Continued on branch: + - `TASK22028-phase15-hardening-slice` +- Added a consistency-only file-proxy hardening slice across: + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` +- Changes focused on: + - standardized required-input guards via `hasValue` + - removed unused imports/dead constants from touched proxy handlers + - preserved existing response contracts and status semantics +- Added focused test pack: + - `tests/phase16/service-behaviour.test.cjs` + +### Phase 16 validation evidence snapshot + +- phase6 parity + phase6–phase15 behavioural suites: pass +- phase16 behavioural suite: pass (4/4) +- lint: warnings only (pre-existing) + ## Likely next steps 1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 95dc12a5..951271c7 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -978,3 +978,43 @@ Follow-ups: - Continue small-batch consistency hardening for remaining sensitive handlers where guard patterns are still inconsistent. - Keep manual happy-path checks constrained to feasible local dependencies; document downstream/environment-caused failures separately. + +--- + +### CL-027: Phase 16 hardening — file proxy required-input guard consistency slice + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy}.js`, `tests/phase16/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Continue within the same branch using separate commits to apply a small consistency-only hardening slice for sensitive file proxy handlers. +impact: More consistent required-input validation and cleaner proxy handler internals without changing response-shape/signature contracts. +status: completed + +Summary: + +- Continued on `TASK22028-phase15-hardening-slice` as a separate commit. +- Standardized required-input guards using a local `hasValue` helper in 3 file proxy handlers. +- Removed unused imports/constants from touched files. +- Added `tests/phase16/service-behaviour.test.cjs` with negative-path coverage and one valid-input contract check. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase16/service-behaviour.test.cjs` -> pass (4/4) +- `npm run lint` -> warnings only (pre-existing) +- Manual checks (`localhost:3002`): missing required inputs across selected proxies -> 400 + +Follow-ups: + +- Continue incremental endpoint/file proxy consistency slices with same contract-preserving constraints. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 02938dbb..5741b58a 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -650,6 +650,59 @@ - `tests/phase15/service-behaviour.test.cjs` 2. Re-run phase6–phase15 tests and lint to confirm parity. +## Latest update (2026-03-13 — Phase 16 file-proxy consistency slice) + +- Continued on branch: + - `TASK22028-phase15-hardening-slice` +- Delivered a small consistency-only hardening slice across 3 sensitive file proxy handlers: + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` +- Scope delivered: + - removed unused imports/constants in touched handlers to reduce noisy/unsafe dead code + - standardized required input checks using shared local `hasValue` pattern + - preserved response shape/signature/status behavior contracts +- Added focused tests: + - `tests/phase16/service-behaviour.test.cjs` + - negative-path checks for missing required container/casefolder inputs + - one valid-input happy-path contract check for `getbloblistproxy` (200 + passthrough shape) + +### Validation snapshot (Phase 16) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> **pass** (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase15/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase16/service-behaviour.test.cjs` -> **pass** (4/4) +- `npm run lint` -> **warnings only** (pre-existing `react-hooks/exhaustive-deps` warnings) + +### Manual HTTP checks (Phase 16) + +- Dev server on `http://localhost:3002`. +- Negative-path checks: + - `/api/file/getbloblistproxy?casefolderID=case-1` -> **400** + - `/api/file/getbloblistproxy?container=c1` -> **400** + - `/api/file/getrepsblobproxy` -> **400** + - `/api/file/getawaitingsubmissionfromblobproxy` -> **400** +- Feasible valid-input spot-check: + - `/api/file/getbloblistproxy?container=c1&casefolderID=case-1` -> **400** (expected downstream/local dependency behavior; input guard passed) + +### Rollback plan (Phase 16) + +1. Revert commit `c196edb` or files: + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` + - `tests/phase16/service-behaviour.test.cjs` +2. Re-run phase6–phase16 tests and lint. + ## Outstanding risks / gaps - Navigation regressions across EN/CY + route query combinations. diff --git a/memory-bank/refactor-plan-actions-index.md b/memory-bank/refactor-plan-actions-index.md index 33b4d679..48ee9923 100644 --- a/memory-bank/refactor-plan-actions-index.md +++ b/memory-bank/refactor-plan-actions-index.md @@ -505,6 +505,37 @@ Status key: `[x] done`, `[ ] pending` - `tests/phase15/service-behaviour.test.cjs` - Re-run phase6–phase15 baseline tests and lint after rollback to confirm parity. +### Phase 16 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Continue on current branch with separate commit for additional small slice + - branch: `TASK22028-phase15-hardening-slice` +2. `[x]` Apply minimal consistency hardening in 3 file proxy handlers + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` +3. `[x]` Standardize required-input guard pattern + - added local `hasValue` checks for required params + - removed unused/dead imports/constants in touched files +4. `[x]` Add focused Phase 16 tests + - added `tests/phase16/service-behaviour.test.cjs` + - missing-input negative-path checks + one valid-input contract check +5. `[x]` Execute validation bundle + - phase6 parity + phase6–phase16 behaviour tests pass + - lint warnings only (pre-existing) +6. `[x]` Execute manual checks + - missing required-input proxy calls -> 400 + +### Phase 16 rollback notes + +- Revert commit `c196edb` (or files): + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` + - `tests/phase16/service-behaviour.test.cjs` +- Re-run phase6–phase16 baseline tests and lint. + ## Safe execution mode for migration chunks (required) To reduce terminal hangs during bulk migration work, run refactor chunks in **safe stepwise mode** instead of long chained commands. From 49dd99016a47679588b66ceb98a37cf9d2b94412 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 16:30:23 +0000 Subject: [PATCH 4/8] TASK22028: allow signer hash for deleteblobcase browser flow --- pages/api/endpoint/gethash_api.js | 1 + tests/phase14/service-behaviour.test.cjs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pages/api/endpoint/gethash_api.js b/pages/api/endpoint/gethash_api.js index 5927cf87..7b241718 100644 --- a/pages/api/endpoint/gethash_api.js +++ b/pages/api/endpoint/gethash_api.js @@ -16,6 +16,7 @@ ApiProxy.get(async (req, res) => { "/api/endpoint/getportallogin_api", "/api/file/upload", "/api/file/uploadsinglefile", + "/api/file/deleteblobcase", "/api/file/createrepcompletemessage_api", "/api/file/createappealcompletemessage_api" ]; diff --git a/tests/phase14/service-behaviour.test.cjs b/tests/phase14/service-behaviour.test.cjs index 86bb0714..28e7a8a7 100644 --- a/tests/phase14/service-behaviour.test.cjs +++ b/tests/phase14/service-behaviour.test.cjs @@ -132,6 +132,26 @@ test("gethash_api returns hash for allow-listed getportallogin path", async () = }); }); +test("gethash_api returns hash for allow-listed deleteblobcase path", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "&hash=deletecase", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { path: "/api/file/deleteblobcase?container=c1&casefolderID=r1" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + hash: "&hash=deletecase" + }); +}); + test("gethash_api rejects unauthenticated requests with 401", async () => { const mod = loadModule("pages/api/endpoint/gethash_api.js", { hashAPIPath: () => "&hash=expected", From e404a6d6dbf3d4815f6fc2e4cab617e3460ca6a7 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 16:31:21 +0000 Subject: [PATCH 5/8] TASK22028: document signer allow-list follow-up --- memory-bank/activeContext.md | 8 +++++++ memory-bank/change-log.md | 27 ++++++++++++++++++++++ memory-bank/progress.md | 14 +++++++++++ memory-bank/refactor-plan-actions-index.md | 15 ++++++++++++ 4 files changed, 64 insertions(+) diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index ed984a4f..d327bdce 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -521,6 +521,14 @@ - phase16 behavioural suite: pass (4/4) - lint: warnings only (pre-existing) +## Follow-up status (2026-03-13) + +- Applied targeted signer allow-list fix for browser delete-awaiting-submission path. +- Updated `pages/api/endpoint/gethash_api.js` allow-list with: + - `/api/file/deleteblobcase` +- Added focused behavioural test: + - `tests/phase14/service-behaviour.test.cjs` now includes allow-list coverage for deleteblobcase path. + ## Likely next steps 1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 951271c7..03f54885 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1018,3 +1018,30 @@ Validation: Follow-ups: - Continue incremental endpoint/file proxy consistency slices with same contract-preserving constraints. + +--- + +### CL-028: Follow-up fix — signer allow-list expanded for deleteblobcase browser path + +date: 2026-03-13 +author: Cline +scope: `pages/api/endpoint/gethash_api.js`, `tests/phase14/service-behaviour.test.cjs` +type: change +rationale: Resolve runtime failure where browser-side `deleteAwaitingSubmissionsFromBlob` hash signing requested a path not present in signer allow-list. +impact: Restores browser-safe hash signing for delete-blob-case flow without weakening auth/session guard on signer endpoint. +status: completed + +Summary: + +- Added `/api/file/deleteblobcase` to `gethash_api` allow-list. +- Added focused test coverage in phase-14 suite for the new allow-listed path. + +Validation: + +- `node tests/phase14/service-behaviour.test.cjs` -> pass (6/6) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `npm run lint` -> warnings only (pre-existing) + +Follow-ups: + +- Keep signer allow-list additions minimal and path-specific. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 5741b58a..5461ff83 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -703,6 +703,20 @@ - `tests/phase16/service-behaviour.test.cjs` 2. Re-run phase6–phase16 tests and lint. +## Latest update (2026-03-13 — signer allow-list follow-up) + +- Branch: `TASK22028-phase15-hardening-slice` +- Fix applied: + - added `/api/file/deleteblobcase` to signer endpoint allow-list in `pages/api/endpoint/gethash_api.js` +- Test update: + - extended `tests/phase14/service-behaviour.test.cjs` with deleteblobcase allow-list coverage + +### Validation snapshot (follow-up) + +- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (6/6) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `npm run lint` -> **warnings only** (pre-existing) + ## Outstanding risks / gaps - Navigation regressions across EN/CY + route query combinations. diff --git a/memory-bank/refactor-plan-actions-index.md b/memory-bank/refactor-plan-actions-index.md index 48ee9923..3dd92adc 100644 --- a/memory-bank/refactor-plan-actions-index.md +++ b/memory-bank/refactor-plan-actions-index.md @@ -536,6 +536,21 @@ Status key: `[x] done`, `[ ] pending` - `tests/phase16/service-behaviour.test.cjs` - Re-run phase6–phase16 baseline tests and lint. +### Follow-up completion snapshot (2026-03-13) + +1. `[x]` Address browser signer failure for delete-awaiting-submission path + - added signer allow-list entry: `/api/file/deleteblobcase` +2. `[x]` Add focused behavioural test coverage + - extended `tests/phase14/service-behaviour.test.cjs` +3. `[x]` Validate + - phase14 behaviour tests pass (6/6) + - phase7 behaviour tests pass (10/10) + - lint warnings only (pre-existing) + +### Follow-up rollback notes + +- Revert commit `49dd990` to undo allow-list/test follow-up. + ## Safe execution mode for migration chunks (required) To reduce terminal hangs during bulk migration work, run refactor chunks in **safe stepwise mode** instead of long chained commands. From 6283786aeb2c4c765f0366d1aa724beabcea66dd Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 16:40:51 +0000 Subject: [PATCH 6/8] TASK22028: expand signer coverage for delete flows --- actions/services/documentDirectService.js | 24 +++-- actions/services/portalDirectService.js | 51 +++++---- memory-bank/activeContext.md | 12 +++ memory-bank/change-log.md | 37 +++++++ memory-bank/refactor-plan-actions-index.md | 14 +++ pages/api/endpoint/gethash_api.js | 3 + tests/phase14/service-behaviour.test.cjs | 66 ++++++++++++ tests/phase7/service-behaviour.test.cjs | 119 +++++++++++++++++++-- 8 files changed, 282 insertions(+), 44 deletions(-) diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js index d0afabb8..4934576a 100644 --- a/actions/services/documentDirectService.js +++ b/actions/services/documentDirectService.js @@ -92,11 +92,13 @@ export const deleteAwaitingSubmissionsFromBlob = ( "&casefolderID=" + casefolderID; - var config = { - method: "get", - url: queryUrl + hashAPIPath(queryUrl) - }; - return axios(config) + return buildHashedQueryUrl(queryUrl) + .then((signedUrl) => + axios({ + method: "get", + url: signedUrl + }) + ) .then((res) => { return res.data; }) @@ -118,11 +120,13 @@ export const deleteMyRepresentationsFromBlob = ( "&repfile=" + repfile; - var config = { - method: "get", - url: queryUrl + hashAPIPath(queryUrl) - }; - return axios(config) + return buildHashedQueryUrl(queryUrl) + .then((signedUrl) => + axios({ + method: "get", + url: signedUrl + }) + ) .then((res) => { return res.data; }) diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 9bf17190..1e112810 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -182,19 +182,21 @@ export const deleteMyRepresentations = (myRepresentationsID) => { "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=" + myRepresentationsID; - var config = { - method: "delete", - url: queryUrl + hashAPIPath(queryUrl), - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - "Accept": "application/json;odata.metadata=none", - "Prefer": 'odata.include-annotations="*",return=representation', - "Content-Type": "application/json" - } - }; - - return axios(config) + return buildHashedQueryUrl(queryUrl) + .then((signedUrl) => + axios({ + method: "delete", + url: signedUrl, + headers: { + "OData-MaxVersion": "4.0", + "OData-Version": "4.0", + "Accept": "application/json;odata.metadata=none", + "Prefer": + 'odata.include-annotations="*",return=representation', + "Content-Type": "application/json" + } + }) + ) .then((res) => { return res.data; }) @@ -224,17 +226,20 @@ export const deleteAwaitingSubmissions = (incidentID) => { export const deleteWatchedCases = async (watchedCaseID) => { var queryUrl = "/api/endpoint/deletewatchedcases_api?watchedCaseID=" + watchedCaseID; - var config = { - method: "delete", - url: queryUrl - }; - try { - const res = await axios(config); - return res.data; - } catch (error) { - consoleLogger(error); - } + return buildHashedQueryUrl(queryUrl) + .then((signedUrl) => + axios({ + method: "delete", + url: signedUrl + }) + ) + .then((res) => { + return res.data; + }) + .catch((error) => { + consoleLogger(error); + }); }; export const sendCaseCompleteMessage = async ( diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index d327bdce..09b6101a 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -529,6 +529,18 @@ - Added focused behavioural test: - `tests/phase14/service-behaviour.test.cjs` now includes allow-list coverage for deleteblobcase path. +## Follow-up status (2026-03-13 — additional delete flows) + +- Expanded signer allow-list for additional browser delete paths: + - `/api/endpoint/deletemyrepresentations_api` + - `/api/endpoint/deletewatchedcases_api` + - `/api/file/deleteblobrep` +- Migrated affected direct-service delete calls to signer-based hash retrieval: + - `portalDirectService.deleteWatchedCases` + - `portalDirectService.deleteMyRepresentations` + - `documentDirectService.deleteAwaitingSubmissionsFromBlob` + - `documentDirectService.deleteMyRepresentationsFromBlob` + ## Likely next steps 1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 03f54885..dbc43589 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1045,3 +1045,40 @@ Validation: Follow-ups: - Keep signer allow-list additions minimal and path-specific. + +--- + +### CL-029: Follow-up fix — signer coverage for additional delete flows + +date: 2026-03-13 +author: Cline +scope: `actions/services/{portalDirectService,documentDirectService}.js`, `pages/api/endpoint/gethash_api.js`, `tests/phase7/service-behaviour.test.cjs`, `tests/phase14/service-behaviour.test.cjs` +type: change +rationale: Resolve browser hash-signing gaps for additional delete operations still using direct client hash generation. +impact: Restores browser-safe signing for delete watched cases, delete my representations, and delete blob representation/awaiting-submission paths while retaining authenticated allow-list signer model. +status: completed + +Summary: + +- Updated signer allow-list to include: + - `/api/endpoint/deletemyrepresentations_api` + - `/api/endpoint/deletewatchedcases_api` + - `/api/file/deleteblobrep` +- Migrated direct service calls to signer-based hash retrieval for: + - `portalDirectService.deleteWatchedCases` + - `portalDirectService.deleteMyRepresentations` + - `documentDirectService.deleteAwaitingSubmissionsFromBlob` + - `documentDirectService.deleteMyRepresentationsFromBlob` +- Extended focused tests: + - `tests/phase7/service-behaviour.test.cjs` (service signer usage) + - `tests/phase14/service-behaviour.test.cjs` (allow-list coverage) + +Validation: + +- `node tests/phase7/service-behaviour.test.cjs` -> pass (12/12) +- `node tests/phase14/service-behaviour.test.cjs` -> pass (9/9) +- `npm run lint` -> warnings only (pre-existing) + +Follow-ups: + +- Keep allow-list expansion path-scoped and driven by concrete browser-call requirements only. diff --git a/memory-bank/refactor-plan-actions-index.md b/memory-bank/refactor-plan-actions-index.md index 3dd92adc..cbca2cb2 100644 --- a/memory-bank/refactor-plan-actions-index.md +++ b/memory-bank/refactor-plan-actions-index.md @@ -551,6 +551,20 @@ Status key: `[x] done`, `[ ] pending` - Revert commit `49dd990` to undo allow-list/test follow-up. +### Follow-up completion snapshot (2026-03-13 — additional delete flows) + +1. `[x]` Expand signer allow-list for required browser delete paths +2. `[x]` Move remaining delete service calls to signer-based hash retrieval +3. `[x]` Extend focused service/signer tests +4. `[x]` Validate + - phase7 behaviour tests pass (12/12) + - phase14 behaviour tests pass (9/9) + - lint warnings only (pre-existing) + +### Follow-up rollback notes (additional delete flows) + +- Revert the follow-up commit for this slice to restore prior hashing behavior. + ## Safe execution mode for migration chunks (required) To reduce terminal hangs during bulk migration work, run refactor chunks in **safe stepwise mode** instead of long chained commands. diff --git a/pages/api/endpoint/gethash_api.js b/pages/api/endpoint/gethash_api.js index 7b241718..4c1ce596 100644 --- a/pages/api/endpoint/gethash_api.js +++ b/pages/api/endpoint/gethash_api.js @@ -14,9 +14,12 @@ ApiProxy.get(async (req, res) => { const queryPath = req.query.path; const allowedPrefix = [ "/api/endpoint/getportallogin_api", + "/api/endpoint/deletemyrepresentations_api", + "/api/endpoint/deletewatchedcases_api", "/api/file/upload", "/api/file/uploadsinglefile", "/api/file/deleteblobcase", + "/api/file/deleteblobrep", "/api/file/createrepcompletemessage_api", "/api/file/createappealcompletemessage_api" ]; diff --git a/tests/phase14/service-behaviour.test.cjs b/tests/phase14/service-behaviour.test.cjs index 28e7a8a7..31a3f599 100644 --- a/tests/phase14/service-behaviour.test.cjs +++ b/tests/phase14/service-behaviour.test.cjs @@ -152,6 +152,72 @@ test("gethash_api returns hash for allow-listed deleteblobcase path", async () = }); }); +test("gethash_api returns hash for allow-listed deleteblobrep path", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "&hash=deleterep", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { + path: "/api/file/deleteblobrep?container=c1&casefolderID=r1&repfile=f1" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + hash: "&hash=deleterep" + }); +}); + +test("gethash_api returns hash for allow-listed deletewatchedcases path", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "&hash=watch", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { + path: "/api/endpoint/deletewatchedcases_api?watchedCaseID=123" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + hash: "&hash=watch" + }); +}); + +test("gethash_api returns hash for allow-listed deletemyrepresentations path", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "&hash=delrep", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { + path: "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=abc" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + hash: "&hash=delrep" + }); +}); + test("gethash_api rejects unauthenticated requests with 401", async () => { const mod = loadModule("pages/api/endpoint/gethash_api.js", { hashAPIPath: () => "&hash=expected", diff --git a/tests/phase7/service-behaviour.test.cjs b/tests/phase7/service-behaviour.test.cjs index 00da59c0..305ac424 100644 --- a/tests/phase7/service-behaviour.test.cjs +++ b/tests/phase7/service-behaviour.test.cjs @@ -155,10 +155,15 @@ test("document/getAwaitingSubmissionFromBlob logs and returns undefined on failu test("portal/deleteMyRepresentations appends hash and returns data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); - const hashCalls = []; - const hashAPIPath = (queryPath) => { - hashCalls.push(queryPath); - return "&hash=portal123"; + const signCalls = []; + + axios.getHandler = async (url) => { + if (url.startsWith("/api/endpoint/gethash_api?path=")) { + signCalls.push(url); + return { data: { hash: "&hash=portal123" } }; + } + + throw new Error("Unexpected get url: " + url); }; axios.requestHandler = async () => ({ data: { removed: true } }); @@ -167,28 +172,39 @@ test("portal/deleteMyRepresentations appends hash and returns data", async () => axios, BASE_URL: "", consoleLogger: logger.consoleLogger, - hashAPIPath + hashAPIPath: () => "&hash=fallback" }); const result = await portal.deleteMyRepresentations("rep-1"); assert.deepStrictEqual(normalize(result), { removed: true }); - assert.strictEqual(hashCalls.length, 1); assert.strictEqual( - hashCalls[0], - "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1" + signCalls[0], + "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletemyrepresentations_api%3FmyRepresentationsID%3Drep-1" ); assert.strictEqual( - axios.calls[0].config.url, + axios.calls[0].url, + "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletemyrepresentations_api%3FmyRepresentationsID%3Drep-1" + ); + assert.strictEqual( + axios.calls[1].config.url, "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=portal123" ); - assert.strictEqual(axios.calls[0].config.method, "delete"); + assert.strictEqual(axios.calls[1].config.method, "delete"); }); test("portal/deleteMyRepresentations logs and returns undefined on failure", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); + axios.getHandler = async (url) => { + if (url.startsWith("/api/endpoint/gethash_api?path=")) { + return { data: { hash: "&hash=portal-fail" } }; + } + + throw new Error("Unexpected get url: " + url); + }; + axios.requestHandler = async () => Promise.reject(createAxiosError(401, "Unauthorized")); @@ -196,7 +212,7 @@ test("portal/deleteMyRepresentations logs and returns undefined on failure", asy axios, BASE_URL: "", consoleLogger: logger.consoleLogger, - hashAPIPath: () => "&hash=portal-fail" + hashAPIPath: () => "&hash=fallback" }); const result = await portal.deleteMyRepresentations("rep-2"); @@ -205,6 +221,87 @@ test("portal/deleteMyRepresentations logs and returns undefined on failure", asy assert.strictEqual(logger.calls.length, 1); }); +test("portal/deleteWatchedCases uses signer hash and returns data", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const signCalls = []; + + axios.getHandler = async (url) => { + if (url.startsWith("/api/endpoint/gethash_api?path=")) { + signCalls.push(url); + return { data: { hash: "&hash=watch123" } }; + } + + throw new Error("Unexpected get url: " + url); + }; + + axios.requestHandler = async () => ({ data: { removed: true } }); + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=fallback" + }); + + const result = await portal.deleteWatchedCases("watch-1"); + + assert.deepStrictEqual(normalize(result), { removed: true }); + assert.strictEqual( + signCalls[0], + "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletewatchedcases_api%3FwatchedCaseID%3Dwatch-1" + ); + assert.strictEqual( + axios.calls[1].config.url, + "/api/endpoint/deletewatchedcases_api?watchedCaseID=watch-1&hash=watch123" + ); +}); + +test("document delete blob flows use signer hash", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const signCalls = []; + + axios.getHandler = async (url) => { + if (url.startsWith("/api/endpoint/gethash_api?path=")) { + signCalls.push(url); + return { data: { hash: "&hash=doc123" } }; + } + + throw new Error("Unexpected get url: " + url); + }; + + axios.requestHandler = async () => ({ data: { ok: true } }); + + const document = loadServiceModule("documentDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=fallback" + }); + + const one = await document.deleteAwaitingSubmissionsFromBlob( + "c1", + "case-1" + ); + const two = await document.deleteMyRepresentationsFromBlob( + "c1", + "case-1", + "rep-a" + ); + + assert.deepStrictEqual(normalize(one), { ok: true }); + assert.deepStrictEqual(normalize(two), { ok: true }); + assert.strictEqual( + signCalls[0], + "/api/endpoint/gethash_api?path=%2Fapi%2Ffile%2Fdeleteblobcase%3Fcontainer%3Dc1%26casefolderID%3Dcase-1" + ); + assert.strictEqual( + signCalls[1], + "/api/endpoint/gethash_api?path=%2Fapi%2Ffile%2Fdeleteblobrep%3Fcontainer%3Dc1%26casefolderID%3Dcase-1%26repfile%3Drep-a" + ); +}); + test("account/getPortalLogin appends hash and returns res.data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); From d32d7d016eb8eb727c2f911f2f4a3c6379f9e7ba Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 17:44:27 +0000 Subject: [PATCH 7/8] TASK22028: fix myportal deleteRepItem caseRef and i18n namespace --- components/myportal/topthree_reps.js | 3 +-- components/myportal/viewall.js | 34 +++++++++++++++++++++------- i18n.js | 2 +- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/components/myportal/topthree_reps.js b/components/myportal/topthree_reps.js index ea1a2169..0a52ac96 100644 --- a/components/myportal/topthree_reps.js +++ b/components/myportal/topthree_reps.js @@ -273,8 +273,7 @@ const TopThree = (props) => { deleteRepItem( props.accountDetails .containerID, - showTopThreeArr[key] - .ticketnumber, + showTopThreeArr[key].caseRef, showTopThreeArr[key] .repfile_name ); diff --git a/components/myportal/viewall.js b/components/myportal/viewall.js index 1885c7ea..6532be5a 100644 --- a/components/myportal/viewall.js +++ b/components/myportal/viewall.js @@ -686,7 +686,7 @@ const ViewAllResults = (props) => { ] || "N/A"} {isAwaitingSubmissionDetails && ( -
+
)} {isMyRepresentations && ( -
+
{item.pinswg_emailnotifications === true ? (