feat(FN-2540): extract shared route context and integrated router registrar
- Refactor dashboard routing to use a shared route context scaffold for provider-safe registration - Add register-integrated-routers helper and wire routes.ts to compose integrated routers cleanly - Update route documentation and route types to reflect the new context/registrar structure - Stabilize quick-entry clear assertion and add routing tests covering integrated router registration behavior
This commit is contained in:
@@ -1574,9 +1574,8 @@ describe("ListView Quick Entry", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockOnQuickCreate).toHaveBeenCalled();
|
expect(mockOnQuickCreate).toHaveBeenCalled();
|
||||||
|
expect(input.value).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(input.value).toBe("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows error toast when onQuickCreate fails and keeps input content", async () => {
|
it("shows error toast when onQuickCreate fails and keeps input content", async () => {
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import { join } from "node:path";
|
|||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { createHmac } from "node:crypto";
|
import { createHmac } from "node:crypto";
|
||||||
import { createApiRoutes } from "../routes.js";
|
import { createApiRoutes } from "../routes.js";
|
||||||
|
import {
|
||||||
|
getProjectIdFromRequest as getProjectIdFromRouteRequest,
|
||||||
|
getProjectContext as resolveRouteProjectContext,
|
||||||
|
getScopedStore as resolveRouteScopedStore,
|
||||||
|
} from "../routes/context.js";
|
||||||
import { GitHubClient } from "../github.js";
|
import { GitHubClient } from "../github.js";
|
||||||
import { githubRateLimiter } from "../github-poll.js";
|
import { githubRateLimiter } from "../github-poll.js";
|
||||||
import type { TaskStore, TaskAttachment, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult, ChatSession, ChatMessage } from "@fusion/core";
|
import type { TaskStore, TaskAttachment, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult, ChatSession, ChatMessage } from "@fusion/core";
|
||||||
@@ -205,6 +210,59 @@ async function REQUEST(
|
|||||||
return { status: res.status, body: res.body };
|
return { status: res.status, body: res.body };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("routes/context project scoping helpers", () => {
|
||||||
|
it("prefers query.projectId over body.projectId", () => {
|
||||||
|
const req = {
|
||||||
|
query: { projectId: "query-project" },
|
||||||
|
body: { projectId: "body-project" },
|
||||||
|
} as unknown as express.Request;
|
||||||
|
|
||||||
|
expect(getProjectIdFromRouteRequest(req)).toBe("query-project");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to body.projectId when query.projectId is absent", () => {
|
||||||
|
const req = {
|
||||||
|
query: {},
|
||||||
|
body: { projectId: "body-project" },
|
||||||
|
} as unknown as express.Request;
|
||||||
|
|
||||||
|
expect(getProjectIdFromRouteRequest(req)).toBe("body-project");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getScopedStore returns root store when projectId is missing", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
const req = { query: {}, body: {} } as unknown as express.Request;
|
||||||
|
const getOrCreateSpy = vi.spyOn(projectStoreResolver, "getOrCreateProjectStore");
|
||||||
|
|
||||||
|
const scopedStore = await resolveRouteScopedStore(req, store);
|
||||||
|
|
||||||
|
expect(scopedStore).toBe(store);
|
||||||
|
expect(getOrCreateSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getProjectContext falls back to scoped store when ensureEngine throws", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
const fallbackStore = createMockStore();
|
||||||
|
const getOrCreateSpy = vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValueOnce(fallbackStore);
|
||||||
|
|
||||||
|
const req = { query: { projectId: "proj-123" }, body: {} } as unknown as express.Request;
|
||||||
|
const options = {
|
||||||
|
engineManager: {
|
||||||
|
getEngine: vi.fn().mockReturnValue(undefined),
|
||||||
|
ensureEngine: vi.fn().mockRejectedValue(new Error("startup failed")),
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const context = await resolveRouteProjectContext(req, store, options);
|
||||||
|
|
||||||
|
expect(context.projectId).toBe("proj-123");
|
||||||
|
expect(context.engine).toBeUndefined();
|
||||||
|
expect(context.store).toBe(fallbackStore);
|
||||||
|
expect(options.engineManager.ensureEngine).toHaveBeenCalledWith("proj-123");
|
||||||
|
expect(getOrCreateSpy).toHaveBeenCalledWith("proj-123");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/** Build a minimal multipart/form-data body */
|
/** Build a minimal multipart/form-data body */
|
||||||
function buildMultipart(fieldName: string, filename: string, contentType: string, content: Buffer): { body: Buffer; boundary: string } {
|
function buildMultipart(fieldName: string, filename: string, contentType: string, content: Buffer): { body: Buffer; boundary: string } {
|
||||||
const boundary = "----TestBoundary" + Date.now();
|
const boundary = "----TestBoundary" + Date.now();
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ import {
|
|||||||
hasPrBadgeFieldsChanged,
|
hasPrBadgeFieldsChanged,
|
||||||
hasIssueBadgeFieldsChanged,
|
hasIssueBadgeFieldsChanged,
|
||||||
} from "./github-webhooks.js";
|
} from "./github-webhooks.js";
|
||||||
import { createMissionRouter } from "./mission-routes.js";
|
|
||||||
import { createRoadmapRouter } from "./roadmap-routes.js";
|
|
||||||
import { createInsightsRouter } from "./insights-routes.js";
|
|
||||||
import { getOrCreateProjectStore, invalidateAllGlobalSettingsCaches } from "./project-store-resolver.js";
|
import { getOrCreateProjectStore, invalidateAllGlobalSettingsCaches } from "./project-store-resolver.js";
|
||||||
import { AiSessionStore, SESSION_CLEANUP_DEFAULT_MAX_AGE_MS } from "./ai-session-store.js";
|
import { AiSessionStore, SESSION_CLEANUP_DEFAULT_MAX_AGE_MS } from "./ai-session-store.js";
|
||||||
import { getSession as getPlanningSession, cleanupSession as cleanupPlanningSession } from "./planning.js";
|
import { getSession as getPlanningSession, cleanupSession as cleanupPlanningSession } from "./planning.js";
|
||||||
@@ -62,7 +59,6 @@ import {
|
|||||||
import { getMissionInterviewSession, cleanupMissionInterviewSession } from "./mission-interview.js";
|
import { getMissionInterviewSession, cleanupMissionInterviewSession } from "./mission-interview.js";
|
||||||
import { getTargetInterviewSession, cleanupTargetInterviewSession } from "./milestone-slice-interview.js";
|
import { getTargetInterviewSession, cleanupTargetInterviewSession } from "./milestone-slice-interview.js";
|
||||||
import { writeSSEEvent } from "./sse-buffer.js";
|
import { writeSSEEvent } from "./sse-buffer.js";
|
||||||
import { createDevServerRouter } from "./dev-server-routes.js";
|
|
||||||
import {
|
import {
|
||||||
ApiError,
|
ApiError,
|
||||||
badRequest,
|
badRequest,
|
||||||
@@ -88,6 +84,7 @@ import { registerFilesTerminalWorkspaceRoutes } from "./routes/register-files-te
|
|||||||
import { registerAgentsProjectsNodesRoutes } from "./routes/register-agents-projects-nodes.js";
|
import { registerAgentsProjectsNodesRoutes } from "./routes/register-agents-projects-nodes.js";
|
||||||
import { registerPluginsAutomationRoutes } from "./routes/register-plugins-automation.js";
|
import { registerPluginsAutomationRoutes } from "./routes/register-plugins-automation.js";
|
||||||
import { registerProxyRoutes } from "./routes/register-proxy.js";
|
import { registerProxyRoutes } from "./routes/register-proxy.js";
|
||||||
|
import { registerIntegratedRouters, registerIntegratedDevServerRouter } from "./routes/register-integrated-routers.js";
|
||||||
|
|
||||||
const TASK_DETAIL_ACTIVITY_LOG_LIMIT = 500;
|
const TASK_DETAIL_ACTIVITY_LOG_LIMIT = 500;
|
||||||
|
|
||||||
@@ -2036,8 +2033,9 @@ function checkSessionLock(
|
|||||||
* Public API route entrypoint used by server.ts.
|
* Public API route entrypoint used by server.ts.
|
||||||
*
|
*
|
||||||
* `createApiRoutes()` is intentionally an orchestrator: it builds shared
|
* `createApiRoutes()` is intentionally an orchestrator: it builds shared
|
||||||
* request/project context once, mounts registrar modules in precedence-safe
|
* request/project context once, mounts registrar modules (including integrated
|
||||||
* order, and keeps compatibility exports in this module stable for tests and
|
* router registrars) in precedence-safe order, and keeps compatibility exports
|
||||||
|
* in this module stable for tests and
|
||||||
* downstream consumers (`resolveDiffBase`, `__resetBatchImportRateLimiter`,
|
* downstream consumers (`resolveDiffBase`, `__resetBatchImportRateLimiter`,
|
||||||
* `__setCreateFnAgentForRefine`, `AuthStorageLike`, `ModelRegistryLike`).
|
* `__setCreateFnAgentForRefine`, `AuthStorageLike`, `ModelRegistryLike`).
|
||||||
*/
|
*/
|
||||||
@@ -14512,18 +14510,18 @@ async function persistImportedSkills(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Mission Routes ─────────────────────────────────────────────────────────
|
// ── Integrated domain routers ──────────────────────────────────────────────
|
||||||
// Mount mission routes at /api/missions
|
// Keep this call at the current position to preserve precedence with
|
||||||
router.use("/missions", createMissionRouter(store, options?.missionAutopilot, aiSessionStore, options?.missionExecutionLoop, options?.engineManager));
|
// surrounding route handlers. registerIntegratedRouters() mounts:
|
||||||
|
// - /missions
|
||||||
// ── Roadmap Routes ─────────────────────────────────────────────────────────
|
// - /roadmaps
|
||||||
// Mount roadmap routes at /api/roadmaps
|
// - /insights
|
||||||
router.use("/roadmaps", createRoadmapRouter(store));
|
registerIntegratedRouters({
|
||||||
|
router,
|
||||||
// ── Insights Routes ─────────────────────────────────────────────────────────
|
store,
|
||||||
// Mount insights routes at /api/insights
|
options,
|
||||||
// Uses projectId from query/body for scoping
|
aiSessionStore,
|
||||||
router.use("/insights", createInsightsRouter(store));
|
});
|
||||||
|
|
||||||
// ── Plugin Routes ─────────────────────────────────────────────────────────
|
// ── Plugin Routes ─────────────────────────────────────────────────────────
|
||||||
// Plugin management endpoints with projectId scoping support.
|
// Plugin management endpoints with projectId scoping support.
|
||||||
@@ -17769,11 +17767,9 @@ async function persistImportedSkills(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dev server management routes
|
// Dev server mount intentionally stays in this late position to keep route
|
||||||
const devServerRouter = createDevServerRouter({
|
// precedence unchanged relative to existing wildcard handlers.
|
||||||
projectRoot: store.getRootDir(),
|
registerIntegratedDevServerRouter({ router, store });
|
||||||
});
|
|
||||||
router.use("/dev-server", devServerRouter);
|
|
||||||
|
|
||||||
// Scripts and messaging routes are registered by registerMessagingScriptRoutes().
|
// Scripts and messaging routes are registered by registerMessagingScriptRoutes().
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
|
|
||||||
All registrars receive `ApiRoutesContext` from `./types.ts`, built by `createApiRoutesContext()` in `./context.ts`.
|
All registrars receive `ApiRoutesContext` from `./types.ts`, built by `createApiRoutesContext()` in `./context.ts`.
|
||||||
|
|
||||||
|
Registrars should be typed as `ApiRouteRegistrar` so modules share one explicit registration contract.
|
||||||
|
|
||||||
The context centralizes cross-cutting dependencies so registrars preserve behavior without re-implementing plumbing:
|
The context centralizes cross-cutting dependencies so registrars preserve behavior without re-implementing plumbing:
|
||||||
|
|
||||||
- Request/project scoping: `getProjectIdFromRequest`, `getScopedStore`, `getProjectContext`
|
- Request/project scoping: `getProjectIdFromRequest`, `getScopedStore`, `getProjectContext`
|
||||||
|
- These are also exported from `context.ts` as canonical helpers for future extraction tasks.
|
||||||
- Engine-aware fallback behavior for project-bound and root-store APIs
|
- Engine-aware fallback behavior for project-bound and root-store APIs
|
||||||
- Runtime loggers and diagnostics emitters (`runtimeLogger`, `planningLogger`, `proxyLogger`, `chatLogger`)
|
- Runtime loggers and diagnostics emitters (`runtimeLogger`, `planningLogger`, `proxyLogger`, `chatLogger`)
|
||||||
- Proxy/auth/audit helpers (`proxyToRemoteNode`, `emitRemoteRouteDiagnostic`, `emitAuthSyncAuditLog`)
|
- Proxy/auth/audit helpers (`proxyToRemoteNode`, `emitRemoteRouteDiagnostic`, `emitAuthSyncAuditLog`)
|
||||||
@@ -37,13 +40,15 @@ Express matches in registration order. Keep registrar and in-registrar route ord
|
|||||||
|
|
||||||
If adding a new endpoint, place it in the domain registrar and verify it does not shadow existing handlers.
|
If adding a new endpoint, place it in the domain registrar and verify it does not shadow existing handlers.
|
||||||
|
|
||||||
## Integration mounts that stay in `routes.ts`
|
## Integrated routers
|
||||||
|
|
||||||
These routers remain mounted directly by the orchestrator and must keep their current prefixes/options wiring:
|
Integrated routers are mounted through `register-integrated-routers.ts` and intentionally called from `routes.ts` at precedence-sensitive points:
|
||||||
|
|
||||||
- `createMissionRouter` → `/api/missions`
|
- `registerIntegratedRouters(...)` mounts:
|
||||||
- `createRoadmapRouter` → `/api/roadmaps`
|
- `createMissionRouter` → `/api/missions`
|
||||||
- `createInsightsRouter` → `/api/insights`
|
- `createRoadmapRouter` → `/api/roadmaps`
|
||||||
- `createDevServerRouter` → `/api/dev-server`
|
- `createInsightsRouter` → `/api/insights`
|
||||||
|
- `registerIntegratedDevServerRouter(...)` mounts:
|
||||||
|
- `createDevServerRouter` → `/api/dev-server`
|
||||||
|
|
||||||
Do not re-home these mounts without explicit migration and regression coverage.
|
Keep these calls in their current positions inside `createApiRoutes()` unless an explicit route-ordering migration is planned and regression-tested.
|
||||||
|
|||||||
@@ -58,6 +58,48 @@ function classifyRemoteRouteError(error: unknown): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProjectIdFromRequest(req: Request): string | undefined {
|
||||||
|
if (req.query && typeof req.query.projectId === "string" && req.query.projectId.length > 0) {
|
||||||
|
return req.query.projectId;
|
||||||
|
}
|
||||||
|
if (req.body && typeof req.body.projectId === "string" && req.body.projectId.length > 0) {
|
||||||
|
return req.body.projectId;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getScopedStore(req: Request, store: TaskStore): Promise<TaskStore> {
|
||||||
|
const projectId = getProjectIdFromRequest(req);
|
||||||
|
if (!projectId) return store;
|
||||||
|
return getOrCreateProjectStore(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectContext(
|
||||||
|
req: Request,
|
||||||
|
store: TaskStore,
|
||||||
|
options?: ServerOptions,
|
||||||
|
): Promise<ProjectContext> {
|
||||||
|
const projectId = getProjectIdFromRequest(req);
|
||||||
|
const engineManager = options?.engineManager;
|
||||||
|
|
||||||
|
if (projectId && engineManager) {
|
||||||
|
let engine = engineManager.getEngine(projectId);
|
||||||
|
if (!engine) {
|
||||||
|
try {
|
||||||
|
engine = await engineManager.ensureEngine(projectId);
|
||||||
|
} catch {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (engine) {
|
||||||
|
return { store: engine.getTaskStore(), engine, projectId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopedStore = await getScopedStore(req, store);
|
||||||
|
return { store: scopedStore, engine: undefined, projectId };
|
||||||
|
}
|
||||||
|
|
||||||
export function createApiRoutesContext(store: TaskStore, options?: ServerOptions): ApiRoutesContext {
|
export function createApiRoutesContext(store: TaskStore, options?: ServerOptions): ApiRoutesContext {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const runtimeLogger = options?.runtimeLogger?.child("routes") ?? createRuntimeLogger("routes");
|
const runtimeLogger = options?.runtimeLogger?.child("routes") ?? createRuntimeLogger("routes");
|
||||||
@@ -88,43 +130,8 @@ export function createApiRoutesContext(store: TaskStore, options?: ServerOptions
|
|||||||
return [...projects].sort((a, b) => rankProject(b.path) - rankProject(a.path));
|
return [...projects].sort((a, b) => rankProject(b.path) - rankProject(a.path));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectIdFromRequest(req: Request): string | undefined {
|
const resolveScopedStore = (req: Request): Promise<TaskStore> => getScopedStore(req, store);
|
||||||
if (req.query && typeof req.query.projectId === "string" && req.query.projectId.length > 0) {
|
const resolveProjectContext = (req: Request): Promise<ProjectContext> => getProjectContext(req, store, options);
|
||||||
return req.query.projectId;
|
|
||||||
}
|
|
||||||
if (req.body && typeof req.body.projectId === "string" && req.body.projectId.length > 0) {
|
|
||||||
return req.body.projectId;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getScopedStore(req: Request): Promise<TaskStore> {
|
|
||||||
const projectId = getProjectIdFromRequest(req);
|
|
||||||
if (!projectId) return store;
|
|
||||||
return getOrCreateProjectStore(projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getProjectContext(req: Request): Promise<ProjectContext> {
|
|
||||||
const projectId = getProjectIdFromRequest(req);
|
|
||||||
const engineManager = options?.engineManager;
|
|
||||||
|
|
||||||
if (projectId && engineManager) {
|
|
||||||
let engine = engineManager.getEngine(projectId);
|
|
||||||
if (!engine) {
|
|
||||||
try {
|
|
||||||
engine = await engineManager.ensureEngine(projectId);
|
|
||||||
} catch {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (engine) {
|
|
||||||
return { store: engine.getTaskStore(), engine, projectId };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopedStore = await getScopedStore(req);
|
|
||||||
return { store: scopedStore, engine: undefined, projectId };
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitRemoteRouteDiagnostic(input: RemoteRouteDiagnosticInput): void {
|
function emitRemoteRouteDiagnostic(input: RemoteRouteDiagnosticInput): void {
|
||||||
const logger = runtimeLogger.child("remote-route").child(input.route);
|
const logger = runtimeLogger.child("remote-route").child(input.route);
|
||||||
@@ -391,8 +398,8 @@ export function createApiRoutesContext(store: TaskStore, options?: ServerOptions
|
|||||||
chatLogger,
|
chatLogger,
|
||||||
prioritizeProjectsForCurrentDirectory,
|
prioritizeProjectsForCurrentDirectory,
|
||||||
getProjectIdFromRequest,
|
getProjectIdFromRequest,
|
||||||
getScopedStore,
|
getScopedStore: resolveScopedStore,
|
||||||
getProjectContext,
|
getProjectContext: resolveProjectContext,
|
||||||
emitRemoteRouteDiagnostic,
|
emitRemoteRouteDiagnostic,
|
||||||
emitAuthSyncAuditLog,
|
emitAuthSyncAuditLog,
|
||||||
proxyToRemoteNode,
|
proxyToRemoteNode,
|
||||||
|
|||||||
42
packages/dashboard/src/routes/register-integrated-routers.ts
Normal file
42
packages/dashboard/src/routes/register-integrated-routers.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { Router } from "express";
|
||||||
|
import type { TaskStore } from "@fusion/core";
|
||||||
|
import type { ServerOptions } from "../server.js";
|
||||||
|
import { createMissionRouter } from "../mission-routes.js";
|
||||||
|
import { createRoadmapRouter } from "../roadmap-routes.js";
|
||||||
|
import { createInsightsRouter } from "../insights-routes.js";
|
||||||
|
import { createDevServerRouter } from "../dev-server-routes.js";
|
||||||
|
import type { AiSessionStore } from "../ai-session-store.js";
|
||||||
|
|
||||||
|
interface IntegratedRoutersOptions {
|
||||||
|
router: Router;
|
||||||
|
store: TaskStore;
|
||||||
|
options?: ServerOptions;
|
||||||
|
aiSessionStore?: AiSessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DevServerRouterOptions {
|
||||||
|
router: Router;
|
||||||
|
store: TaskStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerIntegratedRouters({
|
||||||
|
router,
|
||||||
|
store,
|
||||||
|
options,
|
||||||
|
aiSessionStore,
|
||||||
|
}: IntegratedRoutersOptions): void {
|
||||||
|
router.use(
|
||||||
|
"/missions",
|
||||||
|
createMissionRouter(store, options?.missionAutopilot, aiSessionStore, options?.missionExecutionLoop, options?.engineManager),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.use("/roadmaps", createRoadmapRouter(store));
|
||||||
|
router.use("/insights", createInsightsRouter(store));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerIntegratedDevServerRouter({ router, store }: DevServerRouterOptions): void {
|
||||||
|
const devServerRouter = createDevServerRouter({
|
||||||
|
projectRoot: store.getRootDir(),
|
||||||
|
});
|
||||||
|
router.use("/dev-server", devServerRouter);
|
||||||
|
}
|
||||||
@@ -54,3 +54,5 @@ export interface ApiRoutesContext {
|
|||||||
resolveRoutineRunner(req: Request, scope: ScopeValue | undefined): NonNullable<ServerOptions["routineRunner"]>;
|
resolveRoutineRunner(req: Request, scope: ScopeValue | undefined): NonNullable<ServerOptions["routineRunner"]>;
|
||||||
rethrowAsApiError(error: unknown, fallbackMessage?: string): never;
|
rethrowAsApiError(error: unknown, fallbackMessage?: string): never;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiRouteRegistrar = (context: ApiRoutesContext) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user