@formio/uag 2.0.0-rc.1 → 2.0.0-rc.10

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.
Files changed (96) hide show
  1. package/README.md +17 -15
  2. package/bin/uag-eval.js +7 -3
  3. package/bin/uag.js +48 -5
  4. package/lib/UAGFormInterface.d.ts +12 -0
  5. package/lib/UAGFormInterface.js +55 -12
  6. package/lib/UAGProjectInterface.d.ts +9 -1
  7. package/lib/UAGProjectInterface.js +25 -7
  8. package/lib/actionLogs.d.ts +1 -10
  9. package/lib/actionLogs.js +5 -3
  10. package/lib/actions/runTask.js +4 -3
  11. package/lib/agent/anonymous.js +5 -4
  12. package/lib/agent/chat.d.ts +3 -2
  13. package/lib/agent/chat.js +35 -99
  14. package/lib/agent/engines.d.ts +10 -6
  15. package/lib/agent/engines.js +19 -10
  16. package/lib/agent/errors.d.ts +25 -0
  17. package/lib/agent/errors.js +132 -0
  18. package/lib/agent/index.d.ts +1 -1
  19. package/lib/agent/index.js +2 -1
  20. package/lib/agent/remote.d.ts +5 -1
  21. package/lib/agent/remote.js +25 -2
  22. package/lib/agent/router.d.ts +32 -2
  23. package/lib/agent/router.js +37 -25
  24. package/lib/agent/stubs.d.ts +13 -2
  25. package/lib/agent/stubs.js +18 -5
  26. package/lib/agent/tools.d.ts +24 -3
  27. package/lib/agent/tools.js +47 -4
  28. package/lib/config.d.ts +31 -4
  29. package/lib/database.d.ts +3 -8
  30. package/lib/database.js +2 -19
  31. package/lib/documents/convert.d.ts +38 -0
  32. package/lib/documents/convert.js +151 -0
  33. package/lib/documents/converters.d.ts +28 -0
  34. package/lib/documents/converters.js +149 -0
  35. package/lib/documents/detect.d.ts +31 -0
  36. package/lib/documents/detect.js +169 -0
  37. package/lib/documents/index.d.ts +9 -0
  38. package/lib/documents/index.js +43 -0
  39. package/lib/documents/mime.d.ts +39 -0
  40. package/lib/documents/mime.js +100 -0
  41. package/lib/documents/plugins.d.ts +18 -0
  42. package/lib/documents/plugins.js +65 -0
  43. package/lib/documents/readers.d.ts +19 -0
  44. package/lib/documents/readers.js +64 -0
  45. package/lib/documents/routes.d.ts +52 -0
  46. package/lib/documents/routes.js +99 -0
  47. package/lib/documents/types.d.ts +126 -0
  48. package/lib/documents/types.js +2 -0
  49. package/lib/documents/uagJs.d.ts +17 -0
  50. package/lib/documents/uagJs.js +157 -0
  51. package/lib/formflow.d.ts +11 -4
  52. package/lib/formflow.js +5 -5
  53. package/lib/index.d.ts +65 -9
  54. package/lib/index.js +228 -26
  55. package/lib/markdown.d.ts +1 -0
  56. package/lib/markdown.js +166 -0
  57. package/lib/modules/api.d.ts +25 -0
  58. package/lib/modules/api.js +98 -0
  59. package/lib/modules/index.d.ts +4 -0
  60. package/lib/modules/index.js +24 -0
  61. package/lib/modules/routes.d.ts +14 -0
  62. package/lib/modules/routes.js +21 -0
  63. package/lib/modules/types.d.ts +68 -0
  64. package/lib/modules/types.js +5 -0
  65. package/lib/pluginFiles.d.ts +28 -0
  66. package/lib/pluginFiles.js +101 -0
  67. package/lib/plugins.d.ts +17 -0
  68. package/lib/plugins.js +93 -0
  69. package/lib/richText.d.ts +20 -0
  70. package/lib/richText.js +99 -0
  71. package/lib/router.d.ts +9 -0
  72. package/lib/router.js +19 -0
  73. package/lib/runner.d.ts +2 -0
  74. package/lib/runner.js +11 -3
  75. package/lib/runs.d.ts +17 -79
  76. package/lib/runs.js +10 -71
  77. package/lib/testing.d.ts +74 -0
  78. package/lib/testing.js +180 -0
  79. package/lib/tools/SchemaBuilder.d.ts +0 -5
  80. package/lib/tools/SchemaBuilder.js +0 -8
  81. package/lib/tools/agentDecide.js +2 -2
  82. package/lib/tools/agentProvide.d.ts +0 -5
  83. package/lib/tools/agentProvide.js +7 -40
  84. package/lib/tools/collectData.js +2 -1
  85. package/lib/tools/index.d.ts +3 -2
  86. package/lib/tools/index.js +5 -5
  87. package/lib/tools/readDocument.d.ts +17 -0
  88. package/lib/tools/readDocument.js +99 -0
  89. package/lib/tools/submissionUpdate.js +3 -2
  90. package/lib/tools/utils.d.ts +40 -0
  91. package/lib/tools/utils.js +16 -1
  92. package/lib/version.d.ts +2 -0
  93. package/lib/version.js +5 -0
  94. package/package.json +17 -10
  95. package/testing.d.ts +1 -0
  96. package/testing.js +2 -0
package/lib/index.d.ts CHANGED
@@ -1,32 +1,57 @@
1
1
  import { Server, ServerModule } from '@formio/appserver';
2
- import { Router } from 'express';
2
+ import type { LoadedModule, ModuleApi, ModuleRoute, ModuleRouteHandlers, ProjectInterface } from '@formio/appserver';
3
+ import { Request, Response, Router } from 'express';
3
4
  import { UAGProjectInterface } from './UAGProjectInterface';
4
5
  import { UAGFormInterface } from './UAGFormInterface';
5
6
  import { UAGConfig } from './config';
6
7
  import { UAGDatabase } from './database';
7
- export type UAGModule = ServerModule & {
8
+ import type { UAGModuleApi } from './modules';
9
+ export type UAGModule = ServerModule<UAGModuleApi> & {
8
10
  config?: UAGConfig;
9
11
  };
10
12
  /**
11
- * Agentic Formflow keeps its license state inside @formio/appserver-core, so the
12
- * UAG and @formio/appserver must load the same copy of it, at 2.24.1 or later.
13
- * A second copy (an older lockfile, or a package manager that nests it) would
14
- * otherwise fail in confusing ways; say what to do instead.
13
+ * Agentic Formflow keeps its license state inside @formio/appserver-core, and
14
+ * modules are built on its module framework. The UAG loads it only through
15
+ * @formio/appserver, so the two always share one copy, which must be 2.26.4 or
16
+ * later. An older @formio/appserver, or an old lockfile, can still resolve an
17
+ * older one, which would fail in confusing ways; say what to do instead.
15
18
  */
16
- export declare function assertSingleCore(core?: any, appserver?: any, version?: string): void;
19
+ export declare function assertSingleCore(version?: string): void;
20
+ /**
21
+ * isolated-vm, which runs form logic, needs Node's startup snapshot off. Without
22
+ * it nothing fails at once: the process can crash later, in native code. So an
23
+ * embedding server that forgot the flag is stopped here, saying what to do.
24
+ */
25
+ export declare function assertNodeSnapshotDisabled(execArgv?: string[], nodeOptions?: string): void;
17
26
  export declare class UAGServer extends Server {
18
27
  config: UAGConfig;
19
28
  /** The UAG's own database, once it may be used. */
20
29
  database?: UAGDatabase;
21
30
  /** The database between connecting and the license check. */
22
31
  private pendingDatabase?;
32
+ /**
33
+ * Use a module, or a list of modules (each in turn). A module used twice (by
34
+ * its `name`) is used once. `config.tools` adds up across modules; an MCP
35
+ * tool or engine type that two modules define stops startup, naming both.
36
+ * A route's `rateLimit` is the default for its name, unless the operator set one.
37
+ */
38
+ use(module: any, source?: string): void;
39
+ /** MCP tools and engine types, which only one module may define. */
40
+ protected moduleClaims(definition: Record<string, any>): string[];
41
+ /**
42
+ * MCP tools add up across modules (lodash would merge them index by index),
43
+ * and a named module's tools get its API as `extra.module`. Its routes'
44
+ * `rateLimit`s become the defaults for their names.
45
+ */
46
+ protected mergeModuleConfig(config: any, module?: LoadedModule): void;
47
+ /** Route rate-limit defaults, for the names the operator left unset. */
48
+ private routeRateLimits;
23
49
  constructor(config?: UAGConfig);
24
50
  /**
25
51
  * Connect the UAG's own database before the project loads, and check it
26
52
  * without writing: a database the UAG may not use stops startup. It is used
27
53
  * (its collections created, its stores handed out) only once the license is
28
- * known; see enableDatabase. Until then only the license check may write to
29
- * it, to record a license server outage.
54
+ * known; see enableDatabase. Until then nothing writes to it.
30
55
  */
31
56
  protected connectDatabase(): Promise<void>;
32
57
  /**
@@ -36,6 +61,14 @@ export declare class UAGServer extends Server {
36
61
  * otherwise leave it untouched, close it, and say why.
37
62
  */
38
63
  protected enableDatabase(project: UAGProjectInterface): Promise<void>;
64
+ /**
65
+ * An Agent API request that failed before its handlers (a body that is not JSON,
66
+ * one over the size limit) is answered in the Agent API's error shape; every other
67
+ * route keeps Form.io's plain-text answer.
68
+ */
69
+ protected sendError(req: Request, res: Response, status: number, message: string, err?: any): void;
70
+ /** Whether a request is for a module's form route: its method, and a path ending in the route's. */
71
+ private isModuleRouteRequest;
39
72
  /** Browsers reading Agent API responses need the request id too. */
40
73
  get exposedHeaders(): string[];
41
74
  /** The loaded UAG project, once the project router has been built. */
@@ -47,12 +80,35 @@ export declare class UAGServer extends Server {
47
80
  * on the form's own routes.
48
81
  */
49
82
  protected extendRouter(router: Router): Promise<void>;
83
+ /**
84
+ * List the UAG's engines and MCP tools in the plugin registry, beside
85
+ * appserver-core's form actions. They keep loading their own way.
86
+ */
87
+ protected listPlugins(): Promise<void>;
88
+ /** A form's own route segments, which module routes may not use: appserver's, and the agent operations. */
89
+ protected get reservedFormPaths(): string[];
90
+ /** Module routes may not take the names of the UAG's own rate-limited operations. */
91
+ protected get reservedRouteNames(): string[];
92
+ protected get hostName(): string;
93
+ protected get hostVersion(): string;
94
+ /** The module API: appserver's, with the UAG's license check, engines, converters and helpers. */
95
+ protected createModuleApi(module: LoadedModule): ModuleApi;
96
+ /** Module routes run behind what the agent operations run behind: the caller check, body limit, request id, rate limits and error shape. */
97
+ protected moduleRouteHandlers(project: ProjectInterface, route: ModuleRoute<any>, module: LoadedModule): ModuleRouteHandlers;
98
+ protected moduleErrorBody(req: Request, err: unknown): Record<string, any> & {
99
+ status: number;
100
+ };
50
101
  router(): Promise<any>;
51
102
  }
52
103
  export * from '@formio/appserver';
53
104
  export * from './config';
54
105
  export * from './agent';
55
106
  export * from './tools';
107
+ export * from './documents';
108
+ export * from './modules';
109
+ export { sanitizeMarkdown } from './markdown';
110
+ export { usePluginFiles, pluginFiles, pluginFilesIn, DEFAULT_PLUGINS_DIR } from './pluginFiles';
56
111
  export * from './router';
57
112
  export * from './template';
113
+ export { sanitizeRichText, isRichText, RICH_TEXT_TAGS, RICH_TEXT_RULE } from './richText';
58
114
  export { UAGProjectInterface, UAGFormInterface };
package/lib/index.js CHANGED
@@ -13,11 +13,16 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
13
13
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
16
19
  Object.defineProperty(exports, "__esModule", { value: true });
17
- 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_DIR = exports.pluginFilesIn = exports.pluginFiles = exports.usePluginFiles = exports.sanitizeMarkdown = exports.UAGServer = void 0;
18
21
  exports.assertSingleCore = assertSingleCore;
22
+ exports.assertNodeSnapshotDisabled = assertNodeSnapshotDisabled;
19
23
  const appserver_1 = require("@formio/appserver");
20
24
  const formflow_1 = require("./formflow");
25
+ const path_1 = __importDefault(require("path"));
21
26
  const UAGProjectInterface_1 = require("./UAGProjectInterface");
22
27
  Object.defineProperty(exports, "UAGProjectInterface", { enumerable: true, get: function () { return UAGProjectInterface_1.UAGProjectInterface; } });
23
28
  const UAGFormInterface_1 = require("./UAGFormInterface");
@@ -25,26 +30,71 @@ Object.defineProperty(exports, "UAGFormInterface", { enumerable: true, get: func
25
30
  const lodash_1 = require("lodash");
26
31
  const engines_1 = require("./agent/engines");
27
32
  const router_1 = require("./agent/router");
33
+ const errors_1 = require("./agent/errors");
28
34
  const database_1 = require("./database");
29
35
  const runs_1 = require("./runs");
30
36
  const actionLogs_1 = require("./actionLogs");
31
37
  const runner_1 = require("./runner");
32
38
  const runTask_1 = require("./actions/runTask");
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();
33
48
  const debug = require('debug')('formio:uag');
34
49
  /**
35
- * Agentic Formflow keeps its license state inside @formio/appserver-core, so the
36
- * UAG and @formio/appserver must load the same copy of it, at 2.24.1 or later.
37
- * A second copy (an older lockfile, or a package manager that nests it) would
38
- * otherwise fail in confusing ways; say what 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.4 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.
39
55
  */
40
- function assertSingleCore(core = require('@formio/appserver-core'), appserver = require('@formio/appserver'), version = String(require('@formio/appserver-core/package.json').version || '0.0.0')) {
56
+ function assertSingleCore(version = appserverCoreVersion()) {
41
57
  const [major, minor, patch] = version.split('.').map((part) => parseInt(part, 10) || 0);
42
- if (major < 2 || (major === 2 && (minor < 24 || (minor === 24 && patch < 1)))) {
43
- throw new Error(`@formio/uag needs @formio/appserver-core 2.24.1 or later; ${version} is installed.`);
58
+ if (major < 2 || (major === 2 && (minor < 26 || (minor === 26 && patch < 4)))) {
59
+ throw new Error(`@formio/uag needs @formio/appserver-core 2.26.4 or later, through @formio/appserver; ${version} is installed. Upgrade @formio/appserver.`);
60
+ }
61
+ }
62
+ /** The version of the @formio/appserver-core that @formio/appserver loads. */
63
+ function appserverCoreVersion() {
64
+ const appserver = path_1.default.dirname(require.resolve('@formio/appserver/package.json'));
65
+ return String(require(require.resolve('@formio/appserver-core/package.json', { paths: [appserver] })).version || '0.0.0');
66
+ }
67
+ /**
68
+ * isolated-vm, which runs form logic, needs Node's startup snapshot off. Without
69
+ * it nothing fails at once: the process can crash later, in native code. So an
70
+ * embedding server that forgot the flag is stopped here, saying what to do.
71
+ */
72
+ function assertNodeSnapshotDisabled(execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS || '') {
73
+ if (execArgv.includes('--no-node-snapshot') || /(^|\s)--no-node-snapshot(\s|$)/.test(nodeOptions))
74
+ return;
75
+ throw new Error('@formio/uag needs Node started with --no-node-snapshot: isolated-vm, which runs form logic, can crash the process without it. ' +
76
+ 'Start node with --no-node-snapshot, or set NODE_OPTIONS=--no-node-snapshot. The uag command and the formio/uag image do this for you.');
77
+ }
78
+ /** A host name a browser cannot reach: a container or service name, with no dot. */
79
+ function internalHost(url) {
80
+ try {
81
+ const { hostname } = new URL(url);
82
+ return !!hostname && hostname !== 'localhost' && !hostname.includes('.') && !hostname.includes(':');
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ /** Warnings for settings that start fine and fail later, in ways far from their cause. */
89
+ function warnAboutSettings(config, secretGiven) {
90
+ if (!secretGiven) {
91
+ console.warn('[formio-uag] JWT_SECRET is not set, so the UAG signs its tokens with a random secret that lasts only as long as this process: ' +
92
+ 'every token stops working when it restarts, and other instances refuse it. Set JWT_SECRET, the same on every instance.');
44
93
  }
45
- if (appserver.Formflow !== core.Formflow || appserver.ProjectInterface !== core.ProjectInterface) {
46
- throw new Error('Two copies of @formio/appserver-core are installed: one for @formio/uag and an older one for @formio/appserver. ' +
47
- 'Upgrade @formio/appserver to 2.24.1 or later and deduplicate (for example `yarn dedupe @formio/appserver-core`).');
94
+ const auth = config.auth || {};
95
+ if (auth.mcpOAuth && !auth.loginForm && internalHost(String(config.project || ''))) {
96
+ console.warn(`[formio-uag] MCP_OAUTH is on, but LOGIN_FORM is not set: the sign-in page loads ${String(config.project).replace(/\/+$/, '')}/user/login, ` +
97
+ 'an address a person\'s browser cannot reach. Set LOGIN_FORM to the login form\'s public URL.');
48
98
  }
49
99
  }
50
100
  /** The UAG's own database from the environment, when `MONGO` is set. */
@@ -61,9 +111,59 @@ function databaseFromEnv(env = process.env) {
61
111
  };
62
112
  }
63
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
+ }
64
161
  constructor(config) {
162
+ assertNodeSnapshotDisabled();
65
163
  assertSingleCore();
66
164
  const configuredDb = !!config?.db;
165
+ // Read before the server fills in a random one.
166
+ const secretGiven = !!(config?.auth?.secret || (0, lodash_1.get)(process.env, 'JWT_SECRET', '').toString());
67
167
  const uagLicense = (0, lodash_1.get)(process.env, 'UAG_LICENSE', '').toString();
68
168
  if (uagLicense) {
69
169
  console.warn('[formio-uag] UAG_LICENSE is deprecated. Put a Form.io license that includes the UAG in LICENSE_KEY instead; UAG_LICENSE keeps working until UAG 3.0.');
@@ -85,15 +185,28 @@ class UAGServer extends appserver_1.Server {
85
185
  retryDelay: parseInt((0, lodash_1.get)(process.env, 'PROJECT_RETRY_DELAY', 5000).toString(), 10),
86
186
  submissionProxy: true,
87
187
  processAsClient: true,
188
+ // GET /status names the UAG's version beside the Form.io packages it runs on.
189
+ status: { '@formio/uag': version_1.UAG_VERSION },
190
+ // Bodies are parsed before authentication, so keep them to what Form.io accepts.
191
+ maxBodySize: (0, lodash_1.get)(process.env, 'MAX_BODY_SIZE', '16mb').toString(),
88
192
  auth: { pkce: true, mcpOAuth: (0, lodash_1.get)(process.env, 'MCP_OAUTH', '').toString() === 'true' },
89
- 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) }
90
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");
91
203
  // MONGO is the UAG's own database, not a database for the appserver to keep
92
204
  // submissions in: Form.io stores those.
93
205
  if (!configuredDb && this.module.db) {
94
206
  delete this.module.db;
95
207
  delete this.config.db;
96
208
  }
209
+ warnAboutSettings(this.config, secretGiven);
97
210
  this.use({
98
211
  ProjectInterface: UAGProjectInterface_1.UAGProjectInterface,
99
212
  FormInterface: UAGFormInterface_1.UAGFormInterface,
@@ -105,14 +218,12 @@ class UAGServer extends appserver_1.Server {
105
218
  * Connect the UAG's own database before the project loads, and check it
106
219
  * without writing: a database the UAG may not use stops startup. It is used
107
220
  * (its collections created, its stores handed out) only once the license is
108
- * known; see enableDatabase. Until then only the license check may write to
109
- * it, to record a license server outage.
221
+ * known; see enableDatabase. Until then nothing writes to it.
110
222
  */
111
223
  async connectDatabase() {
112
224
  const options = this.config.database;
113
225
  if (!options?.url) {
114
- console.log('[formio-uag] No database (MONGO): runs after the save are kept in memory, remote MCP sign-in (MCP_OAUTH) is unavailable, ' +
115
- 'and the licensed features turn off at once, with no grace period, whenever the license server cannot be reached.');
226
+ console.log('[formio-uag] No database (MONGO): runs after the save are kept in memory, and remote MCP sign-in (MCP_OAUTH) is unavailable.');
116
227
  return;
117
228
  }
118
229
  if (this.database || this.pendingDatabase)
@@ -126,7 +237,6 @@ class UAGServer extends appserver_1.Server {
126
237
  throw err;
127
238
  }
128
239
  this.pendingDatabase = database;
129
- this.config.licenseGrace = database.licenseGrace();
130
240
  }
131
241
  /**
132
242
  * Use the database once the project and its license are loaded: with a
@@ -139,22 +249,27 @@ class UAGServer extends appserver_1.Server {
139
249
  this.pendingDatabase = undefined;
140
250
  if (database) {
141
251
  let refusal = '';
252
+ let grant;
142
253
  try {
143
- project.requireDatabaseLicense(this.config.database.url);
254
+ grant = await (0, formflow_1.useDatabase)(project, {
255
+ url: this.config.database.url,
256
+ authCodes: database.authCodeStore(),
257
+ taskRuns: { collection: database.taskRuns, ids: runs_1.OBJECT_IDS, project: String(project.project?._id || '') }
258
+ });
144
259
  }
145
260
  catch (err) {
146
261
  refusal = err?.message || String(err);
147
262
  }
148
- if (refusal) {
263
+ if (!grant) {
149
264
  console.warn(`[formio-uag] The database (MONGO) is not used: ${refusal} Runs after the save are kept in memory, and remote MCP sign-in (MCP_OAUTH) is off.`);
150
- this.config.licenseGrace = undefined;
151
265
  await database.close().catch(() => { });
152
266
  }
153
267
  else {
154
268
  await database.bootstrap();
155
269
  this.database = database;
156
- (0, lodash_1.set)(this.config, 'auth.authCodeStore', database.authCodeStore());
157
- (0, runner_1.taskRunner)(project).useStore(new runs_1.TaskRunStore(database.taskRuns, String(project.project?._id || '')));
270
+ (0, lodash_1.set)(this.config, 'auth.authCodeStore', grant.authCodeStore);
271
+ if (grant.taskRuns)
272
+ (0, runner_1.taskRunner)(project).useStore(grant.taskRuns);
158
273
  console.log(`[formio-uag] Using the database "${database.name}": runs are recorded, and runs that act with an agent role survive a restart.` +
159
274
  (this.config.auth?.mcpOAuth ? ' Remote MCP sign-in (MCP_OAUTH) is on.' : ''));
160
275
  }
@@ -163,6 +278,26 @@ class UAGServer extends appserver_1.Server {
163
278
  console.warn('[formio-uag] MCP_OAUTH is set, but remote MCP sign-in is off: it needs the UAG\'s database (MONGO), and a license that includes the UAG to use it.');
164
279
  }
165
280
  }
281
+ /**
282
+ * An Agent API request that failed before its handlers (a body that is not JSON,
283
+ * one over the size limit) is answered in the Agent API's error shape; every other
284
+ * route keeps Form.io's plain-text answer.
285
+ */
286
+ sendError(req, res, status, message, err) {
287
+ if (!(0, errors_1.isAgentApiRequest)(req) && !this.isModuleRouteRequest(req))
288
+ return super.sendError(req, res, status, message, err);
289
+ const error = err?.type === 'entity.parse.failed' ? 'The request body is not valid JSON.' : message;
290
+ // Written past any shaping wrapper, which may not have run for this request.
291
+ res.status(status).type('application/json').end(JSON.stringify((0, errors_1.errorBody)((0, errors_1.requestIdOf)(req, res), status, { error })));
292
+ }
293
+ /** Whether a request is for a module's form route: its method, and a path ending in the route's. */
294
+ isModuleRouteRequest(req) {
295
+ const path = String(req.path || '').toLowerCase().replace(/\/+$/, '');
296
+ return this.modules.list.some((module) => Object.keys(module.definition.routes?.form || {}).some((key) => {
297
+ const [method, route] = key.split(' ');
298
+ return req.method === method && path.endsWith(String(route).toLowerCase().replace(/\/+$/, ''));
299
+ }));
300
+ }
166
301
  /** Browsers reading Agent API responses need the request id too. */
167
302
  get exposedHeaders() {
168
303
  return [...super.exposedHeaders, 'X-Request-Id'];
@@ -178,9 +313,15 @@ class UAGServer extends appserver_1.Server {
178
313
  * on the form's own routes.
179
314
  */
180
315
  async extendRouter(router) {
316
+ // A bare function in config.tools (the unreleased factory form) stops startup.
317
+ (0, utils_1.assertToolEntries)(this.config.tools);
181
318
  await super.extendRouter(router);
319
+ // @formio/uag-js, the portal script and stylesheet: public, ahead of the project routes.
320
+ router.use((0, uagJs_1.uagJsRouter)(() => this.plugins, () => this.modules.components()));
182
321
  const uagProject = this.uagProject;
183
322
  if (uagProject) {
323
+ // First: the Agent API's request id and one error shape, for every refusal after it.
324
+ router.use((0, errors_1.agentErrorShape)());
184
325
  router.use(uagProject.agentRouter());
185
326
  // An agent operation of a form the project does not have is answered here, not passed to Form.io.
186
327
  router.use((0, router_1.agentNotFound)(uagProject));
@@ -188,6 +329,52 @@ class UAGServer extends appserver_1.Server {
188
329
  router.use((0, actionLogs_1.ActionLogRouter)(uagProject));
189
330
  }
190
331
  }
332
+ /**
333
+ * List the UAG's engines and MCP tools in the plugin registry, beside
334
+ * appserver-core's form actions. They keep loading their own way.
335
+ */
336
+ async listPlugins() {
337
+ await super.listPlugins();
338
+ // Converters: the converter type and the built-in converters, for task file inputs, read_document and modules.
339
+ (0, plugins_2.registerDocuments)(this.plugins);
340
+ const project = this.uagProject;
341
+ if (project instanceof UAGProjectInterface_1.UAGProjectInterface)
342
+ project.plugins = this.plugins;
343
+ if (project instanceof UAGProjectInterface_1.UAGProjectInterface) {
344
+ // Decide which engines are on before serving anything, so a misconfigured
345
+ // engine stops startup with its name in the error instead of failing the
346
+ // first request.
347
+ project.engines = await (0, engines_1.loadUAGEngines)(project.config || {}, process.env);
348
+ enginesLoaded.add(project);
349
+ (0, plugins_1.listEngines)(this.plugins, project);
350
+ await (0, plugins_1.listTools)(this.plugins, project);
351
+ }
352
+ }
353
+ /** A form's own route segments, which module routes may not use: appserver's, and the agent operations. */
354
+ get reservedFormPaths() {
355
+ return [...super.reservedFormPaths, 'agent'];
356
+ }
357
+ /** Module routes may not take the names of the UAG's own rate-limited operations. */
358
+ get reservedRouteNames() {
359
+ return modules_1.UAG_OPERATIONS;
360
+ }
361
+ get hostName() {
362
+ return 'uag';
363
+ }
364
+ get hostVersion() {
365
+ return version_1.UAG_VERSION;
366
+ }
367
+ /** The module API: appserver's, with the UAG's license check, engines, converters and helpers. */
368
+ createModuleApi(module) {
369
+ return (0, modules_1.uagModuleApi)(super.createModuleApi(module), (0, modules_1.serverDeps)(() => this.uagProject));
370
+ }
371
+ /** Module routes run behind what the agent operations run behind: the caller check, body limit, request id, rate limits and error shape. */
372
+ moduleRouteHandlers(project, route, module) {
373
+ return (0, modules_1.uagRouteHandlers)(project, route.name);
374
+ }
375
+ moduleErrorBody(req, err) {
376
+ return (0, modules_1.uagErrorBody)(req, err);
377
+ }
191
378
  async router() {
192
379
  await this.connectDatabase();
193
380
  const router = await super.router();
@@ -205,10 +392,11 @@ class UAGServer extends appserver_1.Server {
205
392
  }
206
393
  // A database of the UAG's own needs the license, on every edition.
207
394
  await this.enableDatabase(uagProject);
208
- // Decide which engines are on before serving anything, so a misconfigured
209
- // engine stops startup with its name in the error instead of failing the
210
- // first request.
211
- uagProject.engines = await (0, engines_1.loadUAGEngines)(uagProject.config || {}, process.env);
395
+ // The engines are loaded while the plugins are listed (listPlugins); a
396
+ // project interface that is not the UAG's own loads them here.
397
+ if (!enginesLoaded.has(uagProject)) {
398
+ uagProject.engines = await (0, engines_1.loadUAGEngines)(uagProject.config || {}, process.env);
399
+ }
212
400
  // Say which edition this is, and whether Agentic Formflow will run, so an
213
401
  // operator knows before the first request is refused.
214
402
  // A failure here only loses the message; it never stops the UAG starting.
@@ -232,5 +420,19 @@ __exportStar(require("@formio/appserver"), exports);
232
420
  __exportStar(require("./config"), exports);
233
421
  __exportStar(require("./agent"), exports);
234
422
  __exportStar(require("./tools"), exports);
423
+ __exportStar(require("./documents"), exports);
424
+ __exportStar(require("./modules"), exports);
425
+ var markdown_1 = require("./markdown");
426
+ Object.defineProperty(exports, "sanitizeMarkdown", { enumerable: true, get: function () { return markdown_1.sanitizeMarkdown; } });
427
+ var pluginFiles_1 = require("./pluginFiles");
428
+ Object.defineProperty(exports, "usePluginFiles", { enumerable: true, get: function () { return pluginFiles_1.usePluginFiles; } });
429
+ Object.defineProperty(exports, "pluginFiles", { enumerable: true, get: function () { return pluginFiles_1.pluginFiles; } });
430
+ Object.defineProperty(exports, "pluginFilesIn", { enumerable: true, get: function () { return pluginFiles_1.pluginFilesIn; } });
431
+ Object.defineProperty(exports, "DEFAULT_PLUGINS_DIR", { enumerable: true, get: function () { return pluginFiles_1.DEFAULT_PLUGINS_DIR; } });
235
432
  __exportStar(require("./router"), exports);
236
433
  __exportStar(require("./template"), exports);
434
+ var richText_1 = require("./richText");
435
+ Object.defineProperty(exports, "sanitizeRichText", { enumerable: true, get: function () { return richText_1.sanitizeRichText; } });
436
+ Object.defineProperty(exports, "isRichText", { enumerable: true, get: function () { return richText_1.isRichText; } });
437
+ Object.defineProperty(exports, "RICH_TEXT_TAGS", { enumerable: true, get: function () { return richText_1.RICH_TEXT_TAGS; } });
438
+ 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;