feat: move session sharing and collab rooms into a plugin (B12)

This commit is contained in:
LukeGus committed 2026-09-23 23:42:46 -05:00
1 parent 791d8656a1
commit a276f6898c
207 files changed
+35771 -11072

No files matched your search

@@ -29,8 +29,6 @@ describe("bootstrap creates the tables the removed probes covered", () => {
"audit_logs",
"session_recordings",
"api_keys",
"session_shares",
"session_share_participants",
];
beforeEach(() => {
@@ -1,353 +0,0 @@
import { afterEach, describe, expect, it } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { SessionShareRepository } from "../../../database/repositories/session-share-repository.js";
describe("SessionShareRepository", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(
onWrite?: () => void | Promise<void>,
): Promise<SessionShareRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash)
VALUES ('owner-1', 'alice', 'hash'), ('guest-1', 'bob', 'hash');
INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type)
VALUES (1, 'owner-1', 'host-one', '10.0.0.1', 22, 'root', 'password'), (2, 'owner-1', 'host-two', '10.0.0.2', 22, 'root', 'password');
`);
return new SessionShareRepository(context, onWrite);
}
const FAR_FUTURE = "2999-01-01T00:00:00.000Z";
const FAR_PAST = "2000-01-01T00:00:00.000Z";
it("creates a share and finds it by id", async () => {
const repo = await createRepository();
const created = await repo.create({
id: "share-1",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-abc",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
expect(created).toMatchObject({
id: "share-1",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
linkToken: "token-abc",
permissionLevel: "read-only",
});
const found = await repo.findById("share-1");
expect(found).toMatchObject({ id: "share-1", sessionId: "session-1" });
});
it("findByLinkToken excludes revoked shares", async () => {
const repo = await createRepository();
await repo.create({
id: "share-revoked",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-revoked",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
expect(await repo.findByLinkToken("token-revoked")).not.toBeNull();
await repo.revoke("share-revoked", "owner-1");
expect(await repo.findByLinkToken("token-revoked")).toBeNull();
});
it("findByLinkToken excludes expired shares", async () => {
const repo = await createRepository();
await repo.create({
id: "share-expired",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-expired",
permissionLevel: "read-only",
expiresAt: FAR_PAST,
});
expect(await repo.findByLinkToken("token-expired")).toBeNull();
});
it("findByLinkToken returns active, non-expired, non-revoked shares", async () => {
const repo = await createRepository();
await repo.create({
id: "share-active",
hostId: 1,
ownerUserId: "owner-1",
protocol: "vnc",
sessionId: "guac-session-1",
shareType: "link",
linkToken: "token-active",
permissionLevel: "read-write",
expiresAt: FAR_FUTURE,
});
const found = await repo.findByLinkToken("token-active");
expect(found).toMatchObject({
id: "share-active",
protocol: "vnc",
permissionLevel: "read-write",
});
});
it("findSharesTargetingUser returns only active user-targeted shares with host/owner metadata", async () => {
const repo = await createRepository();
await repo.create({
id: "share-user-active",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "user",
targetUserId: "guest-1",
permissionLevel: "read-write",
expiresAt: FAR_FUTURE,
});
// Expired user share for the same target - must be excluded
await repo.create({
id: "share-user-expired",
hostId: 2,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-2",
shareType: "user",
targetUserId: "guest-1",
permissionLevel: "read-only",
expiresAt: FAR_PAST,
});
// Link share, not targeting a user - must be excluded even though it's active
await repo.create({
id: "share-link-active",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-3",
shareType: "link",
linkToken: "token-unrelated",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
const shares = await repo.findSharesTargetingUser("guest-1");
expect(shares).toHaveLength(1);
expect(shares[0]).toMatchObject({
id: "share-user-active",
hostName: "host-one",
ownerUsername: "alice",
});
});
it("revoke only affects the requesting owner's own share", async () => {
const repo = await createRepository();
await repo.create({
id: "share-owned",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-owned",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
expect(await repo.revoke("share-owned", "guest-1")).toBe(false);
expect(await repo.revoke("share-owned", "owner-1")).toBe(true);
});
it("revokeAsAdmin revokes regardless of owner", async () => {
const repo = await createRepository();
await repo.create({
id: "share-admin-target",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-admin",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
expect(await repo.revokeAsAdmin("share-admin-target")).toBe(true);
expect(await repo.findByLinkToken("token-admin")).toBeNull();
});
it("deleteExpiredShares removes only expired rows", async () => {
const repo = await createRepository();
await repo.create({
id: "share-old",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-old",
permissionLevel: "read-only",
expiresAt: FAR_PAST,
});
await repo.create({
id: "share-current",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-2",
shareType: "link",
linkToken: "token-current",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
const deletedCount = await repo.deleteExpiredShares();
expect(deletedCount).toBe(1);
expect(await repo.findById("share-old")).toBeNull();
expect(await repo.findById("share-current")).not.toBeNull();
});
it("touchShareUsage increments joinCount and sets lastJoinedAt", async () => {
const repo = await createRepository();
await repo.create({
id: "share-touch",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-touch",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
await repo.touchShareUsage("share-touch", "2026-01-01T00:00:00.000Z");
let row = await repo.findById("share-touch");
expect(row?.joinCount).toBe(1);
expect(row?.lastJoinedAt).toBe("2026-01-01T00:00:00.000Z");
await repo.touchShareUsage("share-touch", "2026-01-02T00:00:00.000Z");
row = await repo.findById("share-touch");
expect(row?.joinCount).toBe(2);
});
it("records and closes participant joins", async () => {
const repo = await createRepository();
await repo.create({
id: "share-participants",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-participants",
permissionLevel: "read-write",
expiresAt: FAR_FUTURE,
});
const participant = await repo.recordParticipantJoin(
"share-participants",
null,
"Guest",
);
expect(participant).toMatchObject({
shareId: "share-participants",
userId: null,
guestLabel: "Guest",
});
expect(participant.leftAt).toBeNull();
await repo.recordParticipantLeave(participant.id);
});
it("write hook fires on mutating operations", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
await repo.create({
id: "share-write-hook",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-write-hook",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
expect(writeCount).toBe(1);
await repo.revoke("share-write-hook", "owner-1");
expect(writeCount).toBe(2);
});
it("deleteSharesForHost removes all shares for a host", async () => {
const repo = await createRepository();
await repo.create({
id: "share-host-1a",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-1",
shareType: "link",
linkToken: "token-h1a",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
await repo.create({
id: "share-host-1b",
hostId: 1,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-2",
shareType: "link",
linkToken: "token-h1b",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
await repo.create({
id: "share-host-2",
hostId: 2,
ownerUserId: "owner-1",
protocol: "ssh",
sessionId: "session-3",
shareType: "link",
linkToken: "token-h2",
permissionLevel: "read-only",
expiresAt: FAR_FUTURE,
});
expect(await repo.deleteSharesForHost(1)).toBe(2);
expect(await repo.findById("share-host-2")).not.toBeNull();
});
});
@@ -204,7 +204,7 @@ describe("plugins route", () => {
id: "guest-plugin",
manifestJson: JSON.stringify({
capabilities: ["hosts:read"],
contributes: { guest: true },
contributes: { guest: true, guestViews: ["shared"] },
}),
}),
);
@@ -225,7 +225,10 @@ describe("plugins route", () => {
expect(body.map((plugin: { id: string }) => plugin.id)).toEqual([
"guest-plugin",
]);
expect(body[0].contributes).toEqual({ guest: true });
expect(body[0].contributes).toEqual({
guest: true,
guestViews: ["shared"],
});
expect(body[0]).not.toHaveProperty("capabilities");
expect(body[0]).not.toHaveProperty("lastError");
});