@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.
- package/CHANGELOG.md +225 -0
- package/drizzle/0000_loose_yellow_claw.sql +28 -0
- package/drizzle/meta/0000_snapshot.json +187 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +42 -0
- package/src/db.ts +20 -0
- package/src/health-check-plugin-integration.test.ts +93 -0
- package/src/index.ts +419 -0
- package/src/integration/event-bus.integration.test.ts +313 -0
- package/src/logger.ts +65 -0
- package/src/openapi-router.ts +177 -0
- package/src/plugin-lifecycle.test.ts +276 -0
- package/src/plugin-manager/api-router.ts +163 -0
- package/src/plugin-manager/core-services.ts +312 -0
- package/src/plugin-manager/dependency-sorter.ts +103 -0
- package/src/plugin-manager/deregistration-guard.ts +41 -0
- package/src/plugin-manager/extension-points.ts +85 -0
- package/src/plugin-manager/index.ts +13 -0
- package/src/plugin-manager/plugin-admin-router.ts +89 -0
- package/src/plugin-manager/plugin-loader.ts +464 -0
- package/src/plugin-manager/types.ts +14 -0
- package/src/plugin-manager.test.ts +464 -0
- package/src/plugin-manager.ts +431 -0
- package/src/rpc-rest-compat.test.ts +80 -0
- package/src/schema.ts +46 -0
- package/src/services/config-service.test.ts +66 -0
- package/src/services/config-service.ts +322 -0
- package/src/services/event-bus.test.ts +469 -0
- package/src/services/event-bus.ts +317 -0
- package/src/services/health-check-registry.test.ts +101 -0
- package/src/services/health-check-registry.ts +27 -0
- package/src/services/jwt.ts +45 -0
- package/src/services/keystore.test.ts +198 -0
- package/src/services/keystore.ts +136 -0
- package/src/services/plugin-installer.test.ts +90 -0
- package/src/services/plugin-installer.ts +70 -0
- package/src/services/queue-manager.ts +382 -0
- package/src/services/queue-plugin-registry.ts +17 -0
- package/src/services/queue-proxy.ts +182 -0
- package/src/services/service-registry.ts +35 -0
- package/src/test-preload.ts +114 -0
- package/src/utils/plugin-discovery.test.ts +383 -0
- package/src/utils/plugin-discovery.ts +157 -0
- package/src/utils/strip-public-schema.ts +40 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, it, expect, mock, beforeEach, spyOn } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createMockEventBus,
|
|
4
|
+
createMockPluginInstaller,
|
|
5
|
+
createMockQueueManager,
|
|
6
|
+
type MockEventBus,
|
|
7
|
+
type MockPluginInstaller,
|
|
8
|
+
} from "@checkstack/test-utils-backend";
|
|
9
|
+
import { coreServices, coreHooks } from "@checkstack/backend-api";
|
|
10
|
+
|
|
11
|
+
// Note: ./db and ./logger are mocked via test-preload.ts (bunfig.toml preload)
|
|
12
|
+
// This ensures mocks are in place BEFORE any module imports them
|
|
13
|
+
|
|
14
|
+
import { PluginManager } from "./plugin-manager";
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Tests
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe("Plugin Lifecycle", () => {
|
|
21
|
+
let pluginManager: PluginManager;
|
|
22
|
+
let mockEventBus: MockEventBus;
|
|
23
|
+
let mockInstaller: MockPluginInstaller;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
pluginManager = new PluginManager();
|
|
27
|
+
mockEventBus = createMockEventBus();
|
|
28
|
+
mockInstaller = createMockPluginInstaller();
|
|
29
|
+
|
|
30
|
+
// Access internal registry to override factory (factories take precedence)
|
|
31
|
+
const registry = (pluginManager as never)["registry"] as {
|
|
32
|
+
registerFactory: <T>(ref: { id: string }, factory: () => T) => void;
|
|
33
|
+
register: <T>(ref: { id: string }, impl: T) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Override EventBus factory with our mock
|
|
37
|
+
registry.registerFactory(
|
|
38
|
+
coreServices.eventBus,
|
|
39
|
+
() => mockEventBus as never
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Register services
|
|
43
|
+
registry.register(coreServices.pluginInstaller, mockInstaller as never);
|
|
44
|
+
registry.register(
|
|
45
|
+
coreServices.queueManager,
|
|
46
|
+
createMockQueueManager() as never
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("requestInstallation", () => {
|
|
51
|
+
it("should emit pluginInstallationRequested broadcast", async () => {
|
|
52
|
+
await pluginManager.requestInstallation("test-plugin", "/path/to/plugin");
|
|
53
|
+
|
|
54
|
+
expect(mockEventBus._emittedEvents).toContainEqual({
|
|
55
|
+
hook: coreHooks.pluginInstallationRequested.id,
|
|
56
|
+
payload: { pluginId: "test-plugin", pluginPath: "/path/to/plugin" },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("requestDeregistration", () => {
|
|
62
|
+
it("should emit pluginDeregistrationRequested broadcast", async () => {
|
|
63
|
+
await pluginManager.requestDeregistration("test-plugin", {
|
|
64
|
+
deleteSchema: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(mockEventBus._emittedEvents).toContainEqual({
|
|
68
|
+
hook: coreHooks.pluginDeregistrationRequested.id,
|
|
69
|
+
payload: { pluginId: "test-plugin", deleteSchema: false },
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should include deleteSchema flag in broadcast", async () => {
|
|
74
|
+
await pluginManager.requestDeregistration("test-plugin", {
|
|
75
|
+
deleteSchema: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(mockEventBus._emittedEvents).toContainEqual({
|
|
79
|
+
hook: coreHooks.pluginDeregistrationRequested.id,
|
|
80
|
+
payload: { pluginId: "test-plugin", deleteSchema: true },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("setupLifecycleListeners", () => {
|
|
86
|
+
it("should register listeners for installation broadcasts", async () => {
|
|
87
|
+
await pluginManager.setupLifecycleListeners();
|
|
88
|
+
|
|
89
|
+
// Spy on loadSinglePlugin and make it a no-op to avoid actual import
|
|
90
|
+
const loadSpy = spyOn(
|
|
91
|
+
pluginManager,
|
|
92
|
+
"loadSinglePlugin"
|
|
93
|
+
).mockImplementation(async () => {});
|
|
94
|
+
await mockEventBus._triggerBroadcast(
|
|
95
|
+
coreHooks.pluginInstallationRequested,
|
|
96
|
+
{
|
|
97
|
+
pluginId: "broadcast-plugin",
|
|
98
|
+
pluginPath: "/broadcast/path",
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(loadSpy).toHaveBeenCalledWith(
|
|
103
|
+
"broadcast-plugin",
|
|
104
|
+
"/broadcast/path"
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should register listeners for deregistration broadcasts", async () => {
|
|
109
|
+
await pluginManager.setupLifecycleListeners();
|
|
110
|
+
|
|
111
|
+
// Trigger a broadcast and verify the listener responds
|
|
112
|
+
const deregSpy = spyOn(pluginManager, "deregisterPlugin");
|
|
113
|
+
await mockEventBus._triggerBroadcast(
|
|
114
|
+
coreHooks.pluginDeregistrationRequested,
|
|
115
|
+
{
|
|
116
|
+
pluginId: "broadcast-plugin",
|
|
117
|
+
deleteSchema: true,
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(deregSpy).toHaveBeenCalledWith("broadcast-plugin", {
|
|
122
|
+
deleteSchema: true,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("loadSinglePlugin", () => {
|
|
128
|
+
it("should emit pluginInstalling local hook", async () => {
|
|
129
|
+
// Try loading - will fail import but should still emit installing hook
|
|
130
|
+
try {
|
|
131
|
+
await pluginManager.loadSinglePlugin(
|
|
132
|
+
"nonexistent-plugin",
|
|
133
|
+
"/nonexistent/path"
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
// Expected to fail since plugin doesn't exist
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Should have emitted pluginInstalling locally
|
|
140
|
+
expect(mockEventBus._localEmittedEvents).toContainEqual({
|
|
141
|
+
hook: coreHooks.pluginInstalling.id,
|
|
142
|
+
payload: { pluginId: "nonexistent-plugin" },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should call installer if import fails", async () => {
|
|
147
|
+
try {
|
|
148
|
+
await pluginManager.loadSinglePlugin(
|
|
149
|
+
"test-remote-plugin",
|
|
150
|
+
"/nonexistent/path"
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
// Expected to fail at the final import, but installer should be called
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Installer should have been called when imports failed
|
|
157
|
+
expect(mockInstaller._installCalls).toContain("test-remote-plugin");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("deregisterPlugin", () => {
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
// Setup cleanup handlers for a plugin
|
|
164
|
+
const cleanupHandlers = (pluginManager as never)[
|
|
165
|
+
"cleanupHandlers"
|
|
166
|
+
] as Map<string, (() => Promise<void>)[]>;
|
|
167
|
+
cleanupHandlers.set("test-plugin", [async () => {}]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should emit pluginDeregistering local hook", async () => {
|
|
171
|
+
await pluginManager.deregisterPlugin("test-plugin", {
|
|
172
|
+
deleteSchema: false,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const deregisteringEvent = mockEventBus._localEmittedEvents.find(
|
|
176
|
+
(e) => e.hook === coreHooks.pluginDeregistering.id
|
|
177
|
+
);
|
|
178
|
+
expect(deregisteringEvent).toBeDefined();
|
|
179
|
+
expect(deregisteringEvent?.payload).toMatchObject({
|
|
180
|
+
pluginId: "test-plugin",
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should emit pluginDeregistered after cleanup", async () => {
|
|
185
|
+
await pluginManager.deregisterPlugin("test-plugin", {
|
|
186
|
+
deleteSchema: false,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const deregisteredEvent = mockEventBus._emittedEvents.find(
|
|
190
|
+
(e) => e.hook === coreHooks.pluginDeregistered.id
|
|
191
|
+
);
|
|
192
|
+
expect(deregisteredEvent).toBeDefined();
|
|
193
|
+
expect(deregisteredEvent?.payload).toMatchObject({
|
|
194
|
+
pluginId: "test-plugin",
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should run cleanup handlers in reverse order", async () => {
|
|
199
|
+
const callOrder: number[] = [];
|
|
200
|
+
const cleanupHandlers = (pluginManager as never)[
|
|
201
|
+
"cleanupHandlers"
|
|
202
|
+
] as Map<string, (() => Promise<void>)[]>;
|
|
203
|
+
|
|
204
|
+
cleanupHandlers.set("test-plugin", [
|
|
205
|
+
async () => {
|
|
206
|
+
callOrder.push(1);
|
|
207
|
+
},
|
|
208
|
+
async () => {
|
|
209
|
+
callOrder.push(2);
|
|
210
|
+
},
|
|
211
|
+
async () => {
|
|
212
|
+
callOrder.push(3);
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
await pluginManager.deregisterPlugin("test-plugin", {
|
|
217
|
+
deleteSchema: false,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(callOrder).toEqual([3, 2, 1]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should remove plugin router", async () => {
|
|
224
|
+
const pluginRpcRouters = (pluginManager as never)[
|
|
225
|
+
"pluginRpcRouters"
|
|
226
|
+
] as Map<string, unknown>;
|
|
227
|
+
pluginRpcRouters.set("test-plugin", { mockRouter: true });
|
|
228
|
+
|
|
229
|
+
await pluginManager.deregisterPlugin("test-plugin", {
|
|
230
|
+
deleteSchema: false,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(pluginRpcRouters.has("test-plugin")).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should clear permissions for plugin", async () => {
|
|
237
|
+
const registeredPermissions = (pluginManager as never)[
|
|
238
|
+
"registeredPermissions"
|
|
239
|
+
] as { pluginId: string; id: string }[];
|
|
240
|
+
|
|
241
|
+
// Clear existing permissions first
|
|
242
|
+
while (registeredPermissions.length > 0) {
|
|
243
|
+
registeredPermissions.pop();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Add test permissions
|
|
247
|
+
registeredPermissions.push(
|
|
248
|
+
{ pluginId: "test-plugin", id: "test-plugin.perm1" },
|
|
249
|
+
{ pluginId: "test-plugin", id: "test-plugin.perm2" },
|
|
250
|
+
{ pluginId: "other-plugin", id: "other-plugin.perm1" }
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
await pluginManager.deregisterPlugin("test-plugin", {
|
|
254
|
+
deleteSchema: false,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Use getAllPermissions() which returns the current array
|
|
258
|
+
const remaining = pluginManager.getAllPermissions();
|
|
259
|
+
expect(remaining).toHaveLength(1);
|
|
260
|
+
expect(remaining[0].id).toBe("other-plugin.perm1");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("registerCoreRouter", () => {
|
|
265
|
+
it("should add router to pluginRpcRouters", () => {
|
|
266
|
+
const mockRouter = { test: true };
|
|
267
|
+
|
|
268
|
+
pluginManager.registerCoreRouter("admin", mockRouter);
|
|
269
|
+
|
|
270
|
+
const pluginRpcRouters = (pluginManager as never)[
|
|
271
|
+
"pluginRpcRouters"
|
|
272
|
+
] as Map<string, unknown>;
|
|
273
|
+
expect(pluginRpcRouters.get("admin")).toBe(mockRouter);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { Hono, Context } from "hono";
|
|
2
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { RPCHandler } from "@orpc/server/fetch";
|
|
4
|
+
import {
|
|
5
|
+
coreServices,
|
|
6
|
+
AuthService,
|
|
7
|
+
RpcContext,
|
|
8
|
+
Logger,
|
|
9
|
+
Fetch,
|
|
10
|
+
HealthCheckRegistry,
|
|
11
|
+
type EmitHookFn,
|
|
12
|
+
type Hook,
|
|
13
|
+
} from "@checkstack/backend-api";
|
|
14
|
+
import type {
|
|
15
|
+
QueuePluginRegistry,
|
|
16
|
+
QueueManager,
|
|
17
|
+
} from "@checkstack/queue-api";
|
|
18
|
+
import type { ServiceRegistry } from "../services/service-registry";
|
|
19
|
+
import type { EventBus } from "@checkstack/backend-api";
|
|
20
|
+
import type { PluginMetadata } from "@checkstack/common";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates the API route handler for Hono.
|
|
24
|
+
* Extracted from PluginManager for better organization.
|
|
25
|
+
*/
|
|
26
|
+
export function createApiRouteHandler({
|
|
27
|
+
registry,
|
|
28
|
+
pluginRpcRouters,
|
|
29
|
+
pluginHttpHandlers,
|
|
30
|
+
pluginMetadataRegistry,
|
|
31
|
+
}: {
|
|
32
|
+
registry: ServiceRegistry;
|
|
33
|
+
pluginRpcRouters: Map<string, unknown>;
|
|
34
|
+
pluginHttpHandlers: Map<string, (req: Request) => Promise<Response>>;
|
|
35
|
+
pluginMetadataRegistry: Map<string, PluginMetadata>;
|
|
36
|
+
}) {
|
|
37
|
+
// Helper to get service from registry
|
|
38
|
+
async function getService<T>(ref: {
|
|
39
|
+
id: string;
|
|
40
|
+
T: T;
|
|
41
|
+
}): Promise<T | undefined> {
|
|
42
|
+
try {
|
|
43
|
+
return await registry.get(ref, { pluginId: "core" });
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return async function handleApiRequest(c: Context) {
|
|
50
|
+
// Extract pluginId from Hono path parameter (/api/:pluginId/*)
|
|
51
|
+
const pluginId = c.req.param("pluginId") || "";
|
|
52
|
+
const pathname = new URL(c.req.raw.url).pathname;
|
|
53
|
+
|
|
54
|
+
// Build RPC handler lazily at request time
|
|
55
|
+
// This ensures all plugins registered during init are included
|
|
56
|
+
const rootRpcRouter: Record<string, unknown> = {};
|
|
57
|
+
for (const [pluginId, router] of pluginRpcRouters.entries()) {
|
|
58
|
+
rootRpcRouter[pluginId] = router;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rpcHandler = new RPCHandler(
|
|
62
|
+
rootRpcRouter as ConstructorParameters<typeof RPCHandler>[0]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Resolve core services for RPC context
|
|
66
|
+
const auth = await getService(coreServices.auth);
|
|
67
|
+
const logger = await getService(coreServices.logger);
|
|
68
|
+
const db = await getService(coreServices.database);
|
|
69
|
+
const fetch = await getService(coreServices.fetch);
|
|
70
|
+
const healthCheckRegistry = await getService(
|
|
71
|
+
coreServices.healthCheckRegistry
|
|
72
|
+
);
|
|
73
|
+
const queuePluginRegistry = await getService(
|
|
74
|
+
coreServices.queuePluginRegistry
|
|
75
|
+
);
|
|
76
|
+
const queueManager = await getService(coreServices.queueManager);
|
|
77
|
+
const eventBus = await getService(coreServices.eventBus);
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
!auth ||
|
|
81
|
+
!logger ||
|
|
82
|
+
!db ||
|
|
83
|
+
!fetch ||
|
|
84
|
+
!healthCheckRegistry ||
|
|
85
|
+
!queuePluginRegistry ||
|
|
86
|
+
!queueManager ||
|
|
87
|
+
!eventBus
|
|
88
|
+
) {
|
|
89
|
+
return c.json({ error: "Core services not initialized" }, 500);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const user = await (auth as AuthService).authenticate(c.req.raw);
|
|
93
|
+
|
|
94
|
+
// Create emitHook function using eventBus
|
|
95
|
+
const emitHook: EmitHookFn = async <T>(hook: Hook<T>, payload: T) => {
|
|
96
|
+
await (eventBus as EventBus).emit(hook, payload);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Lookup plugin metadata from registry
|
|
100
|
+
const pluginMetadata: PluginMetadata | undefined =
|
|
101
|
+
pluginMetadataRegistry.get(pluginId);
|
|
102
|
+
|
|
103
|
+
if (!pluginMetadata) {
|
|
104
|
+
return c.json({ error: "Plugin metadata not found in registry" }, 500);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const context: RpcContext = {
|
|
108
|
+
pluginMetadata,
|
|
109
|
+
auth: auth as AuthService,
|
|
110
|
+
logger: logger as Logger,
|
|
111
|
+
db: db as NodePgDatabase<Record<string, unknown>>,
|
|
112
|
+
fetch: fetch as Fetch,
|
|
113
|
+
healthCheckRegistry: healthCheckRegistry as HealthCheckRegistry,
|
|
114
|
+
queuePluginRegistry: queuePluginRegistry as QueuePluginRegistry,
|
|
115
|
+
queueManager: queueManager as QueueManager,
|
|
116
|
+
user,
|
|
117
|
+
emitHook,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// 1. Try oRPC first
|
|
121
|
+
try {
|
|
122
|
+
const { matched, response } = await rpcHandler.handle(c.req.raw, {
|
|
123
|
+
prefix: "/api",
|
|
124
|
+
context,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (matched) {
|
|
128
|
+
return c.newResponse(response.body, response);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.debug(`RPC mismatch for: ${c.req.method} ${pathname}`);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.error(`RPC Handler error: ${String(error)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Try native handlers
|
|
137
|
+
// Sort by path length (descending) to ensure more specific paths are tried first
|
|
138
|
+
const sortedHandlers = [...pluginHttpHandlers.entries()].toSorted(function (
|
|
139
|
+
a,
|
|
140
|
+
b
|
|
141
|
+
) {
|
|
142
|
+
return b[0].length - a[0].length;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
for (const [path, handler] of sortedHandlers) {
|
|
146
|
+
if (pathname.startsWith(path)) {
|
|
147
|
+
return handler(c.req.raw);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return c.json({ error: "Not Found" }, 404);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Registers the /api/:pluginId/* route with Hono.
|
|
157
|
+
*/
|
|
158
|
+
export function registerApiRoute(
|
|
159
|
+
rootRouter: Hono,
|
|
160
|
+
handler: ReturnType<typeof createApiRouteHandler>
|
|
161
|
+
) {
|
|
162
|
+
rootRouter.all("/api/:pluginId/*", handler);
|
|
163
|
+
}
|