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) <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-06-12 19:20:00 -05:00 committed by screentinker
parent 538f4a7b03
commit 3305e79e61
2 changed files with 39 additions and 16 deletions

View file

@ -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;

View file

@ -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');
});