mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
added additional permissions, implemented mcp authentication
This commit is contained in:
10
.cursor/mcp.json
Normal file
10
.cursor/mcp.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"bknd": {
|
||||||
|
"url": "http://localhost:3000/mcp",
|
||||||
|
"headers": {
|
||||||
|
"API_KEY": "value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"hono": "4.8.3",
|
"hono": "4.8.3",
|
||||||
"json-schema-library": "10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"jsonv-ts": "0.6.0",
|
"jsonv-ts": "0.6.2",
|
||||||
"kysely": "0.27.6",
|
"kysely": "0.27.6",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
|
|||||||
@@ -236,6 +236,8 @@ export class AuthController extends Controller {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async (params, c) => {
|
async (params, c) => {
|
||||||
|
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.createUser, c);
|
||||||
|
|
||||||
return c.json(await this.auth.createUser(params));
|
return c.json(await this.auth.createUser(params));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -251,6 +253,8 @@ export class AuthController extends Controller {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async (params, c) => {
|
async (params, c) => {
|
||||||
|
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.createToken, c);
|
||||||
|
|
||||||
const user = await getUser(params);
|
const user = await getUser(params);
|
||||||
return c.json({ user, token: await this.auth.authenticator.jwt(user) });
|
return c.json({ user, token: await this.auth.authenticator.jwt(user) });
|
||||||
},
|
},
|
||||||
@@ -268,6 +272,8 @@ export class AuthController extends Controller {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async (params, c) => {
|
async (params, c) => {
|
||||||
|
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.changePassword, c);
|
||||||
|
|
||||||
const user = await getUser(params);
|
const user = await getUser(params);
|
||||||
if (!(await this.auth.changePassword(user.id, params.password))) {
|
if (!(await this.auth.changePassword(user.id, params.password))) {
|
||||||
throw new Error("Failed to change password");
|
throw new Error("Failed to change password");
|
||||||
@@ -287,6 +293,8 @@ export class AuthController extends Controller {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async (params, c) => {
|
async (params, c) => {
|
||||||
|
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.testPassword, c);
|
||||||
|
|
||||||
const pw = this.auth.authenticator.strategy("password") as PasswordStrategy;
|
const pw = this.auth.authenticator.strategy("password") as PasswordStrategy;
|
||||||
const controller = pw.getController(this.auth.authenticator);
|
const controller = pw.getController(this.auth.authenticator);
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ import { Permission } from "core/security/Permission";
|
|||||||
|
|
||||||
export const createUser = new Permission("auth.user.create");
|
export const createUser = new Permission("auth.user.create");
|
||||||
//export const updateUser = new Permission("auth.user.update");
|
//export const updateUser = new Permission("auth.user.update");
|
||||||
|
export const testPassword = new Permission("auth.user.password.test");
|
||||||
|
export const changePassword = new Permission("auth.user.password.change");
|
||||||
|
export const createToken = new Permission("auth.user.token.create");
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export interface UserPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
|
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
|
||||||
export const cookieConfig = $object("config_auth_cookie", {
|
export const cookieConfig = s
|
||||||
|
.strictObject({
|
||||||
path: s.string({ default: "/" }),
|
path: s.string({ default: "/" }),
|
||||||
sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
|
sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
|
||||||
secure: s.boolean({ default: true }),
|
secure: s.boolean({ default: true }),
|
||||||
@@ -53,16 +54,13 @@ export const cookieConfig = $object("config_auth_cookie", {
|
|||||||
pathSuccess: s.string({ default: "/" }),
|
pathSuccess: s.string({ default: "/" }),
|
||||||
pathLoggedOut: s.string({ default: "/" }),
|
pathLoggedOut: s.string({ default: "/" }),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial();
|
||||||
.strict();
|
|
||||||
|
|
||||||
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
|
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
|
||||||
// see auth.integration test for further details
|
// see auth.integration test for further details
|
||||||
|
|
||||||
export const jwtConfig = $object(
|
export const jwtConfig = s.strictObject(
|
||||||
"config_auth_jwt",
|
|
||||||
{
|
{
|
||||||
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
|
|
||||||
secret: secret({ default: "" }),
|
secret: secret({ default: "" }),
|
||||||
alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(),
|
alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(),
|
||||||
expires: s.number().optional(), // seconds
|
expires: s.number().optional(), // seconds
|
||||||
@@ -72,7 +70,7 @@ export const jwtConfig = $object(
|
|||||||
{
|
{
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
).strict();
|
);
|
||||||
|
|
||||||
export const authenticatorConfig = s.object({
|
export const authenticatorConfig = s.object({
|
||||||
jwt: jwtConfig,
|
jwt: jwtConfig,
|
||||||
@@ -378,13 +376,24 @@ export class Authenticator<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @todo: don't extract user from token, but from the database or cache
|
// @todo: don't extract user from token, but from the database or cache
|
||||||
async resolveAuthFromRequest(c: Context): Promise<SafeUser | undefined> {
|
async resolveAuthFromRequest(c: Context | Request | Headers): Promise<SafeUser | undefined> {
|
||||||
let token: string | undefined;
|
let headers: Headers;
|
||||||
if (c.req.raw.headers.has("Authorization")) {
|
let is_context = false;
|
||||||
const bearerHeader = String(c.req.header("Authorization"));
|
if (c instanceof Headers) {
|
||||||
token = bearerHeader.replace("Bearer ", "");
|
headers = c;
|
||||||
|
} else if (c instanceof Request) {
|
||||||
|
headers = c.headers;
|
||||||
} else {
|
} else {
|
||||||
token = await this.getAuthCookie(c);
|
is_context = true;
|
||||||
|
headers = c.req.raw.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
let token: string | undefined;
|
||||||
|
if (headers.has("Authorization")) {
|
||||||
|
const bearerHeader = String(headers.get("Authorization"));
|
||||||
|
token = bearerHeader.replace("Bearer ", "");
|
||||||
|
} else if (is_context) {
|
||||||
|
token = await this.getAuthCookie(c as Context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export const mcp: CliCommand = (program) =>
|
|||||||
program
|
program
|
||||||
.command("mcp")
|
.command("mcp")
|
||||||
.description("mcp server")
|
.description("mcp server")
|
||||||
|
.option("--verbose", "verbose output")
|
||||||
|
.option("--config <config>", "config file")
|
||||||
|
.option("--db-url <db>", "database url, can be any valid sqlite url")
|
||||||
.option("--port <port>", "port to listen on", "3000")
|
.option("--port <port>", "port to listen on", "3000")
|
||||||
.option("--path <path>", "path to listen on", "/mcp")
|
.option("--path <path>", "path to listen on", "/mcp")
|
||||||
.option(
|
.option(
|
||||||
@@ -21,12 +24,17 @@ export const mcp: CliCommand = (program) =>
|
|||||||
.action(action);
|
.action(action);
|
||||||
|
|
||||||
async function action(options: {
|
async function action(options: {
|
||||||
|
verbose?: boolean;
|
||||||
|
config?: string;
|
||||||
|
dbUrl?: string;
|
||||||
port?: string;
|
port?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
logLevel?: string;
|
logLevel?: string;
|
||||||
}) {
|
}) {
|
||||||
const app = await makeAppFromEnv({
|
const app = await makeAppFromEnv({
|
||||||
|
config: options.config,
|
||||||
|
dbUrl: options.dbUrl,
|
||||||
server: "node",
|
server: "node",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,6 +91,8 @@ async function action(options: {
|
|||||||
fetch: hono.fetch,
|
fetch: hono.fetch,
|
||||||
port: Number(options.port) || 3000,
|
port: Number(options.port) || 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
console.info(`Server is running on http://localhost:${options.port}${options.path}`);
|
console.info(`Server is running on http://localhost:${options.port}${options.path}`);
|
||||||
console.info(
|
console.info(
|
||||||
`⚙️ Tools (${server.tools.length}):\n${server.tools.map((t) => `- ${t.name}`).join("\n")}\n`,
|
`⚙️ Tools (${server.tools.length}):\n${server.tools.map((t) => `- ${t.name}`).join("\n")}\n`,
|
||||||
@@ -91,3 +101,4 @@ async function action(options: {
|
|||||||
`📚 Resources (${server.resources.length}):\n${server.resources.map((r) => `- ${r.name}`).join("\n")}`,
|
`📚 Resources (${server.resources.length}):\n${server.resources.map((r) => `- ${r.name}`).join("\n")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ export const user: CliCommand = (program) => {
|
|||||||
.addArgument(
|
.addArgument(
|
||||||
new Argument("<action>", "action to perform").choices(["create", "update", "token"]),
|
new Argument("<action>", "action to perform").choices(["create", "update", "token"]),
|
||||||
)
|
)
|
||||||
|
.option("--config <config>", "config file")
|
||||||
|
.option("--db-url <db>", "database url, can be any valid sqlite url")
|
||||||
.action(action);
|
.action(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function action(action: "create" | "update" | "token", options: any) {
|
async function action(action: "create" | "update" | "token", options: any) {
|
||||||
const app = await makeAppFromEnv({
|
const app = await makeAppFromEnv({
|
||||||
|
config: options.config,
|
||||||
|
dbUrl: options.dbUrl,
|
||||||
server: "node",
|
server: "node",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,7 @@ export class DataController extends Controller {
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { force, drop } = c.req.valid("query");
|
const { force, drop } = c.req.valid("query");
|
||||||
//console.log("force", force);
|
|
||||||
const tables = await this.em.schema().introspect();
|
const tables = await this.em.schema().introspect();
|
||||||
//console.log("tables", tables);
|
|
||||||
const changes = await this.em.schema().sync({
|
const changes = await this.em.schema().sync({
|
||||||
force,
|
force,
|
||||||
drop,
|
drop,
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ export const fieldsSchemaObject = objectTransform(FIELDS, (field, name) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
export const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject));
|
export const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject));
|
||||||
export const entityFields = s.record(fieldsSchema);
|
export const entityFields = s.record(fieldsSchema, { default: {} });
|
||||||
export type TAppDataField = s.Static<typeof fieldsSchema>;
|
export type TAppDataField = s.Static<typeof fieldsSchema>;
|
||||||
export type TAppDataEntityFields = s.Static<typeof entityFields>;
|
export type TAppDataEntityFields = s.Static<typeof entityFields>;
|
||||||
|
|
||||||
export const entitiesSchema = s.strictObject({
|
export const entitiesSchema = s.strictObject({
|
||||||
name: s.string().optional(), // @todo: verify, old schema wasn't strict (req in UI)
|
name: s.string().optional(), // @todo: verify, old schema wasn't strict (req in UI)
|
||||||
type: s.string({ enum: entityTypes, default: "regular" }),
|
type: s.string({ enum: entityTypes, default: "regular" }).optional(),
|
||||||
config: entityConfigSchema,
|
config: entityConfigSchema.optional(),
|
||||||
fields: entityFields,
|
fields: entityFields.optional(),
|
||||||
});
|
});
|
||||||
export type TAppDataEntity = s.Static<typeof entitiesSchema>;
|
export type TAppDataEntity = s.Static<typeof entitiesSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ import {
|
|||||||
|
|
||||||
// @todo: entity must be migrated to typebox
|
// @todo: entity must be migrated to typebox
|
||||||
export const entityConfigSchema = s
|
export const entityConfigSchema = s
|
||||||
.strictObject({
|
.strictObject(
|
||||||
|
{
|
||||||
name: s.string(),
|
name: s.string(),
|
||||||
name_singular: s.string(),
|
name_singular: s.string(),
|
||||||
description: s.string(),
|
description: s.string(),
|
||||||
sort_field: s.string({ default: config.data.default_primary_field }),
|
sort_field: s.string({ default: config.data.default_primary_field }),
|
||||||
sort_dir: s.string({ enum: ["asc", "desc"], default: "asc" }),
|
sort_dir: s.string({ enum: ["asc", "desc"], default: "asc" }),
|
||||||
primary_format: s.string({ enum: primaryFieldTypes }),
|
primary_format: s.string({ enum: primaryFieldTypes }),
|
||||||
})
|
},
|
||||||
|
{ default: {} },
|
||||||
|
)
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
export type EntityConfig = s.Static<typeof entityConfigSchema>;
|
export type EntityConfig = s.Static<typeof entityConfigSchema>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { App, SafeUser } from "bknd";
|
import type { App, Permission, SafeUser } from "bknd";
|
||||||
import { type Context, type Env, Hono } from "hono";
|
import { type Context, type Env, Hono } from "hono";
|
||||||
import * as middlewares from "modules/middlewares";
|
import * as middlewares from "modules/middlewares";
|
||||||
import type { EntityManager } from "data/entities";
|
import type { EntityManager } from "data/entities";
|
||||||
@@ -19,20 +19,6 @@ export interface ServerEnv extends Env {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* export type ServerEnv = Env & {
|
|
||||||
Variables: {
|
|
||||||
app: App;
|
|
||||||
// to prevent resolving auth multiple times
|
|
||||||
auth?: {
|
|
||||||
resolved: boolean;
|
|
||||||
registered: boolean;
|
|
||||||
skip: boolean;
|
|
||||||
user?: SafeUser;
|
|
||||||
};
|
|
||||||
html?: string;
|
|
||||||
};
|
|
||||||
}; */
|
|
||||||
|
|
||||||
export class Controller {
|
export class Controller {
|
||||||
protected middlewares = middlewares;
|
protected middlewares = middlewares;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { App } from "bknd";
|
||||||
import type { EventManager } from "core/events";
|
import type { EventManager } from "core/events";
|
||||||
import type { Connection } from "data/connection";
|
import type { Connection } from "data/connection";
|
||||||
import type { EntityManager } from "data/entities";
|
import type { EntityManager } from "data/entities";
|
||||||
@@ -11,6 +12,10 @@ import type { McpServer } from "bknd/utils";
|
|||||||
|
|
||||||
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
|
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
|
||||||
|
|
||||||
|
export type ModuleBuildContextMcpContext = {
|
||||||
|
app: App;
|
||||||
|
ctx: () => ModuleBuildContext;
|
||||||
|
};
|
||||||
export type ModuleBuildContext = {
|
export type ModuleBuildContext = {
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
server: Hono<ServerEnv>;
|
server: Hono<ServerEnv>;
|
||||||
@@ -20,7 +25,7 @@ export type ModuleBuildContext = {
|
|||||||
logger: DebugLogger;
|
logger: DebugLogger;
|
||||||
flags: (typeof Module)["ctx_flags"];
|
flags: (typeof Module)["ctx_flags"];
|
||||||
helper: ModuleHelper;
|
helper: ModuleHelper;
|
||||||
mcp: McpServer<{ ctx: () => ModuleBuildContext }>;
|
mcp: McpServer<ModuleBuildContextMcpContext>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class Module<Schema extends object = object> {
|
export abstract class Module<Schema extends object = object> {
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { Entity } from "data/entities";
|
|||||||
import type { EntityIndex, Field } from "data/fields";
|
import type { EntityIndex, Field } from "data/fields";
|
||||||
import { entityTypes } from "data/entities/Entity";
|
import { entityTypes } from "data/entities/Entity";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import type { ModuleBuildContext } from "./Module";
|
import type { ModuleBuildContext, ModuleBuildContextMcpContext } from "./Module";
|
||||||
import type { EntityRelation } from "data/relations";
|
import type { EntityRelation } from "data/relations";
|
||||||
|
import type { Permission } from "core/security/Permission";
|
||||||
|
import { Exception } from "core/errors";
|
||||||
|
import { invariant } from "bknd/utils";
|
||||||
|
|
||||||
export class ModuleHelper {
|
export class ModuleHelper {
|
||||||
constructor(protected ctx: Omit<ModuleBuildContext, "helper">) {}
|
constructor(protected ctx: Omit<ModuleBuildContext, "helper">) {}
|
||||||
@@ -110,4 +113,21 @@ export class ModuleHelper {
|
|||||||
|
|
||||||
entity.__replaceField(name, newField);
|
entity.__replaceField(name, newField);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async throwUnlessGranted(
|
||||||
|
permission: Permission | string,
|
||||||
|
c: { context: ModuleBuildContextMcpContext; request: Request },
|
||||||
|
) {
|
||||||
|
invariant(c.context.app, "app is not available in mcp context");
|
||||||
|
invariant(c.request instanceof Request, "request is not available in mcp context");
|
||||||
|
|
||||||
|
const user = await c.context.app.module.auth.authenticator.resolveAuthFromRequest(c.request);
|
||||||
|
|
||||||
|
if (!this.ctx.guard.granted(permission, user)) {
|
||||||
|
throw new Exception(
|
||||||
|
`Permission "${typeof permission === "string" ? permission : permission.name}" not granted`,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,6 +273,11 @@ export class ModuleManager {
|
|||||||
: new EntityManager([], this.connection, [], [], this.emgr);
|
: new EntityManager([], this.connection, [], [], this.emgr);
|
||||||
this.guard = new Guard();
|
this.guard = new Guard();
|
||||||
this.mcp = new McpServer(undefined as any, {
|
this.mcp = new McpServer(undefined as any, {
|
||||||
|
app: new Proxy(this, {
|
||||||
|
get: () => {
|
||||||
|
throw new Error("app is not available in mcp context");
|
||||||
|
},
|
||||||
|
}) as any,
|
||||||
ctx: () => this.ctx(),
|
ctx: () => this.ctx(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,15 +354,19 @@ export class SystemController extends Controller {
|
|||||||
const { mcp } = this.app.modules.ctx();
|
const { mcp } = this.app.modules.ctx();
|
||||||
const { version, ...appConfig } = this.app.toJSON();
|
const { version, ...appConfig } = this.app.toJSON();
|
||||||
|
|
||||||
mcp.resource("system_config", "bknd://system/config", (c) =>
|
mcp.resource("system_config", "bknd://system/config", async (c) => {
|
||||||
c.json(this.app.toJSON(), {
|
await c.context.ctx().helper.throwUnlessGranted(SystemPermissions.configRead, c);
|
||||||
|
|
||||||
|
return c.json(this.app.toJSON(), {
|
||||||
title: "System Config",
|
title: "System Config",
|
||||||
}),
|
});
|
||||||
)
|
})
|
||||||
.resource(
|
.resource(
|
||||||
"system_config_module",
|
"system_config_module",
|
||||||
"bknd://system/config/{module}",
|
"bknd://system/config/{module}",
|
||||||
(c, { module }) => {
|
async (c, { module }) => {
|
||||||
|
await this.ctx.helper.throwUnlessGranted(SystemPermissions.configRead, c);
|
||||||
|
|
||||||
const m = this.app.modules.get(module as any) as Module;
|
const m = this.app.modules.get(module as any) as Module;
|
||||||
return c.json(m.toJSON(), {
|
return c.json(m.toJSON(), {
|
||||||
title: `Config for ${module}`,
|
title: `Config for ${module}`,
|
||||||
@@ -372,15 +376,19 @@ export class SystemController extends Controller {
|
|||||||
list: Object.keys(appConfig),
|
list: Object.keys(appConfig),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.resource("system_schema", "bknd://system/schema", (c) =>
|
.resource("system_schema", "bknd://system/schema", async (c) => {
|
||||||
c.json(this.app.getSchema(), {
|
await this.ctx.helper.throwUnlessGranted(SystemPermissions.schemaRead, c);
|
||||||
|
|
||||||
|
return c.json(this.app.getSchema(), {
|
||||||
title: "System Schema",
|
title: "System Schema",
|
||||||
}),
|
});
|
||||||
)
|
})
|
||||||
.resource(
|
.resource(
|
||||||
"system_schema_module",
|
"system_schema_module",
|
||||||
"bknd://system/schema/{module}",
|
"bknd://system/schema/{module}",
|
||||||
(c, { module }) => {
|
async (c, { module }) => {
|
||||||
|
await this.ctx.helper.throwUnlessGranted(SystemPermissions.schemaRead, c);
|
||||||
|
|
||||||
const m = this.app.modules.get(module as any);
|
const m = this.app.modules.get(module as any);
|
||||||
return c.json(m.getSchema().toJSON(), {
|
return c.json(m.getSchema().toJSON(), {
|
||||||
title: `Schema for ${module}`,
|
title: `Schema for ${module}`,
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -35,7 +35,7 @@
|
|||||||
"hono": "4.8.3",
|
"hono": "4.8.3",
|
||||||
"json-schema-library": "10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"jsonv-ts": "0.6.0",
|
"jsonv-ts": "0.6.2",
|
||||||
"kysely": "0.27.6",
|
"kysely": "0.27.6",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
@@ -2511,7 +2511,7 @@
|
|||||||
|
|
||||||
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
||||||
|
|
||||||
"jsonv-ts": ["jsonv-ts@0.6.0", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-U9u2Gtv5NyYwMvSHPcOwd9Qgg4cUFPepI5vTkwU7Ib01ipEqaRrM/AM+REEbYqiI6LX+7YF+HcMbgCIHkUMhSQ=="],
|
"jsonv-ts": ["jsonv-ts@0.6.2", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-v4rIs0h7hoPiadotSNoLCvKyitUtboizbeydQjfBHb1HJG5ADda+BpNeRFGEGtq0m8405UmpEJ9l2Kt9J4SItQ=="],
|
||||||
|
|
||||||
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user