* feat: shell controller, TabStage, layout and theme plugins with repair view

* feat: move the classic layout into its own bundled plugin

* feat: move color themes into a bundled themes plugin

* fix: plugin tests and type checks work with a linked sdk

* fix: home tab, layout switch and missing layout fallbacks for looks

* chore: add classic layout and themes to the readme

* chore: drop example plugin links from the plugin guide

* feat: plugin filters by type, paged browse, hide plugins needing newer termix

* feat: move desktop, status, import, audit and branding features into plugins

* feat: host lock api so a plugin can load hosts from a config file (#1581)

* chore: add hosts config to the plugin table

* feat: login policy hook for plugins and optional https redirect (#1584)

* chore: add login security to the plugin table

* feat: api key socket tickets so a master can open remote sessions (#1654)

* feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654)

* chore: add sync to the plugin table

* feat: move the sync screens and wizard out of core into the plugin (#1580)

* fix: add a neutral linked desktop hook for plugins (#1580)

* fix: show the linked account's name and admin flag in the app (#1580)

* feat: ssh transport providers, core ssh relay and plugin folders api

* feat: support FIDO2 security keys through ssh agent

* feat: add companion panels beside host tabs

* feat: mTLS client certificates for desktop server links

* feat: native web view for plugins in the desktop app

* fix: detect the desktop link window by Electron's default user agent

Fixes #1675

* fix: changelog for desktop link fix

* fix: keep text primary keys in the plugin data export

* fix: ship sharp binaries for Windows arm64 builds

* feat: client cert on the sync link types (sdk 1.0.18)

* chore: update translations

* feat: themed title bar for the desktop app (#1690)

* chore: changelog for the themed title bar

* fix: draw the title bar in the sidebar color

* feat: per host layout for the terminal panel bar

* chore: sdk 1.0.19 for the panel bar layout

* chore: bundle all 43 plugins for 26.10.3

* chore: update translations

---------

Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com>
Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-20 16:03:29 -05:00
1 parent aff29a9581
commit a89ccc6b07
577 files changed
+76479 -101831

No files matched your search

@@ -5,12 +5,6 @@ import {
createCurrentUserRepository,
} from "../repositories/factory.js";
import { apiLogger } from "../../utils/logger.js";
import { exportFilename, toCsv, toNdjson } from "../../utils/audit-export.js";
import {
logAudit,
getAuditUsername,
getRequestMeta,
} from "../../utils/audit-logger.js";
async function isAdminUser(userId: string | undefined): Promise<boolean> {
if (!userId) return false;
@@ -138,125 +132,4 @@ export function registerAuditLogRoutes(
.json({ error: "Failed to fetch audit log actions" });
}
});
/**
* @openapi
* /audit-logs/export:
* get:
* summary: Export audit logs
* description: Streams the full filtered result set as CSV or NDJSON. Accepts the same filters as GET /audit-logs. Admin only. The export is itself audited.
* tags:
* - Audit
* parameters:
* - in: query
* name: format
* schema: { type: string, enum: [csv, ndjson], default: csv }
* - in: query
* name: userId
* schema: { type: string }
* - in: query
* name: action
* schema: { type: string }
* - in: query
* name: resourceType
* schema: { type: string }
* - in: query
* name: success
* schema: { type: string, enum: [true, false] }
* - in: query
* name: startDate
* schema: { type: string, format: date-time }
* - in: query
* name: endDate
* schema: { type: string, format: date-time }
* responses:
* 200:
* description: Audit log file.
* 403:
* description: Not authorized.
* 500:
* description: Failed to export audit logs.
*/
router.get("/audit-logs/export", authenticateJWT, async (req, res) => {
const authReq = req as AuthenticatedRequest;
try {
if (!(await isAdminUser(authReq.userId))) {
return res.status(403).json({ error: "Not authorized" });
}
const format = req.query.format === "ndjson" ? "ndjson" : "csv";
const { userId, action, resourceType, success, startDate, endDate } =
req.query as Record<string, string | undefined>;
const filters = {
userId,
action,
resourceType,
success:
success !== undefined && success !== ""
? success === "true"
: undefined,
startDate,
endDate,
};
res.setHeader(
"Content-Type",
format === "csv" ? "text/csv; charset=utf-8" : "application/x-ndjson",
);
res.setHeader(
"Content-Disposition",
`attachment; filename="${exportFilename(format, new Date())}"`,
);
// Streamed in batches: an export is unbounded by definition, and the
// whole point is to move data out before retention drops it.
const BATCH = 500;
let offset = 0;
let exported = 0;
for (;;) {
const rows = await createCurrentAuditLogRepository().listForExport({
filters,
limit: BATCH,
offset,
});
if (rows.length === 0) break;
if (format === "csv") {
// Header only on the first batch.
const chunk = toCsv(rows);
res.write(
offset === 0 ? chunk : chunk.slice(chunk.indexOf("\n") + 1),
);
} else {
res.write(toNdjson(rows));
}
exported += rows.length;
offset += rows.length;
if (rows.length < BATCH) break;
}
res.end();
// Reading the whole trail is itself worth recording.
const { ipAddress, userAgent } = getRequestMeta(req);
void logAudit({
userId: authReq.userId!,
username: await getAuditUsername(authReq.userId!),
action: "export_audit_logs",
resourceType: "audit_log",
details: JSON.stringify({ format, exported, filters }),
ipAddress,
userAgent,
success: true,
});
} catch (err) {
apiLogger.error("Failed to export audit logs", err);
if (!res.headersSent) {
return res.status(500).json({ error: "Failed to export audit logs" });
}
res.end();
}
});
}