feat: harden plugin api for separate repos, signed updates and generic external login

This commit is contained in:
LukeGus committed 2026-09-29 18:11:59 -05:00
1 parent fb9c168b6b
commit 80944301dd
310 files changed
+6577 -4927

No files matched your search

+6
View File
@@ -55,5 +55,11 @@
"adminGroupDesc": "Users in this group are granted admin. Leave empty to disable group sync.",
"caCert": "Custom CA Certificate",
"caCertDesc": "Optional. PEM-encoded CA certificate for OIDC providers using a private or self-signed CA. Leave empty to use the system trust store."
},
"settings": {
"silentLoginDefault": {
"label": "Sign in with SSO automatically",
"description": "Send visitors straight to the first SSO provider instead of showing the login form. The OIDC_SILENT_LOGIN_DEFAULT environment variable overrides this."
}
}
}
+8
View File
@@ -41,6 +41,14 @@
"type": "custom",
"component": "providers",
"permission": "manage"
},
{
"key": "silentLoginDefault",
"type": "boolean",
"labelKey": "settings.silentLoginDefault.label",
"descriptionKey": "settings.silentLoginDefault.description",
"default": false,
"permission": "manage"
}
]
},
+18 -2
View File
@@ -6,14 +6,30 @@
"scripts": {
"build": "termix-plugin build",
"test": "termix-plugin test",
"typecheck": "tsc -b ../../tsconfig.plugins.backend.json ../../tsconfig.plugins.frontend.json",
"typecheck": "tsc -p .",
"validate": "termix-plugin validate"
},
"dependencies": {
"jose": "^6.2.12",
"lucide-react": "^1.33.0",
"undici": "^8.10.2"
},
"devDependencies": {
"@termix/plugin-sdk": "*"
"@termix/plugin-sdk": "^1.0.0",
"@testing-library/dom": "^10.4.2",
"@testing-library/react": "^16.3.3",
"@types/node": "^26.2.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"better-sqlite3": "^13.0.3",
"jsdom": "^30.0.1",
"vitest": "^4.1.11"
},
"peerDependencies": {
"@termix/plugin-sdk": "^1.0.0",
"drizzle-orm": "^0.45.2",
"express": "^5.2.1",
"react": "^19.2.8",
"sonner": "^2.0.8"
}
}
+16 -3
View File
@@ -5,6 +5,16 @@ import { createProviderStore } from "./providers.js";
import { createSsoLogin, METHOD_ID } from "./login.js";
import { PUBLIC_PATHS, registerSsoRoutes } from "./routes.js";
/**
* Whether the login page starts the first provider without a click. The
* OIDC_SILENT_LOGIN_DEFAULT environment variable pins it either way.
*/
export async function silentLoginDefault(ctx: PluginContext): Promise<boolean> {
const pinned = process.env.OIDC_SILENT_LOGIN_DEFAULT;
if (pinned !== undefined) return pinned.trim().toLowerCase() === "true";
return (await ctx.settings.get("silentLoginDefault")) === true;
}
export async function activate(ctx: PluginContext) {
const table = await ctx.db.define(providers);
const store = createProviderStore(ctx, table);
@@ -17,11 +27,14 @@ export async function activate(ctx: PluginContext) {
icon: "key-round",
kind: "redirect",
external: true,
describe: async () =>
(await store.listInstances()).map((instance) => ({
describe: async () => {
const autoStart = await silentLoginDefault(ctx);
return (await store.listInstances()).map((instance, index) => ({
...instance,
enabled: true,
})),
...(autoStart && index === 0 ? { autoStart: true } : {}),
}));
},
start: login.start,
callback: login.callback,
});
+2
View File
@@ -57,6 +57,7 @@ export async function startServer(
permissions?: string[];
linkedUsers?: Record<string, number>;
before?: (sqlite: TestDb["sqlite"]) => void;
settings?: Record<string, unknown>;
} = {},
): Promise<TestServer> {
const db = await createTestDb(pluginDir, { before: options.before });
@@ -74,6 +75,7 @@ export async function startServer(
permissions: options.permissions,
linkedUsers: options.linkedUsers,
baseUrl: "https://termix.test",
settings: options.settings,
});
const { activate } = await import("../../src/backend/index.js");
+25
View File
@@ -185,6 +185,31 @@ describe("the login method", () => {
]);
});
it("marks the first provider to start on its own with silent sign-in on", async () => {
const s = await startLegacy({ settings: { silentLoginDefault: true } });
expect(await s.mock.auth.loginMethods[0].describe!()).toEqual([
{
id: "3",
label: "Keycloak",
type: "oidc",
enabled: true,
autoStart: true,
},
]);
});
it("lets OIDC_SILENT_LOGIN_DEFAULT override the setting", async () => {
const s = await startLegacy({ settings: { silentLoginDefault: true } });
vi.stubEnv("OIDC_SILENT_LOGIN_DEFAULT", "false");
const [instance] = await s.mock.auth.loginMethods[0].describe!();
expect(instance.autoStart).toBeUndefined();
vi.stubEnv("OIDC_SILENT_LOGIN_DEFAULT", "true");
s.mock.settings.clear();
expect((await s.mock.auth.loginMethods[0].describe!())[0].autoStart).toBe(
true,
);
});
it("falls back to the provider configured through the environment", async () => {
server = await startServer();
vi.stubEnv("OIDC_CLIENT_ID", "env-client");