@formio/uag 2.0.0-rc.7 → 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 CHANGED
@@ -243,7 +243,7 @@ Engines run inside the UAG, or on an [engine server](./packages/engine-server) (
243
243
  | You run | You need |
244
244
  |---|---|
245
245
  | MCP and conversation forms (including a form's chat, `{form}/agent/chat`), on Form.io Open Source or a hosted project | Nothing |
246
- | MCP and conversation forms, on a self-hosted Form.io Enterprise project | A Form.io license in `LICENSE_KEY` (development hostnames such as `localhost` excepted) |
246
+ | MCP and conversation forms, on a self-hosted Form.io Enterprise project | A Form.io license that includes the UAG, in `LICENSE_KEY` (development hostnames such as `localhost` excepted) |
247
247
  | Agentic workflows and Agentic Formflow (the Agent API's `extract`, `decide` and `generate`, the **Agent: Run task** action, the `agent_decide` and `agent_provide` tools, `uag-eval`) on a self-hosted project, Open Source or Enterprise | A Form.io license that includes the UAG, in `LICENSE_KEY` |
248
248
  | The same on a project on Form.io's hosted service (`https://*.form.io`) | Nothing, as the project runs under Form.io's license; except `uag-eval`, which runs offline and needs `LICENSE_KEY` |
249
249
  | The UAG's own database (`MONGO`), and with it remote MCP sign-in (`MCP_OAUTH`), on any project | A Form.io license that includes the UAG, unless the project and the database are both on development hosts (`localhost`, `*.localhost`, `lvh.me`, `*.local`) |
@@ -253,7 +253,8 @@ A hosted project needs a license after all when the UAG weakens the proof that i
253
253
 
254
254
  ## Extending the UAG
255
255
 
256
- - **Modules** add MCP tools, override the built-in tools and their response templates, add form actions (the UAG runs a module's own actions; Form.io runs its own), and ship a project template of forms and resources that the UAG installs. Mount one over `/app/module` in the container, or name it in `UAG_MODULE` for the `uag` command. See [Modules](https://ai.form.io/developers/modules), [`module/Readme.md`](./module/Readme.md) and the [Claude Desktop example](./demo/samples/custom-module).
256
+ - **Modules** add MCP tools, override the built-in tools and their response templates, add form actions (the UAG runs a module's own actions; Form.io runs its own), and ship a project template of forms and resources that the UAG installs. A module can also add routes on every form, formio.js components (served in the UAG's `/uag-js/uag.use.js`), plugins, and settings of its own. Mount one over `/app/module` in the container, or name it in `UAG_MODULE` for the `uag` command. See [Modules](https://ai.form.io/developers/modules), [`module/Readme.md`](./module/Readme.md) and the [Claude Desktop example](./demo/samples/custom-module).
257
+ - **Plugins** add capabilities of a known type, such as converters that read more file types and the document processes an AI Document runs. List them in a module, or in a plugins file: the one `UAG_PLUGINS` names, or in the image, one per folder under `/app/plugins`, where add-ons such as `formio/uag-documents` put theirs. Plugins and modules are trusted code: they run in the UAG process. See [Plugins](https://ai.form.io/developers/plugins) and [AI Document](https://ai.form.io/developers/ai-document).
257
258
  - **Node.js.** Embed the UAG in an Express application, or run it with `npx uag`:
258
259
 
259
260
  ```js
@@ -319,10 +320,11 @@ The full table, with the Agent API's errors: [Troubleshooting](https://ai.form.i
319
320
  | Package | Version | Works with UAG | What it is |
320
321
  |---|---|---|---|
321
322
  | [`@formio/uag`](https://www.npmjs.com/package/@formio/uag), image [`formio/uag`](https://hub.docker.com/r/formio/uag) | 2.x | — | The UAG. The image includes every official engine. |
322
- | `@formio/uag-jev`, `-claude`, `-openai`, `-grok`, `-gemini` | 2.x | ≥ 2.0.0 | The [engines](./engines), versioned independently of the UAG. |
323
+ | `@formio/uag-jev`, `-claude`, `-openai`, `-grok`, `-gemini` | 2.x | ≥ 2.0.0 | The [engines](./engines), released with the UAG at the same version. |
323
324
  | [`@formio/uag-sdk`](./packages/engine-sdk) | 2.x | ≥ 2.0.0 | Engine contract version 1, remote engine protocol version 1. |
324
325
  | [`@formio/uag-engines`](./packages/engine-server), image `formio/uag-engines` | 2.x | ≥ 2.0.0 | The engine server. |
325
- | [`@formio/uag-js`](./packages/uag-js) | 2.x | ≥ 2.0.0 | The formio.js module: saves `uag` forms through the UAG, adds Agent Input, and renders forms as a chat. Runs in the browser. |
326
+ | [`@formio/uag-js`](./packages/uag-js) | 2.x | ≥ 2.0.0 | The formio.js module: saves `uag` forms through the UAG, adds Agent Input, and renders forms as a chat. Runs in the browser; the UAG also serves it at `/uag-js/`. |
327
+ | [`@formio/uag-extras`](https://github.com/formio/uag-extras), image `formio/uag-documents` | 1.x | ≥ 2.0.0-rc.7 | Versioned on its own: the AI Document module, document plugins and converters (`npm install @formio/uag-extras@^1`). The image is an add-on that runs beside `formio/uag` and fills its plugins folder (`formio/uag-documents:1`): see [AI Document](https://ai.form.io/developers/ai-document#run-it-with-docker). |
326
328
  | `@formio/uag-claude` | 1.3.x | 1.12.0 – 1.13.x | The retired Claude integration (against 2.0, only its direct commands). Security fixes only; see [MIGRATING-2.0.md](./MIGRATING-2.0.md). |
327
329
 
328
330
  `@formio/uag-claude` 2.x is the Claude *engine*, not the 1.x integration; pin `@formio/uag-claude@^1` if you still install the integration. Upgrading from 1.x: [MIGRATING-2.0.md](./MIGRATING-2.0.md).
@@ -335,6 +337,6 @@ The developer documentation is published at [ai.form.io/developers](https://ai.f
335
337
  |---|---|---|
336
338
  | **Start here** | [How the UAG works](https://ai.form.io/developers) · [Agentic Coding Plugin](https://ai.form.io/developers/agentic-coding) · [Quickstart](https://ai.form.io/developers/quickstart) | [Demo site](./demo/README.md) |
337
339
  | **Concepts** | [Concepts](https://ai.form.io/developers/concepts) · [Form properties](https://ai.form.io/developers/form-properties) | |
338
- | **Build** | [MCP tools](https://ai.form.io/developers/mcp-tools) · [Agent API](https://ai.form.io/developers/agent-api) · [Workflows](https://ai.form.io/developers/workflows) · [JavaScript (formio.js)](https://ai.form.io/developers/renderer) · [Chat widget](https://ai.form.io/developers/chat) · [Connect Claude](https://ai.form.io/developers/connect-claude) | [`@formio/uag-js`](./packages/uag-js/README.md) · [Claude Desktop module](./demo/samples/custom-module/Readme.md) |
339
- | **Extend** | [Node.js API](https://ai.form.io/developers/node-api) · [Modules](https://ai.form.io/developers/modules) · [Engines](https://ai.form.io/developers/engines) · [Engine SDK](https://ai.form.io/developers/engine-sdk) · [Engine server](https://ai.form.io/developers/engine-server) | [Modules](./module/Readme.md) · [Response templates](./demo/samples/custom-module/module/templates/Readme.md) · [Engines](./engines) · [Engine SDK](./packages/engine-sdk/README.md) · [Engine server](./packages/engine-server/README.md) · [Engine setups](./demo/samples/engines/Readme.md) |
340
+ | **Build** | [MCP tools](https://ai.form.io/developers/mcp-tools) · [Agent API](https://ai.form.io/developers/agent-api) · [Workflows](https://ai.form.io/developers/workflows) · [JavaScript (formio.js)](https://ai.form.io/developers/renderer) · [Chat widget](https://ai.form.io/developers/chat) · [AI Document](https://ai.form.io/developers/ai-document) · [Connect Claude](https://ai.form.io/developers/connect-claude) | [`@formio/uag-js`](./packages/uag-js/README.md) · [Claude Desktop module](./demo/samples/custom-module/Readme.md) |
341
+ | **Extend** | [Node.js API](https://ai.form.io/developers/node-api) · [Modules](https://ai.form.io/developers/modules) · [Plugins](https://ai.form.io/developers/plugins) · [Engines](https://ai.form.io/developers/engines) · [Engine SDK](https://ai.form.io/developers/engine-sdk) · [Engine server](https://ai.form.io/developers/engine-server) | [Modules](./module/Readme.md) · [Response templates](./demo/samples/custom-module/module/templates/Readme.md) · [Engines](./engines) · [Engine SDK](./packages/engine-sdk/README.md) · [Engine server](./packages/engine-server/README.md) · [Engine setups](./demo/samples/engines/Readme.md) |
340
342
  | **Operate** | [Security](https://ai.form.io/developers/security) · [Deployment](https://ai.form.io/developers/deployment) · [Evaluation](https://ai.form.io/developers/evaluation) · [Troubleshooting](https://ai.form.io/developers/troubleshooting) | [Docker Hub](./DOCKERHUB.md) · [Migrating to 2.0](./MIGRATING-2.0.md) · [Security policy](./SECURITY.md) · [End-to-end tests](./test/e2e/Readme.md) |
package/bin/uag.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * Engines are found among the installed packages, so a custom image holds only
13
13
  * the engines it installs:
14
14
  *
15
- * npm install @formio/uag @formio/uag-claude
15
+ * npm install @formio/uag@rc @formio/uag-claude@rc
16
16
  * UAG_ENGINES=claude CLAUDE_API_KEY=... npx uag
17
17
  */
18
18
  // Before anything else, so a module that reads the environment sees .env too.
@@ -42,7 +42,8 @@ async function start(uagModule) {
42
42
  // A module may be exported as a promise (an async module).
43
43
  const resolved = await uagModule;
44
44
  if (resolved) server.use(resolved);
45
- // Then the plugins file: UAG_PLUGINS, or the image's /app/plugins/index.mjs.
45
+ // Then the plugins files: UAG_PLUGINS, or the image's /app/plugins (its
46
+ // index.mjs, or each add-on's folder under it).
46
47
  await usePluginFiles(server);
47
48
  const app = Express();
48
49
  // Behind a reverse proxy, its forwarded headers give the public URL (TRUST_PROXY).
@@ -52,11 +53,49 @@ async function start(uagModule) {
52
53
  // names; a misconfigured engine stops startup with its name in the error.
53
54
  app.use(await server.router());
54
55
  const port = process.env.PORT || 3200;
55
- await new Promise((resolve) => app.listen(port, resolve));
56
+ const listener = await new Promise((resolve) => {
57
+ const started = app.listen(port, () => resolve(started));
58
+ });
59
+ stopOnSignals(server, listener);
56
60
  console.log(`Form.io UAG server running on port ${port}`);
57
61
  console.log(`Visit http://localhost:${port} to access the application`);
58
62
  }
59
63
 
64
+ /**
65
+ * On SIGTERM or SIGINT, stop taking connections and destroy the server, which
66
+ * calls each module's `close` and each plugin's, then exit. A shutdown that
67
+ * takes longer than `timeoutMs` exits with 1.
68
+ */
69
+ function stopOnSignals(server, listener, { signals = ['SIGTERM', 'SIGINT'], exit = (code) => process.exit(code), on = process, timeoutMs = 10000 } = {}) {
70
+ let stopping = false;
71
+ const stop = async (signal) => {
72
+ if (stopping) return;
73
+ stopping = true;
74
+ console.log(`Received ${signal}: stopping the UAG.`);
75
+ const timer = setTimeout(() => {
76
+ console.error(`The UAG did not stop within ${timeoutMs} ms.`);
77
+ exit(1);
78
+ }, timeoutMs);
79
+ if (timer.unref) timer.unref();
80
+ if (listener) listener.close();
81
+ let code = 0;
82
+ try {
83
+ await server.destroy();
84
+ }
85
+ catch (error) {
86
+ console.error('Failed to stop the UAG cleanly:', error);
87
+ code = 1;
88
+ }
89
+ clearTimeout(timer);
90
+ exit(code);
91
+ };
92
+ // Not once: a repeated signal (a terminal's Ctrl-C reaches the restarted
93
+ // process twice, from the terminal and from its parent) must not fall back
94
+ // to Node's default handler and end the shutdown early.
95
+ for (const signal of signals) on.on(signal, () => stop(signal));
96
+ return stop;
97
+ }
98
+
60
99
  function fail(error) {
61
100
  console.error('Failed to start server:', error);
62
101
  process.exit(1);
@@ -74,4 +113,4 @@ if (require.main === module) {
74
113
  }
75
114
  }
76
115
 
77
- module.exports = { start, fail };
116
+ module.exports = { start, fail, stopOnSignals };
@@ -12,7 +12,9 @@ export declare const textConverter: Converter;
12
12
  * decoded). A PNG is decoded whole, in memory and synchronously, so its pixel
13
13
  * count is checked from its header first: a small, highly compressed file can
14
14
  * describe an image far larger than its size suggests. JPEGs are embedded
15
- * without being decoded. A converter setting, `maxPixels`, changes it.
15
+ * without being decoded. The converter's `maxPixels` setting changes it
16
+ * (config.plugins.settings.converter['image-pdf'].maxPixels, or
17
+ * DOCUMENTS_MAX_IMAGE_PIXELS).
16
18
  */
17
19
  export declare const MAX_IMAGE_PIXELS = 50000000;
18
20
  /** A PNG's width and height, from its IHDR header; undefined when it has none. */
@@ -91,7 +91,9 @@ const PAGE = { width: 595.28, height: 841.89, margin: 36 };
91
91
  * decoded). A PNG is decoded whole, in memory and synchronously, so its pixel
92
92
  * count is checked from its header first: a small, highly compressed file can
93
93
  * describe an image far larger than its size suggests. JPEGs are embedded
94
- * without being decoded. A converter setting, `maxPixels`, changes it.
94
+ * without being decoded. The converter's `maxPixels` setting changes it
95
+ * (config.plugins.settings.converter['image-pdf'].maxPixels, or
96
+ * DOCUMENTS_MAX_IMAGE_PIXELS).
95
97
  */
96
98
  exports.MAX_IMAGE_PIXELS = 50000000;
97
99
  /** A PNG's width and height, from its IHDR header; undefined when it has none. */
@@ -110,6 +112,14 @@ exports.imagePdfConverter = {
110
112
  apiVersion: 1,
111
113
  accepts: [{ type: mime_1.MIME.png, ext: ['.png'] }, { type: mime_1.MIME.jpeg, ext: ['.jpg', '.jpeg'] }],
112
114
  produces: ['pdf'],
115
+ settings: {
116
+ maxPixels: {
117
+ type: 'number',
118
+ env: 'DOCUMENTS_MAX_IMAGE_PIXELS',
119
+ default: exports.MAX_IMAGE_PIXELS,
120
+ description: 'The largest PNG, in pixels (width × height), converted; a larger one is refused before it is decoded.'
121
+ }
122
+ },
113
123
  async convert(input, options) {
114
124
  options.signal.throwIfAborted();
115
125
  if (input.type === mime_1.MIME.png) {
package/lib/index.d.ts CHANGED
@@ -108,7 +108,7 @@ export * from './tools';
108
108
  export * from './documents';
109
109
  export * from './modules';
110
110
  export { sanitizeMarkdown } from './markdown';
111
- export { usePluginFiles, pluginsFile, DEFAULT_PLUGINS_FILE } from './pluginFiles';
111
+ export { usePluginFiles, pluginFiles, pluginFilesIn, DEFAULT_PLUGINS_DIR } from './pluginFiles';
112
112
  export * from './router';
113
113
  export * from './template';
114
114
  export { sanitizeRichText, isRichText, RICH_TEXT_TAGS, RICH_TEXT_RULE } from './richText';
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.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;
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;
21
21
  exports.assertSingleCore = assertSingleCore;
22
22
  exports.assertNodeSnapshotDisabled = assertNodeSnapshotDisabled;
23
23
  const appserver_1 = require("@formio/appserver");
@@ -424,8 +424,9 @@ var markdown_1 = require("./markdown");
424
424
  Object.defineProperty(exports, "sanitizeMarkdown", { enumerable: true, get: function () { return markdown_1.sanitizeMarkdown; } });
425
425
  var pluginFiles_1 = require("./pluginFiles");
426
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; } });
427
+ Object.defineProperty(exports, "pluginFiles", { enumerable: true, get: function () { return pluginFiles_1.pluginFiles; } });
428
+ Object.defineProperty(exports, "pluginFilesIn", { enumerable: true, get: function () { return pluginFiles_1.pluginFilesIn; } });
429
+ Object.defineProperty(exports, "DEFAULT_PLUGINS_DIR", { enumerable: true, get: function () { return pluginFiles_1.DEFAULT_PLUGINS_DIR; } });
429
430
  __exportStar(require("./router"), exports);
430
431
  __exportStar(require("./template"), exports);
431
432
  var richText_1 = require("./richText");
@@ -1,16 +1,28 @@
1
1
  import type { UAGServer } from './index';
2
- /** Where the curated image keeps its plugins file. */
3
- export declare const DEFAULT_PLUGINS_FILE = "/app/plugins/index.mjs";
2
+ /** The image's plugins folder: add-ons are mounted in folders under it. */
3
+ export declare const DEFAULT_PLUGINS_DIR = "/app/plugins";
4
4
  /**
5
- * The plugins file to load: UAG_PLUGINS (a path, or "off"), or else the
6
- * image's default when it exists. Undefined when there is none.
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`.
7
11
  */
8
- export declare function pluginsFile(env?: Record<string, string | undefined>, defaultFile?: string): string | undefined;
12
+ export declare function pluginFilesIn(target: string): string[];
9
13
  /**
10
- * Load the plugins file, if any, and use it, named by its path in error
11
- * messages. Its default export is a plugin or a list of plugins; or a module
12
- * (an extra, with its plugins), or a list mixing modules and
13
- * plugins, each module used in turn. Plugins are trusted code: they run in
14
- * the UAG process.
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.
15
16
  */
16
- export declare function usePluginFiles(server: Pick<UAGServer, 'use'>, env?: Record<string, string | undefined>, defaultFile?: string): Promise<string | undefined>;
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[]>;
@@ -3,26 +3,59 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_PLUGINS_FILE = void 0;
7
- exports.pluginsFile = pluginsFile;
6
+ exports.DEFAULT_PLUGINS_DIR = void 0;
7
+ exports.pluginFilesIn = pluginFilesIn;
8
+ exports.pluginFiles = pluginFiles;
8
9
  exports.usePluginFiles = usePluginFiles;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const url_1 = require("url");
12
13
  const appserver_1 = require("@formio/appserver");
13
- /** Where the curated image keeps its plugins file. */
14
- exports.DEFAULT_PLUGINS_FILE = '/app/plugins/index.mjs';
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
+ }
15
48
  /**
16
- * The plugins file to load: UAG_PLUGINS (a path, or "off"), or else the
17
- * image's default when it exists. Undefined when there is none.
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.
18
51
  */
19
- function pluginsFile(env = process.env, defaultFile = exports.DEFAULT_PLUGINS_FILE) {
52
+ function pluginFiles(env = process.env, defaultDir = exports.DEFAULT_PLUGINS_DIR) {
20
53
  const setting = String(env.UAG_PLUGINS || '').trim();
21
54
  if (setting.toLowerCase() === 'off')
22
- return undefined;
55
+ return [];
23
56
  if (setting)
24
- return path_1.default.resolve(setting);
25
- return fs_1.default.existsSync(defaultFile) ? defaultFile : undefined;
57
+ return pluginFilesIn(path_1.default.resolve(setting));
58
+ return fs_1.default.existsSync(defaultDir) ? pluginFilesIn(defaultDir) : [];
26
59
  }
27
60
  /** A real dynamic import(), kept one after compilation to CommonJS. */
28
61
  const dynamicImport = new Function('specifier', 'return import(specifier)');
@@ -36,27 +69,33 @@ const dynamicImport = new Function('specifier', 'return import(specifier)');
36
69
  const MODULE_KEYS = ['uag', 'routes', 'components', 'initialize', 'plugins', 'pluginTypes', 'config', 'formRoutes', 'actions', 'hooks', 'providers', 'processors'];
37
70
  const isModule = (entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry) && typeof entry.type !== 'string'
38
71
  && (MODULE_KEYS.some((key) => key in entry) || typeof entry.settings === 'function');
39
- /**
40
- * Load the plugins file, if any, and use it, named by its path in error
41
- * messages. Its default export is a plugin or a list of plugins; or a module
42
- * (an extra, with its plugins), or a list mixing modules and
43
- * plugins, each module used in turn. Plugins are trusted code: they run in
44
- * the UAG process.
45
- */
46
- async function usePluginFiles(server, env = process.env, defaultFile = exports.DEFAULT_PLUGINS_FILE) {
47
- const file = pluginsFile(env, defaultFile);
48
- if (!file)
49
- return undefined;
72
+ /** Use one plugins file, named by its path in error messages. */
73
+ async function usePluginFile(server, file) {
50
74
  const exported = (await dynamicImport((0, url_1.pathToFileURL)(file).href))?.default;
51
75
  const entries = [].concat(exported ?? []);
52
76
  if (entries.some(isModule)) {
53
77
  const plugins = entries.filter((entry) => !isModule(entry));
54
78
  server.use([...entries.filter(isModule), ...(plugins.length ? [{ plugins }] : [])], file);
55
79
  console.log(`[formio-uag] Plugins and modules loaded from ${file}.`);
56
- return file;
80
+ return;
57
81
  }
58
82
  const plugins = await (0, appserver_1.loadPluginFile)(file);
59
83
  server.use({ plugins }, file);
60
84
  console.log(`[formio-uag] Plugins loaded from ${file}.`);
61
- return 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;
62
101
  }
package/lib/testing.js CHANGED
@@ -48,9 +48,10 @@ function testForm(definition, options = {}) {
48
48
  }
49
49
  return bytes;
50
50
  }
51
- if (options.files)
52
- return options.files(value, readOptions);
53
- throw new Error('testForm reads base64 (data: URL) files only: give createTestModuleApi or mountModuleRoutes a "files" function for others.');
51
+ const files = options.files || this?.files;
52
+ if (files)
53
+ return files(value, readOptions);
54
+ throw new Error('testForm reads base64 (data: URL) files only: give testForm, createTestModuleApi or mountModuleRoutes a "files" function for others.');
54
55
  }
55
56
  };
56
57
  return form;
@@ -96,6 +97,10 @@ function settingsOf(module, options) {
96
97
  throw new Error(`Module ${module.name}: "settings" returned a promise; it must return the settings themselves (no async)`);
97
98
  return Object.freeze({ ...(settings || {}) });
98
99
  }
100
+ /** The form, reading stored values that are not base64 through `files` unless it has its own. */
101
+ function withFiles(form, files) {
102
+ return files && form && typeof form === 'object' && !form.files ? { ...form, files } : form;
103
+ }
99
104
  /** The UAG's module API for a module, in a test. */
100
105
  async function createTestModuleApi(module, options = {}) {
101
106
  const loaded = loadedOf(module, settingsOf(module, options));
@@ -121,6 +126,11 @@ async function createTestModuleApi(module, options = {}) {
121
126
  if (options.plugins?.length)
122
127
  registry.add(options.plugins, 'test');
123
128
  await registry.initialize({ server: {} });
129
+ if (options.files) {
130
+ // A form without its own "files" function reads other stored values through this one.
131
+ const read = api.files.read;
132
+ api.files.read = (form, value, auth, readOptions) => read(withFiles(form, options.files), value, auth, readOptions);
133
+ }
124
134
  return api;
125
135
  }
126
136
  /**
@@ -155,7 +165,7 @@ async function mountModuleRoutes(module, options) {
155
165
  };
156
166
  const router = (0, appserver_1.moduleFormRouter)({}, loaded, api, host);
157
167
  const withForm = (req, res, next) => {
158
- req.form = options.form;
168
+ req.form = withFiles(options.form, options.files);
159
169
  req.auth = options.auth || testAuth();
160
170
  next();
161
171
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formio/uag",
3
- "version": "2.0.0-rc.7",
3
+ "version": "2.0.0-rc.8",
4
4
  "mcpName": "io.form/formio-uag",
5
5
  "description": "The Form.io Universal Agent Gateway (UAG).",
6
6
  "main": "lib/index.js",
@@ -45,9 +45,9 @@
45
45
  "author": "Form.io Developers",
46
46
  "license": "MIT",
47
47
  "dependencies": {
48
- "@formio/appserver": "^2.26.0",
48
+ "@formio/appserver": "^2.26.1",
49
49
  "@formio/core": "2.9.0",
50
- "@formio/uag-sdk": "^2.0.0-rc.7",
50
+ "@formio/uag-sdk": "^2.0.0-rc.8",
51
51
  "@modelcontextprotocol/sdk": "1.32.2",
52
52
  "cors": "^2.8.6",
53
53
  "debug": "^4.4.3",