mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 20:00:08 +00:00
fix(mesh): gate Trigger 2 re-bootstrap on actual api_token change (#1076)
The PUT /api/nodes/:id handler closed and re-dialed the mesh callback bridge on every save that included api_token in the body, even when the token was unchanged. The frontend always sends the full formData on Save, so renames and compose_dir edits against a mesh-enabled proxy remote produced a wasted closeBridge + ensureBridge round-trip and a spurious manager_rejected entry in the activity log. Gate the re-bootstrap on a real value diff against the persisted token. Adds an existing-node lookup (returns 404 on missing id, which the handler previously lacked) so the comparison has the pre-update value. Reorders the guards so resource-not-found beats payload validation. Adds one integration test for the same-token path; the existing three trigger-2 cases continue to assert close + ensure firing on a real rotation, no-token-in-payload, and mesh-disabled.
This commit is contained in:
@@ -174,4 +174,24 @@ describe('Trigger 2: api_token rotation forces re-bootstrap', () => {
|
||||
expect(closeSpy).not.toHaveBeenCalled();
|
||||
expect(ensureSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fire when api_token in payload equals the current value', async () => {
|
||||
const closeSpy = vi.spyOn(MeshProxyTunnelDialer.getInstance(), 'closeBridge');
|
||||
const ensureSpy = vi.spyOn(MeshProxyTunnelDialer.getInstance(), 'ensureBridge')
|
||||
.mockResolvedValue(null);
|
||||
const nodeId = seedProxyNode(true);
|
||||
const existingToken = DatabaseService.getInstance().getNode(nodeId)?.api_token;
|
||||
expect(existingToken).toBeTruthy();
|
||||
|
||||
const res = await request(app)
|
||||
.put(`/api/nodes/${nodeId}`)
|
||||
.set('Authorization', authHeader)
|
||||
.send({ name: `renamed-${uniqueSuffix()}`, api_token: existingToken });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
await new Promise((r) => setImmediate(r));
|
||||
|
||||
expect(closeSpy).not.toHaveBeenCalled();
|
||||
expect(ensureSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,6 +224,11 @@ nodesRouter.put('/:id', async (req: Request, res: Response) => {
|
||||
const id = parseInt(nodeId);
|
||||
const updates = req.body;
|
||||
|
||||
const existingNode = DatabaseService.getInstance().getNode(id);
|
||||
if (!existingNode) {
|
||||
return res.status(404).json({ error: 'Node not found' });
|
||||
}
|
||||
|
||||
if (updates.api_url !== undefined && updates.api_url !== '') {
|
||||
const urlCheck = isValidRemoteUrl(updates.api_url);
|
||||
if (!urlCheck.valid) {
|
||||
@@ -239,11 +244,15 @@ nodesRouter.put('/:id', async (req: Request, res: Response) => {
|
||||
// Trigger 2: if the api_token was rotated on a mesh-enabled proxy-mode
|
||||
// remote, close the existing callback bridge and re-dial. The next
|
||||
// ensureBridge mints a JWT with the fresh token fingerprint so the
|
||||
// remote's tunnel auth gate accepts the upgrade.
|
||||
if (typeof updates.api_token === 'string') {
|
||||
const node = DatabaseService.getInstance().getNode(id);
|
||||
// remote's tunnel auth gate accepts the upgrade. Gate on actual value
|
||||
// change so a Save that only edits name / compose_dir does not cycle
|
||||
// the bridge (the frontend sends the full formData on every Save).
|
||||
const tokenChanged =
|
||||
typeof updates.api_token === 'string' &&
|
||||
updates.api_token !== existingNode.api_token;
|
||||
if (tokenChanged) {
|
||||
const meshEnabled = DatabaseService.getInstance().getNodeMeshEnabled(id);
|
||||
if (node && node.type === 'remote' && node.mode === 'proxy' && meshEnabled) {
|
||||
if (existingNode.type === 'remote' && existingNode.mode === 'proxy' && meshEnabled) {
|
||||
MeshProxyTunnelDialer.getInstance().closeBridge(id, 'peer token rotated');
|
||||
void MeshProxyTunnelDialer.getInstance().ensureBridge(id).catch((err) => {
|
||||
console.warn(`[Mesh] proactive re-bootstrap on token rotation failed for node ${id}: ${(err as Error).message}`);
|
||||
|
||||
Reference in New Issue
Block a user