@formio/uag 1.13.2 → 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 (115) hide show
  1. package/README.md +237 -535
  2. package/bin/uag-eval.js +99 -0
  3. package/bin/uag.js +116 -0
  4. package/lib/UAGFormInterface.d.ts +72 -2
  5. package/lib/UAGFormInterface.js +226 -57
  6. package/lib/UAGProjectInterface.d.ts +45 -5
  7. package/lib/UAGProjectInterface.js +187 -36
  8. package/lib/actionLogs.d.ts +24 -0
  9. package/lib/actionLogs.js +149 -0
  10. package/lib/actions/runTask.d.ts +43 -0
  11. package/lib/actions/runTask.js +324 -0
  12. package/lib/agent/anonymous.d.ts +34 -0
  13. package/lib/agent/anonymous.js +99 -0
  14. package/lib/agent/chat.d.ts +114 -0
  15. package/lib/agent/chat.js +475 -0
  16. package/lib/agent/engines.d.ts +17 -0
  17. package/lib/agent/engines.js +46 -0
  18. package/lib/agent/errors.d.ts +25 -0
  19. package/lib/agent/errors.js +132 -0
  20. package/lib/agent/index.d.ts +4 -0
  21. package/lib/agent/index.js +19 -0
  22. package/lib/agent/remote.d.ts +71 -0
  23. package/lib/agent/remote.js +252 -0
  24. package/lib/agent/requestId.d.ts +4 -0
  25. package/lib/agent/requestId.js +19 -0
  26. package/lib/agent/router.d.ts +61 -0
  27. package/lib/agent/router.js +307 -0
  28. package/lib/agent/stubs.d.ts +37 -0
  29. package/lib/agent/stubs.js +39 -0
  30. package/lib/agent/tools.d.ts +31 -0
  31. package/lib/agent/tools.js +76 -0
  32. package/lib/config.d.ts +97 -3
  33. package/lib/config.js +19 -0
  34. package/lib/database.d.ts +70 -0
  35. package/lib/database.js +212 -0
  36. package/lib/documents/convert.d.ts +38 -0
  37. package/lib/documents/convert.js +151 -0
  38. package/lib/documents/converters.d.ts +28 -0
  39. package/lib/documents/converters.js +149 -0
  40. package/lib/documents/detect.d.ts +31 -0
  41. package/lib/documents/detect.js +169 -0
  42. package/lib/documents/index.d.ts +9 -0
  43. package/lib/documents/index.js +43 -0
  44. package/lib/documents/mime.d.ts +39 -0
  45. package/lib/documents/mime.js +100 -0
  46. package/lib/documents/plugins.d.ts +18 -0
  47. package/lib/documents/plugins.js +65 -0
  48. package/lib/documents/readers.d.ts +19 -0
  49. package/lib/documents/readers.js +64 -0
  50. package/lib/documents/routes.d.ts +52 -0
  51. package/lib/documents/routes.js +99 -0
  52. package/lib/documents/types.d.ts +126 -0
  53. package/lib/documents/types.js +2 -0
  54. package/lib/documents/uagJs.d.ts +17 -0
  55. package/lib/documents/uagJs.js +157 -0
  56. package/lib/formflow.d.ts +47 -0
  57. package/lib/formflow.js +28 -0
  58. package/lib/identity.d.ts +53 -0
  59. package/lib/identity.js +107 -0
  60. package/lib/index.d.ts +98 -1
  61. package/lib/index.js +382 -8
  62. package/lib/markdown.d.ts +1 -0
  63. package/lib/markdown.js +166 -0
  64. package/lib/modules/api.d.ts +25 -0
  65. package/lib/modules/api.js +98 -0
  66. package/lib/modules/index.d.ts +4 -0
  67. package/lib/modules/index.js +24 -0
  68. package/lib/modules/routes.d.ts +14 -0
  69. package/lib/modules/routes.js +21 -0
  70. package/lib/modules/types.d.ts +68 -0
  71. package/lib/modules/types.js +5 -0
  72. package/lib/pluginFiles.d.ts +28 -0
  73. package/lib/pluginFiles.js +101 -0
  74. package/lib/plugins.d.ts +17 -0
  75. package/lib/plugins.js +93 -0
  76. package/lib/richText.d.ts +20 -0
  77. package/lib/richText.js +99 -0
  78. package/lib/router.d.ts +9 -0
  79. package/lib/router.js +34 -25
  80. package/lib/runner.d.ts +149 -0
  81. package/lib/runner.js +353 -0
  82. package/lib/runs.d.ts +27 -0
  83. package/lib/runs.js +13 -0
  84. package/lib/template.js +7 -9
  85. package/lib/templates/uagComponentNotFound.md +5 -5
  86. package/lib/testing.d.ts +74 -0
  87. package/lib/testing.js +180 -0
  88. package/lib/tools/SchemaBuilder.d.ts +3 -3
  89. package/lib/tools/SchemaBuilder.js +4 -4
  90. package/lib/tools/agentDecide.d.ts +8 -0
  91. package/lib/tools/agentDecide.js +57 -0
  92. package/lib/tools/agentProvide.d.ts +8 -0
  93. package/lib/tools/agentProvide.js +97 -0
  94. package/lib/tools/collectData.js +3 -2
  95. package/lib/tools/confirmSubmission.js +1 -1
  96. package/lib/tools/fetchExternalData.js +8 -17
  97. package/lib/tools/findSubmission.js +62 -56
  98. package/lib/tools/getFieldInfo.js +1 -1
  99. package/lib/tools/getFormFields.js +1 -1
  100. package/lib/tools/getForms.js +1 -1
  101. package/lib/tools/index.d.ts +4 -1
  102. package/lib/tools/index.js +21 -14
  103. package/lib/tools/readDocument.d.ts +17 -0
  104. package/lib/tools/readDocument.js +99 -0
  105. package/lib/tools/submissionUpdate.js +4 -3
  106. package/lib/tools/submitForm.js +1 -1
  107. package/lib/tools/utils.d.ts +40 -0
  108. package/lib/tools/utils.js +16 -1
  109. package/lib/version.d.ts +2 -0
  110. package/lib/version.js +5 -0
  111. package/package.json +49 -19
  112. package/testing.d.ts +1 -0
  113. package/testing.js +2 -0
  114. package/lib/tools/agentProvideData.d.ts +0 -3
  115. package/lib/tools/agentProvideData.js +0 -62
@@ -1,21 +1,36 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.UAGProjectInterface = void 0;
4
7
  const appserver_1 = require("@formio/appserver");
8
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ const formflow_1 = require("./formflow");
10
+ const runner_1 = require("./runner");
5
11
  const express_1 = require("express");
6
- const tools_1 = require("./tools");
7
12
  const router_1 = require("./router");
8
13
  const template_1 = require("./template");
9
14
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
15
+ const version_1 = require("./version");
16
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
17
+ const router_2 = require("./agent/router");
18
+ const readers_1 = require("./documents/readers");
19
+ const tools_1 = require("./agent/tools");
10
20
  const debug = require('debug')('formio:uag:UAGProjectInterface');
11
21
  class UAGProjectInterface extends appserver_1.ProjectInterface {
22
+ /** The names of the UAG forms the project currently has. */
23
+ get formNames() {
24
+ return Object.values(this.forms).map((form) => form.uag?.name).filter(Boolean);
25
+ }
12
26
  get config() { return appserver_1.ProjectInterface.module?.config || {}; }
13
27
  constructor(endpoint) {
14
28
  super(endpoint);
15
29
  this.user = null;
16
- this.formNames = [];
17
30
  this.uagTemplate = null;
18
- this.mcpServer = new mcp_js_1.McpServer({ name: 'formio-uag', version: '1.0.0' });
31
+ /** Engines enabled at startup; empty until UAGServer.router() resolves them. */
32
+ this.engines = new formflow_1.EngineRegistry();
33
+ this.mcpServer = new mcp_js_1.McpServer({ name: 'formio-uag', version: version_1.UAG_VERSION });
19
34
  }
20
35
  async initialize() {
21
36
  await super.initialize();
@@ -24,27 +39,40 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
24
39
  }
25
40
  // Build a fresh McpServer with all tools registered. A new instance is created
26
41
  // per request so concurrent requests never share (and overwrite) a transport.
27
- async buildMcpServer() {
28
- const server = new mcp_js_1.McpServer({ name: 'formio-uag', version: '1.0.0' });
29
- // Get the standard UAG tools.
30
- const tools = await (0, tools_1.getTools)(this);
31
- // Add the custom tools from config.
32
- if (this.config?.tools?.length) {
33
- tools.push(...this.config.tools);
42
+ // A scoped token narrows the tools to its own list, and only ever to tools
43
+ // annotated read-only: no model may write through /mcp with one.
44
+ async buildMcpServer(scope) {
45
+ const server = new mcp_js_1.McpServer({ name: 'formio-uag', version: version_1.UAG_VERSION });
46
+ // The standard UAG tools, plus the custom tools from config.
47
+ let tools = await (0, tools_1.allTools)(this);
48
+ if (scope) {
49
+ tools = (0, formflow_1.toolsForScope)(tools, scope).filter((tool) => tool.annotations?.readOnlyHint === true);
34
50
  }
35
- // Iterate and register all the tools.
51
+ // Register all the tools (allTools() keeps only the named ones).
36
52
  for (const tool of tools) {
37
- if (tool?.name) {
38
- server.registerTool(tool.name, {
39
- title: tool.title,
40
- description: tool.description,
41
- inputSchema: tool.inputSchema,
42
- // Carries readOnly/destructive/idempotent/openWorld through to the
43
- // client. Tools from config.tools may omit them, so this stays
44
- // conditional rather than sending `annotations: undefined`.
45
- ...(tool.annotations && { annotations: tool.annotations })
46
- }, tool.execute);
47
- }
53
+ server.registerTool(tool.name, {
54
+ title: tool.title,
55
+ description: tool.description,
56
+ // Always a schema, even an empty one: the MCP SDK calls a tool
57
+ // registered without one as `execute(extra)`, and every tool is
58
+ // `execute(args, extra)`, as Agent API research calls it.
59
+ inputSchema: tool.inputSchema || {},
60
+ // Carries readOnly/destructive/idempotent/openWorld through to the
61
+ // client. Tools from config.tools may omit them, so this stays
62
+ // conditional rather than sending `annotations: undefined`.
63
+ ...(tool.annotations && { annotations: tool.annotations })
64
+ }, (args, extra) => (0, tools_1.runTool)(tool, this, args, extra));
65
+ }
66
+ // A scoped token that allows no tools must still answer tools/list, with
67
+ // an empty list: clients list tools first,
68
+ // and a server with nothing registered would not offer tools at all.
69
+ if (!tools.length) {
70
+ server.server.registerCapabilities({ tools: {} });
71
+ server.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: [] }));
72
+ server.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => ({
73
+ content: [{ type: 'text', text: `The tool "${request.params.name}" is not available to this token.` }],
74
+ isError: true
75
+ }));
48
76
  }
49
77
  return server;
50
78
  }
@@ -52,32 +80,92 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
52
80
  const formInterface = super.addForm(form, machineName);
53
81
  if (!formInterface)
54
82
  return;
55
- machineName = formInterface?.form?.machineName;
56
- if (form.tags?.includes('uag')) {
57
- debug(`Registering UAG form: ${machineName}`);
58
- formInterface.uag = {
59
- machineName: machineName,
60
- name: form.name || form.path || machineName,
61
- title: form.title || form.name || machineName,
62
- description: form.properties?.description || `A form to submit new ${form.title} records.`
63
- };
64
- this.formNames.push(formInterface.uag.name);
83
+ // The registration itself is derived from the form by UAGFormInterface.setForm().
84
+ if (formInterface.uag) {
85
+ debug(`Registering UAG form: ${formInterface.uag.machineName}`);
65
86
  }
66
87
  return formInterface;
67
88
  }
68
- uagRouter() {
89
+ /**
90
+ * The UAG's routes: `/mcp`, and unless excluded, `/agent/capabilities`.
91
+ * UAGServer mounts capabilities separately, ahead of the project router. A
92
+ * form's agent operations are on the form's own routes (UAGFormInterface).
93
+ */
94
+ uagRouter(includeAgentApi = true) {
69
95
  const router = (0, express_1.Router)();
70
96
  const uagRouter = (0, router_1.UAGRouter)(this);
71
97
  router.use('/mcp', (req, res, next) => this.authorizeRequest(req, res, next), uagRouter);
98
+ if (includeAgentApi) {
99
+ router.use(this.agentRouter());
100
+ }
72
101
  return router;
73
102
  }
103
+ /** `/agent/capabilities`, behind its own authentication. */
104
+ agentRouter() {
105
+ return (0, router_2.AgentRouter)(this, (0, formflow_1.formflowAuthenticate)(this));
106
+ }
107
+ /** Whether runs after the save are recorded and survive a restart (the UAG's database). */
108
+ durableRuns() {
109
+ return (0, runner_1.taskRunner)(this).durable;
110
+ }
111
+ /** The engines enabled for Agentic Formflow. */
112
+ formflowEngines() {
113
+ return this.engines;
114
+ }
115
+ /**
116
+ * Agentic Formflow's routing, Agent API and auth settings. The engine
117
+ * settings stay here: the UAG loads the engines, not core.
118
+ */
119
+ formflowConfig() {
120
+ const { engines, engineTypes, ...config } = this.config || {};
121
+ return config;
122
+ }
123
+ /** A form tagged "uag", or null. */
124
+ async formflowForm(name) {
125
+ const form = await this.getForm(name);
126
+ return form?.uag ? form : null;
127
+ }
128
+ /**
129
+ * Readers for uploaded documents: one built from the installed converters,
130
+ * which reads every type with a route to text.
131
+ */
132
+ formflowFileReaders() {
133
+ return this.plugins ? [(0, readers_1.converterFileReader)(this.plugins)] : [];
134
+ }
135
+ /** Every MCP tool, as Agentic Formflow tools. */
136
+ async formflowTools() {
137
+ return (await (0, tools_1.allTools)(this)).map((tool) => (0, tools_1.formflowTool)(tool, this));
138
+ }
74
139
  async authorizeRequest(req, res, next) {
75
140
  try {
141
+ let authorization = req.get('authorization');
142
+ // A scoped token carries the identity it was issued for (the caller's
143
+ // own token, or admin) and limits the tools /mcp offers.
144
+ const bearer = (0, formflow_1.bearerOf)(authorization);
145
+ const scoped = bearer ? (0, formflow_1.readScopedToken)(this, bearer) : null;
146
+ if (scoped) {
147
+ req.uagScope = scoped;
148
+ if (scoped.admin) {
149
+ req.auth = new appserver_1.AuthRequest(this, true);
150
+ return next();
151
+ }
152
+ if (!scoped.bearer) {
153
+ throw new Error('Scoped token has no identity');
154
+ }
155
+ authorization = `Bearer ${scoped.bearer}`;
156
+ }
76
157
  const pkce = this.provider('auth.pkce');
77
158
  if (!pkce) {
78
159
  throw new Error('No PKCE provider configured');
79
160
  }
80
- const auth = await pkce?.authorize(req.get('authorization'));
161
+ // A scoped token may carry a Form.io user token (the caller's x-jwt-token,
162
+ // or one the UAG signed for a run's research), not one of /mcp's own
163
+ // access tokens: it is verified as the project verifies x-jwt-token.
164
+ if (scoped?.bearer && !jsonwebtoken_1.default.decode(scoped.bearer)?.sub) {
165
+ req.auth = await this.formioTokenAuth(pkce, scoped.bearer);
166
+ return next();
167
+ }
168
+ const auth = await pkce.authorize(authorization);
81
169
  if (!auth || !auth.user) {
82
170
  throw new Error('Unauthorized');
83
171
  }
@@ -85,13 +173,76 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
85
173
  next();
86
174
  }
87
175
  catch (err) {
176
+ // Point MCP clients at OAuth sign-in only when it is on (MCP_OAUTH, with the database).
177
+ const pkce = this.provider('auth.pkce');
178
+ const metadata = pkce?.oauthReady ? `, resource_metadata="${pkce.publicUrl(req)}/.well-known/oauth-protected-resource"` : '';
179
+ // RFC 6750 3.1: a request that presented a token is told it is invalid; one
180
+ // that presented none gets no error code, only the challenge.
181
+ const presented = !!(0, formflow_1.bearerOf)(req.get('authorization'));
182
+ const description = String(err?.message || err || 'Unauthorized').replace(/["\\]/g, '');
183
+ const challenge = presented ? `, error="invalid_token", error_description="${description}"` : '';
88
184
  return res.status(401).set({
89
- 'WWW-Authenticate': `Bearer realm="uag-server", resource_metadata="${this.config?.baseUrl}/.well-known/oauth-protected-resource"`
185
+ 'WWW-Authenticate': `Bearer realm="uag-server"${challenge}${metadata}`
90
186
  }).json({
91
- detail: err.message || err
187
+ error: presented ? 'invalid_token' : 'unauthorized',
188
+ error_description: presented ? description : 'Send an access token: Authorization: Bearer <token>.'
92
189
  });
93
190
  }
94
191
  }
192
+ /**
193
+ * The user a Form.io token stands for, as the project's x-jwt-token
194
+ * authentication decides it: a token tied to a Form.io session is asked of
195
+ * Form.io; one signed with this UAG's secret is read from its claims, bound
196
+ * to this project; any other is asked of Form.io. Never an administrator:
197
+ * an administrator's research carries no user token.
198
+ */
199
+ async formioTokenAuth(provider, token) {
200
+ const auth = new appserver_1.AuthRequest(this);
201
+ const projectId = String(this.project?._id || '');
202
+ const fromFormio = async () => {
203
+ const user = await provider.formioUser(token);
204
+ if (!user)
205
+ throw new Error('Unauthorized');
206
+ // A user of another project is refused, as /mcp's own sign-in refuses one.
207
+ if (user.project && String(user.project) !== projectId)
208
+ throw new Error('Forbidden');
209
+ return auth.fromFormioUser(user, token, this);
210
+ };
211
+ if (this.proxyMode && appserver_1.AuthProvider.sessionToken(token)) {
212
+ await fromFormio();
213
+ }
214
+ else {
215
+ const secret = this.config?.auth?.secret;
216
+ let verified = null;
217
+ try {
218
+ if (secret)
219
+ verified = jsonwebtoken_1.default.verify(token, secret);
220
+ }
221
+ catch (err) {
222
+ if (err?.name === 'TokenExpiredError')
223
+ throw new Error('Token Expired');
224
+ }
225
+ if (verified) {
226
+ // Only a token for this project: one naming no project (or another) is refused.
227
+ if (!projectId || String(verified.project?._id || '') !== projectId)
228
+ throw new Error('Forbidden');
229
+ // Kept as it is, never signed again: a token the UAG signed for a run
230
+ // keeps its roles and its short lifetime when forwarded to Form.io.
231
+ auth.token = token;
232
+ await auth.fromAuthToken(verified, this);
233
+ }
234
+ else if (this.proxyMode) {
235
+ await fromFormio();
236
+ }
237
+ else {
238
+ throw new Error('Unauthorized');
239
+ }
240
+ }
241
+ if (!auth.user)
242
+ throw new Error('Unauthorized');
243
+ auth.admin = false;
244
+ return auth;
245
+ }
95
246
  mcpJSONResponse(data = {}, isError = false) {
96
247
  return {
97
248
  content: [
@@ -0,0 +1,24 @@
1
+ import { Router } from 'express';
2
+ import type { TaskRunRecord } from './runs';
3
+ import type { UAGProjectInterface } from './UAGProjectInterface';
4
+ /** A run record as a Form.io action item. */
5
+ export declare function actionItem(record: TaskRunRecord): {
6
+ _id: string;
7
+ title: string;
8
+ action: string;
9
+ project: string;
10
+ form: string;
11
+ submission?: string | undefined;
12
+ handler: "after" | "before";
13
+ method: string;
14
+ state: string;
15
+ messages: {
16
+ datetime: string;
17
+ info: string;
18
+ data: any;
19
+ }[];
20
+ data: any;
21
+ created: string;
22
+ modified: string;
23
+ };
24
+ export declare function ActionLogRouter(project: UAGProjectInterface): Router;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.actionItem = actionItem;
4
+ exports.ActionLogRouter = ActionLogRouter;
5
+ const appserver_1 = require("@formio/appserver");
6
+ const express_1 = require("express");
7
+ const lodash_1 = require("lodash");
8
+ const runner_1 = require("./runner");
9
+ const debug = require('debug')('formio:uag:actionLogs');
10
+ /**
11
+ * Action Logs in a portal stage connected to the UAG. The portal asks the
12
+ * stage's URL for `/action` (Form.io Enterprise's action items). The UAG asks
13
+ * Form.io with the caller's own credential, so Form.io decides who may see the
14
+ * logs, and adds its Run task records to Form.io's entries, newest first.
15
+ *
16
+ * Without the UAG's database there are no records, and these requests pass
17
+ * through to Form.io unchanged.
18
+ */
19
+ const STATES = { pending: 'new', running: 'inprogress', done: 'complete', failed: 'error' };
20
+ const STATUSES = { new: 'pending', inprogress: 'running', complete: 'done', error: 'failed' };
21
+ const MAX_PAGE = 1000;
22
+ /** The caller's credential, as sent, for Form.io. */
23
+ function credential(req) {
24
+ return Object.fromEntries(Object.entries(req.headers)
25
+ .filter(([name, value]) => appserver_1.Actions.CREDENTIAL_HEADERS.includes(name.toLowerCase()) && typeof value === 'string'));
26
+ }
27
+ /** A run record as a Form.io action item. */
28
+ function actionItem(record) {
29
+ const messages = [
30
+ { datetime: record.created.toISOString(), info: `Run task "${record.task}" ${record.durable ? 'queued' : 'started'} (${record.answer === 'decisions' ? 'decision fields only' : 'the whole task'}).`, data: {} }
31
+ ];
32
+ if (record.started) {
33
+ messages.push({ datetime: record.started.toISOString(), info: `Started (attempt ${record.attempts}).`, data: {} });
34
+ }
35
+ if (record.finished) {
36
+ const written = record.written || [];
37
+ const cleared = record.cleared || [];
38
+ const refused = record.refused || [];
39
+ const left = refused.length ? ` Left out answers the form refused: ${refused.join(', ')}.` : '';
40
+ const info = (record.status === 'failed'
41
+ ? `Failed: ${record.error || 'unknown error'}`
42
+ : record.saved
43
+ ? `Saved ${written.length} field(s)${written.length ? `: ${written.join(', ')}` : ''}${cleared.length ? `; cleared ${cleared.join(', ')}` : ''}.`
44
+ : 'Nothing changed, so nothing was saved.') + left;
45
+ messages.push({ datetime: record.finished.toISOString(), info, data: { engines: record.engines || [], written, cleared, ...(refused.length && { refused }) } });
46
+ }
47
+ return {
48
+ _id: String(record._id),
49
+ title: record.action,
50
+ action: 'agentRunTask',
51
+ project: record.project,
52
+ form: record.form,
53
+ ...(record.submission && { submission: record.submission }),
54
+ handler: record.handler,
55
+ method: record.method,
56
+ state: STATES[record.status],
57
+ messages,
58
+ data: {
59
+ task: record.task,
60
+ answer: record.answer,
61
+ durable: record.durable,
62
+ ...(record.actingFor?._id && { actingFor: { _id: record.actingFor._id, ...(record.actingFor.email && { email: record.actingFor.email }) } }),
63
+ ...(record.agentRole && { agentRole: record.agentRole })
64
+ },
65
+ created: record.created.toISOString(),
66
+ modified: record.modified.toISOString()
67
+ };
68
+ }
69
+ /** The records filter for the portal's query (`form`, `submission`, `handler`, `method`, `state`, `title__regex`). */
70
+ function filterOf(project, query) {
71
+ const filter = { project: String(project.project?._id || '') };
72
+ for (const field of ['form', 'submission', 'handler', 'method']) {
73
+ if (typeof query[field] === 'string' && query[field])
74
+ filter[field] = query[field];
75
+ }
76
+ if (typeof query.state === 'string' && query.state)
77
+ filter.status = STATUSES[query.state] || '__none__';
78
+ // Matched as text: a portal filter is never run as a regular expression against the database.
79
+ if (typeof query.title__regex === 'string' && query.title__regex)
80
+ filter.action = { $regex: (0, lodash_1.escapeRegExp)(query.title__regex), $options: 'i' };
81
+ return filter;
82
+ }
83
+ const number = (value, fallback) => {
84
+ const parsed = parseInt(String(value ?? ''), 10);
85
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
86
+ };
87
+ function ActionLogRouter(project) {
88
+ const router = (0, express_1.Router)();
89
+ /** Ask Form.io, as the caller, for its action items. */
90
+ const ask = (req, path, query) => fetch(`${project.endpoint}${path}${query.toString() ? `?${query}` : ''}`, { headers: { ...credential(req), Accept: 'application/json' } });
91
+ /** Relay Form.io's answer as it is. */
92
+ const relay = async (res, response) => {
93
+ const range = response.headers.get('content-range');
94
+ if (range)
95
+ res.set('Content-Range', range);
96
+ res.status(response.status).type(response.headers.get('content-type') || 'application/json').send(await response.text());
97
+ };
98
+ router.get('/action', async (req, res, next) => {
99
+ const records = (0, runner_1.taskRunner)(project).records;
100
+ if (!records || !project.endpoint)
101
+ return next();
102
+ try {
103
+ const skip = number(req.query.skip, 0);
104
+ // A limit of 0 is the default page, not every record.
105
+ const limit = Math.min(number(req.query.limit, 10) || 10, MAX_PAGE);
106
+ // Both lists are sorted the same way, so the page is among the first skip + limit of each.
107
+ const query = new URLSearchParams(Object.entries(req.query).filter(([, value]) => typeof value === 'string'));
108
+ query.set('skip', '0');
109
+ query.set('limit', String(Math.min(skip + limit, MAX_PAGE)));
110
+ query.set('sort', '-modified');
111
+ const response = await ask(req, '/action', query);
112
+ if (!response.ok)
113
+ return relay(res, response);
114
+ const formio = await response.json();
115
+ const formioTotal = Number((response.headers.get('content-range') || '').split('/')[1]) || formio.length;
116
+ const own = await records.list(filterOf(project, req.query), Math.min(skip + limit, MAX_PAGE));
117
+ const merged = [...formio, ...own.items.map(actionItem)]
118
+ .sort((a, b) => Date.parse(b.modified || b.created) - Date.parse(a.modified || a.created));
119
+ const page = merged.slice(skip, skip + limit);
120
+ const total = formioTotal + own.total;
121
+ res.set('Content-Range', page.length ? `${skip}-${skip + page.length - 1}/${total}` : `*/${total}`);
122
+ res.json(page);
123
+ }
124
+ catch (err) {
125
+ debug(`Action Logs failed: ${err?.message || err}`);
126
+ next(err);
127
+ }
128
+ });
129
+ router.get('/action/:id', async (req, res, next) => {
130
+ const records = (0, runner_1.taskRunner)(project).records;
131
+ if (!records || !project.endpoint)
132
+ return next();
133
+ try {
134
+ const record = await records.get(String(req.params.id));
135
+ if (!record || record.project !== String(project.project?._id || '')) {
136
+ return relay(res, await ask(req, `/action/${encodeURIComponent(String(req.params.id))}`, new URLSearchParams()));
137
+ }
138
+ // Form.io decides who may read Action Logs.
139
+ const allowed = await ask(req, '/action', new URLSearchParams({ limit: '1' }));
140
+ if (!allowed.ok)
141
+ return relay(res, allowed);
142
+ res.json(actionItem(record));
143
+ }
144
+ catch (err) {
145
+ next(err);
146
+ }
147
+ });
148
+ return router;
149
+ }
@@ -0,0 +1,43 @@
1
+ import { Action, FormInterface, SubmissionRequest, SubmissionResponse } from '@formio/appserver';
2
+ import { NextFunction } from 'express';
3
+ export declare const RUN_TASK = "agentRunTask";
4
+ /**
5
+ * Remove, from what a client sends, the values of the fields each of the form's
6
+ * Run task actions owns and marks `uagAuthoritative`: on an update they keep
7
+ * their stored values; on a create they start empty. This covers every Run
8
+ * task action on the form, whatever its method, conditions or when it runs, so
9
+ * a client can never store a value the server decides: not before the task
10
+ * runs, and not through a method the task does not run on. The runner's own
11
+ * save is not a client's, and is left alone.
12
+ *
13
+ * As `before:submission:<op>` hooks, these run after an update has loaded the
14
+ * stored submission and before the request's data replaces it.
15
+ */
16
+ export declare function ownedFieldHooks(): {
17
+ 'before:submission:create': (form: FormInterface) => Promise<FormInterface | ((req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void>)>;
18
+ 'before:submission:update': (form: FormInterface) => Promise<FormInterface | ((req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void>)>;
19
+ };
20
+ /**
21
+ * Decide how a submission is created through the UAG, from the form's
22
+ * submission access and its Run task actions that run on create:
23
+ * - a caller whose roles may create submissions creates it with their own token;
24
+ * - otherwise, when an action names an agent role the form grants "Create All",
25
+ * and the caller has one of the actions' submitter roles, the agent creates it
26
+ * on their behalf, owned by them;
27
+ * - otherwise the create is refused.
28
+ * Run as a `before:access:create` hook, before the caller's access is checked.
29
+ */
30
+ export declare function onBehalfHook(): (form: FormInterface) => Promise<FormInterface | ((req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void | SubmissionResponse>)>;
31
+ /** The hooks Run task needs: owned fields kept from clients, and creation on a user's behalf. */
32
+ export declare function runTaskHooks(): {
33
+ 'before:submission:create': (form: FormInterface) => Promise<FormInterface | ((req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void>)>;
34
+ 'before:submission:update': (form: FormInterface) => Promise<FormInterface | ((req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void>)>;
35
+ 'before:access:create': (form: FormInterface) => Promise<FormInterface | ((req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void | SubmissionResponse>)>;
36
+ };
37
+ /**
38
+ * "Agent: Run task": run one of the form's tasks on a submission sent to the
39
+ * UAG, before the save (the submitter waits, and Form.io stores the result as
40
+ * the one save) or after it, in the background (the results are saved as an
41
+ * update, which runs the form's update actions in turn).
42
+ */
43
+ export declare const RunTaskAction: Action;