diff --git a/docs/openapi.json b/docs/openapi.json index e566dd9f..7411d6ef 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2781,18 +2781,42 @@ "schema": { "type": "object", "required": [ - "plugins", - "total" + "success", + "data", + "requestId", + "timestamp" ], "properties": { - "plugins": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PluginRecord" + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "object", + "required": [ + "plugins", + "total" + ], + "properties": { + "plugins": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PluginRecord" + } + }, + "total": { + "type": "integer" + } } }, - "total": { - "type": "integer" + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" } } } @@ -2825,7 +2849,31 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PluginRecord" + "type": "object", + "required": [ + "success", + "data", + "requestId", + "timestamp" + ], + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/PluginRecord" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } } } } @@ -2882,7 +2930,31 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PluginRecord" + "type": "object", + "required": [ + "success", + "data", + "requestId", + "timestamp" + ], + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/PluginRecord" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } } } } @@ -2970,29 +3042,53 @@ "schema": { "type": "object", "required": [ - "plugin" + "success", + "data", + "requestId", + "timestamp" ], "properties": { - "plugin": { - "$ref": "#/components/schemas/PluginRecord" + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "hook": { - "nullable": true, + "data": { "type": "object", + "required": [ + "plugin" + ], "properties": { - "ok": { - "type": "boolean" + "plugin": { + "$ref": "#/components/schemas/PluginRecord" }, "hook": { - "type": "string" - }, - "pluginId": { - "type": "string" - }, - "sandboxed": { - "type": "boolean" + "nullable": true, + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "hook": { + "type": "string" + }, + "pluginId": { + "type": "string" + }, + "sandboxed": { + "type": "boolean" + } + } } } + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" } } } @@ -3064,7 +3160,31 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PluginRecord" + "type": "object", + "required": [ + "success", + "data", + "requestId", + "timestamp" + ], + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/PluginRecord" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } } } } @@ -3131,7 +3251,31 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Dispute" + "type": "object", + "required": [ + "success", + "data", + "requestId", + "timestamp" + ], + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/Dispute" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } } } } @@ -3167,18 +3311,42 @@ "schema": { "type": "object", "required": [ - "disputes", - "total" + "success", + "data", + "requestId", + "timestamp" ], "properties": { - "disputes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Dispute" + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "object", + "required": [ + "disputes", + "total" + ], + "properties": { + "disputes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Dispute" + } + }, + "total": { + "type": "integer" + } } }, - "total": { - "type": "integer" + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" } } } @@ -3222,18 +3390,42 @@ "schema": { "type": "object", "required": [ - "dispute", - "events" + "success", + "data", + "requestId", + "timestamp" ], "properties": { - "dispute": { - "$ref": "#/components/schemas/Dispute" + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeEvent" + "data": { + "type": "object", + "required": [ + "dispute", + "events" + ], + "properties": { + "dispute": { + "$ref": "#/components/schemas/Dispute" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DisputeEvent" + } + } } + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" } } } @@ -3291,7 +3483,31 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Dispute" + "type": "object", + "required": [ + "success", + "data", + "requestId", + "timestamp" + ], + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/Dispute" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } } } } @@ -3332,18 +3548,42 @@ "schema": { "type": "object", "required": [ - "disputes", - "total" + "success", + "data", + "requestId", + "timestamp" ], "properties": { - "disputes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Dispute" + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "object", + "required": [ + "disputes", + "total" + ], + "properties": { + "disputes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Dispute" + } + }, + "total": { + "type": "integer" + } } }, - "total": { - "type": "integer" + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" } } } diff --git a/src/contracts/responseEnvelope.contract.test.ts b/src/contracts/responseEnvelope.contract.test.ts index 341c9131..84b48e4b 100644 --- a/src/contracts/responseEnvelope.contract.test.ts +++ b/src/contracts/responseEnvelope.contract.test.ts @@ -1,12 +1,27 @@ +import jwt from 'jsonwebtoken'; import request from 'supertest'; import { createApp } from '../app.js'; +import { NotFoundError } from '../errors/index.js'; import type { ApiEnvelope } from '../types/ResponseEnvelope.js'; +function makeAuthHeader(userId: string): string { + const secret = process.env.JWT_SECRET ?? 'test-jwt-secret'; + return `Bearer ${jwt.sign({ userId, sub: userId }, secret)}`; +} + describe('Response Envelope Contract Tests', () => { let app: ReturnType; beforeAll(() => { app = createApp(); + const stack = (app as unknown as { _router?: { stack?: Array<{ name?: string }> } })._router?.stack; + if (stack) { + const errIdx = stack.findIndex((l) => l.name === 'errorHandler'); + app.use((_req, _res, next) => next(new NotFoundError('Endpoint not found'))); + const notFoundLayer = stack.pop()!; + if (errIdx !== -1) stack.splice(errIdx, 0, notFoundLayer); + else stack.push(notFoundLayer); + } }); describe('GET /api/health', () => { @@ -156,6 +171,108 @@ describe('Response Envelope Contract Tests', () => { expect(diff).toBeGreaterThanOrEqual(0); expect(diff).toBeLessThan(5000); // Should be within 5 seconds } + expect(afterTime.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime()); + }); + }); + + describe('Marketplace plugins and billing disputes envelopes', () => { + const assertEnvelope = (body: ApiEnvelope, expectedSuccess: boolean) => { + expect(body).toHaveProperty('success', expectedSuccess); + expect(typeof body.requestId).toBe('string'); + expect(body.requestId.length).toBeGreaterThan(0); + expect(typeof body.timestamp).toBe('string'); + expect(body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + if (body.success) { + expect(body).toHaveProperty('data'); + } else { + expect(body).toHaveProperty('error'); + expect(typeof body.error.code).toBe('string'); + expect(typeof body.error.message).toBe('string'); + } + }; + + it('wraps marketplace plugin routes in canonical envelopes', async () => { + const listRes = await request(app).get('/api/marketplace/plugins'); + expect(listRes.status).toBe(200); + assertEnvelope(listRes.body as ApiEnvelope, true); + + const createRes = await request(app) + .post('/api/marketplace/plugins') + .set('Authorization', makeAuthHeader('envelope-plugin-user')) + .send({ + id: 'envelope-test-plugin', + name: 'Envelope Test Plugin', + version: '1.0.0', + hooks: ['before_charge'], + }); + expect(createRes.status).toBe(201); + assertEnvelope(createRes.body as ApiEnvelope, true); + + const getRes = await request(app).get('/api/marketplace/plugins/envelope-test-plugin'); + expect(getRes.status).toBe(200); + assertEnvelope(getRes.body as ApiEnvelope, true); + + const installRes = await request(app) + .post('/api/marketplace/plugins/envelope-test-plugin/install') + .set('Authorization', makeAuthHeader('envelope-plugin-user')); + expect(installRes.status).toBe(200); + assertEnvelope(installRes.body as ApiEnvelope, true); + + const uninstallRes = await request(app) + .delete('/api/marketplace/plugins/envelope-test-plugin/install') + .set('Authorization', makeAuthHeader('envelope-plugin-user')); + expect(uninstallRes.status).toBe(200); + assertEnvelope(uninstallRes.body as ApiEnvelope, true); + + const notFoundRes = await request(app).get('/api/marketplace/plugins/missing-plugin'); + expect(notFoundRes.status).toBe(404); + assertEnvelope(notFoundRes.body as ApiEnvelope, false); + }); + + it('wraps billing dispute routes in canonical envelopes', async () => { + process.env.ADMIN_API_KEY = process.env.ADMIN_API_KEY || 'test-admin-key'; + + const openRes = await request(app) + .post('/api/billing/disputes') + .set('Authorization', makeAuthHeader('envelope-dispute-user')) + .send({ + usage_event_id: 'evt-envelope-test-1', + reason: 'Verify canonical envelope on dispute routes', + }); + expect(openRes.status).toBe(201); + assertEnvelope(openRes.body as ApiEnvelope, true); + const disputeId = (openRes.body as { data: { id: string } }).data.id; + + const listRes = await request(app) + .get('/api/billing/disputes') + .set('Authorization', makeAuthHeader('envelope-dispute-user')); + expect(listRes.status).toBe(200); + assertEnvelope(listRes.body as ApiEnvelope, true); + + const detailRes = await request(app) + .get(`/api/billing/disputes/${disputeId}`) + .set('Authorization', makeAuthHeader('envelope-dispute-user')); + expect(detailRes.status).toBe(200); + assertEnvelope(detailRes.body as ApiEnvelope, true); + + const adminListRes = await request(app) + .get('/api/billing/disputes/admin/all') + .set('x-admin-api-key', process.env.ADMIN_API_KEY); + expect(adminListRes.status).toBe(200); + assertEnvelope(adminListRes.body as ApiEnvelope, true); + + const resolveRes = await request(app) + .post(`/api/billing/disputes/${disputeId}/resolve`) + .set('x-admin-api-key', process.env.ADMIN_API_KEY) + .send({ resolution: 'UPHELD', notes: 'Contract verified' }); + expect(resolveRes.status).toBe(200); + assertEnvelope(resolveRes.body as ApiEnvelope, true); + + const notFoundRes = await request(app) + .get('/api/billing/disputes/missing-dispute') + .set('Authorization', makeAuthHeader('envelope-dispute-user')); + expect(notFoundRes.status).toBe(404); + assertEnvelope(notFoundRes.body as ApiEnvelope, false); }); }); }); diff --git a/src/routes/billing/disputes.test.ts b/src/routes/billing/disputes.test.ts index 5b6afde3..8e03321a 100644 --- a/src/routes/billing/disputes.test.ts +++ b/src/routes/billing/disputes.test.ts @@ -18,6 +18,7 @@ jest.mock('better-sqlite3', () => { }); import express from 'express'; +import jwt from 'jsonwebtoken'; import request from 'supertest'; import { errorHandler } from '../../middleware/errorHandler.js'; import { createDisputesRouter } from './disputes.js'; @@ -37,6 +38,13 @@ const ADMIN_KEY = 'test-admin-key'; function buildApp(svc?: DisputeService) { const app = express(); app.use(express.json()); + app.use((req, _res, next) => { + const uid = req.headers['x-user-id']; + if (typeof uid === 'string' && !req.headers.authorization) { + req.headers.authorization = `Bearer ${jwt.sign({ userId: uid, sub: uid }, process.env.JWT_SECRET ?? 'test-jwt-secret')}`; + } + next(); + }); app.use('/api/billing/disputes', createDisputesRouter({ disputeService: svc })); app.use(errorHandler); return app; @@ -211,8 +219,11 @@ describe('POST /api/billing/disputes', () => { .set('x-user-id', 'u1') .send({ usage_event_id: 'evt-1', reason: 'wrong charge' }); expect(res.status).toBe(201); - expect(res.body.status).toBe('OPEN'); - expect(res.body.opened_by).toBe('u1'); + expect(res.body.success).toBe(true); + expect(typeof res.body.requestId).toBe('string'); + expect(typeof res.body.timestamp).toBe('string'); + expect(res.body.data.status).toBe('OPEN'); + expect(res.body.data.opened_by).toBe('u1'); }); it('returns 409 for duplicate usage_event_id', async () => { @@ -240,8 +251,9 @@ describe('GET /api/billing/disputes', () => { .get('/api/billing/disputes') .set('x-user-id', 'u1'); expect(res.status).toBe(200); - expect(res.body.total).toBe(1); - expect(res.body.disputes[0].opened_by).toBe('u1'); + expect(res.body.success).toBe(true); + expect(res.body.data.total).toBe(1); + expect(res.body.data.disputes[0].opened_by).toBe('u1'); }); }); @@ -269,8 +281,9 @@ describe('GET /api/billing/disputes/:id', () => { .get(`/api/billing/disputes/${d.id}`) .set('x-user-id', 'u1'); expect(res.status).toBe(200); - expect(res.body.dispute.id).toBe(d.id); - expect(Array.isArray(res.body.events)).toBe(true); + expect(res.body.success).toBe(true); + expect(res.body.data.dispute.id).toBe(d.id); + expect(Array.isArray(res.body.data.events)).toBe(true); }); it('returns 404 for unknown dispute', async () => { @@ -300,7 +313,8 @@ describe('POST /api/billing/disputes/:id/resolve', () => { .set('x-admin-api-key', ADMIN_KEY) .send({ resolution: 'REFUNDED' }); expect(res.status).toBe(200); - expect(res.body.status).toBe('REFUNDED'); + expect(res.body.success).toBe(true); + expect(res.body.data.status).toBe('REFUNDED'); }); it('resolves a dispute as admin (UPHELD)', async () => { @@ -312,7 +326,8 @@ describe('POST /api/billing/disputes/:id/resolve', () => { .set('x-admin-api-key', ADMIN_KEY) .send({ resolution: 'UPHELD', notes: 'charge was correct' }); expect(res.status).toBe(200); - expect(res.body.status).toBe('UPHELD'); + expect(res.body.success).toBe(true); + expect(res.body.data.status).toBe('UPHELD'); }); it('returns 400 for invalid resolution value', async () => { @@ -363,6 +378,7 @@ describe('GET /api/billing/disputes/admin/all', () => { .get('/api/billing/disputes/admin/all') .set('x-admin-api-key', ADMIN_KEY); expect(res.status).toBe(200); - expect(res.body.total).toBe(2); + expect(res.body.success).toBe(true); + expect(res.body.data.total).toBe(2); }); }); diff --git a/src/routes/billing/disputes.ts b/src/routes/billing/disputes.ts index d2d5b47f..29c4c454 100644 --- a/src/routes/billing/disputes.ts +++ b/src/routes/billing/disputes.ts @@ -17,6 +17,7 @@ import { adminAuth } from '../../middleware/adminAuth.js'; import { bodyValidator } from '../../middleware/validate.js'; import { logger } from '../../logger.js'; import { UnauthorizedError } from '../../errors/index.js'; +import { successEnvelope, getRequestId } from '../../lib/envelope.js'; import { openDisputeSchema, resolveDisputeSchema, @@ -48,7 +49,7 @@ export function createDisputesRouter(deps: DisputesRouterDeps = {}): Router { usageEventId: input.usage_event_id, }); - res.status(201).json(dispute); + res.status(201).json(successEnvelope(dispute, getRequestId(req))); } catch (err) { next(err); } @@ -63,7 +64,7 @@ export function createDisputesRouter(deps: DisputesRouterDeps = {}): Router { try { const actor = res.locals.authenticatedUser!.id; const disputes = svc.listForDeveloper(actor); - res.json({ disputes, total: disputes.length }); + res.json(successEnvelope({ disputes, total: disputes.length }, getRequestId(req))); } catch (err) { next(err); } @@ -72,10 +73,10 @@ export function createDisputesRouter(deps: DisputesRouterDeps = {}): Router { // ── GET /admin/all — admin lists all disputes ─────────────────────────── // Registered before /:id so 'admin' is not treated as a dispute id - router.get('/admin/all', adminAuth, (_req, res, next) => { + router.get('/admin/all', adminAuth, (req, res, next) => { try { const disputes = svc.listAll(); - res.json({ disputes, total: disputes.length }); + res.json(successEnvelope({ disputes, total: disputes.length }, getRequestId(req))); } catch (err) { next(err); } @@ -90,7 +91,7 @@ export function createDisputesRouter(deps: DisputesRouterDeps = {}): Router { const actor = res.locals.authenticatedUser!.id; const dispute = svc.getDisputeForDeveloper(req.params.id, actor); const events = svc.getEvents(dispute.id); - res.json({ dispute, events }); + res.json(successEnvelope({ dispute, events }, getRequestId(req))); } catch (err) { next(err); } @@ -119,7 +120,7 @@ export function createDisputesRouter(deps: DisputesRouterDeps = {}): Router { notes: input.notes, }); - res.json(dispute); + res.json(successEnvelope(dispute, getRequestId(req))); } catch (err) { next(err); } diff --git a/src/routes/marketplace/plugins.test.ts b/src/routes/marketplace/plugins.test.ts index 2a510216..7aa36c4e 100644 --- a/src/routes/marketplace/plugins.test.ts +++ b/src/routes/marketplace/plugins.test.ts @@ -19,6 +19,7 @@ jest.mock('better-sqlite3', () => { }); import express from 'express'; +import jwt from 'jsonwebtoken'; import request from 'supertest'; import { errorHandler } from '../../middleware/errorHandler.js'; import { createPluginsRouter } from './plugins.js'; @@ -47,6 +48,13 @@ const validManifest: PluginManifest = { function buildApp(repo?: InMemoryPluginRepository) { const app = express(); app.use(express.json()); + app.use((req, _res, next) => { + const uid = req.headers['x-user-id']; + if (typeof uid === 'string' && !req.headers.authorization) { + req.headers.authorization = `Bearer ${jwt.sign({ userId: uid, sub: uid }, process.env.JWT_SECRET ?? 'test-jwt-secret')}`; + } + next(); + }); app.use('/api/marketplace/plugins', createPluginsRouter({ pluginRepository: repo })); app.use(errorHandler); return app; @@ -223,8 +231,11 @@ describe('GET /api/marketplace/plugins', () => { it('returns empty list initially', async () => { const res = await request(buildApp()).get('/api/marketplace/plugins'); expect(res.status).toBe(200); - expect(res.body.plugins).toHaveLength(0); - expect(res.body.total).toBe(0); + expect(res.body.success).toBe(true); + expect(typeof res.body.requestId).toBe('string'); + expect(typeof res.body.timestamp).toBe('string'); + expect(res.body.data.plugins).toHaveLength(0); + expect(res.body.data.total).toBe(0); }); it('lists registered plugins', async () => { @@ -232,8 +243,9 @@ describe('GET /api/marketplace/plugins', () => { repo.register(validManifest, 'user-1'); const res = await request(buildApp(repo)).get('/api/marketplace/plugins'); expect(res.status).toBe(200); - expect(res.body.total).toBe(1); - expect(res.body.plugins[0].manifest.id).toBe(validManifest.id); + expect(res.body.success).toBe(true); + expect(res.body.data.total).toBe(1); + expect(res.body.data.plugins[0].manifest.id).toBe(validManifest.id); }); }); @@ -259,9 +271,10 @@ describe('POST /api/marketplace/plugins', () => { .set('x-user-id', 'user-1') .send(validManifest); expect(res.status).toBe(201); - expect(res.body.manifest.id).toBe(validManifest.id); - expect(res.body.owner_id).toBe('user-1'); - expect(res.body.status).toBe('available'); + expect(res.body.success).toBe(true); + expect(res.body.data.manifest.id).toBe(validManifest.id); + expect(res.body.data.owner_id).toBe('user-1'); + expect(res.body.data.status).toBe('available'); }); it('returns 409 when registering a duplicate plugin', async () => { @@ -287,7 +300,8 @@ describe('GET /api/marketplace/plugins/:id', () => { repo.register(validManifest, 'user-1'); const res = await request(buildApp(repo)).get(`/api/marketplace/plugins/${validManifest.id}`); expect(res.status).toBe(200); - expect(res.body.manifest.id).toBe(validManifest.id); + expect(res.body.success).toBe(true); + expect(res.body.data.manifest.id).toBe(validManifest.id); }); }); @@ -314,12 +328,13 @@ describe('POST /api/marketplace/plugins/:id/install', () => { .post(`/api/marketplace/plugins/${validManifest.id}/install`) .set('x-user-id', 'user-1'); expect(res.status).toBe(200); - expect(res.body.plugin.status).toBe('installed'); - expect(res.body.plugin.installed_by).toBe('user-1'); + expect(res.body.success).toBe(true); + expect(res.body.data.plugin.status).toBe('installed'); + expect(res.body.data.plugin.installed_by).toBe('user-1'); // before_charge is declared, so hook should be fired - expect(res.body.hook).not.toBeNull(); - expect(res.body.hook.ok).toBe(true); - expect(res.body.hook.sandboxed).toBe(true); + expect(res.body.data.hook).not.toBeNull(); + expect(res.body.data.hook.ok).toBe(true); + expect(res.body.data.hook.sandboxed).toBe(true); }); it('returns 409 when already installed', async () => { @@ -340,7 +355,8 @@ describe('POST /api/marketplace/plugins/:id/install', () => { .post(`/api/marketplace/plugins/${noBeforeCharge.id}/install`) .set('x-user-id', 'user-1'); expect(res.status).toBe(200); - expect(res.body.hook).toBeNull(); + expect(res.body.success).toBe(true); + expect(res.body.data.hook).toBeNull(); }); }); @@ -362,7 +378,8 @@ describe('DELETE /api/marketplace/plugins/:id/install', () => { .delete(`/api/marketplace/plugins/${validManifest.id}/install`) .set('x-user-id', 'user-1'); expect(res.status).toBe(200); - expect(res.body.status).toBe('available'); + expect(res.body.success).toBe(true); + expect(res.body.data.status).toBe('available'); }); it('returns 400 when plugin is not installed', async () => { diff --git a/src/routes/marketplace/plugins.ts b/src/routes/marketplace/plugins.ts index b28e585d..276beb04 100644 --- a/src/routes/marketplace/plugins.ts +++ b/src/routes/marketplace/plugins.ts @@ -18,6 +18,7 @@ import { adminAuth } from '../../middleware/adminAuth.js'; import { bodyValidator } from '../../middleware/validate.js'; import { logger } from '../../logger.js'; import { NotFoundError } from '../../errors/index.js'; +import { successEnvelope, getRequestId } from '../../lib/envelope.js'; import { pluginManifestSchema, defaultPluginRepository, @@ -45,10 +46,10 @@ export function createPluginsRouter(deps: PluginRouterDeps = {}): Router { const repo = deps.pluginRepository ?? defaultPluginRepository; // ── GET / — list all plugins ──────────────────────────────────────────── - router.get('/', (_req, res, next) => { + router.get('/', (req, res, next) => { try { const plugins = repo.list(); - res.json({ plugins, total: plugins.length }); + res.json(successEnvelope({ plugins, total: plugins.length }, getRequestId(req))); } catch (err) { next(err); } @@ -70,7 +71,7 @@ export function createPluginsRouter(deps: PluginRouterDeps = {}): Router { version: manifest.version, }); - res.status(201).json(record); + res.status(201).json(successEnvelope(record, getRequestId(req))); } catch (err) { next(err); } @@ -84,7 +85,7 @@ export function createPluginsRouter(deps: PluginRouterDeps = {}): Router { if (!record) { return next(new NotFoundError(`Plugin '${req.params.id}' not found`)); } - res.json(record); + res.json(successEnvelope(record, getRequestId(req))); } catch (err) { next(err); } @@ -111,7 +112,7 @@ export function createPluginsRouter(deps: PluginRouterDeps = {}): Router { sandboxed: hookResult?.sandboxed ?? false, }); - res.status(200).json({ plugin: record, hook: hookResult }); + res.status(200).json(successEnvelope({ plugin: record, hook: hookResult }, getRequestId(req))); } catch (err) { next(err); } @@ -129,7 +130,7 @@ export function createPluginsRouter(deps: PluginRouterDeps = {}): Router { logger.audit('PLUGIN_UNINSTALLED', actor, { pluginId: req.params.id }); - res.status(200).json(record); + res.status(200).json(successEnvelope(record, getRequestId(req))); } catch (err) { next(err); }