@creezio/mcp-facade 0.22.0
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/README.md +420 -0
- package/dist/.creezio-src-hash.json +4 -0
- package/dist/admin/adapters.d.ts +10 -0
- package/dist/admin/adapters.d.ts.map +1 -0
- package/dist/admin/adapters.js +21 -0
- package/dist/admin/adapters.js.map +1 -0
- package/dist/admin/http-routes.d.ts +11 -0
- package/dist/admin/http-routes.d.ts.map +1 -0
- package/dist/admin/http-routes.js +66 -0
- package/dist/admin/http-routes.js.map +1 -0
- package/dist/admin/index.d.ts +12 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +8 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/mcp-admin.d.ts +165 -0
- package/dist/admin/mcp-admin.d.ts.map +1 -0
- package/dist/admin/mcp-admin.js +419 -0
- package/dist/admin/mcp-admin.js.map +1 -0
- package/dist/admin/tool-policy-guard.d.ts +106 -0
- package/dist/admin/tool-policy-guard.d.ts.map +1 -0
- package/dist/admin/tool-policy-guard.js +187 -0
- package/dist/admin/tool-policy-guard.js.map +1 -0
- package/dist/admin/types.d.ts +62 -0
- package/dist/admin/types.d.ts.map +1 -0
- package/dist/admin/types.js +6 -0
- package/dist/admin/types.js.map +1 -0
- package/dist/brand-facade.d.ts +41 -0
- package/dist/brand-facade.d.ts.map +1 -0
- package/dist/brand-facade.js +67 -0
- package/dist/brand-facade.js.map +1 -0
- package/dist/core-tools.d.ts +22 -0
- package/dist/core-tools.d.ts.map +1 -0
- package/dist/core-tools.js +93 -0
- package/dist/core-tools.js.map +1 -0
- package/dist/dyn-import.d.ts +13 -0
- package/dist/dyn-import.d.ts.map +1 -0
- package/dist/dyn-import.js +2 -0
- package/dist/dyn-import.js.map +1 -0
- package/dist/facade.d.ts +40 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +334 -0
- package/dist/facade.js.map +1 -0
- package/dist/hono-bind.d.ts +48 -0
- package/dist/hono-bind.d.ts.map +1 -0
- package/dist/hono-bind.js +70 -0
- package/dist/hono-bind.js.map +1 -0
- package/dist/hono-proxy.d.ts +63 -0
- package/dist/hono-proxy.d.ts.map +1 -0
- package/dist/hono-proxy.js +185 -0
- package/dist/hono-proxy.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +84 -0
- package/dist/jwt.js.map +1 -0
- package/dist/module-ops-tools.d.ts +43 -0
- package/dist/module-ops-tools.d.ts.map +1 -0
- package/dist/module-ops-tools.js +301 -0
- package/dist/module-ops-tools.js.map +1 -0
- package/dist/namespace.d.ts +23 -0
- package/dist/namespace.d.ts.map +1 -0
- package/dist/namespace.js +51 -0
- package/dist/namespace.js.map +1 -0
- package/dist/oauth/cors-policy.d.ts +3 -0
- package/dist/oauth/cors-policy.d.ts.map +1 -0
- package/dist/oauth/cors-policy.js +25 -0
- package/dist/oauth/cors-policy.js.map +1 -0
- package/dist/oauth/hono-app.d.ts +18 -0
- package/dist/oauth/hono-app.d.ts.map +1 -0
- package/dist/oauth/hono-app.js +175 -0
- package/dist/oauth/hono-app.js.map +1 -0
- package/dist/oauth/index.d.ts +9 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +6 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth/rate-limit.d.ts +11 -0
- package/dist/oauth/rate-limit.d.ts.map +1 -0
- package/dist/oauth/rate-limit.js +32 -0
- package/dist/oauth/rate-limit.js.map +1 -0
- package/dist/oauth/routes.d.ts +8 -0
- package/dist/oauth/routes.d.ts.map +1 -0
- package/dist/oauth/routes.js +671 -0
- package/dist/oauth/routes.js.map +1 -0
- package/dist/oauth/store.d.ts +113 -0
- package/dist/oauth/store.d.ts.map +1 -0
- package/dist/oauth/store.js +390 -0
- package/dist/oauth/store.js.map +1 -0
- package/dist/oauth/types.d.ts +97 -0
- package/dist/oauth/types.d.ts.map +1 -0
- package/dist/oauth/types.js +5 -0
- package/dist/oauth/types.js.map +1 -0
- package/dist/open-external-tab-host-tools.d.ts +67 -0
- package/dist/open-external-tab-host-tools.d.ts.map +1 -0
- package/dist/open-external-tab-host-tools.js +92 -0
- package/dist/open-external-tab-host-tools.js.map +1 -0
- package/dist/policy.d.ts +59 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +88 -0
- package/dist/policy.js.map +1 -0
- package/dist/runtime.d.ts +22 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +20 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +163 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist-cjs/admin/adapters.js +27 -0
- package/dist-cjs/admin/adapters.js.map +1 -0
- package/dist-cjs/admin/http-routes.js +69 -0
- package/dist-cjs/admin/http-routes.js.map +1 -0
- package/dist-cjs/admin/index.js +40 -0
- package/dist-cjs/admin/index.js.map +1 -0
- package/dist-cjs/admin/mcp-admin.js +441 -0
- package/dist-cjs/admin/mcp-admin.js.map +1 -0
- package/dist-cjs/admin/tool-policy-guard.js +193 -0
- package/dist-cjs/admin/tool-policy-guard.js.map +1 -0
- package/dist-cjs/admin/types.js +7 -0
- package/dist-cjs/admin/types.js.map +1 -0
- package/dist-cjs/brand-facade.js +70 -0
- package/dist-cjs/brand-facade.js.map +1 -0
- package/dist-cjs/core-tools.js +97 -0
- package/dist-cjs/core-tools.js.map +1 -0
- package/dist-cjs/dyn-import.js +5 -0
- package/dist-cjs/dyn-import.js.map +1 -0
- package/dist-cjs/facade.js +337 -0
- package/dist-cjs/facade.js.map +1 -0
- package/dist-cjs/hono-bind.js +74 -0
- package/dist-cjs/hono-bind.js.map +1 -0
- package/dist-cjs/hono-proxy.js +189 -0
- package/dist-cjs/hono-proxy.js.map +1 -0
- package/dist-cjs/index.js +120 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/jwt.js +91 -0
- package/dist-cjs/jwt.js.map +1 -0
- package/dist-cjs/module-ops-tools.js +308 -0
- package/dist-cjs/module-ops-tools.js.map +1 -0
- package/dist-cjs/namespace.js +56 -0
- package/dist-cjs/namespace.js.map +1 -0
- package/dist-cjs/oauth/cors-policy.js +29 -0
- package/dist-cjs/oauth/cors-policy.js.map +1 -0
- package/dist-cjs/oauth/hono-app.js +178 -0
- package/dist-cjs/oauth/hono-app.js.map +1 -0
- package/dist-cjs/oauth/index.js +48 -0
- package/dist-cjs/oauth/index.js.map +1 -0
- package/dist-cjs/oauth/rate-limit.js +37 -0
- package/dist-cjs/oauth/rate-limit.js.map +1 -0
- package/dist-cjs/oauth/routes.js +674 -0
- package/dist-cjs/oauth/routes.js.map +1 -0
- package/dist-cjs/oauth/store.js +418 -0
- package/dist-cjs/oauth/store.js.map +1 -0
- package/dist-cjs/oauth/types.js +6 -0
- package/dist-cjs/oauth/types.js.map +1 -0
- package/dist-cjs/open-external-tab-host-tools.js +96 -0
- package/dist-cjs/open-external-tab-host-tools.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/policy.js +93 -0
- package/dist-cjs/policy.js.map +1 -0
- package/dist-cjs/runtime.js +24 -0
- package/dist-cjs/runtime.js.map +1 -0
- package/dist-cjs/types.js +3 -0
- package/dist-cjs/types.js.map +1 -0
- package/package.json +92 -0
- package/ui/index.ts +5 -0
- package/ui/mcp-admin-client.tsx +270 -0
- package/ui/primitives/badge.tsx +37 -0
- package/ui/primitives/button.tsx +44 -0
- package/ui/primitives/card.tsx +44 -0
- package/ui/primitives/cn.ts +6 -0
- package/ui/primitives/input.tsx +19 -0
- package/ui/primitives/tabs.tsx +51 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Policies MCP H4 — deny cross-layer cohérent api-kernel H2.
|
|
4
|
+
* H5 — plugin ACL (see/execute) alignée Product Hub.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.denyCrossLayerToolCall = denyCrossLayerToolCall;
|
|
8
|
+
exports.composeToolPolicies = composeToolPolicies;
|
|
9
|
+
exports.createDenyUnauthorizedPluginToolPolicy = createDenyUnauthorizedPluginToolPolicy;
|
|
10
|
+
/**
|
|
11
|
+
* Deny-by-default si la charge utile tente un échappement cross-layer
|
|
12
|
+
* (même sémantique que api-kernel `__cross/` / `core/` escaping).
|
|
13
|
+
*/
|
|
14
|
+
function denyCrossLayerToolCall(ctx) {
|
|
15
|
+
const args = ctx.args || {};
|
|
16
|
+
for (const [key, value] of Object.entries(args)) {
|
|
17
|
+
const blob = `${key}=${stringifyArg(value)}`.toLowerCase();
|
|
18
|
+
if (blob.includes("__cross/") ||
|
|
19
|
+
blob.includes("__cross") ||
|
|
20
|
+
/(^|[=\s/])core\//.test(blob) ||
|
|
21
|
+
blob.includes("/../") ||
|
|
22
|
+
blob.includes("..\\")) {
|
|
23
|
+
return { allow: false, reason: "cross_layer_denied" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Un tool module/plugin ne peut pas se faire passer pour core via alias.
|
|
27
|
+
if (ctx.isAlias && ctx.space === "core") {
|
|
28
|
+
return { allow: false, reason: "alias_to_core_denied" };
|
|
29
|
+
}
|
|
30
|
+
// Spoof explicite de couche cible.
|
|
31
|
+
const targetSpace = args.targetSpace ?? args.target_space ?? args.space;
|
|
32
|
+
if (typeof targetSpace === "string" &&
|
|
33
|
+
targetSpace !== ctx.space &&
|
|
34
|
+
(targetSpace === "core" ||
|
|
35
|
+
targetSpace === "module" ||
|
|
36
|
+
targetSpace === "plugin")) {
|
|
37
|
+
return { allow: false, reason: "cross_layer_space_spoof" };
|
|
38
|
+
}
|
|
39
|
+
return { allow: true };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compose plusieurs policies (premier deny gagne).
|
|
43
|
+
*/
|
|
44
|
+
function composeToolPolicies(...policies) {
|
|
45
|
+
return async (ctx) => {
|
|
46
|
+
for (const p of policies) {
|
|
47
|
+
const d = await p(ctx);
|
|
48
|
+
if (!d.allow)
|
|
49
|
+
return d;
|
|
50
|
+
}
|
|
51
|
+
return { allow: true };
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* H5 — deny tools `plugin.<id>.*` si l'acteur n'a pas `execute`.
|
|
56
|
+
* Injecter `decidePluginAccess` depuis `@creezio/product-hub` pour
|
|
57
|
+
* une décision identique à l'API / control-plane.
|
|
58
|
+
*/
|
|
59
|
+
function createDenyUnauthorizedPluginToolPolicy(opts) {
|
|
60
|
+
const resolveActor = opts.resolveActor ||
|
|
61
|
+
((ctx) => ({
|
|
62
|
+
orgId: ctx.orgId ?? ctx.claims?.orgId ?? null,
|
|
63
|
+
userId: ctx.subject || null,
|
|
64
|
+
isOwner: Boolean(ctx.claims?.isOwner),
|
|
65
|
+
isServiceKey: ctx.subject === "opaque-token" || ctx.subject === "anonymous",
|
|
66
|
+
}));
|
|
67
|
+
return (ctx) => {
|
|
68
|
+
if (ctx.space !== "plugin")
|
|
69
|
+
return { allow: true };
|
|
70
|
+
const pluginId = ctx.ownerId;
|
|
71
|
+
if (!pluginId) {
|
|
72
|
+
return { allow: false, reason: "acl_plugin_owner_missing" };
|
|
73
|
+
}
|
|
74
|
+
const actor = resolveActor(ctx);
|
|
75
|
+
const decision = opts.decide(opts.getPolicy(pluginId), actor, "execute");
|
|
76
|
+
if (!decision.allow)
|
|
77
|
+
return { allow: false, reason: decision.reason };
|
|
78
|
+
return { allow: true };
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function stringifyArg(value) {
|
|
82
|
+
if (value == null)
|
|
83
|
+
return "";
|
|
84
|
+
if (typeof value === "string")
|
|
85
|
+
return value;
|
|
86
|
+
try {
|
|
87
|
+
return JSON.stringify(value);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return String(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Contrat MCP produit (D1/C2 → M9) — une seule stack.
|
|
4
|
+
*
|
|
5
|
+
* - Exécuteur HTTP public : **Hono** `GET|POST /mcp`.
|
|
6
|
+
* - Façade Electron `@creezio/mcp-facade` : adaptateur brand-mounts (tests /
|
|
7
|
+
* offline) **ou** proxy vers Hono dès qu'un upstream local est annoncé.
|
|
8
|
+
*
|
|
9
|
+
* Hermes / Cursor / ChatGPT → toujours `{base}/mcp`, jamais un 2ᵉ serveur
|
|
10
|
+
* MCP dans le process Electron.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.MCP_PRODUCT_EXECUTOR = void 0;
|
|
14
|
+
exports.resolveMcpFacadeRole = resolveMcpFacadeRole;
|
|
15
|
+
exports.MCP_PRODUCT_EXECUTOR = "hono";
|
|
16
|
+
function resolveMcpFacadeRole(mode, upstream) {
|
|
17
|
+
if (mode === "local-adapter")
|
|
18
|
+
return "local-brand-adapter";
|
|
19
|
+
if (mode === "hono-proxy")
|
|
20
|
+
return "hono-proxy";
|
|
21
|
+
// hono-preferred
|
|
22
|
+
return upstream ? "hono-proxy" : "local-brand-adapter";
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAiBH,oDAQC;AAvBY,QAAA,oBAAoB,GAAG,MAAe,CAAC;AAepD,SAAgB,oBAAoB,CAClC,IAAmB,EACnB,QAAuB;IAEvB,IAAI,IAAI,KAAK,eAAe;QAAE,OAAO,qBAAqB,CAAC;IAC3D,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,YAAY,CAAC;IAC/C,iBAAiB;IACjB,OAAO,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@creezio/mcp-facade",
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "MCP proxy unifié H4 + admin policies/clients/UI (N6)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist-cjs/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist-cjs/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./ui": {
|
|
16
|
+
"types": "./ui/index.ts",
|
|
17
|
+
"import": "./ui/index.ts",
|
|
18
|
+
"default": "./ui/index.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"dist-cjs",
|
|
24
|
+
"ui"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@creezio/api-kernel": "^0.22.0",
|
|
32
|
+
"@creezio/platform-core": "^0.22.0",
|
|
33
|
+
"hono": "^4.12.32",
|
|
34
|
+
"jose": "^6.2.5",
|
|
35
|
+
"zod": "^4.4.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.15.3",
|
|
39
|
+
"@types/react": "^18.3.12",
|
|
40
|
+
"typescript": "^5.8.3"
|
|
41
|
+
},
|
|
42
|
+
"license": "UNLICENSED",
|
|
43
|
+
"module": "./dist/index.js",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
46
|
+
"@radix-ui/react-tabs": "^1.1.0",
|
|
47
|
+
"class-variance-authority": "^0.7.0",
|
|
48
|
+
"clsx": "^2.0.0",
|
|
49
|
+
"lucide-react": ">=0.400.0",
|
|
50
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
51
|
+
"sonner": ">=1.0.0",
|
|
52
|
+
"tailwind-merge": "^2.0.0 || ^3.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@radix-ui/react-slot": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@radix-ui/react-tabs": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"class-variance-authority": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"clsx": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"lucide-react": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"react": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"sonner": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"tailwind-merge": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"registry": "https://registry.npmjs.org",
|
|
82
|
+
"access": "public"
|
|
83
|
+
},
|
|
84
|
+
"repository": {
|
|
85
|
+
"type": "git",
|
|
86
|
+
"url": "https://github.com/creezio/creezio.git",
|
|
87
|
+
"directory": "packages/mcp-facade"
|
|
88
|
+
},
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=22.5"
|
|
91
|
+
}
|
|
92
|
+
}
|
package/ui/index.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { CheckCircle2, Copy, Download, RefreshCw, ShieldAlert, XCircle } from "lucide-react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import { Badge } from "./primitives/badge";
|
|
8
|
+
import { Button } from "./primitives/button";
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from "./primitives/card";
|
|
10
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./primitives/tabs";
|
|
11
|
+
|
|
12
|
+
type Status = {
|
|
13
|
+
ready: boolean;
|
|
14
|
+
publicUrl: string | null;
|
|
15
|
+
mcpUrl: string | null;
|
|
16
|
+
oauthReady: boolean;
|
|
17
|
+
jwtConfigured: boolean;
|
|
18
|
+
toolCount: number;
|
|
19
|
+
enabledToolCount: number;
|
|
20
|
+
clientCount: number;
|
|
21
|
+
enabledClientCount: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Tool = {
|
|
25
|
+
name: string;
|
|
26
|
+
category: string;
|
|
27
|
+
access: "read" | "write";
|
|
28
|
+
requiredScope: string;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
allowedRoles: string[];
|
|
31
|
+
annotations?: Record<string, boolean>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type Client = {
|
|
35
|
+
client_id: string;
|
|
36
|
+
client_name: string | null;
|
|
37
|
+
redirect_uris: string[];
|
|
38
|
+
token_endpoint_auth_method: string;
|
|
39
|
+
scope: string | null;
|
|
40
|
+
created_at: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
revoked_at: string | null;
|
|
43
|
+
last_used_at: string | null;
|
|
44
|
+
active_refresh_tokens: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type Diagnostics = {
|
|
48
|
+
healthy: boolean;
|
|
49
|
+
checks: Array<{ id: string; ok: boolean; message: string }>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type Metrics = {
|
|
53
|
+
requests: number;
|
|
54
|
+
errors: number;
|
|
55
|
+
errorRate: number;
|
|
56
|
+
averageDurationMs: number;
|
|
57
|
+
p95DurationMs: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
async function api<T>(url: string, init?: RequestInit): Promise<T> {
|
|
61
|
+
const response = await fetch(url, init);
|
|
62
|
+
const body = (await response.json().catch(() => ({}))) as T & { error?: string };
|
|
63
|
+
if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function McpAdminClient({ logsSlot }: { logsSlot?: ReactNode } = {}) {
|
|
68
|
+
const [status, setStatus] = useState<Status | null>(null);
|
|
69
|
+
const [tools, setTools] = useState<Tool[]>([]);
|
|
70
|
+
const [clients, setClients] = useState<Client[]>([]);
|
|
71
|
+
const [diagnostics, setDiagnostics] = useState<Diagnostics | null>(null);
|
|
72
|
+
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
|
73
|
+
const [loading, setLoading] = useState(true);
|
|
74
|
+
const [error, setError] = useState("");
|
|
75
|
+
|
|
76
|
+
const load = useCallback(async () => {
|
|
77
|
+
setLoading(true);
|
|
78
|
+
setError("");
|
|
79
|
+
try {
|
|
80
|
+
const [nextStatus, toolData, clientData, nextDiagnostics, nextMetrics] =
|
|
81
|
+
await Promise.all([
|
|
82
|
+
api<Status>("/api/v1/admin/mcp/status"),
|
|
83
|
+
api<{ tools: Tool[] }>("/api/v1/admin/mcp/tools"),
|
|
84
|
+
api<{ clients: Client[] }>("/api/v1/admin/mcp/clients"),
|
|
85
|
+
api<Diagnostics>("/api/v1/admin/mcp/diagnostics"),
|
|
86
|
+
api<Metrics>("/api/v1/admin/mcp/metrics"),
|
|
87
|
+
]);
|
|
88
|
+
setStatus(nextStatus);
|
|
89
|
+
setTools(toolData.tools);
|
|
90
|
+
setClients(clientData.clients);
|
|
91
|
+
setDiagnostics(nextDiagnostics);
|
|
92
|
+
setMetrics(nextMetrics);
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
setError(cause instanceof Error ? cause.message : "Chargement impossible");
|
|
95
|
+
} finally {
|
|
96
|
+
setLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
void load();
|
|
102
|
+
}, [load]);
|
|
103
|
+
|
|
104
|
+
async function toggleTool(tool: Tool) {
|
|
105
|
+
try {
|
|
106
|
+
await api(`/api/v1/admin/mcp/policies/${encodeURIComponent(tool.name)}`, {
|
|
107
|
+
method: "PATCH",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ enabled: !tool.enabled }),
|
|
110
|
+
});
|
|
111
|
+
toast.success(`${tool.name} ${tool.enabled ? "désactivé" : "activé"}`);
|
|
112
|
+
await load();
|
|
113
|
+
} catch (cause) {
|
|
114
|
+
toast.error(cause instanceof Error ? cause.message : "Échec");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function toggleClient(client: Client) {
|
|
119
|
+
try {
|
|
120
|
+
await api(`/api/v1/admin/mcp/clients/${encodeURIComponent(client.client_id)}`, {
|
|
121
|
+
method: "PATCH",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify({ enabled: !client.enabled }),
|
|
124
|
+
});
|
|
125
|
+
await load();
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
toast.error(cause instanceof Error ? cause.message : "Échec");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function revokeClient(client: Client) {
|
|
132
|
+
if (!window.confirm(`Révoquer définitivement ${client.client_name || client.client_id} ?`)) return;
|
|
133
|
+
try {
|
|
134
|
+
await api(`/api/v1/admin/mcp/clients/${encodeURIComponent(client.client_id)}`, {
|
|
135
|
+
method: "DELETE",
|
|
136
|
+
});
|
|
137
|
+
await load();
|
|
138
|
+
} catch (cause) {
|
|
139
|
+
toast.error(cause instanceof Error ? cause.message : "Échec");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function rotateSecret(client: Client) {
|
|
144
|
+
if (!window.confirm("Tourner le secret et révoquer les refresh tokens existants ?")) return;
|
|
145
|
+
try {
|
|
146
|
+
const result = await api<{ clientSecret: string }>(
|
|
147
|
+
`/api/v1/admin/mcp/clients/${encodeURIComponent(client.client_id)}/rotate-secret`,
|
|
148
|
+
{ method: "POST" },
|
|
149
|
+
);
|
|
150
|
+
window.prompt("Nouveau secret — copiez-le maintenant, il ne sera plus affiché :", result.clientSecret);
|
|
151
|
+
await load();
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
toast.error(cause instanceof Error ? cause.message : "Échec");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function copy(value: string | null) {
|
|
158
|
+
if (!value) return;
|
|
159
|
+
await navigator.clipboard.writeText(value);
|
|
160
|
+
toast.success("URL copiée");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className="space-y-4">
|
|
165
|
+
<div className="flex items-center justify-between">
|
|
166
|
+
<p className="text-sm text-slate-500">Pilotage du serveur, des tools et des accès OAuth.</p>
|
|
167
|
+
<Button variant="outline" size="sm" onClick={() => void load()} disabled={loading}>
|
|
168
|
+
<RefreshCw className={`mr-1.5 h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />
|
|
169
|
+
Actualiser
|
|
170
|
+
</Button>
|
|
171
|
+
</div>
|
|
172
|
+
{error ? <div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700">{error}</div> : null}
|
|
173
|
+
|
|
174
|
+
<Tabs defaultValue="overview">
|
|
175
|
+
<TabsList className="h-auto flex-wrap">
|
|
176
|
+
<TabsTrigger value="overview">Vue d’ensemble</TabsTrigger>
|
|
177
|
+
<TabsTrigger value="tools">Tools</TabsTrigger>
|
|
178
|
+
<TabsTrigger value="access">Accès/clients</TabsTrigger>
|
|
179
|
+
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
180
|
+
<TabsTrigger value="connection">Connexion</TabsTrigger>
|
|
181
|
+
</TabsList>
|
|
182
|
+
|
|
183
|
+
<TabsContent value="overview" className="space-y-4">
|
|
184
|
+
<div className="flex justify-end">
|
|
185
|
+
<Button variant="outline" size="sm" asChild>
|
|
186
|
+
<a href="/api/v1/admin/mcp/diagnostics/export" download>
|
|
187
|
+
<Download className="mr-1.5 h-3.5 w-3.5" />
|
|
188
|
+
Export diagnostic expurgé
|
|
189
|
+
</a>
|
|
190
|
+
</Button>
|
|
191
|
+
</div>
|
|
192
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
193
|
+
<Stat label="Tools actifs" value={`${status?.enabledToolCount || 0}/${status?.toolCount || 0}`} />
|
|
194
|
+
<Stat label="Clients actifs" value={`${status?.enabledClientCount || 0}/${status?.clientCount || 0}`} />
|
|
195
|
+
<Stat label="Appels MCP" value={String(metrics?.requests || 0)} />
|
|
196
|
+
<Stat label="Erreurs" value={String(metrics?.errors || 0)} />
|
|
197
|
+
</div>
|
|
198
|
+
<Card>
|
|
199
|
+
<CardHeader><CardTitle className="text-base">Diagnostics</CardTitle></CardHeader>
|
|
200
|
+
<CardContent className="grid gap-2 sm:grid-cols-2">
|
|
201
|
+
{diagnostics?.checks.map((check) => (
|
|
202
|
+
<div key={check.id} className="flex items-center gap-2 rounded-lg border p-3 text-sm">
|
|
203
|
+
{check.ok ? <CheckCircle2 className="h-4 w-4 text-emerald-600" /> : <XCircle className="h-4 w-4 text-red-600" />}
|
|
204
|
+
{check.message}
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
</CardContent>
|
|
208
|
+
</Card>
|
|
209
|
+
<Card>
|
|
210
|
+
<CardHeader><CardTitle className="text-base">Performance processus</CardTitle></CardHeader>
|
|
211
|
+
<CardContent className="grid gap-3 sm:grid-cols-3 text-sm">
|
|
212
|
+
<div>Erreur : {((metrics?.errorRate || 0) * 100).toFixed(1)} %</div>
|
|
213
|
+
<div>Moyenne : {metrics?.averageDurationMs || 0} ms</div>
|
|
214
|
+
<div>P95 : {metrics?.p95DurationMs || 0} ms</div>
|
|
215
|
+
</CardContent>
|
|
216
|
+
</Card>
|
|
217
|
+
</TabsContent>
|
|
218
|
+
|
|
219
|
+
<TabsContent value="tools">
|
|
220
|
+
<div className="overflow-x-auto rounded-xl border bg-white">
|
|
221
|
+
<table className="min-w-full text-left text-sm">
|
|
222
|
+
<thead className="border-b bg-slate-50 text-xs uppercase text-slate-500"><tr><th className="p-3">Tool</th><th className="p-3">Catégorie</th><th className="p-3">Scope</th><th className="p-3">Annotations</th><th className="p-3">État</th></tr></thead>
|
|
223
|
+
<tbody>{tools.map((tool) => <tr key={tool.name} className="border-b last:border-0">
|
|
224
|
+
<td className="p-3 font-mono text-xs">{tool.name}</td>
|
|
225
|
+
<td className="p-3">{tool.category}</td>
|
|
226
|
+
<td className="p-3"><Badge variant={tool.access === "read" ? "secondary" : "warning"}>{tool.requiredScope}</Badge></td>
|
|
227
|
+
<td className="p-3 text-xs text-slate-500">{tool.annotations?.readOnlyHint ? "lecture" : "mutation"}{tool.annotations?.destructiveHint ? " · destructif" : ""}</td>
|
|
228
|
+
<td className="p-3"><Button size="sm" variant={tool.enabled ? "outline" : "destructive"} onClick={() => void toggleTool(tool)}>{tool.enabled ? "Actif" : "Désactivé"}</Button></td>
|
|
229
|
+
</tr>)}</tbody>
|
|
230
|
+
</table>
|
|
231
|
+
</div>
|
|
232
|
+
</TabsContent>
|
|
233
|
+
|
|
234
|
+
<TabsContent value="access">
|
|
235
|
+
<div className="space-y-3">{clients.map((client) => (
|
|
236
|
+
<Card key={client.client_id}>
|
|
237
|
+
<CardContent className="flex flex-col gap-3 p-4 lg:flex-row lg:items-center lg:justify-between">
|
|
238
|
+
<div className="min-w-0">
|
|
239
|
+
<div className="flex items-center gap-2"><strong>{client.client_name || "Client sans nom"}</strong><Badge variant={client.enabled ? "success" : "muted"}>{client.revoked_at ? "Révoqué" : client.enabled ? "Actif" : "Désactivé"}</Badge></div>
|
|
240
|
+
<div className="mt-1 truncate font-mono text-xs text-slate-500">{client.client_id}</div>
|
|
241
|
+
<div className="mt-1 text-xs text-slate-500">{client.scope} · {client.active_refresh_tokens} refresh token(s) actif(s) · dernier usage {client.last_used_at || "jamais"}</div>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="flex gap-2">
|
|
244
|
+
{!client.revoked_at && client.token_endpoint_auth_method !== "none" ? <Button variant="outline" size="sm" onClick={() => void rotateSecret(client)}>Tourner le secret</Button> : null}
|
|
245
|
+
{!client.revoked_at ? <Button variant="outline" size="sm" onClick={() => void toggleClient(client)}>{client.enabled ? "Désactiver" : "Activer"}</Button> : null}
|
|
246
|
+
{!client.revoked_at ? <Button variant="destructive" size="sm" onClick={() => void revokeClient(client)}>Révoquer</Button> : null}
|
|
247
|
+
</div>
|
|
248
|
+
</CardContent>
|
|
249
|
+
</Card>
|
|
250
|
+
))}
|
|
251
|
+
{!clients.length ? <div className="rounded-xl border bg-white p-8 text-center text-sm text-slate-500">Aucun client OAuth enregistré.</div> : null}</div>
|
|
252
|
+
</TabsContent>
|
|
253
|
+
|
|
254
|
+
<TabsContent value="logs">{logsSlot ?? <div className="rounded-xl border bg-white p-8 text-center text-sm text-slate-500">Slot logs — passez logsSlot=<RequestLogsClient /> depuis la marque.</div>}</TabsContent>
|
|
255
|
+
|
|
256
|
+
<TabsContent value="connection" className="space-y-4">
|
|
257
|
+
<Card><CardHeader><CardTitle className="text-base">URL MCP</CardTitle></CardHeader><CardContent>
|
|
258
|
+
<div className="flex gap-2"><code className="min-w-0 flex-1 truncate rounded bg-slate-100 p-3 text-sm">{status?.mcpUrl || "Tunnel non configuré"}</code><Button variant="outline" onClick={() => void copy(status?.mcpUrl || null)} disabled={!status?.mcpUrl}><Copy className="h-4 w-4" /></Button></div>
|
|
259
|
+
{!status?.publicUrl ? <div className="mt-3 flex gap-2 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800"><ShieldAlert className="h-4 w-4 shrink-0" />Configurez APP_PUBLIC_URL ou MCP_PUBLIC_URL pour ChatGPT, Claude et Cursor. Hermes loopback continue d’utiliser sa clé API locale.</div> : null}
|
|
260
|
+
</CardContent></Card>
|
|
261
|
+
<Card><CardHeader><CardTitle className="text-base">Clients compatibles</CardTitle></CardHeader><CardContent className="text-sm text-slate-600">Dans Cursor, ChatGPT ou Claude, ajoutez l’URL ci-dessus comme serveur MCP HTTP. OAuth 2.1, PKCE S256 et l’enregistrement dynamique sont détectés automatiquement.</CardContent></Card>
|
|
262
|
+
</TabsContent>
|
|
263
|
+
</Tabs>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function Stat({ label, value }: { label: string; value: string }) {
|
|
269
|
+
return <Card><CardContent className="p-4"><div className="text-2xl font-semibold tabular-nums">{value}</div><div className="text-xs text-slate-500">{label}</div></CardContent></Card>;
|
|
270
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "./cn";
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
"inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "border-transparent bg-slate-900 text-white",
|
|
11
|
+
secondary: "border-transparent bg-slate-100 text-slate-700",
|
|
12
|
+
outline: "border-slate-200 text-slate-700",
|
|
13
|
+
success: "border-transparent bg-emerald-100 text-emerald-800",
|
|
14
|
+
warning: "border-transparent bg-amber-100 text-amber-800",
|
|
15
|
+
danger: "border-transparent bg-red-100 text-red-800",
|
|
16
|
+
info: "border-transparent bg-sky-100 text-sky-800",
|
|
17
|
+
muted: "border-transparent bg-slate-50 text-slate-500",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { variant: "default" },
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export interface BadgeProps
|
|
25
|
+
extends React.HTMLAttributes<HTMLSpanElement>,
|
|
26
|
+
VariantProps<typeof badgeVariants> {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* <span> et non <div> : un badge est utilisé DANS des <p>, <a> et <span>
|
|
30
|
+
* (fiche produit, dashboard, préférences…). Un <div> y est du HTML invalide :
|
|
31
|
+
* le parseur navigateur restructure le DOM (ferme le <p> avant le div) et
|
|
32
|
+
* l'hydratation React échoue (« Hydration failed » #418/#422, cascade
|
|
33
|
+
* possible en #310) — écran « Application error » dans l'app desktop.
|
|
34
|
+
*/
|
|
35
|
+
export function Badge({ className, variant, ...props }: BadgeProps) {
|
|
36
|
+
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "./cn";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-slate-900 text-white hover:bg-slate-800",
|
|
12
|
+
secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200",
|
|
13
|
+
outline: "border border-slate-200 bg-white hover:bg-slate-50",
|
|
14
|
+
ghost: "hover:bg-slate-100",
|
|
15
|
+
destructive: "bg-red-600 text-white hover:bg-red-700",
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: "h-9 px-4 py-2",
|
|
19
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
20
|
+
lg: "h-10 rounded-md px-8",
|
|
21
|
+
icon: "h-9 w-9",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export interface ButtonProps
|
|
29
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
30
|
+
VariantProps<typeof buttonVariants> {
|
|
31
|
+
asChild?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
35
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
36
|
+
const Comp = asChild ? Slot : "button";
|
|
37
|
+
return (
|
|
38
|
+
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
Button.displayName = "Button";
|
|
43
|
+
|
|
44
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<div
|
|
7
|
+
ref={ref}
|
|
8
|
+
className={cn("rounded-xl border border-slate-200 bg-white shadow-sm", className)}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
Card.displayName = "Card";
|
|
14
|
+
|
|
15
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
16
|
+
({ className, ...props }, ref) => (
|
|
17
|
+
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
CardHeader.displayName = "CardHeader";
|
|
21
|
+
|
|
22
|
+
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
23
|
+
({ className, ...props }, ref) => (
|
|
24
|
+
<h3 ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
CardTitle.displayName = "CardTitle";
|
|
28
|
+
|
|
29
|
+
const CardDescription = React.forwardRef<
|
|
30
|
+
HTMLParagraphElement,
|
|
31
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
32
|
+
>(({ className, ...props }, ref) => (
|
|
33
|
+
<p ref={ref} className={cn("text-sm text-slate-500", className)} {...props} />
|
|
34
|
+
));
|
|
35
|
+
CardDescription.displayName = "CardDescription";
|
|
36
|
+
|
|
37
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
38
|
+
({ className, ...props }, ref) => (
|
|
39
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
CardContent.displayName = "CardContent";
|
|
43
|
+
|
|
44
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
5
|
+
({ className, type, ...props }, ref) => (
|
|
6
|
+
<input
|
|
7
|
+
type={type}
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex h-9 w-full rounded-md border border-slate-200 bg-white px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 disabled:cursor-not-allowed disabled:opacity-50",
|
|
10
|
+
className,
|
|
11
|
+
)}
|
|
12
|
+
ref={ref}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
Input.displayName = "Input";
|
|
18
|
+
|
|
19
|
+
export { Input };
|