@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
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RunTaskAction = exports.RUN_TASK = void 0;
4
+ exports.ownedFieldHooks = ownedFieldHooks;
5
+ exports.onBehalfHook = onBehalfHook;
6
+ exports.runTaskHooks = runTaskHooks;
7
+ const appserver_1 = require("@formio/appserver");
8
+ const formflow_1 = require("../formflow");
9
+ const runner_1 = require("../runner");
10
+ const protocol_1 = require("@formio/uag-sdk/protocol");
11
+ const requestId_1 = require("../agent/requestId");
12
+ const errors_1 = require("../agent/errors");
13
+ const identity_1 = require("../identity");
14
+ const debug = require('debug')('formio:uag:actions:runTask');
15
+ exports.RUN_TASK = 'agentRunTask';
16
+ /** The tasks a form defines. */
17
+ const tasksOf = (form) => Object.keys(form.uagFields || {});
18
+ /** The form's Run task actions, for a method (create or update) when one is given, whatever their conditions. */
19
+ function runTaskActions(form, method) {
20
+ return Object.values(form.project.template?.actions || {}).filter((action) => action?.name === exports.RUN_TASK
21
+ && (action.form?.split(':').pop() || '') === form.form.machineName
22
+ && (!method || (action.method || []).includes(method))
23
+ && action.settings?.task);
24
+ }
25
+ /**
26
+ * Remove, from what a client sends, the values of the fields each of the form's
27
+ * Run task actions owns and marks `uagAuthoritative`: on an update they keep
28
+ * their stored values; on a create they start empty. This covers every Run
29
+ * task action on the form, whatever its method, conditions or when it runs, so
30
+ * a client can never store a value the server decides: not before the task
31
+ * runs, and not through a method the task does not run on. The runner's own
32
+ * save is not a client's, and is left alone.
33
+ *
34
+ * As `before:submission:<op>` hooks, these run after an update has loaded the
35
+ * stored submission and before the request's data replaces it.
36
+ */
37
+ function ownedFieldHooks() {
38
+ const hook = (op) => async (form) => {
39
+ if (!runTaskActions(form).length)
40
+ return form;
41
+ return async (req, res, next) => {
42
+ if (req[runner_1.RUN_TASK_SAVE])
43
+ return next();
44
+ const actions = runTaskActions(form);
45
+ const tasks = new Set(tasksOf(form));
46
+ if (op === 'update') {
47
+ let body = req.body || {};
48
+ for (const { settings } of actions) {
49
+ if (tasks.has(settings.task))
50
+ body = (0, formflow_1.withoutOwnedValues)(form, settings.task, body, req.submission || { data: {} });
51
+ }
52
+ req.body = body;
53
+ }
54
+ else {
55
+ let submission = req.submission || { data: {} };
56
+ for (const { settings } of actions) {
57
+ if (tasks.has(settings.task))
58
+ submission = (0, formflow_1.withoutOwnedValues)(form, settings.task, submission);
59
+ }
60
+ req.submission = submission;
61
+ }
62
+ next();
63
+ };
64
+ };
65
+ return { 'before:submission:create': hook('create'), 'before:submission:update': hook('update') };
66
+ }
67
+ /**
68
+ * Decide how a submission is created through the UAG, from the form's
69
+ * submission access and its Run task actions that run on create:
70
+ * - a caller whose roles may create submissions creates it with their own token;
71
+ * - otherwise, when an action names an agent role the form grants "Create All",
72
+ * and the caller has one of the actions' submitter roles, the agent creates it
73
+ * on their behalf, owned by them;
74
+ * - otherwise the create is refused.
75
+ * Run as a `before:access:create` hook, before the caller's access is checked.
76
+ */
77
+ function onBehalfHook() {
78
+ return async (form) => {
79
+ if (!runTaskActions(form, 'create').some((action) => action.settings.agentRole))
80
+ return form;
81
+ return async (req, res, next) => {
82
+ const project = form.project;
83
+ const access = form.accessInfo[appserver_1.AuthorizedMethod.CREATE];
84
+ const actingFor = (0, identity_1.actingForOf)(req.auth, project);
85
+ const has = (roles) => appserver_1.AuthRequest.hasRole(actingFor.roles, roles, project);
86
+ if (req.auth?.admin || has(access[appserver_1.AuthorizedStateType.OWN]) || has(access[appserver_1.AuthorizedStateType.ALL]))
87
+ return next();
88
+ const actions = runTaskActions(form, 'create').filter((action) => action.settings.agentRole);
89
+ const roles = [...new Set(actions.map((action) => project.roleId(String(action.settings.agentRole))))];
90
+ if (roles.length > 1) {
91
+ console.error(`[formio-uag] The Run task actions of "${form.name}" that run on create name different agent roles ` +
92
+ `(${actions.map((action) => action.title || action.machineName).join(', ')}). Choose one agent role for them all.`);
93
+ return res.status(500).json({ name: 'ConfigurationError', message: `The form "${form.name}" is not configured correctly: its Run task actions name different agent roles.`, code: 'configuration_error' });
94
+ }
95
+ const [role] = roles;
96
+ const submitters = actions.flatMap((action) => action.settings.submitterRoles || []).map(String);
97
+ if (!appserver_1.AuthRequest.hasRole([role], access[appserver_1.AuthorizedStateType.ALL], project) || !has(submitters)) {
98
+ return res.status(403).json({ name: 'ForbiddenError', message: 'You may not submit this form.', code: 'forbidden' });
99
+ }
100
+ try {
101
+ req.forwardAuth = await (0, identity_1.agentAuth)(project, actingFor, role, { task: actions[0].settings.task });
102
+ }
103
+ catch (err) {
104
+ if (err instanceof formflow_1.FormflowError)
105
+ return refuse(res, err);
106
+ throw err;
107
+ }
108
+ next();
109
+ };
110
+ };
111
+ }
112
+ /** The hooks Run task needs: owned fields kept from clients, and creation on a user's behalf. */
113
+ function runTaskHooks() {
114
+ return { ...ownedFieldHooks(), 'before:access:create': onBehalfHook() };
115
+ }
116
+ /** The request's X-Request-Id (or the one the runner's save carries on), passed to the engines and the runs it starts. */
117
+ function requestIdOf(req) {
118
+ return req[runner_1.RUN_TASK_REQUEST_ID] || (0, protocol_1.safeRequestId)(req.get?.('x-request-id')) || undefined;
119
+ }
120
+ /** The runs made before the save on this request, by action, until the save gives them a submission. */
121
+ function runsBefore(req) {
122
+ const request = req;
123
+ request.uagRunsBefore = request.uagRunsBefore || new Map();
124
+ return request.uagRunsBefore;
125
+ }
126
+ /** Answer a Formflow error as JSON with its status. */
127
+ function refuse(res, err) {
128
+ res.status(err.status).json({
129
+ name: err.name,
130
+ message: err.message,
131
+ code: err.code || (0, errors_1.errorCode)(err.status),
132
+ ...(err.edition && { edition: err.edition })
133
+ });
134
+ }
135
+ /**
136
+ * "Agent: Run task": run one of the form's tasks on a submission sent to the
137
+ * UAG, before the save (the submitter waits, and Form.io stores the result as
138
+ * the one save) or after it, in the background (the results are saved as an
139
+ * update, which runs the form's update actions in turn).
140
+ */
141
+ exports.RunTaskAction = {
142
+ get info() {
143
+ return {
144
+ name: exports.RUN_TASK,
145
+ title: 'Agent: Run task',
146
+ group: 'default',
147
+ description: 'Runs one of the form\'s agent tasks on the submission, before the save or after it in the background.',
148
+ priority: 20,
149
+ defaults: {
150
+ handler: ['before', 'after'],
151
+ method: ['create']
152
+ },
153
+ access: {
154
+ handler: false,
155
+ method: true
156
+ }
157
+ };
158
+ },
159
+ async settingsForm(form) {
160
+ const tasks = tasksOf(form);
161
+ // An agent never acts with an admin role.
162
+ const roles = Object.values(form.project.access?.roles || {})
163
+ .filter((role) => role?._id && !role.admin)
164
+ .map((role) => ({ label: role.title || String(role._id), value: String(role._id) }));
165
+ return [
166
+ ...(tasks.length ? [] : [{
167
+ type: 'htmlelement',
168
+ key: 'noTasks',
169
+ tag: 'p',
170
+ content: 'This form defines no task. Mark its components with the "uag" property to define one.'
171
+ }]),
172
+ {
173
+ type: 'select',
174
+ input: true,
175
+ key: 'task',
176
+ label: 'Task',
177
+ tooltip: 'The task to run, from the tasks the form defines.',
178
+ dataSrc: 'values',
179
+ data: { values: tasks.map((task) => ({ label: task, value: task })) },
180
+ validate: { required: true }
181
+ },
182
+ {
183
+ type: 'radio',
184
+ input: true,
185
+ key: 'answer',
186
+ label: 'Answer',
187
+ defaultValue: 'task',
188
+ values: [
189
+ { label: 'The whole task', value: 'task' },
190
+ { label: 'Its decision fields only', value: 'decisions' }
191
+ ],
192
+ validate: { required: true }
193
+ },
194
+ {
195
+ type: 'select',
196
+ input: true,
197
+ key: 'agentRole',
198
+ label: 'Agent role',
199
+ tooltip: 'Act at Form.io with this role, on behalf of the user, to save the task\'s results and to create submissions users may not create themselves. Needs JWT_SECRET to be the same as Form.io Enterprise\'s. Leave empty to act with the user\'s own token.',
200
+ dataSrc: 'values',
201
+ data: { values: roles }
202
+ },
203
+ {
204
+ type: 'select',
205
+ input: true,
206
+ key: 'submitterRoles',
207
+ label: 'Submitter roles',
208
+ tooltip: 'Who may submit this form through the agent when the form does not let their roles create submissions: the agent creates it on their behalf.',
209
+ multiple: true,
210
+ dataSrc: 'values',
211
+ data: { values: roles },
212
+ customConditional: 'show = !!data.agentRole;'
213
+ },
214
+ {
215
+ type: 'radio',
216
+ input: true,
217
+ key: 'when',
218
+ label: 'When',
219
+ defaultValue: 'after',
220
+ values: [
221
+ { label: 'After the save, in the background', value: 'after' },
222
+ { label: 'Before the save (the submitter waits)', value: 'before' }
223
+ ],
224
+ validate: { required: true }
225
+ }
226
+ ];
227
+ },
228
+ async executor(form, action, handler, method) {
229
+ const settings = action?.settings || {};
230
+ const when = settings.when === 'before' ? 'before' : 'after';
231
+ const answer = settings.answer === 'decisions' ? 'decisions' : 'task';
232
+ const project = form.project;
233
+ const title = action?.title || exports.RUN_TASK;
234
+ if (!settings.task) {
235
+ return async (req, res, next) => next();
236
+ }
237
+ if (when === 'before' && handler === 'before') {
238
+ return async (req, res, next) => {
239
+ // The runner's own save carries the agent's results.
240
+ if (req[runner_1.RUN_TASK_SAVE])
241
+ return next();
242
+ try {
243
+ const identity = (0, formflow_1.callerIdentity)(req.auth, req);
244
+ // The owned fields were already made the server's own (ownedFieldHooks).
245
+ const requestId = requestIdOf(req);
246
+ const run = await (0, requestId_1.withRequestId)(requestId, () => (0, formflow_1.formflow)(project).runTask(form, settings.task, req.submission || { data: {} }, req.auth, { answer, stored: true, identity, requestId }));
247
+ req.submission = run.submission;
248
+ // Recorded once the save gives the submission its id (below).
249
+ const failed = run.failed.map((group) => group.error || 'no engine could answer').join('; ');
250
+ runsBefore(req).set(action, {
251
+ status: failed ? 'failed' : 'done',
252
+ written: run.written,
253
+ cleared: run.cleared,
254
+ engines: run.results.map((result) => ({ kind: result.kind, engine: result.engine ?? null, model: result.model ?? null })),
255
+ saved: true,
256
+ ...(failed && { error: failed })
257
+ });
258
+ return next();
259
+ }
260
+ catch (err) {
261
+ if (err instanceof formflow_1.FormflowError)
262
+ return refuse(res, err);
263
+ throw err;
264
+ }
265
+ };
266
+ }
267
+ if (when === 'before') {
268
+ // After the save: record the run made before it, now that the submission has its id.
269
+ return async (req, res, next) => {
270
+ const outcome = runsBefore(req).get(action);
271
+ const saved = res.resource?.item;
272
+ if (outcome && saved?._id) {
273
+ await (0, runner_1.taskRunner)(project).recordBefore({
274
+ form: form.name, formId: String(form.form._id || ''), method, submission: String(saved._id),
275
+ task: settings.task, answer, action: title, actingFor: (0, identity_1.actingForOf)(req.auth, project), requestId: requestIdOf(req)
276
+ }, outcome);
277
+ }
278
+ next();
279
+ };
280
+ }
281
+ if (handler !== 'after') {
282
+ return async (req, res, next) => next();
283
+ }
284
+ return async (req, res, next) => {
285
+ const saved = res.resource?.item;
286
+ const chain = req[runner_1.RUN_TASK_CHAIN] || 0;
287
+ if (saved?._id && chain > runner_1.MAX_CHAIN) {
288
+ console.error(`[formio-uag] Run task "${settings.task}" was not run on submission ${saved._id} of "${form.name}": ` +
289
+ `${runner_1.MAX_CHAIN} runs in a row were each started by the save of the one before. Check that the form's Run task actions do not start each other without end.`);
290
+ }
291
+ else if (saved?._id) {
292
+ // A run the runner's own save starts acts for the same user as the run that saved:
293
+ // with the caller's own token, or as the user with tokens the UAG signs, never
294
+ // with the agent role of the run that saved.
295
+ const actingFor = req[runner_1.RUN_TASK_ACTING_FOR] || (0, identity_1.actingForOf)(req.auth, project);
296
+ const chained = req[runner_1.RUN_TASK_CALLER];
297
+ const caller = chained || req.auth;
298
+ const asUser = !chained && !!req[runner_1.RUN_TASK_AS_USER];
299
+ // A durable run is recorded before the response is sent.
300
+ await (0, runner_1.taskRunner)(project).enqueue({
301
+ form: form.name,
302
+ formId: String(form.form._id || ''),
303
+ method,
304
+ submission: String(saved._id),
305
+ task: settings.task,
306
+ answer,
307
+ actingFor,
308
+ action: title,
309
+ requestId: requestIdOf(req),
310
+ ...(chain && { chain }),
311
+ ...(settings.agentRole
312
+ ? { agentRole: project.roleId(String(settings.agentRole)) }
313
+ : asUser
314
+ ? { asUser: true }
315
+ : { caller: caller.clone(), identity: chained ? req[runner_1.RUN_TASK_IDENTITY] : (0, formflow_1.callerIdentity)(caller, req) })
316
+ });
317
+ }
318
+ else {
319
+ debug(`${form.name}: no saved submission to run "${settings.task}" on`);
320
+ }
321
+ next();
322
+ };
323
+ }
324
+ };
@@ -0,0 +1,34 @@
1
+ import { NextFunction, Request, Response } from 'express';
2
+ import type { UAGProjectInterface } from '../UAGProjectInterface';
3
+ /** The anonymous limits in force: a per-address and a total count per minute. */
4
+ export declare function anonymousLimits(project: UAGProjectInterface): {
5
+ perAddress: number;
6
+ total: number;
7
+ };
8
+ /**
9
+ * Counts anonymous requests in fixed one-minute windows, per client address and
10
+ * in total. Addresses are counted only while the total allows, so a window never
11
+ * holds more addresses than the total limit.
12
+ */
13
+ export declare class AnonymousLimiter {
14
+ private windowStart;
15
+ private total;
16
+ private addresses;
17
+ /** Count a request, or say how many seconds remain until one would be allowed. */
18
+ take(address: string, limits: {
19
+ perAddress: number;
20
+ total: number;
21
+ }, now?: number): {
22
+ allowed: true;
23
+ } | {
24
+ allowed: false;
25
+ retryAfter: number;
26
+ };
27
+ }
28
+ /**
29
+ * Admits a caller with no credential to a form's agent operations only when
30
+ * the form opts in (`uagAnonymous: "true"` in its custom properties), both
31
+ * limits allow it, and the form lets the Anonymous role create submissions.
32
+ * Every refusal comes before any engine is called.
33
+ */
34
+ export declare function anonymousGate(project: UAGProjectInterface): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnonymousLimiter = void 0;
4
+ exports.anonymousLimits = anonymousLimits;
5
+ exports.anonymousGate = anonymousGate;
6
+ const formflow_1 = require("../formflow");
7
+ const debug = require('debug')('formio:uag:agent');
8
+ const WINDOW_MS = 60000;
9
+ /** A number from the configuration, else the environment, else the default. */
10
+ function limitOf(configured, env, fallback) {
11
+ for (const value of [configured, env]) {
12
+ if (value === undefined || value === null || value === '')
13
+ continue;
14
+ const number = Number(value);
15
+ if (Number.isFinite(number) && number >= 0)
16
+ return Math.floor(number);
17
+ }
18
+ return fallback;
19
+ }
20
+ /** The anonymous limits in force: a per-address and a total count per minute. */
21
+ function anonymousLimits(project) {
22
+ const configured = project.formflowConfig()?.agentApi?.anonymous || {};
23
+ return {
24
+ perAddress: limitOf(configured.rateLimit, process.env.AGENT_ANONYMOUS_RATE_LIMIT, 10),
25
+ total: limitOf(configured.totalRateLimit, process.env.AGENT_ANONYMOUS_TOTAL_RATE_LIMIT, 100)
26
+ };
27
+ }
28
+ /**
29
+ * Counts anonymous requests in fixed one-minute windows, per client address and
30
+ * in total. Addresses are counted only while the total allows, so a window never
31
+ * holds more addresses than the total limit.
32
+ */
33
+ class AnonymousLimiter {
34
+ constructor() {
35
+ this.windowStart = 0;
36
+ this.total = 0;
37
+ this.addresses = new Map();
38
+ }
39
+ /** Count a request, or say how many seconds remain until one would be allowed. */
40
+ take(address, limits, now = Date.now()) {
41
+ if (now - this.windowStart >= WINDOW_MS) {
42
+ this.windowStart = now;
43
+ this.total = 0;
44
+ this.addresses.clear();
45
+ }
46
+ const retryAfter = Math.max(1, Math.ceil((this.windowStart + WINDOW_MS - now) / 1000));
47
+ if (this.total >= limits.total)
48
+ return { allowed: false, retryAfter };
49
+ const count = this.addresses.get(address) || 0;
50
+ if (count >= limits.perAddress)
51
+ return { allowed: false, retryAfter };
52
+ this.addresses.set(address, count + 1);
53
+ this.total++;
54
+ return { allowed: true };
55
+ }
56
+ }
57
+ exports.AnonymousLimiter = AnonymousLimiter;
58
+ const limiters = new WeakMap();
59
+ function limiterFor(project) {
60
+ let limiter = limiters.get(project);
61
+ if (!limiter) {
62
+ limiter = new AnonymousLimiter();
63
+ limiters.set(project, limiter);
64
+ }
65
+ return limiter;
66
+ }
67
+ /**
68
+ * Admits a caller with no credential to a form's agent operations only when
69
+ * the form opts in (`uagAnonymous: "true"` in its custom properties), both
70
+ * limits allow it, and the form lets the Anonymous role create submissions.
71
+ * Every refusal comes before any engine is called.
72
+ */
73
+ function anonymousGate(project) {
74
+ return (req, res, next) => {
75
+ const request = req;
76
+ const form = request.form;
77
+ const limits = anonymousLimits(project);
78
+ if (limits.perAddress === 0 || limits.total === 0 || !(0, formflow_1.flagOn)(form?.form?.properties?.uagAnonymous)) {
79
+ return res.status(401).json({ error: 'Sign in to use this form\'s agent operations and routes.', code: 'sign_in_required' });
80
+ }
81
+ const taken = limiterFor(project).take(String(req.ip || req.socket?.remoteAddress || ''), limits);
82
+ if (!taken.allowed) {
83
+ debug(`Refused an anonymous ${req.method} ${req.path}: over the anonymous rate limit.`);
84
+ res.set('Retry-After', String(taken.retryAfter));
85
+ return res.status(429).json({ error: 'Too many requests. Try again shortly.', code: 'rate_limited' });
86
+ }
87
+ let permissions = {};
88
+ try {
89
+ permissions = request.auth?.formPermissions?.(form) || {};
90
+ }
91
+ catch {
92
+ permissions = {};
93
+ }
94
+ if (!permissions.create) {
95
+ return res.status(403).json({ error: 'Anonymous visitors may not submit this form.', code: 'forbidden' });
96
+ }
97
+ next();
98
+ };
99
+ }
@@ -0,0 +1,114 @@
1
+ import type { AuthRequest } from "@formio/appserver";
2
+ import { CHAT_INSTRUCTIONS } from "../formflow";
3
+ import type { AgentEngine } from "../formflow";
4
+ import type { UAGProjectInterface } from "../UAGProjectInterface";
5
+ import type { ChatField, ChatFields, ChatUnsupportedField, FormFieldError, UAGFormInterface } from "../UAGFormInterface";
6
+ /**
7
+ * A form's chat: one stateless turn of a conversation that fills the form in.
8
+ *
9
+ * The client sends the conversation so far and the form's current data; one
10
+ * agent engine reads them, through the same `extract` contract as the Agent
11
+ * API, and returns the values the person gave plus the next message to them.
12
+ * The values are checked against the form here, and the form never leaves the
13
+ * client's hands: nothing is stored and nothing is saved. The person submits
14
+ * the form themselves, through the UAG, where its own actions run.
15
+ *
16
+ * Chat belongs with the MCP conversation tools, not Agentic Formflow: it runs
17
+ * no task, decision engine or research, and needs no license.
18
+ */
19
+ export type ChatMessage = {
20
+ role: 'user' | 'assistant';
21
+ content: string;
22
+ };
23
+ export type ChatRequest = {
24
+ messages: ChatMessage[];
25
+ data: Record<string, any>;
26
+ /** Fields the person set in the form itself: the turn never changes them. */
27
+ locked: string[];
28
+ };
29
+ export type ChatFieldResult = {
30
+ path: string;
31
+ label: string;
32
+ status: 'suggested' | 'invalid';
33
+ value: any;
34
+ error?: string;
35
+ };
36
+ export type ChatRemaining = {
37
+ required: {
38
+ path: string;
39
+ label: string;
40
+ }[];
41
+ optional: {
42
+ path: string;
43
+ label: string;
44
+ }[];
45
+ unsupported: ChatUnsupportedField[];
46
+ };
47
+ export type ChatResponse = {
48
+ requestId?: string;
49
+ reply: string;
50
+ patch: Record<string, any>;
51
+ data: Record<string, any>;
52
+ fields: Record<string, ChatFieldResult>;
53
+ errors: FormFieldError[];
54
+ remaining: ChatRemaining;
55
+ complete: boolean;
56
+ confirmed: boolean;
57
+ engine: {
58
+ name: string;
59
+ model: string;
60
+ latencyMs: number;
61
+ toolCalls: {
62
+ name: string;
63
+ isError: boolean;
64
+ }[];
65
+ usage?: {
66
+ inputTokens?: number | null;
67
+ outputTokens?: number | null;
68
+ };
69
+ };
70
+ };
71
+ /**
72
+ * The two values of a turn that are not form fields. A Form.io key cannot
73
+ * start with `-`, so no field's path can be either, and model APIs that limit
74
+ * property names to letters, digits, `_`, `.` and `-` accept them.
75
+ */
76
+ export declare const REPLY = "-reply";
77
+ export declare const CONFIRMED = "-confirmed";
78
+ export declare const CHAT_DEFAULTS: {
79
+ engines: string[];
80
+ maxIterations: number;
81
+ maxMessages: number;
82
+ maxMessageLength: number;
83
+ maxTranscriptChars: number;
84
+ };
85
+ export type ChatSettings = typeof CHAT_DEFAULTS & {
86
+ timeout?: number;
87
+ };
88
+ /** The chat settings in force (`agentApi.chat`), with their defaults. */
89
+ export declare function chatSettings(project: UAGProjectInterface): ChatSettings;
90
+ /** Read and check a turn's body, or throw a 400. Nothing is called before this passes. */
91
+ export declare function readChatRequest(body: any, settings: ChatSettings): ChatRequest;
92
+ /**
93
+ * The newest messages that fit in `maxChars`, starting at one the person
94
+ * wrote. The newest message always stays, whatever its length.
95
+ */
96
+ export declare function trimTranscript(messages: ChatMessage[], maxChars: number): ChatMessage[];
97
+ export declare function transcriptText(messages: ChatMessage[]): string;
98
+ /** The instructions every turn runs with (Agentic Formflow's, which builds each turn's request). */
99
+ export { CHAT_INSTRUCTIONS };
100
+ /** The JSON schema of a field's value, for structured extraction. */
101
+ export declare function chatValueSchema(form: UAGFormInterface, field: ChatField): Record<string, any>;
102
+ /** What the form still needs, from its fields on the current data. */
103
+ export declare function remainingOf(fields: ChatFields): ChatRemaining;
104
+ export type ChatContext = {
105
+ auth: AuthRequest;
106
+ requestId?: string;
107
+ };
108
+ /** The agent engines that may serve chat, in order. */
109
+ export declare function chatEngines(project: UAGProjectInterface, settings?: ChatSettings): AgentEngine[];
110
+ /**
111
+ * Run one turn of a form's chat. The caller has already been authenticated,
112
+ * and checked for create access to the form.
113
+ */
114
+ export declare function chatTurn(project: UAGProjectInterface, form: UAGFormInterface, body: any, context: ChatContext): Promise<ChatResponse>;