@formio/uag 2.0.0-rc.6 → 2.0.0-rc.8
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 +8 -6
- package/bin/uag.js +48 -5
- package/lib/UAGFormInterface.d.ts +12 -0
- package/lib/UAGFormInterface.js +30 -3
- package/lib/UAGProjectInterface.d.ts +9 -1
- package/lib/UAGProjectInterface.js +14 -3
- package/lib/agent/anonymous.js +1 -1
- package/lib/agent/engines.d.ts +10 -0
- package/lib/agent/engines.js +15 -1
- package/lib/agent/errors.d.ts +9 -0
- package/lib/agent/errors.js +20 -0
- package/lib/agent/index.d.ts +1 -1
- package/lib/agent/index.js +2 -1
- package/lib/agent/remote.d.ts +5 -1
- package/lib/agent/remote.js +25 -2
- package/lib/agent/router.d.ts +32 -2
- package/lib/agent/router.js +17 -11
- package/lib/agent/stubs.d.ts +13 -2
- package/lib/agent/stubs.js +18 -5
- package/lib/agent/tools.d.ts +24 -3
- package/lib/agent/tools.js +47 -4
- package/lib/config.d.ts +31 -2
- package/lib/documents/convert.d.ts +38 -0
- package/lib/documents/convert.js +151 -0
- package/lib/documents/converters.d.ts +28 -0
- package/lib/documents/converters.js +149 -0
- package/lib/documents/detect.d.ts +31 -0
- package/lib/documents/detect.js +169 -0
- package/lib/documents/index.d.ts +9 -0
- package/lib/documents/index.js +43 -0
- package/lib/documents/mime.d.ts +39 -0
- package/lib/documents/mime.js +100 -0
- package/lib/documents/plugins.d.ts +18 -0
- package/lib/documents/plugins.js +65 -0
- package/lib/documents/readers.d.ts +19 -0
- package/lib/documents/readers.js +64 -0
- package/lib/documents/routes.d.ts +52 -0
- package/lib/documents/routes.js +99 -0
- package/lib/documents/types.d.ts +126 -0
- package/lib/documents/types.js +2 -0
- package/lib/documents/uagJs.d.ts +17 -0
- package/lib/documents/uagJs.js +157 -0
- package/lib/formflow.d.ts +4 -1
- package/lib/formflow.js +2 -2
- package/lib/index.d.ts +50 -6
- package/lib/index.js +151 -15
- package/lib/markdown.d.ts +1 -0
- package/lib/markdown.js +166 -0
- package/lib/modules/api.d.ts +22 -0
- package/lib/modules/api.js +100 -0
- package/lib/modules/index.d.ts +4 -0
- package/lib/modules/index.js +24 -0
- package/lib/modules/routes.d.ts +14 -0
- package/lib/modules/routes.js +21 -0
- package/lib/modules/types.d.ts +68 -0
- package/lib/modules/types.js +5 -0
- package/lib/pluginFiles.d.ts +28 -0
- package/lib/pluginFiles.js +101 -0
- package/lib/plugins.d.ts +17 -0
- package/lib/plugins.js +93 -0
- package/lib/richText.d.ts +20 -0
- package/lib/richText.js +99 -0
- package/lib/router.d.ts +9 -0
- package/lib/router.js +19 -0
- package/lib/testing.d.ts +74 -0
- package/lib/testing.js +175 -0
- package/lib/tools/index.d.ts +2 -1
- package/lib/tools/index.js +5 -2
- package/lib/tools/readDocument.d.ts +17 -0
- package/lib/tools/readDocument.js +99 -0
- package/lib/tools/submissionUpdate.js +3 -2
- package/lib/tools/utils.d.ts +40 -0
- package/lib/tools/utils.js +16 -1
- package/package.json +10 -4
- package/testing.d.ts +1 -0
- package/testing.js +2 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import type { ModuleApi, ModuleRequest, ModuleRoute } from '@formio/appserver';
|
|
3
|
+
import type { Converter, ConverterOutput, Representation } from '../documents/types';
|
|
4
|
+
import type { FileRoute, Pins, AcceptEntry } from '../documents/routes';
|
|
5
|
+
/** A module route's request in the UAG: also its request id. */
|
|
6
|
+
export type UAGModuleRequest = ModuleRequest & {
|
|
7
|
+
requestId: string;
|
|
8
|
+
};
|
|
9
|
+
/** The names of the UAG's own rate-limited operations, which module routes may not take. */
|
|
10
|
+
export declare const UAG_OPERATIONS: string[];
|
|
11
|
+
/** An agent engine a module may use: its name and model, and generate, which checks the license first. */
|
|
12
|
+
export type GeneratingEngine = {
|
|
13
|
+
name: string;
|
|
14
|
+
model: string;
|
|
15
|
+
accepts: Representation[];
|
|
16
|
+
generate(request: any): Promise<any>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The module API the UAG gives a module (version 1): appserver's, with the
|
|
20
|
+
* license check, engines, converters and the UAG's helpers. The project and
|
|
21
|
+
* the engines themselves are never handed out.
|
|
22
|
+
*/
|
|
23
|
+
export type UAGModuleApi<Settings extends Record<string, any> = Record<string, any>> = ModuleApi<Settings> & {
|
|
24
|
+
/** Throws 403 license_required unless Agentic Formflow is allowed. */
|
|
25
|
+
requireLicense(): Promise<void>;
|
|
26
|
+
converters: {
|
|
27
|
+
list(): Converter[];
|
|
28
|
+
identify(buffer: Buffer, hint: {
|
|
29
|
+
name: string;
|
|
30
|
+
type?: string;
|
|
31
|
+
}): string;
|
|
32
|
+
route(type: string, inputs: Representation[][], pins?: Pins): FileRoute | null;
|
|
33
|
+
accept(inputs: Representation[][]): AcceptEntry[];
|
|
34
|
+
run(steps: Converter[], input: {
|
|
35
|
+
buffer: Buffer;
|
|
36
|
+
type: string;
|
|
37
|
+
name: string;
|
|
38
|
+
}, to: Representation, options: {
|
|
39
|
+
signal: AbortSignal;
|
|
40
|
+
}): Promise<ConverterOutput>;
|
|
41
|
+
stepName(converter: {
|
|
42
|
+
name: string;
|
|
43
|
+
version?: string;
|
|
44
|
+
}): string;
|
|
45
|
+
};
|
|
46
|
+
engines: {
|
|
47
|
+
/**
|
|
48
|
+
* The named engine, or the first agent engine that generates and reads every
|
|
49
|
+
* representation in `needs`; throws 503 no_engine. Its `generate` checks the
|
|
50
|
+
* license first, so no module calls a model outside it.
|
|
51
|
+
*/
|
|
52
|
+
select(options: {
|
|
53
|
+
name?: string;
|
|
54
|
+
needs?: Representation[];
|
|
55
|
+
}): GeneratingEngine;
|
|
56
|
+
};
|
|
57
|
+
markdown: {
|
|
58
|
+
sanitize(text: string): string;
|
|
59
|
+
fromHtml(html: string): string;
|
|
60
|
+
};
|
|
61
|
+
pdf: {
|
|
62
|
+
pageCount(pdf: Buffer): Promise<number | undefined>;
|
|
63
|
+
};
|
|
64
|
+
/** Who the request is from, for limits: the user or token, or, for an anonymous caller, `anonymous:<ip>`. */
|
|
65
|
+
callerKey(req: Request): string;
|
|
66
|
+
};
|
|
67
|
+
/** A form route of a UAG module. */
|
|
68
|
+
export type UAGModuleRoute = ModuleRoute<UAGModuleApi>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UAG_OPERATIONS = void 0;
|
|
4
|
+
/** The names of the UAG's own rate-limited operations, which module routes may not take. */
|
|
5
|
+
exports.UAG_OPERATIONS = ['extract', 'decide', 'generate', 'chat', 'capabilities'];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { UAGServer } from './index';
|
|
2
|
+
/** The image's plugins folder: add-ons are mounted in folders under it. */
|
|
3
|
+
export declare const DEFAULT_PLUGINS_DIR = "/app/plugins";
|
|
4
|
+
/**
|
|
5
|
+
* The plugins files a path holds, in load order:
|
|
6
|
+
* - a file: that file;
|
|
7
|
+
* - a folder with an `index.mjs`: that file alone (one plugins folder);
|
|
8
|
+
* - any other folder: the `index.mjs` of each folder directly in it, in
|
|
9
|
+
* name order (one per add-on), skipping folders whose name starts with
|
|
10
|
+
* "." and `node_modules`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function pluginFilesIn(target: string): string[];
|
|
13
|
+
/**
|
|
14
|
+
* The plugins files to load: those UAG_PLUGINS names (a file, a folder, or
|
|
15
|
+
* "off"), or else those in the image's plugins folder when it exists.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pluginFiles(env?: Record<string, string | undefined>, defaultDir?: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Load the plugins files, if any, and use each in turn, named by its path in
|
|
20
|
+
* error messages. A file's default export is a plugin or a list of plugins;
|
|
21
|
+
* or a module (an extra, with its plugins), or a list mixing modules and
|
|
22
|
+
* plugins, each module used in turn. Several files merge as several modules
|
|
23
|
+
* do: a module used again by name is used once, and two files that register
|
|
24
|
+
* the same plugin, plugin type, route or component stop startup, naming both.
|
|
25
|
+
* Plugins are trusted code: they run in the UAG process. Returns the files
|
|
26
|
+
* loaded.
|
|
27
|
+
*/
|
|
28
|
+
export declare function usePluginFiles(server: Pick<UAGServer, 'use'>, env?: Record<string, string | undefined>, defaultDir?: string): Promise<string[]>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_PLUGINS_DIR = void 0;
|
|
7
|
+
exports.pluginFilesIn = pluginFilesIn;
|
|
8
|
+
exports.pluginFiles = pluginFiles;
|
|
9
|
+
exports.usePluginFiles = usePluginFiles;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const url_1 = require("url");
|
|
13
|
+
const appserver_1 = require("@formio/appserver");
|
|
14
|
+
/** The image's plugins folder: add-ons are mounted in folders under it. */
|
|
15
|
+
exports.DEFAULT_PLUGINS_DIR = '/app/plugins';
|
|
16
|
+
/** The file that makes a folder one plugins folder. */
|
|
17
|
+
const INDEX = 'index.mjs';
|
|
18
|
+
const isDirectory = (target) => {
|
|
19
|
+
try {
|
|
20
|
+
return fs_1.default.statSync(target).isDirectory();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The plugins files a path holds, in load order:
|
|
28
|
+
* - a file: that file;
|
|
29
|
+
* - a folder with an `index.mjs`: that file alone (one plugins folder);
|
|
30
|
+
* - any other folder: the `index.mjs` of each folder directly in it, in
|
|
31
|
+
* name order (one per add-on), skipping folders whose name starts with
|
|
32
|
+
* "." and `node_modules`.
|
|
33
|
+
*/
|
|
34
|
+
function pluginFilesIn(target) {
|
|
35
|
+
if (!isDirectory(target))
|
|
36
|
+
return [target];
|
|
37
|
+
const own = path_1.default.join(target, INDEX);
|
|
38
|
+
if (fs_1.default.existsSync(own))
|
|
39
|
+
return [own];
|
|
40
|
+
return fs_1.default.readdirSync(target, { withFileTypes: true })
|
|
41
|
+
.filter((entry) => !entry.name.startsWith('.') && entry.name !== 'node_modules')
|
|
42
|
+
.filter((entry) => entry.isDirectory() || (entry.isSymbolicLink() && isDirectory(path_1.default.join(target, entry.name))))
|
|
43
|
+
.map((entry) => entry.name)
|
|
44
|
+
.sort()
|
|
45
|
+
.map((name) => path_1.default.join(target, name, INDEX))
|
|
46
|
+
.filter((file) => fs_1.default.existsSync(file));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The plugins files to load: those UAG_PLUGINS names (a file, a folder, or
|
|
50
|
+
* "off"), or else those in the image's plugins folder when it exists.
|
|
51
|
+
*/
|
|
52
|
+
function pluginFiles(env = process.env, defaultDir = exports.DEFAULT_PLUGINS_DIR) {
|
|
53
|
+
const setting = String(env.UAG_PLUGINS || '').trim();
|
|
54
|
+
if (setting.toLowerCase() === 'off')
|
|
55
|
+
return [];
|
|
56
|
+
if (setting)
|
|
57
|
+
return pluginFilesIn(path_1.default.resolve(setting));
|
|
58
|
+
return fs_1.default.existsSync(defaultDir) ? pluginFilesIn(defaultDir) : [];
|
|
59
|
+
}
|
|
60
|
+
/** A real dynamic import(), kept one after compilation to CommonJS. */
|
|
61
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
62
|
+
/**
|
|
63
|
+
* The keys only a module has: an entry with one of them (and no plugin `type`)
|
|
64
|
+
* is a module, anything else a plugin. Plugins have a `name` and `settings`
|
|
65
|
+
* too, so a name says nothing, and `settings` counts only as a function.
|
|
66
|
+
* (`uag` too, so a module written for the old shape is refused saying what to
|
|
67
|
+
* change, not read as a plugin.)
|
|
68
|
+
*/
|
|
69
|
+
const MODULE_KEYS = ['uag', 'routes', 'components', 'initialize', 'plugins', 'pluginTypes', 'config', 'formRoutes', 'actions', 'hooks', 'providers', 'processors'];
|
|
70
|
+
const isModule = (entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry) && typeof entry.type !== 'string'
|
|
71
|
+
&& (MODULE_KEYS.some((key) => key in entry) || typeof entry.settings === 'function');
|
|
72
|
+
/** Use one plugins file, named by its path in error messages. */
|
|
73
|
+
async function usePluginFile(server, file) {
|
|
74
|
+
const exported = (await dynamicImport((0, url_1.pathToFileURL)(file).href))?.default;
|
|
75
|
+
const entries = [].concat(exported ?? []);
|
|
76
|
+
if (entries.some(isModule)) {
|
|
77
|
+
const plugins = entries.filter((entry) => !isModule(entry));
|
|
78
|
+
server.use([...entries.filter(isModule), ...(plugins.length ? [{ plugins }] : [])], file);
|
|
79
|
+
console.log(`[formio-uag] Plugins and modules loaded from ${file}.`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const plugins = await (0, appserver_1.loadPluginFile)(file);
|
|
83
|
+
server.use({ plugins }, file);
|
|
84
|
+
console.log(`[formio-uag] Plugins loaded from ${file}.`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Load the plugins files, if any, and use each in turn, named by its path in
|
|
88
|
+
* error messages. A file's default export is a plugin or a list of plugins;
|
|
89
|
+
* or a module (an extra, with its plugins), or a list mixing modules and
|
|
90
|
+
* plugins, each module used in turn. Several files merge as several modules
|
|
91
|
+
* do: a module used again by name is used once, and two files that register
|
|
92
|
+
* the same plugin, plugin type, route or component stop startup, naming both.
|
|
93
|
+
* Plugins are trusted code: they run in the UAG process. Returns the files
|
|
94
|
+
* loaded.
|
|
95
|
+
*/
|
|
96
|
+
async function usePluginFiles(server, env = process.env, defaultDir = exports.DEFAULT_PLUGINS_DIR) {
|
|
97
|
+
const files = pluginFiles(env, defaultDir);
|
|
98
|
+
for (const file of files)
|
|
99
|
+
await usePluginFile(server, file);
|
|
100
|
+
return files;
|
|
101
|
+
}
|
package/lib/plugins.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PluginLog, PluginRegistry, PluginType } from '@formio/appserver';
|
|
2
|
+
import type { UAGProjectInterface } from './UAGProjectInterface';
|
|
3
|
+
/**
|
|
4
|
+
* Engines and MCP tools, listed in the plugin registry (phase 1). The UAG keeps
|
|
5
|
+
* loading them its own way (UAG_ENGINES, config.tools); the registry only lists
|
|
6
|
+
* them, so they appear in GET {project}/plugins. Settings values are never listed.
|
|
7
|
+
*/
|
|
8
|
+
export declare const enginePluginType: PluginType;
|
|
9
|
+
export declare const toolPluginType: PluginType;
|
|
10
|
+
/** List each loaded engine type: its instances, kinds, what agent instances read, and whether they generate. */
|
|
11
|
+
export declare function listEngines(registry: PluginRegistry, project: UAGProjectInterface): void;
|
|
12
|
+
/**
|
|
13
|
+
* List every MCP tool the project serves (built-in, then config.tools). A name
|
|
14
|
+
* served twice is listed once, with a warning: listing never refuses what the
|
|
15
|
+
* tool loader allows.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listTools(registry: PluginRegistry, project: UAGProjectInterface, log?: PluginLog): Promise<void>;
|
package/lib/plugins.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toolPluginType = exports.enginePluginType = void 0;
|
|
4
|
+
exports.listEngines = listEngines;
|
|
5
|
+
exports.listTools = listTools;
|
|
6
|
+
const uag_sdk_1 = require("@formio/uag-sdk");
|
|
7
|
+
const tools_1 = require("./agent/tools");
|
|
8
|
+
const engines_1 = require("./agent/engines");
|
|
9
|
+
const version_1 = require("./version");
|
|
10
|
+
/**
|
|
11
|
+
* Engines and MCP tools, listed in the plugin registry (phase 1). The UAG keeps
|
|
12
|
+
* loading them its own way (UAG_ENGINES, config.tools); the registry only lists
|
|
13
|
+
* them, so they appear in GET {project}/plugins. Settings values are never listed.
|
|
14
|
+
*/
|
|
15
|
+
exports.enginePluginType = {
|
|
16
|
+
type: 'engine',
|
|
17
|
+
title: 'Engine',
|
|
18
|
+
apiVersions: [1],
|
|
19
|
+
editForm: 'none',
|
|
20
|
+
// A misconfigured engine stops startup.
|
|
21
|
+
unavailable: 'error',
|
|
22
|
+
registration: 'internal',
|
|
23
|
+
stability: 'stable',
|
|
24
|
+
validate: (plugin) => uag_sdk_1.ENGINE_NAME.test(plugin.name) ? [] : [`engine name "${plugin.name}" must be lowercase letters, digits and "-", starting with a letter`],
|
|
25
|
+
manifest: (plugin) => ({
|
|
26
|
+
kinds: plugin.kinds,
|
|
27
|
+
accepts: plugin.accepts,
|
|
28
|
+
generate: plugin.generate,
|
|
29
|
+
instances: plugin.instances
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
exports.toolPluginType = {
|
|
33
|
+
type: 'tool',
|
|
34
|
+
title: 'MCP tool',
|
|
35
|
+
apiVersions: [1],
|
|
36
|
+
editForm: 'none',
|
|
37
|
+
unavailable: 'error',
|
|
38
|
+
registration: 'internal',
|
|
39
|
+
stability: 'stable',
|
|
40
|
+
validate: (plugin) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(plugin.name) ? [] : [`tool name "${plugin.name}" must start with a letter and hold only letters, digits, "_" and "-"`],
|
|
41
|
+
manifest: (plugin) => ({ annotations: plugin.annotations ?? null })
|
|
42
|
+
};
|
|
43
|
+
const semver = (version) => (version && /^\d+\.\d+\.\d+/.test(version)) ? version : version_1.UAG_VERSION;
|
|
44
|
+
/** List each loaded engine type: its instances, kinds, what agent instances read, and whether they generate. */
|
|
45
|
+
function listEngines(registry, project) {
|
|
46
|
+
const engines = project.formflowEngines();
|
|
47
|
+
const entries = (0, engines_1.engineTypesOf)(engines).map((type) => {
|
|
48
|
+
const instances = type.instances.map((name) => engines.get(name)).filter(Boolean);
|
|
49
|
+
const agents = instances.filter((engine) => engine.kind === 'agent');
|
|
50
|
+
const accepts = [...new Set(agents.flatMap((engine) => Array.isArray(engine.accepts) ? engine.accepts : ['text']))];
|
|
51
|
+
return {
|
|
52
|
+
type: 'engine',
|
|
53
|
+
name: type.name,
|
|
54
|
+
title: type.title,
|
|
55
|
+
version: semver(type.version),
|
|
56
|
+
apiVersion: 1,
|
|
57
|
+
kinds: type.kinds,
|
|
58
|
+
accepts,
|
|
59
|
+
generate: agents.some((engine) => typeof engine.generate === 'function'),
|
|
60
|
+
instances: type.instances
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
registry.addType(exports.enginePluginType, '@formio/uag');
|
|
64
|
+
registry.add(entries, 'UAG_ENGINES', { server: true });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* List every MCP tool the project serves (built-in, then config.tools). A name
|
|
68
|
+
* served twice is listed once, with a warning: listing never refuses what the
|
|
69
|
+
* tool loader allows.
|
|
70
|
+
*/
|
|
71
|
+
async function listTools(registry, project, log = console) {
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const entries = [];
|
|
74
|
+
for (const tool of await (0, tools_1.allTools)(project)) {
|
|
75
|
+
const name = String(tool.name);
|
|
76
|
+
if (seen.has(name)) {
|
|
77
|
+
log.warn(`[plugins] tool:${name} is defined more than once; it is listed once`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
seen.add(name);
|
|
81
|
+
entries.push({
|
|
82
|
+
type: 'tool',
|
|
83
|
+
name,
|
|
84
|
+
title: tool.title || name,
|
|
85
|
+
description: tool.description,
|
|
86
|
+
version: version_1.UAG_VERSION,
|
|
87
|
+
apiVersion: 1,
|
|
88
|
+
annotations: tool.annotations
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
registry.addType(exports.toolPluginType, '@formio/uag');
|
|
92
|
+
registry.add(entries, 'config.tools', { server: true });
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Component } from '@formio/core';
|
|
2
|
+
/**
|
|
3
|
+
* HTML for rich text fields (textareas with the CKEditor or Quill editor).
|
|
4
|
+
*
|
|
5
|
+
* What an agent engine writes into such a field is reduced to the markup the
|
|
6
|
+
* editors keep, because the engine's input can be steered (a document it reads
|
|
7
|
+
* is someone else's text), and the field is later rendered for reviewers. The
|
|
8
|
+
* HTML is parsed and written out again: allowed elements with their allowed
|
|
9
|
+
* attributes, text escaped, everything else dropped. An element that is not
|
|
10
|
+
* allowed is dropped and its children kept, except for elements whose content
|
|
11
|
+
* is never text to show (scripts, styles, frames, embedded documents).
|
|
12
|
+
*/
|
|
13
|
+
/** Allowed elements, each with the attributes it may keep. */
|
|
14
|
+
export declare const RICH_TEXT_TAGS: Record<string, string[]>;
|
|
15
|
+
/** Whether a component is a rich text field, whose value is HTML. */
|
|
16
|
+
export declare function isRichText(component: Component | undefined): boolean;
|
|
17
|
+
/** Reduce HTML to the markup rich text editors keep (RICH_TEXT_TAGS). */
|
|
18
|
+
export declare function sanitizeRichText(html: string): string;
|
|
19
|
+
/** The rule agents are given for a rich text field's value. */
|
|
20
|
+
export declare const RICH_TEXT_RULE: string;
|
package/lib/richText.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RICH_TEXT_RULE = exports.RICH_TEXT_TAGS = void 0;
|
|
4
|
+
exports.isRichText = isRichText;
|
|
5
|
+
exports.sanitizeRichText = sanitizeRichText;
|
|
6
|
+
const node_html_parser_1 = require("node-html-parser");
|
|
7
|
+
/**
|
|
8
|
+
* HTML for rich text fields (textareas with the CKEditor or Quill editor).
|
|
9
|
+
*
|
|
10
|
+
* What an agent engine writes into such a field is reduced to the markup the
|
|
11
|
+
* editors keep, because the engine's input can be steered (a document it reads
|
|
12
|
+
* is someone else's text), and the field is later rendered for reviewers. The
|
|
13
|
+
* HTML is parsed and written out again: allowed elements with their allowed
|
|
14
|
+
* attributes, text escaped, everything else dropped. An element that is not
|
|
15
|
+
* allowed is dropped and its children kept, except for elements whose content
|
|
16
|
+
* is never text to show (scripts, styles, frames, embedded documents).
|
|
17
|
+
*/
|
|
18
|
+
/** Allowed elements, each with the attributes it may keep. */
|
|
19
|
+
exports.RICH_TEXT_TAGS = {
|
|
20
|
+
p: [], br: [], hr: [],
|
|
21
|
+
strong: [], b: [], em: [], i: [], u: [], s: [], sub: [], sup: [], code: [], pre: [],
|
|
22
|
+
h1: [], h2: [], h3: [], h4: [], h5: [], h6: [],
|
|
23
|
+
ul: [], ol: [], li: [],
|
|
24
|
+
blockquote: [],
|
|
25
|
+
a: ['href'],
|
|
26
|
+
figure: [], table: [], thead: [], tbody: [], tfoot: [], tr: [],
|
|
27
|
+
th: ['colspan', 'rowspan'], td: ['colspan', 'rowspan']
|
|
28
|
+
};
|
|
29
|
+
/** Elements dropped with everything inside them. */
|
|
30
|
+
const DROPPED_WITH_CONTENT = new Set(['script', 'style', 'noscript', 'template', 'iframe', 'frame', 'frameset', 'object', 'embed', 'svg', 'math', 'head', 'title', 'textarea', 'select']);
|
|
31
|
+
const VOID = new Set(['br', 'hr']);
|
|
32
|
+
/** Whether a component is a rich text field, whose value is HTML. */
|
|
33
|
+
function isRichText(component) {
|
|
34
|
+
const editor = component?.editor;
|
|
35
|
+
return component?.type === 'textarea' && (editor === 'ckeditor' || editor === 'quill');
|
|
36
|
+
}
|
|
37
|
+
function escapeText(text) {
|
|
38
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
39
|
+
}
|
|
40
|
+
function escapeAttribute(value) {
|
|
41
|
+
return escapeText(value).replace(/"/g, '"');
|
|
42
|
+
}
|
|
43
|
+
/** Only links to web pages and mail addresses: never `javascript:`, `data:` or the like. */
|
|
44
|
+
function safeHref(value) {
|
|
45
|
+
const href = String(value || '').replace(/[\u0000-\u001F\u007F\s]+/g, '');
|
|
46
|
+
return /^(https?:\/\/|mailto:)/i.test(href) ? String(value).trim() : null;
|
|
47
|
+
}
|
|
48
|
+
function attributesOf(element, allowed) {
|
|
49
|
+
let out = '';
|
|
50
|
+
for (const name of allowed) {
|
|
51
|
+
const raw = element.getAttribute(name);
|
|
52
|
+
if (raw === undefined || raw === null)
|
|
53
|
+
continue;
|
|
54
|
+
let value = raw;
|
|
55
|
+
if (name === 'href')
|
|
56
|
+
value = safeHref(raw);
|
|
57
|
+
if ((name === 'colspan' || name === 'rowspan'))
|
|
58
|
+
value = /^\d{1,3}$/.test(raw.trim()) ? raw.trim() : null;
|
|
59
|
+
if (value === null)
|
|
60
|
+
continue;
|
|
61
|
+
out += ` ${name}="${escapeAttribute(value)}"`;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function write(node) {
|
|
66
|
+
if (node.nodeType === node_html_parser_1.NodeType.TEXT_NODE) {
|
|
67
|
+
// `text` is the decoded text; it is escaped once, here.
|
|
68
|
+
return escapeText(node.text);
|
|
69
|
+
}
|
|
70
|
+
if (node.nodeType !== node_html_parser_1.NodeType.ELEMENT_NODE)
|
|
71
|
+
return '';
|
|
72
|
+
const element = node;
|
|
73
|
+
const children = () => element.childNodes.map(write).join('');
|
|
74
|
+
const tag = String(element.rawTagName || '').toLowerCase();
|
|
75
|
+
if (!tag)
|
|
76
|
+
return children();
|
|
77
|
+
if (DROPPED_WITH_CONTENT.has(tag))
|
|
78
|
+
return '';
|
|
79
|
+
const allowed = exports.RICH_TEXT_TAGS[tag];
|
|
80
|
+
if (!allowed)
|
|
81
|
+
return children();
|
|
82
|
+
const open = `<${tag}${attributesOf(element, allowed)}>`;
|
|
83
|
+
return VOID.has(tag) ? open : `${open}${children()}</${tag}>`;
|
|
84
|
+
}
|
|
85
|
+
/** Reduce HTML to the markup rich text editors keep (RICH_TEXT_TAGS). */
|
|
86
|
+
function sanitizeRichText(html) {
|
|
87
|
+
const root = (0, node_html_parser_1.parse)(String(html ?? ''), {
|
|
88
|
+
comment: false,
|
|
89
|
+
lowerCaseTagName: true,
|
|
90
|
+
// Read as raw text, so their content is never parsed as markup; they are dropped.
|
|
91
|
+
blockTextElements: { script: true, style: true, noscript: true }
|
|
92
|
+
});
|
|
93
|
+
return root.childNodes.map(write).join('').trim();
|
|
94
|
+
}
|
|
95
|
+
/** The rule agents are given for a rich text field's value. */
|
|
96
|
+
exports.RICH_TEXT_RULE = 'The value is HTML for a rich text editor. Use only these elements: ' +
|
|
97
|
+
Object.keys(exports.RICH_TEXT_TAGS).join(', ') +
|
|
98
|
+
', with no attributes except href on a (http, https or mailto links only) and colspan and rowspan on td and th. ' +
|
|
99
|
+
'No other elements, styles, classes, images or scripts, which are removed. Write HTML, not Markdown, and do not wrap it in a code fence.';
|
package/lib/router.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { UAGProjectInterface } from './UAGProjectInterface';
|
|
3
|
+
import type { ScopedTokenClaims } from './formflow';
|
|
4
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hold a scoped token to its tools whatever built the server: a project
|
|
7
|
+
* interface a module replaces may override buildMcpServer() and register tools
|
|
8
|
+
* without the scope it was given. Every tool the token does not list, and every
|
|
9
|
+
* tool not annotated read-only, is disabled, so it is neither listed nor run.
|
|
10
|
+
*/
|
|
11
|
+
export declare function restrictToScope(server: McpServer, scope: Pick<ScopedTokenClaims, 'tools'>): void;
|
|
3
12
|
export declare function UAGRouter(project: UAGProjectInterface): Router;
|
package/lib/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.restrictToScope = restrictToScope;
|
|
3
4
|
exports.UAGRouter = UAGRouter;
|
|
4
5
|
const express_1 = require("express");
|
|
5
6
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
@@ -30,6 +31,22 @@ const handleResponseError = (error, res) => {
|
|
|
30
31
|
res.status(500).json(createJsonRpcErrorResponse(-32603, "Internal Server Error"));
|
|
31
32
|
return false;
|
|
32
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Hold a scoped token to its tools whatever built the server: a project
|
|
36
|
+
* interface a module replaces may override buildMcpServer() and register tools
|
|
37
|
+
* without the scope it was given. Every tool the token does not list, and every
|
|
38
|
+
* tool not annotated read-only, is disabled, so it is neither listed nor run.
|
|
39
|
+
*/
|
|
40
|
+
function restrictToScope(server, scope) {
|
|
41
|
+
const allowed = new Set(scope.tools || []);
|
|
42
|
+
// The SDK keeps registered tools here; it has no public list of them.
|
|
43
|
+
const registered = server._registeredTools || {};
|
|
44
|
+
for (const [name, tool] of Object.entries(registered)) {
|
|
45
|
+
if (!allowed.has(name) || tool?.annotations?.readOnlyHint !== true) {
|
|
46
|
+
tool.disable?.();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
/**
|
|
34
51
|
* Build a per-request McpServer + transport (stateless mode) and tear both down
|
|
35
52
|
* when the response closes. A fresh instance per request keeps concurrent requests
|
|
@@ -37,6 +54,8 @@ const handleResponseError = (error, res) => {
|
|
|
37
54
|
*/
|
|
38
55
|
const connectMcpTransport = async (project, res, scope) => {
|
|
39
56
|
const server = await project.buildMcpServer(scope);
|
|
57
|
+
if (scope)
|
|
58
|
+
restrictToScope(server, scope);
|
|
40
59
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
41
60
|
sessionIdGenerator: undefined, // Use "undefined" to trigger stateless mode.
|
|
42
61
|
});
|
package/lib/testing.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @formio/uag/testing: test a module without a Form.io project or a running UAG.
|
|
3
|
+
*
|
|
4
|
+
* const { createTestModuleApi, testForm, testAuth, mountModuleRoutes } = require('@formio/uag/testing');
|
|
5
|
+
*
|
|
6
|
+
* const api = await createTestModuleApi(MyModule, { config: { myModule: { max: 2 } } });
|
|
7
|
+
* const app = await mountModuleRoutes(MyModule, { form: testForm(definition), auth: testAuth() });
|
|
8
|
+
* await supertest(app).post('/notes/my-module/check').send({ ... });
|
|
9
|
+
*
|
|
10
|
+
* The API is the UAG's own (errors, stream, forms, converters, engine handles,
|
|
11
|
+
* Markdown, callerKey), with stand-ins only for what needs a project: the
|
|
12
|
+
* caller's access (from `testAuth`), the license (`licensed`), engines
|
|
13
|
+
* (`engines`) and stored files (`testForm` reads base64 values; `files` for
|
|
14
|
+
* others). Settings go through the module's own `settings`, with the server's rules.
|
|
15
|
+
*/
|
|
16
|
+
import express from 'express';
|
|
17
|
+
import type { AuthRequest, ModuleForm, ServerModule } from '@formio/appserver';
|
|
18
|
+
import type { UAGModuleApi } from './modules/types';
|
|
19
|
+
/** An engine for tests: an agent engine that generates. */
|
|
20
|
+
export type TestEngine = {
|
|
21
|
+
name: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
accepts?: Array<'text' | 'pdf'>;
|
|
24
|
+
generate(request: any): any;
|
|
25
|
+
kind?: 'agent';
|
|
26
|
+
};
|
|
27
|
+
export type TestModuleOptions = {
|
|
28
|
+
/** The settings as given, instead of running the module's `settings`. */
|
|
29
|
+
settings?: Record<string, any>;
|
|
30
|
+
/** The configuration and environment the module's `settings` gets ({} by default). */
|
|
31
|
+
config?: Record<string, any>;
|
|
32
|
+
env?: Record<string, string | undefined>;
|
|
33
|
+
/** Plugins to register beside the module's own (document plugins, converters). */
|
|
34
|
+
plugins?: any[];
|
|
35
|
+
/** The engines `api.engines.select()` chooses from. */
|
|
36
|
+
engines?: TestEngine[];
|
|
37
|
+
/** Whether Agentic Formflow is licensed (true): false makes `requireLicense` and engine handles refuse with 403. */
|
|
38
|
+
licensed?: boolean;
|
|
39
|
+
/** Reads a stored file value that is not base64 (the test form reads those). */
|
|
40
|
+
files?: (value: any, options: {
|
|
41
|
+
component?: any;
|
|
42
|
+
maxBytes?: number;
|
|
43
|
+
}) => Buffer | null | Promise<Buffer | null>;
|
|
44
|
+
};
|
|
45
|
+
/** A form for tests, as module routes get it: `definition` is the form's JSON. */
|
|
46
|
+
export declare function testForm(definition: Record<string, any>, options?: {
|
|
47
|
+
id?: string;
|
|
48
|
+
files?: TestModuleOptions['files'];
|
|
49
|
+
}): ModuleForm & {
|
|
50
|
+
readFile(value: any, auth: any, options?: any): Promise<Buffer | null>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A caller for tests: a user with these permissions on every form (create and
|
|
54
|
+
* update by default), as a list (`['read']`) or as `formPermissions()` gives
|
|
55
|
+
* them (`{ read: true }`).
|
|
56
|
+
*/
|
|
57
|
+
export declare function testAuth(options?: {
|
|
58
|
+
user?: Record<string, any>;
|
|
59
|
+
permissions?: Array<'create' | 'read' | 'update' | 'delete'> | Partial<Record<'create' | 'read' | 'update' | 'delete', boolean>>;
|
|
60
|
+
admin?: boolean;
|
|
61
|
+
}): AuthRequest;
|
|
62
|
+
/** The UAG's module API for a module, in a test. */
|
|
63
|
+
export declare function createTestModuleApi<Settings extends Record<string, any> = Record<string, any>>(module: ServerModule<any>, options?: TestModuleOptions): Promise<UAGModuleApi<Settings>>;
|
|
64
|
+
/**
|
|
65
|
+
* The module's form routes on an Express app, as the UAG mounts them on a
|
|
66
|
+
* form (by its path, and by `form/<id>`), for supertest: with the form, the
|
|
67
|
+
* caller, a request id, the access check and the UAG's error shape. No rate
|
|
68
|
+
* limits. `api` is created with `options` unless given.
|
|
69
|
+
*/
|
|
70
|
+
export declare function mountModuleRoutes(module: ServerModule<any>, options: TestModuleOptions & {
|
|
71
|
+
form: ReturnType<typeof testForm>;
|
|
72
|
+
auth?: AuthRequest;
|
|
73
|
+
api?: UAGModuleApi;
|
|
74
|
+
}): Promise<express.Express>;
|