From d33ef0c185984d4949e41b9b15631e00814ee339 Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 13 Oct 2026 13:28:13 -0500 Subject: [PATCH 1/5] Fix normalizePort and consolidate delete handlers - Normalize port to a valid integer 1-65535, defaulting to 3002 outside the call. - Consolidate the two DELETE handlers in routes/delete.js into a single deleteRecord handler. Fixes #52, fixes #64 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- bin/tinyNode.js | 14 +++------- routes/delete.js | 69 +++++++++++++----------------------------------- 2 files changed, 22 insertions(+), 61 deletions(-) diff --git a/bin/tinyNode.js b/bin/tinyNode.js index 6f6d77d..2acf357 100644 --- a/bin/tinyNode.js +++ b/bin/tinyNode.js @@ -16,7 +16,7 @@ import http from "http" * Get port from environment and store in Express. */ -const port = normalizePort(process.env.PORT || '3002') +const port = normalizePort(process.env.PORT) || 3002 app.set('port', port) /** @@ -34,19 +34,13 @@ server.on('error', onError) server.on('listening', onListening) /** - * Normalize a port into a number, string, or false. + * Normalize a port into a valid TCP port number or false. */ function normalizePort(val) { - const port = parseInt(val, 10) + const port = Number(val) - if (isNaN(port)) { - // named pipe - return val - } - - if (port >= 0) { - // port number + if (Number.isInteger(port) && port >= 1 && port <= 65535) { return port } diff --git a/routes/delete.js b/routes/delete.js index efd70aa..d79ea89 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -5,27 +5,32 @@ import { fetchRerum } from "../rerum.js" import { isPassthroughRequest, requirePassthroughAllowed, resolveAuthorization } from "../passthrough.js" const router = express.Router() -/* Legacy delete pattern w/body. */ -router.delete('/', verifyJsonContentType, requirePassthroughAllowed, checkAccessToken, async (req, res, next) => { +async function deleteRecord(req, res) { try { - if (!req?.body || !(req.body['@id'] ?? req.body.id)) { + const id = req.params.id ?? req.body?.['@id'] ?? req.body?.id + if (!id) { const err = new Error("No record id to delete! (https://store.rerum.io/v1/API.html#delete)") err.status = 400 throw err } - const deleteBody = JSON.stringify(req.body) + const isPathDelete = Boolean(req.params.id) + const deleteURL = isPathDelete + ? `${process.env.RERUM_API_ADDR}delete/${req.params.id}` + : `${process.env.RERUM_API_ADDR}delete` const deleteOptions = { - body: deleteBody, method: 'DELETE', headers: { 'user-agent': 'Tiny-Things/1.0', 'Origin': process.env.ORIGIN, - 'Authorization': resolveAuthorization(req), - 'Content-Type' : "application/json; charset=utf-8" + 'Authorization': resolveAuthorization(req) } } - const deleteURL = `${process.env.RERUM_API_ADDR}delete` + if (!isPathDelete) { + deleteOptions.body = JSON.stringify(req.body) + deleteOptions.headers['Content-Type'] = "application/json; charset=utf-8" + } + await fetchRerum(deleteURL, deleteOptions) .then(async (resp) => { if (resp.ok) return @@ -54,51 +59,13 @@ router.delete('/', verifyJsonContentType, requirePassthroughAllowed, checkAccess console.error(err) res.status(err.status ?? 500).type('text/plain').send(err.message ?? 'An error occurred') } -}) +} + +/* Legacy delete pattern w/body. */ +router.delete('/', verifyJsonContentType, requirePassthroughAllowed, checkAccessToken, deleteRecord) /* DELETE an object by ID via the RERUM API. */ -router.delete('/:id', requirePassthroughAllowed, checkAccessToken, async (req, res, next) => { - try { - - const deleteURL = `${process.env.RERUM_API_ADDR}delete/${req.params.id}` - const deleteOptions = { - method: "DELETE", - headers: { - 'user-agent': 'Tiny-Things/1.0', - 'Origin': process.env.ORIGIN, - 'Authorization': resolveAuthorization(req), - } - } - await fetchRerum(deleteURL, deleteOptions) - .then(async (resp) => { - if (resp.ok) return - if (isPassthroughRequest(req) && (resp.status === 401 || resp.status === 403)) { - let rerumAuthMessage - try { - rerumAuthMessage = `${resp.status}: ${deleteURL} - ${await resp.text()}` - } catch (e) { - rerumAuthMessage = `${resp.status}: ${deleteURL} - A RERUM error occurred` - } - throw httpError(rerumAuthMessage, resp.status) - } - // The response from RERUM indicates a failure, likely with a specific code and textual body - let rerumErrorMessage - try { - rerumErrorMessage = `${resp.status ?? 500}: ${deleteURL} - ${await resp.text()}` - } catch (e) { - rerumErrorMessage = `500: ${deleteURL} - A RERUM error occurred` - } - const err = new Error(rerumErrorMessage) - err.status = 502 - throw err - }) - res.status(204).end() - } - catch (err) { - console.error(err) - res.status(err.status ?? 500).type('text/plain').send(err.message ?? 'An error occurred') - } -}) +router.delete('/:id', requirePassthroughAllowed, checkAccessToken, deleteRecord) router.all('/', (req, res, next) => { res.status(405).send("Method Not Allowed") From 4d273e925d2922fddf433145baa2f027e30c78bf Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 26 Oct 2026 11:06:54 -0500 Subject: [PATCH 2/5] Always do the '/delete/{id}' version of the delete request when communicating with RERUM API --- routes/delete.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/routes/delete.js b/routes/delete.js index d79ea89..f9e629a 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -13,11 +13,7 @@ async function deleteRecord(req, res) { err.status = 400 throw err } - - const isPathDelete = Boolean(req.params.id) - const deleteURL = isPathDelete - ? `${process.env.RERUM_API_ADDR}delete/${req.params.id}` - : `${process.env.RERUM_API_ADDR}delete` + const deleteURL = `${process.env.RERUM_API_ADDR}delete/${id}` const deleteOptions = { method: 'DELETE', headers: { @@ -26,11 +22,6 @@ async function deleteRecord(req, res) { 'Authorization': resolveAuthorization(req) } } - if (!isPathDelete) { - deleteOptions.body = JSON.stringify(req.body) - deleteOptions.headers['Content-Type'] = "application/json; charset=utf-8" - } - await fetchRerum(deleteURL, deleteOptions) .then(async (resp) => { if (resp.ok) return From 8923dff0b4af7cad6488ef39070e0061335ec168 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 26 Oct 2026 11:14:39 -0500 Subject: [PATCH 3/5] Always do the '/delete/{id}' version of the delete request when communicating with RERUM API --- routes/delete.js | 5 +++-- test/routes/delete.test.js | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/routes/delete.js b/routes/delete.js index f9e629a..0f1c456 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -7,12 +7,13 @@ const router = express.Router() async function deleteRecord(req, res) { try { - const id = req.params.id ?? req.body?.['@id'] ?? req.body?.id - if (!id) { + const uri = req.params.id ?? req.body?.['@id'] ?? req.body?.id + if (!uri) { const err = new Error("No record id to delete! (https://store.rerum.io/v1/API.html#delete)") err.status = 400 throw err } + const id = uri.split("/").pop() const deleteURL = `${process.env.RERUM_API_ADDR}delete/${id}` const deleteOptions = { method: 'DELETE', diff --git a/test/routes/delete.test.js b/test/routes/delete.test.js index b7c0366..a88e710 100644 --- a/test/routes/delete.test.js +++ b/test/routes/delete.test.js @@ -43,11 +43,12 @@ describe("Check that the request/response behavior of the TinyNode delete route assert.equal(response.statusCode, 204) - // Verify upstream contract for body-based delete - assert.match(lastFetchUrl, /\/delete$/, "URL should end with /delete") + // Verify body-based delete is forwarded upstream as a path-based delete + assert.match(lastFetchUrl, /\/delete\/_not_$/, "URL should end with /delete/_not_") assert.equal(lastFetchOptions.method, "DELETE", "Method should be DELETE") assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") - assert.equal(lastFetchOptions.headers["Content-Type"], "application/json; charset=utf-8", "Content-Type header mismatch") + assert.equal(lastFetchOptions.body, undefined, "Body should not be forwarded upstream") + assert.equal(lastFetchOptions.headers["Content-Type"], undefined, "Content-Type should not be sent") response = await request(routeTester) .delete("/delete/00000") From d1b85d8e1308dd7bffe1e9a24c4459b5ca43c743 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 26 Oct 2026 11:25:28 -0500 Subject: [PATCH 4/5] changes during testing and review --- bin/tinyNode.js | 14 +++++--------- routes/delete.js | 6 +++--- test/routes/delete.test.js | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/bin/tinyNode.js b/bin/tinyNode.js index 2acf357..0268494 100644 --- a/bin/tinyNode.js +++ b/bin/tinyNode.js @@ -16,7 +16,9 @@ import http from "http" * Get port from environment and store in Express. */ -const port = normalizePort(process.env.PORT) || 3002 +const envPort = normalizePort(process.env.PORT) +if (process.env.PORT && !envPort) console.warn(`Invalid PORT "${process.env.PORT}", using 3002`) +const port = envPort || 3002 app.set('port', port) /** @@ -56,9 +58,7 @@ function onError(error) { throw error } - const bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port + const bind = 'Port ' + port // handle specific listen errors with friendly messages switch (error.code) { @@ -80,9 +80,5 @@ function onError(error) { */ function onListening() { - const addr = server.address() - const bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port - console.log('Listening on ' + bind) + console.log('Listening on port ' + server.address().port) } diff --git a/routes/delete.js b/routes/delete.js index 0f1c456..677b89c 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -8,13 +8,13 @@ const router = express.Router() async function deleteRecord(req, res) { try { const uri = req.params.id ?? req.body?.['@id'] ?? req.body?.id - if (!uri) { + const id = typeof uri === "string" ? uri.split("/").pop() : undefined + if (!id || id === "." || id === "..") { const err = new Error("No record id to delete! (https://store.rerum.io/v1/API.html#delete)") err.status = 400 throw err } - const id = uri.split("/").pop() - const deleteURL = `${process.env.RERUM_API_ADDR}delete/${id}` + const deleteURL = `${process.env.RERUM_API_ADDR}delete/${encodeURIComponent(id)}` const deleteOptions = { method: 'DELETE', headers: { diff --git a/test/routes/delete.test.js b/test/routes/delete.test.js index a88e710..48284ee 100644 --- a/test/routes/delete.test.js +++ b/test/routes/delete.test.js @@ -59,6 +59,30 @@ describe("Check that the request/response behavior of the TinyNode delete route assert.match(lastFetchUrl, /\/delete\/00000$/, "URL should end with /delete/00000") assert.equal(lastFetchOptions.method, "DELETE", "Method should be DELETE") }) + + it("Extracts the record id from the body 'id' key and from an encoded URI in the path.", async () => { + let response = await request(routeTester) + .delete("/delete") + .send({ id: rerumUri }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 204) + assert.match(lastFetchUrl, /\/delete\/_not_$/, "URL should end with /delete/_not_") + + response = await request(routeTester) + .delete(`/delete/${encodeURIComponent(rerumUri)}`) + + assert.equal(response.statusCode, 204) + assert.match(lastFetchUrl, /\/delete\/_not_$/, "URL should end with /delete/_not_") + + response = await request(routeTester) + .delete("/delete") + .send({ "@id": `${rerumUri}?x=1#f` }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 204) + assert.match(lastFetchUrl, /\/delete\/_not_%3Fx%3D1%23f$/, "id should be URI encoded upstream") + }) }) describe("Check that incorrect TinyNode delete route usage results in expected RESTful responses from RERUM. __rest __core", () => { @@ -87,6 +111,7 @@ describe("Check that incorrect TinyNode delete route usage results in expected R .send("plain text") assert.equal(response.statusCode, 415) }) + }) describe("Delete network failure and passthrough behavior. __rest __core", () => { From 680716643ea4af638026af488a79481d5eaccdb7 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 26 Oct 2026 11:31:51 -0500 Subject: [PATCH 5/5] ready to merge --- test/routes/delete.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/routes/delete.test.js b/test/routes/delete.test.js index 48284ee..0be7b90 100644 --- a/test/routes/delete.test.js +++ b/test/routes/delete.test.js @@ -111,7 +111,6 @@ describe("Check that incorrect TinyNode delete route usage results in expected R .send("plain text") assert.equal(response.statusCode, 415) }) - }) describe("Delete network failure and passthrough behavior. __rest __core", () => {