mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 12:48:10 +00:00
fix(blueprints): fail closed on marker ownership for apply and withdraw (#1694)
* fix(blueprints): fail closed on marker ownership for apply and withdraw Require a matching .blueprint.json under the stack lock, persist required_blueprint_id on deletion intents, remove the legacy remote apply fallback, and protect the marker in the file explorer. * fix(blueprints): add CodeQL path barriers on ownership probes Use the canonical resolve-and-startsWith sanitizer inline at the marker and stack-directory fs sinks so js/path-injection clears. * fix(blueprints): block delete on failed withdraw and defer marker write Refuse Blueprint DELETE when pre-delete withdraw does not complete, and write .blueprint.json only after a successful deploy so failed applies cannot orphan stacks or claim an unapplied revision. * test(blueprints): align lock-order assert with deferred marker write Update the per-stack lock ordering expectations to compose, cleanup, deploy, then marker after the partial-apply fix. * fix(deps): bump postcss past GHSA-r28c-9q8g-f849 for npm audit Raise the Vitest/Vite transitive postcss to 8.5.23 so Backend CI audit --audit-level=high passes.
This commit is contained in:
@@ -129,3 +129,71 @@ describe('POST /api/blueprints/apply-local (node-to-node atomic apply)', () => {
|
||||
expect(StackOpLockService.getInstance().get(1, 'apply-local-busy')?.action).toBe('update');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/blueprints/withdraw-local', () => {
|
||||
let viewerCookie: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const bcrypt = (await import('bcrypt')).default;
|
||||
const passwordHash = await bcrypt.hash('bp-wd-viewer-pass', 1);
|
||||
DatabaseService.getInstance().addUser({
|
||||
username: 'bp-wd-viewer',
|
||||
password_hash: passwordHash,
|
||||
role: 'viewer',
|
||||
});
|
||||
const res = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({ username: 'bp-wd-viewer', password: 'bp-wd-viewer-pass' });
|
||||
const cookies = res.headers['set-cookie'] as string | string[];
|
||||
viewerCookie = Array.isArray(cookies) ? cookies[0] : cookies;
|
||||
});
|
||||
|
||||
it('rejects an invalid stack name', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/blueprints/withdraw-local')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ stackName: '../escape', blueprintId: 1 });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain('Invalid stack name');
|
||||
});
|
||||
|
||||
it('rejects a non-positive blueprintId', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/blueprints/withdraw-local')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ stackName: 'wd-local-stack', blueprintId: 0 });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toMatch(/blueprintId/i);
|
||||
});
|
||||
|
||||
it('returns 403 for a viewer without stack:delete', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/blueprints/withdraw-local')
|
||||
.set('Cookie', viewerCookie)
|
||||
.send({ stackName: 'wd-local-stack', blueprintId: 1 });
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it('returns 409 self_stack_protected for Sencho own stack', async () => {
|
||||
const selfStackGuard = await import('../helpers/selfStackGuard');
|
||||
vi.spyOn(selfStackGuard, 'refuseIfSelfStack').mockImplementation(async (_req, res) => {
|
||||
res.status(409).json({ error: 'self', code: 'self_stack_protected' });
|
||||
return true;
|
||||
});
|
||||
const res = await request(app)
|
||||
.post('/api/blueprints/withdraw-local')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ stackName: 'sencho-self', blueprintId: 1 });
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.code).toBe('self_stack_protected');
|
||||
});
|
||||
|
||||
it('returns already_absent when the stack directory is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/blueprints/withdraw-local')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ stackName: `wd-absent-${Date.now()}`, blueprintId: 42 });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.status).toBe('already_absent');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user