chore: lock the plugin boundary and remove 2.9.0 leftovers

This commit is contained in:
LukeGus committed 2026-09-25 07:24:46 -05:00
1 parent 6489b62ece
commit 2961297989
399 files changed
+3964 -5627

No files matched your search

+1 -1
View File
@@ -127,7 +127,7 @@ export async function bootCore(seed: Database.Database): Promise<BootedCore> {
await initializePlugins();
const { runPluginDataMigrations } =
await import("../../plugins/boot-migrations.js");
await import("../../upgrade/boot-migrations.js");
await runPluginDataMigrations();
return {
@@ -653,3 +653,36 @@ describe("named service providers", () => {
}
});
});
describe("contributes.http.legacyRedirects", () => {
const withRedirects = (legacyRedirects: unknown) =>
validateManifest(base({ contributes: { http: { legacyRedirects } } }));
it("accepts an old URL outside the plugin's namespace", () => {
expect(
withRedirects([
{ from: "/users/oidc/callback", to: "/callback", status: 308 },
{ from: "/host/old-callback", to: "/callback" },
]),
).toEqual([]);
});
it("refuses the plugin framework's own prefixes", () => {
expect(
withRedirects([{ from: "/plugin-api/other/x", to: "/x" }]).join(),
).toMatch(/cannot be under plugin-api/);
});
it("refuses a relative path, a bad status and unknown keys", () => {
const errors = withRedirects([
{ from: "users/x", to: "/x" },
{ from: "/a", to: "x" },
{ from: "/a", to: "/x", status: 302 },
{ from: "/a", to: "/x", extra: true },
]).join();
expect(errors).toMatch(/from must be an absolute path/);
expect(errors).toMatch(/to must be a path/);
expect(errors).toMatch(/status must be 307 or 308/);
expect(errors).toMatch(/extra/);
});
});
@@ -22,7 +22,7 @@ const state = vi.hoisted(() => ({
warnings: [] as string[],
}));
vi.mock("../../plugins/boot-migrations.js", () => ({
vi.mock("../../upgrade/boot-migrations.js", () => ({
runPluginDataMigrations: async () => {},
}));
@@ -224,7 +224,7 @@ describe("namespace escalation is refused", () => {
["admin.users.manage", /reserved core group/],
["hosts.view", /reserved core group/],
["credentials.read", /reserved core group/],
["snippets.edit", /reserved core group/],
["admin.plugins.manage", /reserved core group/],
])("refuses the core name %s", (name, pattern) => {
const { errors } = parseManifest(manifest("sample", [permission(name)]));
+137
View File
@@ -0,0 +1,137 @@
/**
* A user's rows in plugin tables travel with their export and come back on
* import, found through the table definitions plugins registered rather than
* any table name core knows.
*/
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
defineTable,
encryptedText,
id,
refUser,
text,
varchar,
} from "@termix/plugin-sdk/db";
let sqlite: Database.Database;
vi.mock("../../database/db/index.js", () => ({
getDb: () => drizzle(sqlite),
}));
vi.mock("../../utils/logger.js", () => ({
pluginLogger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
const { registerTable, resetPluginData } =
await import("../../plugins/data.js");
const {
importUserPluginRows,
listUserOwnedTables,
readUserPluginRows,
writeUserPluginTables,
} = await import("../../plugins/user-data.js");
const { createTableSql } = await import("../../plugins/table-builder.js");
// Stands for an adopted table; adoptLegacyTable only accepts real core names.
const bookmarks = {
...defineTable("bookmark", {
id: id(),
userId: refUser(),
path: varchar(200).notNull(),
note: text(),
token: encryptedText(),
}),
adopts: "bookmarks_legacy",
};
const shared = defineTable("shared_thing", { id: id(), label: text() });
function createTables(target: Database.Database) {
target.exec("CREATE TABLE users (id TEXT PRIMARY KEY)");
target.exec("CREATE TABLE ssh_data (id INTEGER PRIMARY KEY)");
for (const definition of [bookmarks, shared]) {
for (const statement of createTableSql("sqlite", "fixture", definition)) {
target.exec(statement);
}
}
}
beforeEach(() => {
sqlite = new Database(":memory:");
createTables(sqlite);
sqlite.exec("INSERT INTO users (id) VALUES ('u1'), ('u2')");
sqlite.exec(`INSERT INTO p_fixture_bookmark (user_id, path, note, token)
VALUES ('u1', '/etc', 'config', 'sealed'), ('u2', '/var', null, null)`);
registerTable("fixture", bookmarks);
registerTable("fixture", shared);
});
afterEach(() => {
resetPluginData();
sqlite.close();
});
describe("plugin user data", () => {
it("lists only tables with a user column, without keys or secrets", () => {
const [owned] = listUserOwnedTables();
expect(listUserOwnedTables()).toHaveLength(1);
expect(owned).toMatchObject({
pluginId: "fixture",
table: "p_fixture_bookmark",
legacyName: "bookmarks_legacy",
userColumn: "user_id",
columns: ["user_id", "path", "note"],
});
});
it("reads one user's rows", async () => {
expect(await readUserPluginRows("u1")).toEqual({
p_fixture_bookmark: [{ user_id: "u1", path: "/etc", note: "config" }],
});
});
it("round-trips through an export file into another user", async () => {
// The export file has the user row, as database.ts writes it first.
const file = new Database(":memory:");
file.exec("CREATE TABLE users (id TEXT PRIMARY KEY)");
file.exec("CREATE TABLE ssh_data (id INTEGER PRIMARY KEY)");
file.exec("INSERT INTO users (id) VALUES ('u1')");
expect(await writeUserPluginTables(file, "u1")).toBe(1);
sqlite.exec("DELETE FROM p_fixture_bookmark");
const result = await importUserPluginRows(file, "u2");
expect(result).toEqual({ imported: 1, skipped: 0, errors: [] });
expect(
sqlite.prepare("SELECT user_id, path FROM p_fixture_bookmark").all(),
).toEqual([{ user_id: "u2", path: "/etc" }]);
const again = await importUserPluginRows(file, "u2");
expect(again).toMatchObject({ imported: 0, skipped: 1 });
});
it("reads a pre-2.9 export by the legacy table name", async () => {
const file = new Database(":memory:");
file.exec(
"CREATE TABLE bookmarks_legacy (id INTEGER PRIMARY KEY, user_id TEXT, path TEXT, note TEXT)",
);
file.exec(
"INSERT INTO bookmarks_legacy (user_id, path, note) VALUES ('old', '/srv', 'x')",
);
const result = await importUserPluginRows(file, "u1");
expect(result.imported).toBe(1);
expect(
sqlite
.prepare(
"SELECT user_id, note FROM p_fixture_bookmark WHERE path = '/srv'",
)
.get(),
).toEqual({ user_id: "u1", note: "x" });
});
});