From 3305e79e611f0386804f0d5eab5a4cf8ce151a7c Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Fri, 12 Jun 2026 19:20:00 -0500 Subject: [PATCH] fix(api): consolidate device pairing to /pair, remove vestigial bare endpoint (#90) POST /api/provision was a second pairing endpoint that paired a device by code but, unlike POST /api/provision/pair, did NOT assign a workspace, enforce checkDeviceLimit, or emit device:paired / dashboard:device-added - a silently-diverging duplicate that no client ever called. It now returns 410 Gone and points callers at /pair, so /api/provision/pair is the single, fully-protected pairing endpoint. The mount stays in the JWT-only partition, so a Bearer st_ token still gets 401 (requireAuth) before the 410. Closes #90 Co-Authored-By: Claude Opus 4.8 (1M context) --- server/routes/provisioning.js | 26 ++++++++++---------------- server/test/provisioning.test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 server/test/provisioning.test.js diff --git a/server/routes/provisioning.js b/server/routes/provisioning.js index f3a441a..01ba959 100644 --- a/server/routes/provisioning.js +++ b/server/routes/provisioning.js @@ -1,23 +1,17 @@ const express = require('express'); const router = express.Router(); -const { db } = require('../db/database'); -// Provision (pair) a device by entering its pairing code +// #90: the bare POST /api/provision was a vestigial SECOND pairing endpoint. It paired a +// device by pairing code but - unlike POST /api/provision/pair (server.js) - did NOT +// assign the device to a workspace, did NOT enforce checkDeviceLimit, and did NOT emit +// device:paired / dashboard:device-added. A silently-diverging duplicate of /pair that +// no client ever called (verified). Consolidated to /pair (the single, fully-protected +// pairing endpoint); this path now returns 410 Gone and points callers at the right one. +// +// The mount stays in the JWT-only partition (config/api-surface.js), so a Bearer st_ +// token still gets 401 from requireAuth before ever reaching this handler. router.post('/', (req, res) => { - const { pairing_code } = req.body; - if (!pairing_code) return res.status(400).json({ error: 'pairing_code required' }); - - const device = db.prepare('SELECT * FROM devices WHERE pairing_code = ?').get(pairing_code); - if (!device) return res.status(404).json({ error: 'No device found with that pairing code' }); - - // Clear pairing code and set online - db.prepare(` - UPDATE devices SET pairing_code = NULL, status = 'online', updated_at = strftime('%s','now') - WHERE id = ? - `).run(device.id); - - const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(device.id); - res.json(require('../lib/device-sanitize').stripDeviceSecrets(updated)); + res.status(410).json({ error: 'This endpoint has been removed. Pair a device with POST /api/provision/pair.' }); }); module.exports = router; diff --git a/server/test/provisioning.test.js b/server/test/provisioning.test.js new file mode 100644 index 0000000..5a1ddfa --- /dev/null +++ b/server/test/provisioning.test.js @@ -0,0 +1,29 @@ +'use strict'; + +// #90: the vestigial bare POST /api/provision is consolidated to POST /api/provision/pair. +// It must now return 410 Gone and point callers at /pair. Mounts the router in-process +// (it no longer touches the DB, so no server boot or injection is needed). The token -> +// 401 firewall for /api/provision is covered by the partition test in api.test.js. + +const { test, before, after } = require('node:test'); +const assert = require('node:assert/strict'); +const express = require('express'); +const provisioningRouter = require('../routes/provisioning'); + +const app = express(); +app.use(express.json()); +app.use('/api/provision', provisioningRouter); + +let server, base; +before(() => new Promise((resolve) => { + server = app.listen(0, () => { base = `http://127.0.0.1:${server.address().port}`; resolve(); }); +})); +after(() => { if (server) server.close(); }); + +test('provisioning: the bare POST /api/provision is gone (410, consolidated to /pair)', async () => { + const res = await fetch(base + '/api/provision', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pairing_code: '123456' }), + }); + assert.equal(res.status, 410); + assert.match(JSON.stringify(await res.json()), /provision\/pair/i, 'should point at POST /api/provision/pair'); +});