mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-06 02:25:24 +00:00
v26.10.6 (#1739)
* feat: let the login page send the token back to the mobile app * feat: add host-scripts entry to the plugin sdk * feat: let plugins say how they show in the mobile app * feat: frame host for running one plugin view outside the shell * fix: build the SDK host modules in production mode * chore: note the SDK host build fix in the changelog * feat: list plugin engine ranges for the mobile app * feat: host color and icon columns that sync * chore: update translations and plugin pins * fix: test * fix: show source address when rejecting proxy auth * fix: deleting your own account removes all of its data * fix(deps): update body-parser to 2.4.0 (#1736) * fix: body-parser 보안 패치 반영 * chore: 자동 검사를 막던 기존 JSON 서식 정리 --------- Co-authored-by: renovys <180682734+renovys@users.noreply.github.com> * fix(sdk): decode command output as GBK when it is not valid UTF-8 (#1735) * fix: honor shared credentials on shared hosts (#1730) * fix: honor shared credentials on shared hosts and in personal SSH auth * fix: snapshot shared SSH auth from a credential shared with the host owner * fix: size DataView list columns by the table's own width (#1713) * fix: size DataView list columns by the table's own width The list grid was built from every column, so columns hidden below md/lg left empty tracks and the visible cells landed under the wrong headers. Each width now gets a template of only its visible columns (CSS variables + container queries on the table, so a narrow panel counts, not the window), the header uses the same gap as the rows, and rows use a minimum height so wrapped cells no longer overlap. * test: wait for the title bar offset effect before asserting The bar shows once getTitleBarVisible resolves, but --termix-titlebar-height is set in a later passive effect, so reading it right after findByText raced (3-5 of 40 runs under load, on main too). * chore: rerun CI against dev-26.10.6 * fix: unlock passphrase keys once per connect burst (#1737) * feat: give ssh transports the host origin and sync id (#1717) * fix: wake other clients after a sync push (Plugin-Sync#1) * feat: look up local row ids by sync id for the sync plugin (Plugin-Sync#4) * feat: rail items can show a status dot (Plugin-Sync#5) * fix: add the desktop shared copy banner text (#1709) * feat: sidebar preference for alternating rows (#1712) * fix: striped rows text * feat: shell can open a saved host's editor for plugins (Plugin-SSH-Terminal#36) * fix: show the logo in the macos menu bar (#1728) * fix: number shortcuts skip the home tab (#1724) * fix: show plugins that could not be downloaded as failed (#1714) * fix: pass the host id as text when opening its editor * fix: retry a plugin bundle that was not ready yet (#1708) * fix: let commonjs plugins fall back to core packages on the desktop * fix: let non docker installs switch nginx to https (#1711) * feat: remove a folder share from its hosts too (#1719) * chore: changelog for merged pull requests * fix: accept the release .sig when installing a plugin from a file * feat: folder logins for shared hosts, read only plugin sync, full desktop builds * fix: test * fix: stop dev runner restart loops and narrow plugin watchers * chore: update translations and plugin pins * chore: lint, format, and bump version to 26.10.6 --------- Co-authored-by: renovys <renovy@gmail.com> Co-authored-by: renovys <180682734+renovys@users.noreply.github.com> Co-authored-by: Sebastian Cao <cycsmail@gmail.com> Co-authored-by: Jonathan Rehuel Lewerissa <jonathan.rehuel16@gmail.com> Co-authored-by: Rhiz3K <33246262+Rhiz3K@users.noreply.github.com>
This commit is contained in:
168 files changed
+35063
-339
No files matched your search
@@ -73,7 +73,7 @@ import {
|
||||
pluginFeatures,
|
||||
type PluginManifest,
|
||||
} from "./manifest.js";
|
||||
import { requireSignedPlugins } from "./trust.js";
|
||||
import { requireSignedPlugins, verifyAnyTrusted } from "./trust.js";
|
||||
import {
|
||||
parseChangelog,
|
||||
parseReleaseNotes,
|
||||
@@ -1048,7 +1048,15 @@ export function restoreMissingPlugins(): Promise<LoadedPlugin[]> {
|
||||
await readUninstalled(),
|
||||
);
|
||||
const restored: LoadedPlugin[] = [];
|
||||
for (const id of ids) {
|
||||
// Shown on the Plugins tab, so a plugin that could not be fetched is not
|
||||
// listed as working. It stays on and is tried again on the next start.
|
||||
const markMissing = (id: string, message: string) =>
|
||||
recordState(
|
||||
id,
|
||||
"failed",
|
||||
`Not in this image and could not be downloaded: ${message}`,
|
||||
).catch(() => {});
|
||||
for (const [index, id] of ids.entries()) {
|
||||
try {
|
||||
const record = await repository.findById(id);
|
||||
const { source, release } = await requireRelease(
|
||||
@@ -1075,11 +1083,14 @@ export function restoreMissingPlugins(): Promise<LoadedPlugin[]> {
|
||||
pluginLogger.warn(`Could not restore ${id}: ${message}`, {
|
||||
operation: "plugin_restore_missing",
|
||||
});
|
||||
await markMissing(id, message);
|
||||
// The registry is down or offline, so the rest would fail the same way.
|
||||
if (
|
||||
error instanceof PluginManageError &&
|
||||
error.code === "REGISTRY_UNREACHABLE"
|
||||
) {
|
||||
for (const rest of ids.slice(index + 1))
|
||||
await markMissing(rest, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1514,6 +1525,8 @@ interface StagedUpload {
|
||||
file: string;
|
||||
manifest: PluginManifest;
|
||||
expiresAt: number;
|
||||
/** Set when the upload came with a .sig an official key made. */
|
||||
official: boolean;
|
||||
}
|
||||
|
||||
const uploads = new Map<string, StagedUpload>();
|
||||
@@ -1537,6 +1550,29 @@ export interface UploadPreview {
|
||||
capabilities: string[];
|
||||
/** The version of an earlier upload this one replaces. */
|
||||
replaces: string | null;
|
||||
/** The .sig sent with it was made by the official Termix key. */
|
||||
official: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a .sig sent with an upload. An official signature makes the file
|
||||
* the same as a registry download, so it may stand in for one that could not
|
||||
* be fetched (an offline install). Anything else is refused.
|
||||
*/
|
||||
function checkUploadSignature(
|
||||
buffer: Buffer,
|
||||
signature: string,
|
||||
): { official: boolean } {
|
||||
const sha256 = crypto.createHash("sha256").update(buffer).digest("hex");
|
||||
const result = verifyAnyTrusted(buffer, sha256, signature);
|
||||
if (result.ok === false) {
|
||||
throw new PluginManageError(
|
||||
`The signature does not match this file: ${result.reason}`,
|
||||
400,
|
||||
"INVALID_SIGNATURE",
|
||||
);
|
||||
}
|
||||
return { official: result.tier === "official" };
|
||||
}
|
||||
|
||||
async function assertUploadsAllowed(): Promise<void> {
|
||||
@@ -1593,9 +1629,17 @@ function validateUpload(buffer: Buffer): PluginManifest {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/** Only another upload of the same id can be replaced from a file. */
|
||||
async function assertReplaceable(pluginId: string): Promise<string | null> {
|
||||
/**
|
||||
* Only another upload of the same id can be replaced from a file. An
|
||||
* officially signed file may also fill in a plugin whose code is missing, like
|
||||
* one that was on but could not be downloaded.
|
||||
*/
|
||||
async function assertReplaceable(
|
||||
pluginId: string,
|
||||
official = false,
|
||||
): Promise<string | null> {
|
||||
const { loader } = getPluginRuntime();
|
||||
if (official && !loader.get(pluginId)) return null;
|
||||
if (loader.bundledIds().has(pluginId)) {
|
||||
throw new PluginManageError(
|
||||
`${pluginId} ships with Termix and can only be replaced by a signed release`,
|
||||
@@ -1620,21 +1664,30 @@ async function assertReplaceable(pluginId: string): Promise<string | null> {
|
||||
* Checks an uploaded .tmxplug and holds it until the admin agrees to what
|
||||
* it asks for. Nothing is loaded yet.
|
||||
*/
|
||||
export async function stageUpload(buffer: Buffer): Promise<UploadPreview> {
|
||||
export async function stageUpload(
|
||||
buffer: Buffer,
|
||||
signature: string | null = null,
|
||||
): Promise<UploadPreview> {
|
||||
await assertUploadsAllowed();
|
||||
await clearUploads(Date.now());
|
||||
const manifest = validateUpload(buffer);
|
||||
const replaces = await assertReplaceable(manifest.id);
|
||||
const { official } = signature
|
||||
? checkUploadSignature(buffer, signature)
|
||||
: { official: false };
|
||||
const replaces = await assertReplaceable(manifest.id, official);
|
||||
|
||||
const token = crypto.randomBytes(16).toString("hex");
|
||||
const dir = getDownloadsDir();
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
const file = path.join(dir, `upload-${token}.tmxplug`);
|
||||
await fs.promises.writeFile(file, buffer);
|
||||
// Next to the file, where the loader looks for it.
|
||||
if (signature) await fs.promises.writeFile(`${file}.sig`, signature);
|
||||
uploads.set(token, {
|
||||
file,
|
||||
manifest,
|
||||
expiresAt: Date.now() + UPLOAD_TTL_MS,
|
||||
official,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1647,6 +1700,7 @@ export async function stageUpload(buffer: Buffer): Promise<UploadPreview> {
|
||||
features: pluginFeatures(manifest.features),
|
||||
capabilities: [...manifest.capabilities],
|
||||
replaces,
|
||||
official,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1676,7 +1730,7 @@ export function installUpload(
|
||||
);
|
||||
}
|
||||
assertConsent(options.capabilities, manifest.capabilities);
|
||||
await assertReplaceable(pluginId);
|
||||
await assertReplaceable(pluginId, upload.official);
|
||||
|
||||
const { createCurrentPluginRepository } = await repos();
|
||||
const repository = createCurrentPluginRepository();
|
||||
@@ -1689,6 +1743,7 @@ export function installUpload(
|
||||
plugin = await swapIn(pluginId, { upload: upload.file });
|
||||
} catch (error) {
|
||||
await fs.promises.rm(upload.file, { force: true });
|
||||
await fs.promises.rm(`${upload.file}.sig`, { force: true });
|
||||
const { loader } = getPluginRuntime();
|
||||
if (before && wasEnabled && loader.get(pluginId)) {
|
||||
await startAndRecord(pluginId);
|
||||
@@ -1701,7 +1756,7 @@ export function installUpload(
|
||||
}
|
||||
|
||||
await repository.update(pluginId, {
|
||||
tier: UNVERIFIED_TIER,
|
||||
tier: upload.official ? "official" : UNVERIFIED_TIER,
|
||||
registryId: null,
|
||||
pinnedVersion: null,
|
||||
autoUpdate: false,
|
||||
@@ -1725,7 +1780,7 @@ export function installUpload(
|
||||
}
|
||||
const state = await startAndRecord(pluginId);
|
||||
pluginLogger.warn(
|
||||
`Installed unverified plugin ${pluginId}@${manifest.version} from a file`,
|
||||
`Installed ${upload.official ? "officially signed" : "unverified"} plugin ${pluginId}@${manifest.version} from a file`,
|
||||
{ operation: "plugin_install" },
|
||||
);
|
||||
return { id: pluginId, version: manifest.version, state };
|
||||
|
||||
Reference in new issue
Block a user