feat: add plugin worker protocol, bootstrap and plugin_storage table

Worker-side ctx proxy plus the message envelope it talks over, and the
per-plugin key/value table behind ctx.storage with migrations for all
three dialects. Rows cascade with the plugin so uninstalling leaves
nothing behind.
This commit is contained in:
LukeGus committed 2026-09-21 07:19:01 -05:00
1 parent a1805cb7ae
commit 248698f62d
19 files changed
+30035

No files matched your search

@@ -33,6 +33,7 @@ import { NetworkTopologyRepository } from "./network-topology-repository.js";
import { OpenTabRepository } from "./open-tab-repository.js";
import { OpksshTokenRepository } from "./opkssh-token-repository.js";
import { PluginRepository } from "./plugin-repository.js";
import { PluginStorageRepository } from "./plugin-storage-repository.js";
import { PluginPermissionGrantRepository } from "./plugin-permission-grant-repository.js";
import { PluginRegistryRepository } from "./plugin-registry-repository.js";
import { PluginInstallCountRepository } from "./plugin-install-count-repository.js";
@@ -385,6 +386,13 @@ export function createCurrentPluginPermissionGrantRepository(): PluginPermission
);
}
export function createCurrentPluginStorageRepository(): PluginStorageRepository {
return new PluginStorageRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("plugin_storage_repository_write"),
);
}
export function createCurrentPluginRegistryRepository(): PluginRegistryRepository {
return new PluginRegistryRepository(
createCurrentRepositoryContext(),
@@ -0,0 +1,93 @@
import { and, eq } from "drizzle-orm";
import { pluginStorage } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
import { insertReturning, updateReturning } from "./returning.js";
export type PluginStorageRecord = typeof pluginStorage.$inferSelect;
export class PluginStorageRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async get(pluginId: string, key: string): Promise<string | null> {
const rows = await this.context.drizzle
.select()
.from(pluginStorage)
.where(
and(eq(pluginStorage.pluginId, pluginId), eq(pluginStorage.key, key)),
)
.limit(1);
return rows[0]?.value ?? null;
}
async listKeys(pluginId: string): Promise<string[]> {
const rows = await this.context.drizzle
.select()
.from(pluginStorage)
.where(eq(pluginStorage.pluginId, pluginId));
return rows.map((row) => row.key);
}
async set(
pluginId: string,
key: string,
value: string,
now = new Date().toISOString(),
): Promise<void> {
const existing = await this.context.drizzle
.select()
.from(pluginStorage)
.where(
and(eq(pluginStorage.pluginId, pluginId), eq(pluginStorage.key, key)),
)
.limit(1);
if (existing[0]) {
await updateReturning(
this.context,
pluginStorage,
{ value, updatedAt: now },
and(eq(pluginStorage.pluginId, pluginId), eq(pluginStorage.key, key))!,
);
} else {
await insertReturning(this.context, pluginStorage, {
pluginId,
key,
value,
updatedAt: now,
});
}
await this.afterWrite();
}
async delete(pluginId: string, key: string): Promise<boolean> {
const result = await this.context.drizzle
.delete(pluginStorage)
.where(
and(eq(pluginStorage.pluginId, pluginId), eq(pluginStorage.key, key)),
);
const affected = rowsAffected(result) > 0;
if (affected) await this.afterWrite();
return affected;
}
async deleteByPlugin(pluginId: string): Promise<number> {
const result = await this.context.drizzle
.delete(pluginStorage)
.where(eq(pluginStorage.pluginId, pluginId));
if (rowsAffected(result) > 0) await this.afterWrite();
return rowsAffected(result);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}