@formio/uag 2.0.0-rc.6 → 2.0.0-rc.7
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/bin/uag.js +6 -2
- 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 +26 -0
- package/lib/documents/converters.js +139 -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 +150 -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 +16 -0
- package/lib/pluginFiles.js +62 -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 +165 -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
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.UAGFormInterface = exports.UAGProjectInterface = exports.UAGServer = void 0;
|
|
20
|
+
exports.UAGFormInterface = exports.UAGProjectInterface = exports.RICH_TEXT_RULE = exports.RICH_TEXT_TAGS = exports.isRichText = exports.sanitizeRichText = exports.DEFAULT_PLUGINS_FILE = exports.pluginsFile = exports.usePluginFiles = exports.sanitizeMarkdown = exports.UAGServer = void 0;
|
|
21
21
|
exports.assertSingleCore = assertSingleCore;
|
|
22
22
|
exports.assertNodeSnapshotDisabled = assertNodeSnapshotDisabled;
|
|
23
23
|
const appserver_1 = require("@formio/appserver");
|
|
@@ -37,18 +37,26 @@ const actionLogs_1 = require("./actionLogs");
|
|
|
37
37
|
const runner_1 = require("./runner");
|
|
38
38
|
const runTask_1 = require("./actions/runTask");
|
|
39
39
|
const version_1 = require("./version");
|
|
40
|
+
const utils_1 = require("./tools/utils");
|
|
41
|
+
const plugins_1 = require("./plugins");
|
|
42
|
+
const plugins_2 = require("./documents/plugins");
|
|
43
|
+
const uagJs_1 = require("./documents/uagJs");
|
|
44
|
+
const modules_1 = require("./modules");
|
|
45
|
+
const tools_1 = require("./agent/tools");
|
|
46
|
+
/** Projects whose engines were loaded while the plugins were listed. */
|
|
47
|
+
const enginesLoaded = new WeakSet();
|
|
40
48
|
const debug = require('debug')('formio:uag');
|
|
41
49
|
/**
|
|
42
|
-
* Agentic Formflow keeps its license state inside @formio/appserver-core
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* to do instead.
|
|
50
|
+
* Agentic Formflow keeps its license state inside @formio/appserver-core, and
|
|
51
|
+
* modules are built on its module framework. The UAG loads it only through
|
|
52
|
+
* @formio/appserver, so the two always share one copy, which must be 2.26.0 or
|
|
53
|
+
* later. An older @formio/appserver, or an old lockfile, can still resolve an
|
|
54
|
+
* older one, which would fail in confusing ways; say what to do instead.
|
|
47
55
|
*/
|
|
48
56
|
function assertSingleCore(version = appserverCoreVersion()) {
|
|
49
|
-
const [major, minor
|
|
50
|
-
if (major < 2 || (major === 2 &&
|
|
51
|
-
throw new Error(`@formio/uag needs @formio/appserver-core 2.
|
|
57
|
+
const [major, minor] = version.split('.').map((part) => parseInt(part, 10) || 0);
|
|
58
|
+
if (major < 2 || (major === 2 && minor < 26)) {
|
|
59
|
+
throw new Error(`@formio/uag needs @formio/appserver-core 2.26.0 or later, through @formio/appserver; ${version} is installed. Upgrade @formio/appserver.`);
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
/** The version of the @formio/appserver-core that @formio/appserver loads. */
|
|
@@ -103,6 +111,53 @@ function databaseFromEnv(env = process.env) {
|
|
|
103
111
|
};
|
|
104
112
|
}
|
|
105
113
|
class UAGServer extends appserver_1.Server {
|
|
114
|
+
/**
|
|
115
|
+
* Use a module, or a list of modules (each in turn). A module used twice (by
|
|
116
|
+
* its `name`) is used once. `config.tools` adds up across modules; an MCP
|
|
117
|
+
* tool or engine type that two modules define stops startup, naming both.
|
|
118
|
+
* A route's `rateLimit` is the default for its name, unless the operator set one.
|
|
119
|
+
*/
|
|
120
|
+
use(module, source) {
|
|
121
|
+
module = appserver_1.Server.defaultExport(module);
|
|
122
|
+
if (module && typeof module === 'object' && !Array.isArray(module) && module.uag !== undefined) {
|
|
123
|
+
const label = source || (typeof module.name === 'string' && module.name) || 'a module';
|
|
124
|
+
throw new Error(`The module ${label} has a "uag" key, which UAG modules no longer use: move its keys (name, apiVersion, settings, routes, components, initialize) onto the module itself.`);
|
|
125
|
+
}
|
|
126
|
+
super.use(module, source);
|
|
127
|
+
if (typeof module?.name === 'string' && this.modules.get(module.name)?.definition === module)
|
|
128
|
+
this.routeRateLimits(module);
|
|
129
|
+
}
|
|
130
|
+
/** MCP tools and engine types, which only one module may define. */
|
|
131
|
+
moduleClaims(definition) {
|
|
132
|
+
return [
|
|
133
|
+
...(definition.config?.tools || []).filter((tool) => tool?.name).map((tool) => `MCP tool "${tool.name}"`),
|
|
134
|
+
...Object.keys(definition.config?.engineTypes || {}).map((name) => `engine type "${name}"`)
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* MCP tools add up across modules (lodash would merge them index by index),
|
|
139
|
+
* and a named module's tools get its API as `extra.module`. Its routes'
|
|
140
|
+
* `rateLimit`s become the defaults for their names.
|
|
141
|
+
*/
|
|
142
|
+
mergeModuleConfig(config, module) {
|
|
143
|
+
const { tools, ...rest } = config;
|
|
144
|
+
super.mergeModuleConfig(rest, module);
|
|
145
|
+
if (Array.isArray(tools) && tools.length) {
|
|
146
|
+
if (module)
|
|
147
|
+
for (const tool of tools)
|
|
148
|
+
(0, tools_1.setToolModule)(tool, this.modules.apiFor(module));
|
|
149
|
+
this.config.tools = [...(this.config.tools || []), ...tools];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Route rate-limit defaults, for the names the operator left unset. */
|
|
153
|
+
routeRateLimits(module) {
|
|
154
|
+
for (const route of Object.values(module?.routes?.form || {})) {
|
|
155
|
+
const name = route?.name || module.name;
|
|
156
|
+
if (!route?.rateLimit || (0, lodash_1.get)(this.config, ['agentApi', 'rateLimits', 'caller', name]))
|
|
157
|
+
continue;
|
|
158
|
+
(0, lodash_1.set)(this.config, ['agentApi', 'rateLimits', 'caller', name], route.rateLimit);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
106
161
|
constructor(config) {
|
|
107
162
|
assertNodeSnapshotDisabled();
|
|
108
163
|
assertSingleCore();
|
|
@@ -135,8 +190,16 @@ class UAGServer extends appserver_1.Server {
|
|
|
135
190
|
// Bodies are parsed before authentication, so keep them to what Form.io accepts.
|
|
136
191
|
maxBodySize: (0, lodash_1.get)(process.env, 'MAX_BODY_SIZE', '16mb').toString(),
|
|
137
192
|
auth: { pkce: true, mcpOAuth: (0, lodash_1.get)(process.env, 'MCP_OAUTH', '').toString() === 'true' },
|
|
138
|
-
database: databaseFromEnv()
|
|
193
|
+
database: databaseFromEnv(),
|
|
194
|
+
// Modules an operator turns off, such as one an image includes.
|
|
195
|
+
modules: { disabled: (0, lodash_1.get)(process.env, 'MODULES_DISABLED', '').toString().split(',').map((name) => name.trim()).filter(Boolean) }
|
|
139
196
|
}));
|
|
197
|
+
// The server's own MCP tools and engine types: a module may not define them again.
|
|
198
|
+
for (const tool of (this.config.tools || []))
|
|
199
|
+
if (tool?.name)
|
|
200
|
+
this.modules.claim(`MCP tool "${tool.name}"`, "the server's configuration");
|
|
201
|
+
for (const name of Object.keys(this.config.engineTypes || {}))
|
|
202
|
+
this.modules.claim(`engine type "${name}"`, "the server's configuration");
|
|
140
203
|
// MONGO is the UAG's own database, not a database for the appserver to keep
|
|
141
204
|
// submissions in: Form.io stores those.
|
|
142
205
|
if (!configuredDb && this.module.db) {
|
|
@@ -219,12 +282,20 @@ class UAGServer extends appserver_1.Server {
|
|
|
219
282
|
* route keeps Form.io's plain-text answer.
|
|
220
283
|
*/
|
|
221
284
|
sendError(req, res, status, message, err) {
|
|
222
|
-
if (!(0, errors_1.isAgentApiRequest)(req))
|
|
285
|
+
if (!(0, errors_1.isAgentApiRequest)(req) && !this.isModuleRouteRequest(req))
|
|
223
286
|
return super.sendError(req, res, status, message, err);
|
|
224
287
|
const error = err?.type === 'entity.parse.failed' ? 'The request body is not valid JSON.' : message;
|
|
225
288
|
// Written past any shaping wrapper, which may not have run for this request.
|
|
226
289
|
res.status(status).type('application/json').end(JSON.stringify((0, errors_1.errorBody)((0, errors_1.requestIdOf)(req, res), status, { error })));
|
|
227
290
|
}
|
|
291
|
+
/** Whether a request is for a module's form route: its method, and a path ending in the route's. */
|
|
292
|
+
isModuleRouteRequest(req) {
|
|
293
|
+
const path = String(req.path || '').toLowerCase().replace(/\/+$/, '');
|
|
294
|
+
return this.modules.list.some((module) => Object.keys(module.definition.routes?.form || {}).some((key) => {
|
|
295
|
+
const [method, route] = key.split(' ');
|
|
296
|
+
return req.method === method && path.endsWith(String(route).toLowerCase().replace(/\/+$/, ''));
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
228
299
|
/** Browsers reading Agent API responses need the request id too. */
|
|
229
300
|
get exposedHeaders() {
|
|
230
301
|
return [...super.exposedHeaders, 'X-Request-Id'];
|
|
@@ -240,7 +311,11 @@ class UAGServer extends appserver_1.Server {
|
|
|
240
311
|
* on the form's own routes.
|
|
241
312
|
*/
|
|
242
313
|
async extendRouter(router) {
|
|
314
|
+
// A bare function in config.tools (the unreleased factory form) stops startup.
|
|
315
|
+
(0, utils_1.assertToolEntries)(this.config.tools);
|
|
243
316
|
await super.extendRouter(router);
|
|
317
|
+
// @formio/uag-js, the portal script and stylesheet: public, ahead of the project routes.
|
|
318
|
+
router.use((0, uagJs_1.uagJsRouter)(() => this.plugins, () => this.modules.components()));
|
|
244
319
|
const uagProject = this.uagProject;
|
|
245
320
|
if (uagProject) {
|
|
246
321
|
// First: the Agent API's request id and one error shape, for every refusal after it.
|
|
@@ -252,6 +327,52 @@ class UAGServer extends appserver_1.Server {
|
|
|
252
327
|
router.use((0, actionLogs_1.ActionLogRouter)(uagProject));
|
|
253
328
|
}
|
|
254
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* List the UAG's engines and MCP tools in the plugin registry, beside
|
|
332
|
+
* appserver-core's form actions. They keep loading their own way.
|
|
333
|
+
*/
|
|
334
|
+
async listPlugins() {
|
|
335
|
+
await super.listPlugins();
|
|
336
|
+
// Converters: the converter type and the built-in converters, for task file inputs, read_document and modules.
|
|
337
|
+
(0, plugins_2.registerDocuments)(this.plugins);
|
|
338
|
+
const project = this.uagProject;
|
|
339
|
+
if (project instanceof UAGProjectInterface_1.UAGProjectInterface)
|
|
340
|
+
project.plugins = this.plugins;
|
|
341
|
+
if (project instanceof UAGProjectInterface_1.UAGProjectInterface) {
|
|
342
|
+
// Decide which engines are on before serving anything, so a misconfigured
|
|
343
|
+
// engine stops startup with its name in the error instead of failing the
|
|
344
|
+
// first request.
|
|
345
|
+
project.engines = await (0, engines_1.loadUAGEngines)(project.config || {}, process.env);
|
|
346
|
+
enginesLoaded.add(project);
|
|
347
|
+
(0, plugins_1.listEngines)(this.plugins, project);
|
|
348
|
+
await (0, plugins_1.listTools)(this.plugins, project);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** A form's own route segments, which module routes may not use: appserver's, and the agent operations. */
|
|
352
|
+
get reservedFormPaths() {
|
|
353
|
+
return [...super.reservedFormPaths, 'agent'];
|
|
354
|
+
}
|
|
355
|
+
/** Module routes may not take the names of the UAG's own rate-limited operations. */
|
|
356
|
+
get reservedRouteNames() {
|
|
357
|
+
return modules_1.UAG_OPERATIONS;
|
|
358
|
+
}
|
|
359
|
+
get hostName() {
|
|
360
|
+
return 'uag';
|
|
361
|
+
}
|
|
362
|
+
get hostVersion() {
|
|
363
|
+
return version_1.UAG_VERSION;
|
|
364
|
+
}
|
|
365
|
+
/** The module API: appserver's, with the UAG's license check, engines, converters and helpers. */
|
|
366
|
+
createModuleApi(module) {
|
|
367
|
+
return (0, modules_1.uagModuleApi)(super.createModuleApi(module), (0, modules_1.serverDeps)(() => this.uagProject));
|
|
368
|
+
}
|
|
369
|
+
/** Module routes run behind what the agent operations run behind: the caller check, body limit, request id, rate limits and error shape. */
|
|
370
|
+
moduleRouteHandlers(project, route, module) {
|
|
371
|
+
return (0, modules_1.uagRouteHandlers)(project, route.name);
|
|
372
|
+
}
|
|
373
|
+
moduleErrorBody(req, err) {
|
|
374
|
+
return (0, modules_1.uagErrorBody)(req, err);
|
|
375
|
+
}
|
|
255
376
|
async router() {
|
|
256
377
|
await this.connectDatabase();
|
|
257
378
|
const router = await super.router();
|
|
@@ -269,10 +390,11 @@ class UAGServer extends appserver_1.Server {
|
|
|
269
390
|
}
|
|
270
391
|
// A database of the UAG's own needs the license, on every edition.
|
|
271
392
|
await this.enableDatabase(uagProject);
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
393
|
+
// The engines are loaded while the plugins are listed (listPlugins); a
|
|
394
|
+
// project interface that is not the UAG's own loads them here.
|
|
395
|
+
if (!enginesLoaded.has(uagProject)) {
|
|
396
|
+
uagProject.engines = await (0, engines_1.loadUAGEngines)(uagProject.config || {}, process.env);
|
|
397
|
+
}
|
|
276
398
|
// Say which edition this is, and whether Agentic Formflow will run, so an
|
|
277
399
|
// operator knows before the first request is refused.
|
|
278
400
|
// A failure here only loses the message; it never stops the UAG starting.
|
|
@@ -296,5 +418,18 @@ __exportStar(require("@formio/appserver"), exports);
|
|
|
296
418
|
__exportStar(require("./config"), exports);
|
|
297
419
|
__exportStar(require("./agent"), exports);
|
|
298
420
|
__exportStar(require("./tools"), exports);
|
|
421
|
+
__exportStar(require("./documents"), exports);
|
|
422
|
+
__exportStar(require("./modules"), exports);
|
|
423
|
+
var markdown_1 = require("./markdown");
|
|
424
|
+
Object.defineProperty(exports, "sanitizeMarkdown", { enumerable: true, get: function () { return markdown_1.sanitizeMarkdown; } });
|
|
425
|
+
var pluginFiles_1 = require("./pluginFiles");
|
|
426
|
+
Object.defineProperty(exports, "usePluginFiles", { enumerable: true, get: function () { return pluginFiles_1.usePluginFiles; } });
|
|
427
|
+
Object.defineProperty(exports, "pluginsFile", { enumerable: true, get: function () { return pluginFiles_1.pluginsFile; } });
|
|
428
|
+
Object.defineProperty(exports, "DEFAULT_PLUGINS_FILE", { enumerable: true, get: function () { return pluginFiles_1.DEFAULT_PLUGINS_FILE; } });
|
|
299
429
|
__exportStar(require("./router"), exports);
|
|
300
430
|
__exportStar(require("./template"), exports);
|
|
431
|
+
var richText_1 = require("./richText");
|
|
432
|
+
Object.defineProperty(exports, "sanitizeRichText", { enumerable: true, get: function () { return richText_1.sanitizeRichText; } });
|
|
433
|
+
Object.defineProperty(exports, "isRichText", { enumerable: true, get: function () { return richText_1.isRichText; } });
|
|
434
|
+
Object.defineProperty(exports, "RICH_TEXT_TAGS", { enumerable: true, get: function () { return richText_1.RICH_TEXT_TAGS; } });
|
|
435
|
+
Object.defineProperty(exports, "RICH_TEXT_RULE", { enumerable: true, get: function () { return richText_1.RICH_TEXT_RULE; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sanitizeMarkdown(markdown: string): string;
|
package/lib/markdown.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
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.sanitizeMarkdown = sanitizeMarkdown;
|
|
7
|
+
const markdown_it_1 = __importDefault(require("markdown-it"));
|
|
8
|
+
/**
|
|
9
|
+
* The Markdown contract of AI-generated content: GitHub-flavored Markdown,
|
|
10
|
+
* UTF-8, `\n` line endings, no code fence around the whole value.
|
|
11
|
+
*
|
|
12
|
+
* `sanitizeMarkdown` keeps what the AI Document's renderer shows and nothing
|
|
13
|
+
* that could act in a browser:
|
|
14
|
+
* - raw HTML is removed, keeping its text;
|
|
15
|
+
* - links keep only http, https and mailto targets (others become their text);
|
|
16
|
+
* - images are dropped;
|
|
17
|
+
* - a code fence wrapping the whole value is removed;
|
|
18
|
+
* - trailing whitespace is trimmed and more than two blank lines collapsed.
|
|
19
|
+
* Code blocks and inline code are kept as they are: the renderer escapes them.
|
|
20
|
+
*
|
|
21
|
+
* The result is then parsed as a CommonMark renderer parses it (markdown-it,
|
|
22
|
+
* with tables and strikethrough). A block still holding raw HTML, an image or a
|
|
23
|
+
* link to another target (Markdown that line-by-line cleaning misreads, such as
|
|
24
|
+
* a link whose label holds brackets, or HTML spanning lines) has its `<`, `[`,
|
|
25
|
+
* `]` and `\` escaped, so it shows as text, until the parse finds none.
|
|
26
|
+
*/
|
|
27
|
+
const SAFE_LINK = /^(https?:|mailto:)/i;
|
|
28
|
+
// Up to three spaces: a tab, or four spaces, begins an indented code block, never a fence.
|
|
29
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
30
|
+
/** HTML recognized, so that it is found, and every link target kept, so that it is checked. */
|
|
31
|
+
const parser = new markdown_it_1.default('default', { html: true, linkify: false });
|
|
32
|
+
parser.validateLink = () => true;
|
|
33
|
+
/** The line ranges (from, to: 0-based, end excluded) of blocks holding HTML, an image or a link sanitizeMarkdown never keeps. */
|
|
34
|
+
function unsafeBlocks(markdown) {
|
|
35
|
+
const ranges = [];
|
|
36
|
+
let block = null;
|
|
37
|
+
for (const token of parser.parse(markdown, {})) {
|
|
38
|
+
if (token.map)
|
|
39
|
+
block = token.map;
|
|
40
|
+
if (token.type === 'html_block') {
|
|
41
|
+
if (token.map)
|
|
42
|
+
ranges.push(token.map);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token.type !== 'inline' || !token.children)
|
|
46
|
+
continue;
|
|
47
|
+
const unsafe = token.children.some((child) => child.type === 'html_inline' || child.type === 'image' ||
|
|
48
|
+
(child.type === 'link_open' && !SAFE_LINK.test(String(child.attrGet('href') || '').trim())));
|
|
49
|
+
const lines = token.map || block;
|
|
50
|
+
if (unsafe && lines)
|
|
51
|
+
ranges.push(lines);
|
|
52
|
+
}
|
|
53
|
+
return ranges;
|
|
54
|
+
}
|
|
55
|
+
/** A line whose markup can form no HTML, image or link: its text shows as it is. */
|
|
56
|
+
function escapeLine(line) {
|
|
57
|
+
return line.replace(/[\\<[\]]/g, (char) => `\\${char}`);
|
|
58
|
+
}
|
|
59
|
+
/** The lines of fenced and indented code blocks, which can hold nothing that acts. */
|
|
60
|
+
function codeLines(markdown) {
|
|
61
|
+
const lines = new Set();
|
|
62
|
+
for (const token of parser.parse(markdown, {})) {
|
|
63
|
+
if ((token.type === 'fence' || token.type === 'code_block') && token.map) {
|
|
64
|
+
for (let i = token.map[0]; i < token.map[1]; i++)
|
|
65
|
+
lines.add(i);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
/** Escape the blocks the parse finds unsafe until it finds none; at worst, every line outside code. */
|
|
71
|
+
function escapeUnsafeBlocks(markdown) {
|
|
72
|
+
let text = markdown;
|
|
73
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
74
|
+
const ranges = unsafeBlocks(text);
|
|
75
|
+
if (!ranges.length)
|
|
76
|
+
return text;
|
|
77
|
+
const lines = text.split('\n');
|
|
78
|
+
for (const [from, to] of ranges) {
|
|
79
|
+
for (let i = from; i < to && i < lines.length; i++)
|
|
80
|
+
lines[i] = escapeLine(lines[i]);
|
|
81
|
+
}
|
|
82
|
+
text = lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
const code = codeLines(text);
|
|
85
|
+
text = text.split('\n').map((line, i) => (code.has(i) ? line : escapeLine(line))).join('\n');
|
|
86
|
+
return unsafeBlocks(text).length ? text.split('\n').map(escapeLine).join('\n') : text;
|
|
87
|
+
}
|
|
88
|
+
/** A whole value wrapped in one code fence: its contents. */
|
|
89
|
+
function unwrapFence(text) {
|
|
90
|
+
const trimmed = text.trim();
|
|
91
|
+
const match = /^(`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n?\1\s*$/.exec(trimmed);
|
|
92
|
+
if (!match)
|
|
93
|
+
return text;
|
|
94
|
+
// Only when no other fence of the same kind is inside: then it really is one block.
|
|
95
|
+
if (new RegExp(`^\\s{0,3}${match[1][0] === '`' ? '`' : '~'}{3,}`, 'm').test(match[2]))
|
|
96
|
+
return text;
|
|
97
|
+
return match[2];
|
|
98
|
+
}
|
|
99
|
+
/** Clean the text of one line outside code: HTML, images and unsafe links. */
|
|
100
|
+
function cleanLine(line) {
|
|
101
|
+
// Inline code spans are kept as they are.
|
|
102
|
+
const parts = line.split(/(`+[^`]*`+)/);
|
|
103
|
+
return parts.map((part, i) => {
|
|
104
|
+
if (i % 2 === 1)
|
|
105
|
+
return part;
|
|
106
|
+
let out = part;
|
|
107
|
+
// Images: dropped, inline and reference style.
|
|
108
|
+
out = out.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/!\[[^\]]*\]\[[^\]]*\]/g, '');
|
|
109
|
+
// Links: kept only for safe targets.
|
|
110
|
+
out = out.replace(/\[([^\]]*)\]\(\s*<?((?:[^()\s<>]|\([^()\s]*\))*)>?(?:\s+"[^"]*")?\s*\)/g, (whole, label, target) => SAFE_LINK.test(target) ? `[${label}](${target})` : label);
|
|
111
|
+
// Autolinks: <https://...> kept when safe; anything else in angle brackets is HTML.
|
|
112
|
+
const autolinks = [];
|
|
113
|
+
out = out.replace(/<([a-z][a-z0-9+.-]*:[^>\s]*)>/gi, (whole, target) => {
|
|
114
|
+
if (!SAFE_LINK.test(target))
|
|
115
|
+
return '';
|
|
116
|
+
autolinks.push(`<${target}>`);
|
|
117
|
+
return `\u0000${autolinks.length - 1}\u0000`;
|
|
118
|
+
});
|
|
119
|
+
// Raw HTML: tags removed, their text kept.
|
|
120
|
+
out = out.replace(/<!--[\s\S]*?-->/g, '').replace(/<\/?[A-Za-z][^<>]*>/g, '');
|
|
121
|
+
return out.replace(/\u0000(\d+)\u0000/g, (whole, index) => autolinks[Number(index)]);
|
|
122
|
+
}).join('');
|
|
123
|
+
}
|
|
124
|
+
function sanitizeMarkdown(markdown) {
|
|
125
|
+
const text = unwrapFence(String(markdown ?? '').replace(/\r\n?/g, '\n'));
|
|
126
|
+
const out = [];
|
|
127
|
+
let fence = null;
|
|
128
|
+
// HTML blocks spanning several lines (a comment, a script) are removed whole.
|
|
129
|
+
let inComment = false;
|
|
130
|
+
for (const raw of text.split('\n')) {
|
|
131
|
+
let line = raw;
|
|
132
|
+
const opener = FENCE.exec(line);
|
|
133
|
+
if (fence) {
|
|
134
|
+
if (opener && opener[1][0] === fence[0] && opener[1].length >= fence.length && line.trim().replace(/[`~]/g, '') === '')
|
|
135
|
+
fence = null;
|
|
136
|
+
out.push(line.replace(/\s+$/, ''));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (opener) {
|
|
140
|
+
fence = opener[1];
|
|
141
|
+
out.push(line.replace(/\s+$/, ''));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (inComment) {
|
|
145
|
+
const end = line.indexOf('-->');
|
|
146
|
+
if (end < 0)
|
|
147
|
+
continue;
|
|
148
|
+
line = line.slice(end + 3);
|
|
149
|
+
inComment = false;
|
|
150
|
+
}
|
|
151
|
+
const start = line.indexOf('<!--');
|
|
152
|
+
if (start >= 0 && line.indexOf('-->', start) < 0) {
|
|
153
|
+
line = line.slice(0, start);
|
|
154
|
+
inComment = true;
|
|
155
|
+
}
|
|
156
|
+
// Reference definitions with an unsafe target are dropped.
|
|
157
|
+
const definition = /^\s{0,3}\[[^\]]+\]:\s*<?(\S+?)>?(\s.*)?$/.exec(line);
|
|
158
|
+
if (definition) {
|
|
159
|
+
if (SAFE_LINK.test(definition[1]))
|
|
160
|
+
out.push(line.replace(/\s+$/, ''));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
out.push(cleanLine(line).replace(/\s+$/, ''));
|
|
164
|
+
}
|
|
165
|
+
return escapeUnsafeBlocks(out.join('\n').replace(/\n{4,}/g, '\n\n\n').trim());
|
|
166
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleApi } from '@formio/appserver';
|
|
2
|
+
import type { UAGProjectInterface } from '../UAGProjectInterface';
|
|
3
|
+
import type { PluginRegistry } from '@formio/appserver';
|
|
4
|
+
import type { UAGModuleApi } from './types';
|
|
5
|
+
/** What the UAG's members of the module API run on: the server's, or a test kit's. */
|
|
6
|
+
export type UAGModuleApiDeps = {
|
|
7
|
+
registry(): PluginRegistry;
|
|
8
|
+
/** The enabled engines: by name, and the agent engines in preference order. */
|
|
9
|
+
engines(): {
|
|
10
|
+
get(name: string): any;
|
|
11
|
+
agent: any[];
|
|
12
|
+
} | undefined;
|
|
13
|
+
requireLicense(): Promise<void>;
|
|
14
|
+
requireAccess?: ModuleApi['requireAccess'];
|
|
15
|
+
};
|
|
16
|
+
/** The server's: its one project, looked up when a member is called. */
|
|
17
|
+
export declare function serverDeps(projectOf: () => UAGProjectInterface | undefined): UAGModuleApiDeps;
|
|
18
|
+
/**
|
|
19
|
+
* The UAG's module API (version 1): appserver's, with the Agent API's access
|
|
20
|
+
* check, the license check, converters, engines and helpers, behind stable names.
|
|
21
|
+
*/
|
|
22
|
+
export declare function uagModuleApi(base: ModuleApi, deps: UAGModuleApiDeps): UAGModuleApi;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serverDeps = serverDeps;
|
|
4
|
+
exports.uagModuleApi = uagModuleApi;
|
|
5
|
+
const node_html_markdown_1 = require("node-html-markdown");
|
|
6
|
+
const formflow_1 = require("../formflow");
|
|
7
|
+
const markdown_1 = require("../markdown");
|
|
8
|
+
const detect_1 = require("../documents/detect");
|
|
9
|
+
const convert_1 = require("../documents/convert");
|
|
10
|
+
const plugins_1 = require("../documents/plugins");
|
|
11
|
+
const routes_1 = require("../documents/routes");
|
|
12
|
+
async function pageCount(pdf) {
|
|
13
|
+
try {
|
|
14
|
+
const { PDFDocument } = require('pdf-lib');
|
|
15
|
+
return (await PDFDocument.load(pdf, { ignoreEncryption: true, updateMetadata: false })).getPageCount();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const readsOf = (engine) => (Array.isArray(engine?.accepts) ? engine.accepts : ['text']);
|
|
22
|
+
/** The server's: its one project, looked up when a member is called. */
|
|
23
|
+
function serverDeps(projectOf) {
|
|
24
|
+
const project = () => {
|
|
25
|
+
const found = projectOf();
|
|
26
|
+
if (!found)
|
|
27
|
+
throw new formflow_1.FormflowError(503, 'The server is still starting.', 'not_ready');
|
|
28
|
+
return found;
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
registry: () => {
|
|
32
|
+
const plugins = project().plugins;
|
|
33
|
+
if (!plugins)
|
|
34
|
+
throw new formflow_1.FormflowError(503, 'The server is still starting.', 'not_ready');
|
|
35
|
+
return plugins;
|
|
36
|
+
},
|
|
37
|
+
engines: () => project().formflowEngines(),
|
|
38
|
+
requireLicense: () => (0, formflow_1.formflow)(project()).requireLicense(),
|
|
39
|
+
// The Agent API's own check, so a module's refusals read as the agent operations' do.
|
|
40
|
+
requireAccess: (auth, form, methods) => (0, formflow_1.formflow)(project()).requireAccess(auth, form, methods)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The UAG's module API (version 1): appserver's, with the Agent API's access
|
|
45
|
+
* check, the license check, converters, engines and helpers, behind stable names.
|
|
46
|
+
*/
|
|
47
|
+
function uagModuleApi(base, deps) {
|
|
48
|
+
const registry = deps.registry;
|
|
49
|
+
const requireLicense = deps.requireLicense;
|
|
50
|
+
const api = {
|
|
51
|
+
...base,
|
|
52
|
+
...(deps.requireAccess && { requireAccess: deps.requireAccess }),
|
|
53
|
+
requireLicense,
|
|
54
|
+
converters: {
|
|
55
|
+
list: () => registry().list('converter'),
|
|
56
|
+
identify: (buffer, hint) => (0, detect_1.identify)(buffer, hint, registry().list('converter')),
|
|
57
|
+
route: (type, inputs, pins) => (0, plugins_1.documentsOf)(registry()).routes.route(type, inputs, pins || {}),
|
|
58
|
+
accept: (inputs) => (0, plugins_1.documentsOf)(registry()).routes.accept(inputs),
|
|
59
|
+
run: (steps, input, to, options) => (0, convert_1.runRoute)(steps, input, to, {
|
|
60
|
+
signal: options.signal,
|
|
61
|
+
log: base.log,
|
|
62
|
+
settingsOf: (converter) => registry().settings('converter', converter.name)
|
|
63
|
+
}),
|
|
64
|
+
stepName: (converter) => (0, routes_1.stepName)(converter)
|
|
65
|
+
},
|
|
66
|
+
engines: {
|
|
67
|
+
select({ name, needs = [] }) {
|
|
68
|
+
const engines = deps.engines();
|
|
69
|
+
if (!engines)
|
|
70
|
+
throw new formflow_1.FormflowError(503, 'The engines are not loaded yet.', 'not_ready');
|
|
71
|
+
const capable = (engine) => engine?.kind === 'agent' && typeof engine.generate === 'function' && needs.every((kind) => readsOf(engine).includes(kind));
|
|
72
|
+
const engine = name ? engines.get(name) : engines.agent.find(capable);
|
|
73
|
+
if (!engine || !capable(engine)) {
|
|
74
|
+
const missing = needs.find((kind) => !engine || !readsOf(engine).includes(kind)) || needs[0] || 'text';
|
|
75
|
+
throw new formflow_1.FormflowError(503, name && !engine ? `The engine "${name}" is not enabled.` : `no enabled engine can read ${missing} documents`, 'no_engine');
|
|
76
|
+
}
|
|
77
|
+
// A handle, not the engine: generating always goes through the license check.
|
|
78
|
+
return {
|
|
79
|
+
name: engine.name,
|
|
80
|
+
model: engine.model,
|
|
81
|
+
accepts: readsOf(engine),
|
|
82
|
+
async generate(request) {
|
|
83
|
+
await requireLicense();
|
|
84
|
+
return engine.generate(request);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
markdown: {
|
|
90
|
+
sanitize: (text) => (0, markdown_1.sanitizeMarkdown)(text),
|
|
91
|
+
fromHtml: (html) => node_html_markdown_1.NodeHtmlMarkdown.translate(String(html || '')).trim()
|
|
92
|
+
},
|
|
93
|
+
pdf: { pageCount },
|
|
94
|
+
callerKey: (req) => {
|
|
95
|
+
const key = (0, formflow_1.callerKey)(req.auth);
|
|
96
|
+
return key === 'anonymous' ? `anonymous:${req.ip || req.socket?.remoteAddress || ''}` : key;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return api;
|
|
100
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.uagErrorBody = exports.uagRouteHandlers = exports.serverDeps = exports.uagModuleApi = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
var api_1 = require("./api");
|
|
20
|
+
Object.defineProperty(exports, "uagModuleApi", { enumerable: true, get: function () { return api_1.uagModuleApi; } });
|
|
21
|
+
Object.defineProperty(exports, "serverDeps", { enumerable: true, get: function () { return api_1.serverDeps; } });
|
|
22
|
+
var routes_1 = require("./routes");
|
|
23
|
+
Object.defineProperty(exports, "uagRouteHandlers", { enumerable: true, get: function () { return routes_1.uagRouteHandlers; } });
|
|
24
|
+
Object.defineProperty(exports, "uagErrorBody", { enumerable: true, get: function () { return routes_1.uagErrorBody; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import type { ModuleRouteHandlers } from '@formio/appserver';
|
|
3
|
+
import type { UAGProjectInterface } from '../UAGProjectInterface';
|
|
4
|
+
/**
|
|
5
|
+
* What runs around each module route in the UAG, as around the agent
|
|
6
|
+
* operations: the caller check, the agent operations' JSON body limit and a
|
|
7
|
+
* request id before it; the named operation's rate limits and the Agent API's
|
|
8
|
+
* error shape around its run.
|
|
9
|
+
*/
|
|
10
|
+
export declare function uagRouteHandlers(project: UAGProjectInterface, name: string): ModuleRouteHandlers;
|
|
11
|
+
/** A module route's `failed` event: the Agent API's error shape, with the status the event cannot carry otherwise. */
|
|
12
|
+
export declare function uagErrorBody(req: Request, err: unknown): Record<string, any> & {
|
|
13
|
+
status: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uagRouteHandlers = uagRouteHandlers;
|
|
4
|
+
exports.uagErrorBody = uagErrorBody;
|
|
5
|
+
const router_1 = require("../agent/router");
|
|
6
|
+
const errors_1 = require("../agent/errors");
|
|
7
|
+
/**
|
|
8
|
+
* What runs around each module route in the UAG, as around the agent
|
|
9
|
+
* operations: the caller check, the agent operations' JSON body limit and a
|
|
10
|
+
* request id before it; the named operation's rate limits and the Agent API's
|
|
11
|
+
* error shape around its run.
|
|
12
|
+
*/
|
|
13
|
+
function uagRouteHandlers(project, name) {
|
|
14
|
+
const { json, requestId, handle } = (0, router_1.agentEndpoints)(project);
|
|
15
|
+
return { before: [(0, router_1.agentCredentials)(project), json, requestId], wrap: (run) => handle(name, run) };
|
|
16
|
+
}
|
|
17
|
+
/** A module route's `failed` event: the Agent API's error shape, with the status the event cannot carry otherwise. */
|
|
18
|
+
function uagErrorBody(req, err) {
|
|
19
|
+
const { status, body } = (0, errors_1.agentError)(req.requestId, err);
|
|
20
|
+
return { status, ...body };
|
|
21
|
+
}
|