@formio/appserver 2.0.0-rc.1 → 2.0.0-rc.31

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 (44) hide show
  1. package/README.md +12 -6
  2. package/lib/actions/index.d.ts +3 -156
  3. package/lib/actions/login/LoginAction.d.ts +2 -108
  4. package/lib/actions/login/LoginAction.js +124 -213
  5. package/lib/actions/role/RoleAction.d.ts +2 -61
  6. package/lib/actions/role/RoleAction.js +13 -20
  7. package/lib/actions/save/SaveAction.d.ts +2 -28
  8. package/lib/actions/save/SaveAction.js +27 -45
  9. package/lib/modules/index.d.ts +1 -3
  10. package/lib/modules/index.js +3 -5
  11. package/lib/modules/mongodb/db.d.ts +127 -0
  12. package/lib/modules/mongodb/db.js +832 -0
  13. package/lib/server.d.ts +5 -174
  14. package/lib/server.js +61 -58
  15. package/package.json +25 -25
  16. package/lib/modules/auth/auth.d.ts +0 -5
  17. package/lib/modules/auth/auth.js +0 -70
  18. package/lib/prepare/access.d.ts +0 -3
  19. package/lib/prepare/access.js +0 -62
  20. package/lib/prepare/bcrypt.d.ts +0 -3
  21. package/lib/prepare/bcrypt.js +0 -25
  22. package/lib/prepare/index.d.ts +0 -11
  23. package/lib/prepare/index.js +0 -60
  24. package/lib/prepare/password.d.ts +0 -3
  25. package/lib/prepare/password.js +0 -27
  26. package/lib/prepare/persist.d.ts +0 -3
  27. package/lib/prepare/persist.js +0 -24
  28. package/lib/prepare/protect.d.ts +0 -3
  29. package/lib/prepare/protect.js +0 -28
  30. package/lib/prepare/reference.d.ts +0 -3
  31. package/lib/prepare/reference.js +0 -71
  32. package/lib/types/action.d.ts +0 -7
  33. package/lib/types/action.js +0 -2
  34. package/lib/types/form.d.ts +0 -4
  35. package/lib/types/form.js +0 -2
  36. package/lib/types/index.d.ts +0 -5
  37. package/lib/types/index.js +0 -21
  38. package/lib/types/project.d.ts +0 -4
  39. package/lib/types/project.js +0 -2
  40. package/lib/types/server.d.ts +0 -102
  41. package/lib/types/server.js +0 -2
  42. package/lib/types/submission.d.ts +0 -47
  43. package/lib/types/submission.js +0 -2
  44. package/server.js +0 -1
@@ -8,18 +8,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
11
  Object.defineProperty(exports, "__esModule", { value: true });
15
12
  exports.LoginAction = void 0;
16
- const bcryptjs_1 = __importDefault(require("bcryptjs"));
17
- const has_1 = __importDefault(require("lodash/has"));
18
- const set_1 = __importDefault(require("lodash/set"));
19
- const get_1 = __importDefault(require("lodash/get"));
20
- const unset_1 = __importDefault(require("lodash/unset"));
13
+ const appserver_core_1 = require("@formio/appserver-core");
14
+ const lodash_1 = require("lodash");
21
15
  const debug = require('debug')('formio:action:login');
22
- const error = require('debug')('formio:error');
23
16
  exports.LoginAction = {
24
17
  // The action information.
25
18
  get info() {
@@ -38,22 +31,19 @@ exports.LoginAction = {
38
31
  },
39
32
  };
40
33
  },
41
- /**
42
- * The settings form for this action.
43
- * @param {*} scope
44
- */
45
- settingsForm(scope) {
34
+ settingsForm(form) {
46
35
  return __awaiter(this, void 0, void 0, function* () {
36
+ var _a, _b;
47
37
  const fields = [];
48
38
  const resources = [];
49
- for (let name in scope.template.resources) {
50
- const resource = scope.template.resources[name];
39
+ for (let name in (_a = form.project.template) === null || _a === void 0 ? void 0 : _a.resources) {
40
+ const resource = form.project.template.resources[name];
51
41
  resources.push({
52
42
  label: resource.title,
53
43
  value: resource.name
54
44
  });
55
45
  }
56
- scope.utils.eachComponent(scope.form.components, (component) => {
46
+ appserver_core_1.Utils.eachComponent((_b = form.form) === null || _b === void 0 ? void 0 : _b.components, (component) => {
57
47
  if (['button'].indexOf(component.type) !== -1) {
58
48
  return;
59
49
  }
@@ -65,223 +55,144 @@ exports.LoginAction = {
65
55
  return [
66
56
  {
67
57
  type: 'select',
68
- label: 'Resources',
69
- key: 'resources',
70
- placeholder: 'Select the resources we should login against.',
71
- dataSrc: 'json',
72
- valueProperty: 'value',
73
- data: { json: resources },
74
- multiple: true,
75
- validate: {
76
- required: true,
77
- },
78
- },
79
- {
80
- type: 'select',
58
+ label: 'Authentication Provider',
59
+ key: 'provider',
60
+ placeholder: 'Select the authentication provider to use for login.',
61
+ dataSrc: 'values',
62
+ defaultValue: 'local',
81
63
  input: true,
82
- label: 'Username Field',
83
- key: 'username',
84
- placeholder: 'Select the username field',
85
- dataSrc: 'json',
86
- valueProperty: 'value',
87
- data: { json: fields },
88
- multiple: false,
89
- validate: {
90
- required: true,
91
- },
64
+ data: {
65
+ values: [
66
+ { label: 'Local', value: 'local' },
67
+ { label: 'OpenID Connect', value: 'oidc' },
68
+ ]
69
+ }
92
70
  },
93
71
  {
94
- type: 'select',
95
- label: 'Password Field',
96
- key: 'password',
97
- placeholder: 'Select the password field',
98
- dataSrc: 'json',
99
- valueProperty: 'value',
100
- data: { json: fields },
101
- multiple: false,
102
- validate: {
103
- required: true,
72
+ type: 'panel',
73
+ label: 'Login Settings',
74
+ key: 'localSettings',
75
+ input: false,
76
+ conditional: {
77
+ when: 'provider',
78
+ eq: 'local',
79
+ show: 'true'
104
80
  },
105
- },
106
- {
107
- type: 'textfield',
108
- key: 'allowedAttempts',
109
- input: true,
110
- label: 'Maximum Login Attempts',
111
- description: 'Use 0 for unlimited attempts',
112
- defaultValue: '5',
113
- },
114
- {
115
- type: 'textfield',
116
- key: 'attemptWindow',
117
- input: true,
118
- label: 'Login Attempt Time Window',
119
- description: 'This is the window of time to count the login attempts.',
120
- defaultValue: '30',
121
- suffix: 'seconds',
122
- },
123
- {
124
- type: 'textfield',
125
- key: 'lockWait',
126
- input: true,
127
- label: 'Locked Account Wait Time',
128
- description: 'The amount of time a person needs to wait before they can try to login again.',
129
- defaultValue: '1800',
130
- suffix: 'seconds',
81
+ components: [
82
+ {
83
+ type: 'select',
84
+ label: 'Resources',
85
+ key: 'resources',
86
+ placeholder: 'Select the resources we should login against.',
87
+ dataSrc: 'json',
88
+ valueProperty: 'value',
89
+ data: { json: resources },
90
+ multiple: true,
91
+ input: true,
92
+ validate: {
93
+ required: true,
94
+ },
95
+ },
96
+ {
97
+ type: 'select',
98
+ input: true,
99
+ label: 'Username Field',
100
+ key: 'username',
101
+ placeholder: 'Select the username field',
102
+ dataSrc: 'json',
103
+ valueProperty: 'value',
104
+ data: { json: fields },
105
+ multiple: false,
106
+ validate: {
107
+ required: true,
108
+ },
109
+ },
110
+ {
111
+ type: 'select',
112
+ label: 'Password Field',
113
+ key: 'password',
114
+ placeholder: 'Select the password field',
115
+ dataSrc: 'json',
116
+ valueProperty: 'value',
117
+ data: { json: fields },
118
+ multiple: false,
119
+ input: true,
120
+ validate: {
121
+ required: true,
122
+ },
123
+ },
124
+ {
125
+ type: 'textfield',
126
+ key: 'allowedAttempts',
127
+ input: true,
128
+ label: 'Maximum Login Attempts',
129
+ description: 'Use 0 for unlimited attempts',
130
+ defaultValue: '5',
131
+ },
132
+ {
133
+ type: 'textfield',
134
+ key: 'attemptWindow',
135
+ input: true,
136
+ label: 'Login Attempt Time Window',
137
+ description: 'This is the window of time to count the login attempts.',
138
+ defaultValue: '30',
139
+ suffix: 'seconds',
140
+ },
141
+ {
142
+ type: 'textfield',
143
+ key: 'lockWait',
144
+ input: true,
145
+ label: 'Locked Account Wait Time',
146
+ description: 'The amount of time a person needs to wait before they can try to login again.',
147
+ defaultValue: '1800',
148
+ suffix: 'seconds',
149
+ }
150
+ ]
131
151
  },
132
152
  ];
133
153
  });
134
154
  },
135
- /**
136
- * Format a string to show how long one must wait.
137
- *
138
- * @param time - In seconds.
139
- * @returns {string}
140
- */
141
- waitText(time) {
142
- return (time > 60) ? `${(time / 60)} minutes` : `${time} seconds`;
143
- },
144
- /**
145
- * Checks the login attempts for a certain login.
146
- *
147
- * @param user
148
- * @param next
149
- * @returns {*}
150
- */
151
- checkAttempts(scope, error, user) {
155
+ executor(actionForm, action, handler, method) {
152
156
  return __awaiter(this, void 0, void 0, function* () {
153
- const action = scope.action;
154
- if (!user || !user._id || !action.settings.allowedAttempts) {
155
- return;
156
- }
157
- const allowedAttempts = parseInt(action.settings.allowedAttempts, 10);
158
- if (Number.isNaN(allowedAttempts) || allowedAttempts === 0) {
159
- return;
160
- }
161
- // Initialize the login metadata.
162
- if (!(0, has_1.default)(user, 'metadata.login')) {
163
- (0, set_1.default)(user, 'metadata.login', {});
164
- }
165
- const now = (new Date()).getTime();
166
- const { login: loginMetadata, } = user.metadata;
167
- const lastAttempt = parseInt(loginMetadata.last, 10) || 0;
168
- // See if the login is locked.
169
- if (loginMetadata.locked) {
170
- // Get how long they must wait to be locked out.
171
- let lockWait = parseInt(action.settings.lockWait, 10) || 1800;
172
- // Normalize to milliseconds.
173
- lockWait *= 1000;
174
- // See if the time has expired.
175
- if ((lastAttempt + lockWait) < now) {
176
- // Reset the locked state and attempts totals.
177
- loginMetadata.attempts = 0;
178
- loginMetadata.locked = false;
179
- loginMetadata.last = now;
180
- }
181
- else {
182
- const howLong = (lastAttempt + lockWait) - now;
183
- return `You must wait ${exports.LoginAction.waitText(howLong / 1000)} before you can login.`;
184
- }
157
+ var _a, _b, _c, _d;
158
+ let loginProvider = null;
159
+ if (!action.settings.provider || action.settings.provider !== 'local') {
160
+ loginProvider = new appserver_core_1.LocalAuthProvider(actionForm.project, (0, lodash_1.defaultsDeep)((_b = (_a = actionForm.project) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.auth, {
161
+ local: action.settings
162
+ }));
185
163
  }
186
- else if (error) {
187
- let attemptWindow = parseInt(action.settings.attemptWindow, 10) || 30;
188
- // Normalize to milliseconds.
189
- attemptWindow *= 1000;
190
- // Determine the login attempts within a certain window.
191
- const withinWindow = lastAttempt ? ((lastAttempt + attemptWindow) > now) : false;
192
- if (withinWindow) {
193
- const attempts = (parseInt(loginMetadata.attempts, 10) || 0) + 1;
194
- // If they exceeded the login attempts.
195
- if (attempts >= allowedAttempts) {
196
- const lockWait = parseInt(action.settings.lockWait, 10) || 1800;
197
- error = `Maximum Login attempts. Please wait ${exports.LoginAction.waitText(lockWait)} before trying again.`;
198
- loginMetadata.locked = true;
199
- }
200
- // Set the login attempts.
201
- loginMetadata.attempts = attempts;
202
- }
203
- else {
204
- loginMetadata.attempts = 0;
205
- loginMetadata.last = now;
206
- }
164
+ else if (action.settings.provider === 'oidc') {
165
+ loginProvider = new appserver_core_1.OIDCProvider(actionForm.project, (0, lodash_1.defaultsDeep)((_d = (_c = actionForm.project) === null || _c === void 0 ? void 0 : _c.config) === null || _d === void 0 ? void 0 : _d.auth, {
166
+ oidc: action.settings
167
+ }));
207
168
  }
208
- else {
209
- // If there was no error, then reset the attempts to zero.
210
- loginMetadata.attempts = 0;
211
- loginMetadata.last = now;
169
+ if (loginProvider === null || loginProvider === void 0 ? void 0 : loginProvider.initialize) {
170
+ yield loginProvider.initialize();
212
171
  }
213
- // Update the user record
214
- yield scope.db.update(scope, user._id.toString(), user);
215
- return error;
216
- });
217
- },
218
- /**
219
- * Returns the action middleware.
220
- *
221
- * @param {*} scope
222
- */
223
- executor(scope) {
224
- return __awaiter(this, void 0, void 0, function* () {
225
- const action = scope.action;
226
- const userfield = action.settings.username;
227
- const passfield = action.settings.password;
228
- const forms = yield scope.utils.getForms(scope, action.settings.resources);
229
172
  return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
230
- var _a, _b;
231
173
  debug('Login Action');
232
- const username = (0, get_1.default)((_a = req.body) === null || _a === void 0 ? void 0 : _a.data, userfield);
233
- const password = (0, get_1.default)((_b = req.body) === null || _b === void 0 ? void 0 : _b.data, passfield);
234
- if (!username) {
235
- error('Missing username');
236
- return next('Missing username');
237
- }
238
- if (!password) {
239
- error('Missing password');
240
- return next('Missing password');
174
+ const result = yield (loginProvider === null || loginProvider === void 0 ? void 0 : loginProvider.login(req.auth, req.body, actionForm));
175
+ if (!result) {
176
+ return next('Unable to login');
241
177
  }
242
- // Find the user
243
- debug('Finding user', `${userfield} = ${username}`);
244
- const user = yield scope.utils.findUser(scope, forms, `data.${userfield}`, username);
245
- if (!user) {
246
- error('User not found');
247
- return next('User or password was incorrect');
178
+ if (typeof result === 'string') {
179
+ // If a string is returned, then it is a redirect.
180
+ return res.redirect(result);
248
181
  }
249
- // Get the current password hash.
250
- const hash = (0, get_1.default)(user.data, passfield);
251
- if (!hash || !hash.hash) {
252
- error('User does not have a password');
253
- return next('Your account does not have a password. You must reset your password to login.');
254
- }
255
- // Compare with bcrypt.
256
- debug('Comparing password');
257
- bcryptjs_1.default.compare(password, hash.hash, (err, value) => __awaiter(this, void 0, void 0, function* () {
258
- if (err) {
259
- error(err);
260
- return next(err);
261
- }
262
- // Check the login attempts.
263
- const errorAttempts = yield exports.LoginAction.checkAttempts(scope, !value, user);
264
- if (errorAttempts) {
265
- error(errorAttempts);
266
- return next(errorAttempts === true ? 'User or password was incorrect' : errorAttempts);
267
- }
268
- // Unset the password field before we create a token from it.
269
- (0, unset_1.default)(user.data, passfield);
270
- // Set the user object.
271
- req.user = user;
272
- req.token = yield scope.auth.token(scope.utils.tokenFromUser(user), scope.config.auth.secret, scope.config.auth.expire);
273
- // Set the auth headers.
274
- res.setHeader('x-jwt-token', req.token);
275
- req.headers['x-jwt-token'] = req.token;
182
+ if (typeof result === 'object') {
183
+ res.set('x-jwt-token', req.auth.token);
276
184
  if (res.resource) {
277
- res.resource.item = req.user;
185
+ res.resource.item = req.auth.user;
278
186
  }
279
187
  else {
280
- res.resource = { status: 200, item: req.user };
188
+ res.resource = { status: 200, item: req.auth.user };
281
189
  }
282
- debug('Login Successful', req.user);
190
+ debug('Login Successful', req.auth.user);
283
191
  next();
284
- }));
192
+ }
193
+ else {
194
+ return next('Unable to login');
195
+ }
285
196
  });
286
197
  });
287
198
  }
@@ -1,61 +1,2 @@
1
- import { SubmissionRequest, SubmissionResponse, ActionScope } from "@formio/appserver-types";
2
- import { NextFunction } from "express";
3
- export declare const RoleAction: {
4
- readonly info: {
5
- name: string;
6
- title: string;
7
- description: string;
8
- priority: number;
9
- defaults: {
10
- handler: string[];
11
- method: string[];
12
- };
13
- access: {
14
- handler: boolean;
15
- method: boolean;
16
- };
17
- };
18
- /**
19
- * The settings form for this action.
20
- * @param {*} scope
21
- */
22
- settingsForm(scope: ActionScope): Promise<({
23
- type: string;
24
- input: boolean;
25
- label: string;
26
- key: string;
27
- placeholder: string;
28
- template: string;
29
- dataSrc: string;
30
- data: {
31
- json: string;
32
- };
33
- valueProperty: string;
34
- multiple: boolean;
35
- validate: {
36
- required: boolean;
37
- };
38
- } | {
39
- type: string;
40
- input: boolean;
41
- label: string;
42
- key: string;
43
- placeholder: string;
44
- template: string;
45
- dataSrc: string;
46
- data: {
47
- json: unknown[];
48
- };
49
- valueProperty: string;
50
- multiple: boolean;
51
- validate: {
52
- required: boolean;
53
- };
54
- })[]>;
55
- /**
56
- * Returns the action middleware.
57
- *
58
- * @param {*} scope
59
- */
60
- executor(scope: ActionScope): Promise<(req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void | SubmissionResponse>>;
61
- };
1
+ import { Action } from "@formio/appserver-core";
2
+ export declare const RoleAction: Action;
@@ -30,12 +30,9 @@ exports.RoleAction = {
30
30
  }
31
31
  };
32
32
  },
33
- /**
34
- * The settings form for this action.
35
- * @param {*} scope
36
- */
37
- settingsForm(scope) {
33
+ settingsForm(form) {
38
34
  return __awaiter(this, void 0, void 0, function* () {
35
+ var _a;
39
36
  return [
40
37
  {
41
38
  type: 'select',
@@ -97,7 +94,7 @@ exports.RoleAction = {
97
94
  placeholder: 'Select the Role that this action will Add or Remove.',
98
95
  template: '<span>{{ item.title }}</span>',
99
96
  dataSrc: 'json',
100
- data: { json: Object.values(scope.template.roles) },
97
+ data: { json: Object.values(((_a = form.project.template) === null || _a === void 0 ? void 0 : _a.roles) || {}) },
101
98
  valueProperty: '_id',
102
99
  multiple: false,
103
100
  validate: {
@@ -107,47 +104,43 @@ exports.RoleAction = {
107
104
  ];
108
105
  });
109
106
  },
110
- /**
111
- * Returns the action middleware.
112
- *
113
- * @param {*} scope
114
- */
115
- executor(scope) {
107
+ executor(form, action, handler, method) {
116
108
  return __awaiter(this, void 0, void 0, function* () {
117
109
  return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
110
+ var _a, _b;
118
111
  debug('Role Action');
119
112
  // Check the submission for the submissionId.
120
- if (scope.action.settings.association !== 'existing' && scope.action.settings.association !== 'new') {
113
+ if (action.settings.association !== 'existing' && action.settings.association !== 'new') {
121
114
  error('Invalid setting `association` for the RoleAction; expecting `new` or `existing`.');
122
115
  return res.status(400).send('Invalid setting `association` for the RoleAction; expecting `new` or `existing`.');
123
116
  }
124
117
  // Error if operation type is not valid.
125
- if (!scope.action.settings.type || (scope.action.settings.type !== 'add' && scope.action.settings.type !== 'remove')) {
118
+ if (!action.settings.type || (action.settings.type !== 'add' && action.settings.type !== 'remove')) {
126
119
  error('Invalid setting `type` for the RoleAction; expecting `add` or `remove`.');
127
120
  return res.status(400).send('Invalid setting `type` for the RoleAction; expecting `add` or `remove`.');
128
121
  }
129
122
  // Error if no resource is being returned.
130
- if (scope.action.settings.association === 'new' && res.hasOwnProperty('resource') && !res.resource.item && scope.action.settings.role) {
123
+ if (action.settings.association === 'new' && res.hasOwnProperty('resource') && !((_a = res.resource) === null || _a === void 0 ? void 0 : _a.item) && action.settings.role) {
131
124
  error('Invalid resource was provided for RoleAction association of `new`.');
132
125
  return res.status(400).send('Invalid resource was provided for RoleAction association of `new`.');
133
126
  }
134
127
  // Error if association is existing and valid data was not provided.
135
- if (scope.action.settings.association === 'existing' && !scope.action.settings.role) {
128
+ if (action.settings.association === 'existing' && !action.settings.role) {
136
129
  error('Missing role for RoleAction association of "existing". Must specify role to assign in action settings or a form component named "role"');
137
130
  return res.status(400).send('Missing role for RoleAction association of "existing". Must specify role to assign in action settings or a form component named "role"');
138
131
  }
139
- if (scope.action.settings.association === 'existing' && !res.resource.item) {
132
+ if (action.settings.association === 'existing' && !((_b = res.resource) === null || _b === void 0 ? void 0 : _b.item)) {
140
133
  error('Missing submission for RoleAction association of "existing:. Form must have a resource field named "submission".');
141
134
  return res.status(400).send('Missing submission for RoleAction association of "existing:. Form must have a resource field named "submission".');
142
135
  }
143
136
  // Determine the resources based on the current request.
144
137
  let resource = res.resource ? (res.resource.item || req.body) : req.body;
145
- let role = scope.utils.roleId(scope, scope.action.settings.role);
138
+ let role = form.project.roleId(action.settings.role);
146
139
  if (!resource.roles) {
147
140
  resource.roles = [];
148
141
  }
149
142
  const roleIndex = resource.roles.indexOf(role);
150
- switch (scope.action.settings.type) {
143
+ switch (action.settings.type) {
151
144
  case 'add':
152
145
  if (roleIndex !== -1) {
153
146
  return next();
@@ -163,7 +156,7 @@ exports.RoleAction = {
163
156
  }
164
157
  debug('Updating user with roles', resource.roles);
165
158
  // Update the user.
166
- yield scope.db.update(scope, resource._id, resource, ['roles']);
159
+ yield form.saveSubmission(resource, ['roles']);
167
160
  next();
168
161
  });
169
162
  });
@@ -1,28 +1,2 @@
1
- import { ActionScope, SubmissionRequest, SubmissionResponse } from '@formio/appserver-types';
2
- import { NextFunction } from 'express';
3
- export declare const SaveAction: {
4
- readonly info: {
5
- name: string;
6
- title: string;
7
- description: string;
8
- priority: number;
9
- defaults: {
10
- handler: string[];
11
- method: string[];
12
- };
13
- access: {
14
- handler: boolean;
15
- method: boolean;
16
- };
17
- };
18
- mappingComponents(scope: ActionScope): Promise<any>;
19
- /**
20
- * The settings form for this action.
21
- * @param {*} scope
22
- */
23
- settingsForm(scope: ActionScope): Promise<any>;
24
- saveToForm(scope: ActionScope, resource: string): undefined;
25
- childSubmission(scope: ActionScope, req: SubmissionRequest, res: SubmissionResponse, submission: any): any;
26
- childResponse(scope: ActionScope, req: SubmissionRequest, res: SubmissionResponse): void;
27
- executor(scope: ActionScope): Promise<(req: SubmissionRequest, res: SubmissionResponse, next: NextFunction) => Promise<void>>;
28
- };
1
+ import { Action } from '@formio/appserver-core';
2
+ export declare const SaveAction: Action;