@checkstack/backend 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +225 -0
  2. package/drizzle/0000_loose_yellow_claw.sql +28 -0
  3. package/drizzle/meta/0000_snapshot.json +187 -0
  4. package/drizzle/meta/_journal.json +13 -0
  5. package/drizzle.config.ts +10 -0
  6. package/package.json +42 -0
  7. package/src/db.ts +20 -0
  8. package/src/health-check-plugin-integration.test.ts +93 -0
  9. package/src/index.ts +419 -0
  10. package/src/integration/event-bus.integration.test.ts +313 -0
  11. package/src/logger.ts +65 -0
  12. package/src/openapi-router.ts +177 -0
  13. package/src/plugin-lifecycle.test.ts +276 -0
  14. package/src/plugin-manager/api-router.ts +163 -0
  15. package/src/plugin-manager/core-services.ts +312 -0
  16. package/src/plugin-manager/dependency-sorter.ts +103 -0
  17. package/src/plugin-manager/deregistration-guard.ts +41 -0
  18. package/src/plugin-manager/extension-points.ts +85 -0
  19. package/src/plugin-manager/index.ts +13 -0
  20. package/src/plugin-manager/plugin-admin-router.ts +89 -0
  21. package/src/plugin-manager/plugin-loader.ts +464 -0
  22. package/src/plugin-manager/types.ts +14 -0
  23. package/src/plugin-manager.test.ts +464 -0
  24. package/src/plugin-manager.ts +431 -0
  25. package/src/rpc-rest-compat.test.ts +80 -0
  26. package/src/schema.ts +46 -0
  27. package/src/services/config-service.test.ts +66 -0
  28. package/src/services/config-service.ts +322 -0
  29. package/src/services/event-bus.test.ts +469 -0
  30. package/src/services/event-bus.ts +317 -0
  31. package/src/services/health-check-registry.test.ts +101 -0
  32. package/src/services/health-check-registry.ts +27 -0
  33. package/src/services/jwt.ts +45 -0
  34. package/src/services/keystore.test.ts +198 -0
  35. package/src/services/keystore.ts +136 -0
  36. package/src/services/plugin-installer.test.ts +90 -0
  37. package/src/services/plugin-installer.ts +70 -0
  38. package/src/services/queue-manager.ts +382 -0
  39. package/src/services/queue-plugin-registry.ts +17 -0
  40. package/src/services/queue-proxy.ts +182 -0
  41. package/src/services/service-registry.ts +35 -0
  42. package/src/test-preload.ts +114 -0
  43. package/src/utils/plugin-discovery.test.ts +383 -0
  44. package/src/utils/plugin-discovery.ts +157 -0
  45. package/src/utils/strip-public-schema.ts +40 -0
  46. package/tsconfig.json +6 -0
@@ -0,0 +1,157 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { eq, and } from "drizzle-orm";
4
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
5
+ import { plugins } from "../schema";
6
+
7
+ export interface PluginMetadata {
8
+ packageName: string; // From package.json "name"
9
+ pluginPath: string; // Absolute path to plugin directory
10
+ type: "backend" | "frontend" | "common";
11
+ enabled: boolean;
12
+ }
13
+
14
+ /**
15
+ * Extracts plugin metadata from a plugin directory by reading package.json
16
+ * @param pluginDir - Absolute path to plugin directory
17
+ * @returns PluginMetadata if valid, undefined if invalid/malformed
18
+ */
19
+ export function extractPluginMetadata({
20
+ pluginDir,
21
+ }: {
22
+ pluginDir: string;
23
+ }): PluginMetadata | undefined {
24
+ const pkgJsonPath = path.join(pluginDir, "package.json");
25
+
26
+ if (!fs.existsSync(pkgJsonPath)) {
27
+ return undefined;
28
+ }
29
+
30
+ try {
31
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
32
+
33
+ if (!pkgJson.name || typeof pkgJson.name !== "string") {
34
+ return undefined;
35
+ }
36
+
37
+ // Determine plugin type from package name suffix
38
+ let type: "backend" | "frontend" | "common";
39
+ if (pkgJson.name.endsWith("-backend")) {
40
+ type = "backend";
41
+ } else if (pkgJson.name.endsWith("-frontend")) {
42
+ type = "frontend";
43
+ } else if (pkgJson.name.endsWith("-common")) {
44
+ type = "common";
45
+ } else {
46
+ return undefined; // Not a valid plugin package
47
+ }
48
+
49
+ return {
50
+ packageName: pkgJson.name,
51
+ pluginPath: pluginDir,
52
+ type,
53
+ enabled: true, // Local plugins are always enabled
54
+ };
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Discovers all local plugins in the monorepo workspace
62
+ * Scans both packages/ (core components) and plugins/ (replaceable providers)
63
+ * @param workspaceRoot - Absolute path to workspace root
64
+ * @param type - Optional filter for plugin type (backend, frontend, or common)
65
+ * @returns Array of PluginMetadata for all valid plugins
66
+ */
67
+ export function discoverLocalPlugins({
68
+ workspaceRoot,
69
+ type,
70
+ }: {
71
+ workspaceRoot: string;
72
+ type?: "backend" | "frontend" | "common";
73
+ }): PluginMetadata[] {
74
+ const discovered: PluginMetadata[] = [];
75
+
76
+ // Scan both packages/ (core) and plugins/ (providers)
77
+ const dirsToScan = [
78
+ path.join(workspaceRoot, "core"),
79
+ path.join(workspaceRoot, "plugins"),
80
+ ];
81
+
82
+ for (const scanDir of dirsToScan) {
83
+ if (!fs.existsSync(scanDir)) {
84
+ continue;
85
+ }
86
+
87
+ const entries = fs.readdirSync(scanDir, { withFileTypes: true });
88
+
89
+ for (const entry of entries) {
90
+ if (!entry.isDirectory()) {
91
+ continue;
92
+ }
93
+
94
+ const pluginDir = path.join(scanDir, entry.name);
95
+ const metadata = extractPluginMetadata({ pluginDir });
96
+
97
+ if (metadata && (!type || metadata.type === type)) {
98
+ discovered.push(metadata);
99
+ }
100
+ }
101
+ }
102
+
103
+ return discovered;
104
+ }
105
+
106
+ /**
107
+ * Syncs local plugins to the database
108
+ * - Inserts new plugins
109
+ * - Updates paths for existing plugins (handles renames)
110
+ * - Does not modify remotely installed plugins (isUninstallable=true)
111
+ * @param localPlugins - Array of local plugin metadata
112
+ * @param db - Database connection
113
+ */
114
+ export async function syncPluginsToDatabase({
115
+ localPlugins,
116
+ db,
117
+ }: {
118
+ localPlugins: PluginMetadata[];
119
+ db: NodePgDatabase<Record<string, unknown>>;
120
+ }): Promise<void> {
121
+ for (const plugin of localPlugins) {
122
+ // Check if plugin already exists
123
+ const existing = await db
124
+ .select()
125
+ .from(plugins)
126
+ .where(eq(plugins.name, plugin.packageName))
127
+ .limit(1);
128
+
129
+ if (existing.length === 0) {
130
+ // Insert new plugin
131
+ await db.insert(plugins).values({
132
+ name: plugin.packageName,
133
+ path: plugin.pluginPath,
134
+ type: plugin.type,
135
+ enabled: plugin.enabled,
136
+ isUninstallable: false, // Local plugins are part of monorepo
137
+ });
138
+ } else {
139
+ // Update existing plugin ONLY if it's a local plugin (not remotely installed)
140
+ // This handles the case where a plugin directory was renamed
141
+ if (!existing[0].isUninstallable) {
142
+ await db
143
+ .update(plugins)
144
+ .set({
145
+ path: plugin.pluginPath,
146
+ type: plugin.type,
147
+ })
148
+ .where(
149
+ and(
150
+ eq(plugins.name, plugin.packageName),
151
+ eq(plugins.isUninstallable, false)
152
+ )
153
+ );
154
+ }
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Strip `"public".` schema references from all SQL migration files in a folder.
6
+ *
7
+ * This is called at runtime before migrations are executed, modifying the files
8
+ * in-place to ensure they work correctly with the plugin's search_path.
9
+ *
10
+ * @param migrationsFolder - Path to the drizzle migrations folder
11
+ * @returns Number of files that were modified
12
+ */
13
+ export function stripPublicSchemaFromMigrations(
14
+ migrationsFolder: string
15
+ ): number {
16
+ if (!fs.existsSync(migrationsFolder)) {
17
+ return 0;
18
+ }
19
+
20
+ const sqlFiles = fs
21
+ .readdirSync(migrationsFolder)
22
+ .filter((f) => f.endsWith(".sql"));
23
+
24
+ let modifiedCount = 0;
25
+
26
+ for (const file of sqlFiles) {
27
+ const filePath = path.join(migrationsFolder, file);
28
+ const content = fs.readFileSync(filePath, "utf8");
29
+
30
+ // Replace "public". prefix (with or without quotes around table name)
31
+ const fixed = content.replaceAll('"public".', "");
32
+
33
+ if (fixed !== content) {
34
+ fs.writeFileSync(filePath, fixed, "utf8");
35
+ modifiedCount++;
36
+ }
37
+ }
38
+
39
+ return modifiedCount;
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }