n8n-nodes-base 2.36.2 → 2.37.0
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/dist/cjs/build.tsbuildinfo +1 -1
- package/dist/credentials/TypeformOAuth2Api.credentials.d.ts.map +1 -1
- package/dist/credentials/TypeformOAuth2Api.credentials.js +32 -2
- package/dist/credentials/TypeformOAuth2Api.credentials.js.map +1 -1
- package/dist/known/credentials.json +2 -0
- package/dist/node-definitions/.nodes-hash +1 -1
- package/dist/node-definitions/nodes/n8n-nodes-base/googleAnalytics/v2/resource_report/operation_get.ts +2 -2
- package/dist/node-definitions/nodes/n8n-nodes-base/grist/v1.schema.js +6 -4
- package/dist/node-definitions/nodes/n8n-nodes-base/grist/v1.ts +28 -4
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftOutlook/v2/resource_event/operation_create.ts +2 -2
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/index.ts +3 -1
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/index.schema.js +24 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/index.ts +18 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/index.schema.js +24 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/index.ts +18 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/operation_download.schema.js +26 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/operation_download.ts +49 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/operation_update.schema.js +28 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/operation_update.ts +59 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/operation_upload.schema.js +26 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_file/operation_upload.ts +46 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/index.schema.js +30 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/index.ts +27 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_create.schema.js +25 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_create.ts +47 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_delete.schema.js +25 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_delete.ts +43 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_get.schema.js +26 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_get.ts +48 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_get_all.schema.js +29 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_get_all.ts +68 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_update.schema.js +25 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_update.ts +47 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_upsert.schema.js +25 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_item/operation_upsert.ts +47 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_list/index.schema.js +22 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_list/index.ts +15 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_list/operation_get.schema.js +25 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_list/operation_get.ts +41 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_list/operation_get_all.schema.js +26 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftSharePoint/v2/resource_list/operation_get_all.ts +45 -0
- package/dist/nodes/Confluence/actions/attachment/getMany.operation.d.ts +32 -0
- package/dist/nodes/Confluence/actions/attachment/getMany.operation.d.ts.map +1 -0
- package/dist/nodes/Confluence/actions/attachment/getMany.operation.js +98 -0
- package/dist/nodes/Confluence/actions/attachment/getMany.operation.js.map +1 -0
- package/dist/nodes/Confluence/actions/attachment/index.d.ts +5 -0
- package/dist/nodes/Confluence/actions/attachment/index.d.ts.map +1 -0
- package/dist/nodes/Confluence/actions/attachment/index.js +62 -0
- package/dist/nodes/Confluence/actions/attachment/index.js.map +1 -0
- package/dist/nodes/Confluence/actions/common.d.ts +7 -0
- package/dist/nodes/Confluence/actions/common.d.ts.map +1 -1
- package/dist/nodes/Confluence/actions/common.js +25 -2
- package/dist/nodes/Confluence/actions/common.js.map +1 -1
- package/dist/nodes/Confluence/actions/description.d.ts.map +1 -1
- package/dist/nodes/Confluence/actions/description.js +6 -0
- package/dist/nodes/Confluence/actions/description.js.map +1 -1
- package/dist/nodes/Confluence/actions/page/append.operation.d.ts +6 -0
- package/dist/nodes/Confluence/actions/page/append.operation.d.ts.map +1 -0
- package/dist/nodes/Confluence/actions/page/append.operation.js +85 -0
- package/dist/nodes/Confluence/actions/page/append.operation.js.map +1 -0
- package/dist/nodes/Confluence/actions/page/bodyEnvelope.d.ts +4 -1
- package/dist/nodes/Confluence/actions/page/bodyEnvelope.d.ts.map +1 -1
- package/dist/nodes/Confluence/actions/page/bodyEnvelope.js +28 -7
- package/dist/nodes/Confluence/actions/page/bodyEnvelope.js.map +1 -1
- package/dist/nodes/Confluence/actions/page/delete.operation.d.ts +5 -0
- package/dist/nodes/Confluence/actions/page/delete.operation.d.ts.map +1 -0
- package/dist/nodes/Confluence/actions/page/delete.operation.js +82 -0
- package/dist/nodes/Confluence/actions/page/delete.operation.js.map +1 -0
- package/dist/nodes/Confluence/actions/page/index.d.ts +4 -1
- package/dist/nodes/Confluence/actions/page/index.d.ts.map +1 -1
- package/dist/nodes/Confluence/actions/page/index.js +28 -1
- package/dist/nodes/Confluence/actions/page/index.js.map +1 -1
- package/dist/nodes/Confluence/actions/page/pageWrite.d.ts +17 -0
- package/dist/nodes/Confluence/actions/page/pageWrite.d.ts.map +1 -0
- package/dist/nodes/Confluence/actions/page/pageWrite.js +70 -0
- package/dist/nodes/Confluence/actions/page/pageWrite.js.map +1 -0
- package/dist/nodes/Confluence/actions/page/update.operation.d.ts +5 -0
- package/dist/nodes/Confluence/actions/page/update.operation.d.ts.map +1 -0
- package/dist/nodes/Confluence/actions/page/update.operation.js +72 -0
- package/dist/nodes/Confluence/actions/page/update.operation.js.map +1 -0
- package/dist/nodes/Confluence/actions/router.d.ts +2 -0
- package/dist/nodes/Confluence/actions/router.d.ts.map +1 -1
- package/dist/nodes/Confluence/actions/router.js +15 -1
- package/dist/nodes/Confluence/actions/router.js.map +1 -1
- package/dist/nodes/Confluence/transport/index.d.ts +6 -0
- package/dist/nodes/Confluence/transport/index.d.ts.map +1 -1
- package/dist/nodes/Confluence/transport/index.js +86 -10
- package/dist/nodes/Confluence/transport/index.js.map +1 -1
- package/dist/nodes/Files/ExtractFromFile/actions/moveTo.operation.d.ts.map +1 -1
- package/dist/nodes/Files/ExtractFromFile/actions/moveTo.operation.js +2 -2
- package/dist/nodes/Files/ExtractFromFile/actions/moveTo.operation.js.map +1 -1
- package/dist/nodes/Files/ExtractFromFile/actions/parseIcsCalendar.d.ts +3 -0
- package/dist/nodes/Files/ExtractFromFile/actions/parseIcsCalendar.d.ts.map +1 -0
- package/dist/nodes/Files/ExtractFromFile/actions/parseIcsCalendar.js +10 -0
- package/dist/nodes/Files/ExtractFromFile/actions/parseIcsCalendar.js.map +1 -0
- package/dist/nodes/Form/Form.node.d.ts.map +1 -1
- package/dist/nodes/Form/Form.node.js +8 -0
- package/dist/nodes/Form/Form.node.js.map +1 -1
- package/dist/nodes/Form/interfaces.d.ts +1 -0
- package/dist/nodes/Form/interfaces.d.ts.map +1 -1
- package/dist/nodes/Form/interfaces.js.map +1 -1
- package/dist/nodes/Form/utils/formCompletionUtils.d.ts.map +1 -1
- package/dist/nodes/Form/utils/formCompletionUtils.js +8 -1
- package/dist/nodes/Form/utils/formCompletionUtils.js.map +1 -1
- package/dist/nodes/Form/utils/formNodeUtils.d.ts.map +1 -1
- package/dist/nodes/Form/utils/formNodeUtils.js +14 -3
- package/dist/nodes/Form/utils/formNodeUtils.js.map +1 -1
- package/dist/nodes/Form/utils/utils.d.ts +49 -9
- package/dist/nodes/Form/utils/utils.d.ts.map +1 -1
- package/dist/nodes/Form/utils/utils.js +309 -69
- package/dist/nodes/Form/utils/utils.js.map +1 -1
- package/dist/nodes/Form/v2/FormTriggerV2.node.d.ts +1 -1
- package/dist/nodes/Form/v2/FormTriggerV2.node.d.ts.map +1 -1
- package/dist/nodes/Form/v2/FormTriggerV2.node.js +0 -1
- package/dist/nodes/Form/v2/FormTriggerV2.node.js.map +1 -1
- package/dist/nodes/Google/Calendar/GoogleCalendar.node.d.ts.map +1 -1
- package/dist/nodes/Google/Calendar/GoogleCalendar.node.js +7 -0
- package/dist/nodes/Google/Calendar/GoogleCalendar.node.js.map +1 -1
- package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.d.ts.map +1 -1
- package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.js +82 -7
- package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.js.map +1 -1
- package/dist/nodes/Google/Drive/v2/actions/file/download.operation.js +1 -1
- package/dist/nodes/Google/Drive/v2/actions/file/download.operation.js.map +1 -1
- package/dist/nodes/Grist/Grist.node.d.ts.map +1 -1
- package/dist/nodes/Grist/Grist.node.js +60 -2
- package/dist/nodes/Grist/Grist.node.js.map +1 -1
- package/dist/nodes/Grist/OperationDescription.d.ts.map +1 -1
- package/dist/nodes/Grist/OperationDescription.js +83 -3
- package/dist/nodes/Grist/OperationDescription.js.map +1 -1
- package/dist/nodes/Grist/types.d.ts +10 -0
- package/dist/nodes/Grist/types.d.ts.map +1 -1
- package/dist/nodes/MessageAnAgent/MessageAnAgent.node.d.ts.map +1 -1
- package/dist/nodes/MessageAnAgent/MessageAnAgent.node.js +5 -3
- package/dist/nodes/MessageAnAgent/MessageAnAgent.node.js.map +1 -1
- package/dist/nodes/MessageAnAgent/shared.d.ts.map +1 -1
- package/dist/nodes/MessageAnAgent/shared.js +146 -4
- package/dist/nodes/MessageAnAgent/shared.js.map +1 -1
- package/dist/nodes/MessageAnAgent/v2/MessageAnAgentV2.node.d.ts.map +1 -1
- package/dist/nodes/MessageAnAgent/v2/MessageAnAgentV2.node.js +4 -3
- package/dist/nodes/MessageAnAgent/v2/MessageAnAgentV2.node.js.map +1 -1
- package/dist/nodes/Microsoft/SharePoint/MicrosoftSharePoint.node.d.ts.map +1 -1
- package/dist/nodes/Microsoft/SharePoint/MicrosoftSharePoint.node.js +3 -8
- package/dist/nodes/Microsoft/SharePoint/MicrosoftSharePoint.node.js.map +1 -1
- package/dist/nodes/MongoDb/GenericFunctions.d.ts +1 -0
- package/dist/nodes/MongoDb/GenericFunctions.d.ts.map +1 -1
- package/dist/nodes/MongoDb/GenericFunctions.js +15 -2
- package/dist/nodes/MongoDb/GenericFunctions.js.map +1 -1
- package/dist/nodes/MongoDb/MongoDb.node.d.ts.map +1 -1
- package/dist/nodes/MongoDb/MongoDb.node.js +32 -19
- package/dist/nodes/MongoDb/MongoDb.node.js.map +1 -1
- package/dist/nodes/PagerDuty/GenericFunctions.d.ts.map +1 -1
- package/dist/nodes/PagerDuty/GenericFunctions.js +5 -2
- package/dist/nodes/PagerDuty/GenericFunctions.js.map +1 -1
- package/dist/nodes/Transform/Aggregate/Aggregate.node.d.ts.map +1 -1
- package/dist/nodes/Transform/Aggregate/Aggregate.node.js +1 -4
- package/dist/nodes/Transform/Aggregate/Aggregate.node.js.map +1 -1
- package/dist/nodes/Transform/SplitOut/utils.d.ts.map +1 -1
- package/dist/nodes/Transform/SplitOut/utils.js +2 -4
- package/dist/nodes/Transform/SplitOut/utils.js.map +1 -1
- package/dist/nodes/Transform/utils/utils.d.ts +6 -0
- package/dist/nodes/Transform/utils/utils.d.ts.map +1 -1
- package/dist/nodes/Transform/utils/utils.js +15 -1
- package/dist/nodes/Transform/utils/utils.js.map +1 -1
- package/dist/nodes/Wait/Wait.node.d.ts +1 -1
- package/dist/types/credentials.json +3 -3
- package/dist/types/nodes.json +9 -8
- package/package.json +13 -13
|
@@ -37,7 +37,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.isFormConnected = exports.validateResponseModeConfiguration = exports.prepareFormFields = exports.handleNewlines = exports.getNodeReference = void 0;
|
|
40
|
-
exports.isFormOAuth2Enabled = isFormOAuth2Enabled;
|
|
41
40
|
exports.sanitizeHtml = sanitizeHtml;
|
|
42
41
|
exports.sanitizeCustomCss = sanitizeCustomCss;
|
|
43
42
|
exports.validateSafeRedirectUrl = validateSafeRedirectUrl;
|
|
@@ -46,8 +45,11 @@ exports.prepareFormData = prepareFormData;
|
|
|
46
45
|
exports.addFormResponseDataToReturnItem = addFormResponseDataToReturnItem;
|
|
47
46
|
exports.prepareFormReturnItem = prepareFormReturnItem;
|
|
48
47
|
exports.renderForm = renderForm;
|
|
48
|
+
exports.getHostNavigationPath = getHostNavigationPath;
|
|
49
|
+
exports.respondIfCredentialsNotReady = respondIfCredentialsNotReady;
|
|
49
50
|
exports.generateFormUserAuthToken = generateFormUserAuthToken;
|
|
50
51
|
exports.verifyFormUserAuthToken = verifyFormUserAuthToken;
|
|
52
|
+
exports.setFormAuthCookie = setFormAuthCookie;
|
|
51
53
|
exports.validateFormPageAuth = validateFormPageAuth;
|
|
52
54
|
exports.formShellSummaryText = formShellSummaryText;
|
|
53
55
|
exports.buildFormShellViewModel = buildFormShellViewModel;
|
|
@@ -77,6 +79,9 @@ const interfaces_1 = require("../interfaces");
|
|
|
77
79
|
const FORM_USER_AUTH_TOKEN_TTL_SECONDS = 60 * 60;
|
|
78
80
|
const getNodeReference = (nodeName) => `$(${JSON.stringify(nodeName)})`;
|
|
79
81
|
exports.getNodeReference = getNodeReference;
|
|
82
|
+
function isOptionalString(value) {
|
|
83
|
+
return value === undefined || typeof value === 'string';
|
|
84
|
+
}
|
|
80
85
|
function isFormUserAuthClaims(value) {
|
|
81
86
|
if (typeof value !== 'object' || value === null)
|
|
82
87
|
return false;
|
|
@@ -86,10 +91,17 @@ function isFormUserAuthClaims(value) {
|
|
|
86
91
|
typeof c.firstName === 'string' &&
|
|
87
92
|
typeof c.lastName === 'string' &&
|
|
88
93
|
typeof c.nid === 'string' &&
|
|
89
|
-
typeof c.wid === 'string'
|
|
94
|
+
typeof c.wid === 'string' &&
|
|
95
|
+
isOptionalString(c.wfid) &&
|
|
96
|
+
isOptionalString(c.eid));
|
|
90
97
|
}
|
|
91
|
-
function
|
|
92
|
-
return
|
|
98
|
+
function userFromFormUserAuthClaims(claims) {
|
|
99
|
+
return {
|
|
100
|
+
id: claims.sub,
|
|
101
|
+
email: claims.email,
|
|
102
|
+
firstName: claims.firstName,
|
|
103
|
+
lastName: claims.lastName,
|
|
104
|
+
};
|
|
93
105
|
}
|
|
94
106
|
function sanitizeHtml(text) {
|
|
95
107
|
return (0, sanitize_html_1.default)(text, {
|
|
@@ -231,7 +243,7 @@ function getFieldIdentifier(field, nodeVersion) {
|
|
|
231
243
|
}
|
|
232
244
|
return field.fieldLabel ?? field.fieldName ?? '';
|
|
233
245
|
}
|
|
234
|
-
function prepareFormData({ formTitle, formDescription, formSubmittedHeader, formSubmittedText, redirectUrl, formFields, testRun, query, instanceId, useResponseData, appendAttribution = true, buttonLabel, customCss, nodeVersion, authToken, shellInner, hasAuthenticatedSubmitter, }) {
|
|
246
|
+
function prepareFormData({ formTitle, formDescription, formSubmittedHeader, formSubmittedText, redirectUrl, formFields, testRun, query, instanceId, useResponseData, appendAttribution = true, buttonLabel, customCss, nodeVersion, authToken, shellInner, hasAuthenticatedSubmitter, hostNavigationPath, }) {
|
|
235
247
|
const utm_campaign = instanceId ? `&utm_campaign=${instanceId}` : '';
|
|
236
248
|
const n8nWebsiteLink = `https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger${utm_campaign}`;
|
|
237
249
|
if (formSubmittedText === undefined) {
|
|
@@ -255,6 +267,7 @@ function prepareFormData({ formTitle, formDescription, formSubmittedHeader, form
|
|
|
255
267
|
shellInner: shellInner || undefined,
|
|
256
268
|
// Only set for forms that can be gated, so the rest never ship the client handling.
|
|
257
269
|
hasAuthenticatedSubmitter: hasAuthenticatedSubmitter || undefined,
|
|
270
|
+
hostNavigationPath: hostNavigationPath || undefined,
|
|
258
271
|
};
|
|
259
272
|
if (redirectUrl) {
|
|
260
273
|
const safeUrl = validateSafeRedirectUrl(redirectUrl);
|
|
@@ -487,6 +500,10 @@ function renderForm({ context, res, formTitle, formDescription, formFields, resp
|
|
|
487
500
|
catch (error) { }
|
|
488
501
|
}
|
|
489
502
|
formFields = (0, exports.prepareFormFields)(formFields);
|
|
503
|
+
// A follow-up page the shell navigated its frame to is the shell's inner render
|
|
504
|
+
// just like the flagged page-1 request — it must render the readiness listener
|
|
505
|
+
// and leave the submit button's enabled state to the shell's connect panel.
|
|
506
|
+
const inShell = isHostedInFormShell(context.getRequestObject(), shellInner);
|
|
490
507
|
const data = prepareFormData({
|
|
491
508
|
formTitle,
|
|
492
509
|
formDescription,
|
|
@@ -502,8 +519,9 @@ function renderForm({ context, res, formTitle, formDescription, formFields, resp
|
|
|
502
519
|
customCss,
|
|
503
520
|
nodeVersion: context.getNode().typeVersion,
|
|
504
521
|
authToken,
|
|
505
|
-
shellInner,
|
|
522
|
+
shellInner: inShell,
|
|
506
523
|
hasAuthenticatedSubmitter,
|
|
524
|
+
hostNavigationPath: getHostNavigationPath(context, inShell),
|
|
507
525
|
});
|
|
508
526
|
if (!(0, n8n_core_1.isFormHtmlSandboxingDisabled)()) {
|
|
509
527
|
res.setHeader('Content-Security-Policy', (0, n8n_core_1.getHtmlSandboxCSP)());
|
|
@@ -533,10 +551,75 @@ function buildAbsoluteFormUrl(req) {
|
|
|
533
551
|
const host = headerValue('x-forwarded-host') ?? req.headers.host ?? '';
|
|
534
552
|
return `${protocol}://${host}${req.originalUrl}`;
|
|
535
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* True when this render is the hosting shell's frame — either the shell's own
|
|
556
|
+
* inner render (flagged in the URL) or a follow-up page the shell navigated its
|
|
557
|
+
* frame to. Only then may the form hand its page navigations to the host.
|
|
558
|
+
*
|
|
559
|
+
* A follow-up page is recognised by the `n8nShellHosted` flag the shell appends
|
|
560
|
+
* to every URL it moves its frame to. Being told outright, rather than inferred
|
|
561
|
+
* from `Sec-Fetch-*`, keeps delegation working where those headers never arrive
|
|
562
|
+
* (a proxy that strips them) and keeps a same-origin frame that is *not* the
|
|
563
|
+
* shell from being treated as one.
|
|
564
|
+
*
|
|
565
|
+
* Where those headers do arrive they still have to agree — a same-origin frame —
|
|
566
|
+
* so a top-level document, or a form embedded on someone else's site, keeps
|
|
567
|
+
* navigating itself even with the flag in its URL.
|
|
568
|
+
*
|
|
569
|
+
* Nothing rests on the flag being unguessable: a frame that sets it on itself
|
|
570
|
+
* gains only the ability to ask its own parent to move it, and the shell accepts
|
|
571
|
+
* that ask for this form's own pages alone.
|
|
572
|
+
*/
|
|
573
|
+
function isHostedInFormShell(req, shellInner) {
|
|
574
|
+
if (shellInner)
|
|
575
|
+
return true;
|
|
576
|
+
const secFetchDest = req.headers?.['sec-fetch-dest'];
|
|
577
|
+
const secFetchSite = req.headers?.['sec-fetch-site'];
|
|
578
|
+
return (req.query?.n8nShellHosted === '1' &&
|
|
579
|
+
(secFetchDest === undefined || secFetchDest === 'iframe') &&
|
|
580
|
+
(secFetchSite === undefined || secFetchSite === 'same-origin'));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* The path prefix this render may ask the hosting shell to navigate to, or
|
|
584
|
+
* `undefined` when the render isn't in the shell's frame and so keeps navigating
|
|
585
|
+
* itself.
|
|
586
|
+
*/
|
|
587
|
+
function getHostNavigationPath(context, shellInner) {
|
|
588
|
+
if (!isHostedInFormShell(context.getRequestObject(), shellInner))
|
|
589
|
+
return undefined;
|
|
590
|
+
return getFormWaitingBasePath(context) ?? undefined;
|
|
591
|
+
}
|
|
536
592
|
const isFormConnected = (nodes) => {
|
|
537
593
|
return nodes.some((n) => n.type === n8n_workflow_1.FORM_NODE_TYPE || (n.type === n8n_workflow_1.WAIT_NODE_TYPE && n.parameters?.resume === 'form'));
|
|
538
594
|
};
|
|
539
595
|
exports.isFormConnected = isFormConnected;
|
|
596
|
+
/**
|
|
597
|
+
* Submit-time credential readiness gate, shared by the trigger's POST and the
|
|
598
|
+
* Form node's POST. Answers 428 with the missing-credential list when the
|
|
599
|
+
* submitter's required accounts aren't all connected, 503 when the check itself
|
|
600
|
+
* fails. Returns true when a response was sent and the submission must not
|
|
601
|
+
* proceed. A no-op (`undefined` readiness) outside the dynamic-credentials flow.
|
|
602
|
+
*/
|
|
603
|
+
async function respondIfCredentialsNotReady(context, res) {
|
|
604
|
+
let readiness;
|
|
605
|
+
try {
|
|
606
|
+
readiness = await context.checkTriggerCredentialStatus();
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
// Fail closed. Throwing here is swallowed by the webhook layer, which then
|
|
610
|
+
// enqueues the execution anyway — exactly the doomed run we're preventing.
|
|
611
|
+
context.logger.error('Form submit credential readiness check failed', { error });
|
|
612
|
+
res.status(503).json({ status: 'credential_readiness_check_failed' });
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
if (readiness && !readiness.readyToExecute) {
|
|
616
|
+
// 428, matching the webhook trigger's gate (webhook-helpers.ts) so all
|
|
617
|
+
// trigger paths answer an unconnected credential the same way.
|
|
618
|
+
res.status(428).json((0, n8n_workflow_1.buildCredentialConnectionsRequiredResponse)(readiness));
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
540
623
|
/**
|
|
541
624
|
* Generate a form auth token for n8nUserAuth. The token embeds the user info
|
|
542
625
|
* in a signed JWT so the POST handler can authenticate the submission without
|
|
@@ -545,10 +628,12 @@ exports.isFormConnected = isFormConnected;
|
|
|
545
628
|
* cookie is `SameSite=Lax`).
|
|
546
629
|
*
|
|
547
630
|
* The `nid` and `wid` claims bind the token to a specific node + webhook,
|
|
548
|
-
* preventing replay across forms.
|
|
549
|
-
*
|
|
631
|
+
* preventing replay across forms. `wfid`/`eid` additionally bind it to the
|
|
632
|
+
* workflow and — for tokens minted while a run is in progress — that run, which
|
|
633
|
+
* is what lets the same token be accepted as the form page auth cookie. Signed
|
|
634
|
+
* with HS256 using the instance's hmac signature secret.
|
|
550
635
|
*/
|
|
551
|
-
function generateFormUserAuthToken(node, user) {
|
|
636
|
+
function generateFormUserAuthToken(node, user, binding) {
|
|
552
637
|
const secret = di_1.Container.get(n8n_core_1.InstanceSettings).hmacSignatureSecret;
|
|
553
638
|
const payload = {
|
|
554
639
|
sub: user.id,
|
|
@@ -557,6 +642,8 @@ function generateFormUserAuthToken(node, user) {
|
|
|
557
642
|
lastName: user.lastName,
|
|
558
643
|
nid: node.id,
|
|
559
644
|
wid: node.webhookId ?? '',
|
|
645
|
+
...(binding.workflowId ? { wfid: binding.workflowId } : {}),
|
|
646
|
+
...(binding.executionId ? { eid: binding.executionId } : {}),
|
|
560
647
|
};
|
|
561
648
|
return jsonwebtoken_1.default.sign(payload, secret, {
|
|
562
649
|
algorithm: 'HS256',
|
|
@@ -564,11 +651,27 @@ function generateFormUserAuthToken(node, user) {
|
|
|
564
651
|
});
|
|
565
652
|
}
|
|
566
653
|
/**
|
|
567
|
-
* Verify a form auth token issued by `generateFormUserAuthToken
|
|
568
|
-
* encoded user on success or `null` on
|
|
569
|
-
* wrong signature, wrong node
|
|
654
|
+
* Verify a form auth token issued by `generateFormUserAuthToken` and presented
|
|
655
|
+
* in the `x-auth-token` header. Returns the encoded user on success or `null` on
|
|
656
|
+
* any failure (bad format, expired, wrong signature, wrong node, wrong
|
|
657
|
+
* execution). The caller decides how to surface the failure.
|
|
658
|
+
*
|
|
659
|
+
* A token carrying an `eid` is only accepted for that execution. Tokens minted
|
|
660
|
+
* before a run existed carry none and stay valid for the node they name.
|
|
570
661
|
*/
|
|
571
|
-
function verifyFormUserAuthToken(token, node) {
|
|
662
|
+
function verifyFormUserAuthToken(token, node, executionId) {
|
|
663
|
+
const claims = decodeFormUserAuthClaims(token);
|
|
664
|
+
if (!claims)
|
|
665
|
+
return null;
|
|
666
|
+
if (claims.nid !== node.id)
|
|
667
|
+
return null;
|
|
668
|
+
if (claims.wid !== (node.webhookId ?? ''))
|
|
669
|
+
return null;
|
|
670
|
+
if (claims.eid !== undefined && claims.eid !== executionId)
|
|
671
|
+
return null;
|
|
672
|
+
return userFromFormUserAuthClaims(claims);
|
|
673
|
+
}
|
|
674
|
+
function decodeFormUserAuthClaims(token) {
|
|
572
675
|
const secret = di_1.Container.get(n8n_core_1.InstanceSettings).hmacSignatureSecret;
|
|
573
676
|
let claims;
|
|
574
677
|
try {
|
|
@@ -577,18 +680,24 @@ function verifyFormUserAuthToken(token, node) {
|
|
|
577
680
|
catch {
|
|
578
681
|
return null;
|
|
579
682
|
}
|
|
580
|
-
|
|
683
|
+
return isFormUserAuthClaims(claims) ? claims : null;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Verify a form auth token presented as the form page auth cookie. Unlike the
|
|
687
|
+
* `x-auth-token` variant this is not bound to a single node: the cookie rides
|
|
688
|
+
* along a navigation to whichever node serves the next page of the same form,
|
|
689
|
+
* so it is bound to the workflow (`wfid`) and, once a run exists, to that run
|
|
690
|
+
* (`eid`). Tokens predating those claims are not accepted here.
|
|
691
|
+
*/
|
|
692
|
+
function verifyFormPageAuthCookie(token, workflowId, executionId) {
|
|
693
|
+
const claims = decodeFormUserAuthClaims(token);
|
|
694
|
+
if (!claims)
|
|
581
695
|
return null;
|
|
582
|
-
if (claims.
|
|
696
|
+
if (!claims.wfid || !workflowId || claims.wfid !== workflowId)
|
|
583
697
|
return null;
|
|
584
|
-
if (claims.
|
|
698
|
+
if (claims.eid !== undefined && claims.eid !== executionId)
|
|
585
699
|
return null;
|
|
586
|
-
return
|
|
587
|
-
id: claims.sub,
|
|
588
|
-
email: claims.email,
|
|
589
|
-
firstName: claims.firstName,
|
|
590
|
-
lastName: claims.lastName,
|
|
591
|
-
};
|
|
700
|
+
return userFromFormUserAuthClaims(claims);
|
|
592
701
|
}
|
|
593
702
|
function trimTrailingSlash(url) {
|
|
594
703
|
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
@@ -598,16 +707,21 @@ function trimTrailingSlash(url) {
|
|
|
598
707
|
// sandboxed form page. The token is otherwise already embedded in the form HTML
|
|
599
708
|
// (the page sends it back as `x-auth-token` on POST), so this is not a new exposure.
|
|
600
709
|
const FORM_OAUTH_COOKIE_NAME = 'n8n-form-oauth';
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
710
|
+
/**
|
|
711
|
+
* Derive `secure` from the request scheme (honouring x-forwarded-proto, as
|
|
712
|
+
* buildAbsoluteFormUrl does) rather than config, so form cookies are actually
|
|
713
|
+
* sent back over http in dev while staying Secure over https.
|
|
714
|
+
*/
|
|
715
|
+
function isSecureRequest(req) {
|
|
605
716
|
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
606
717
|
const proto = (typeof forwardedProto === 'string' ? forwardedProto.trim() : '') || req.protocol;
|
|
718
|
+
return proto === 'https';
|
|
719
|
+
}
|
|
720
|
+
function formOAuthCookieOptions(req, resourceUrl) {
|
|
607
721
|
return {
|
|
608
722
|
httpOnly: true,
|
|
609
723
|
sameSite: 'lax', // must be Lax: sent on our own top-level 302 → GET
|
|
610
|
-
secure:
|
|
724
|
+
secure: isSecureRequest(req),
|
|
611
725
|
path: new URL(resourceUrl).pathname, // scope the bearer token to this form
|
|
612
726
|
};
|
|
613
727
|
}
|
|
@@ -617,13 +731,137 @@ function setFormOAuthToken(res, req, resourceUrl, token) {
|
|
|
617
731
|
maxAge: 60_000, // one redirect hop; short by design
|
|
618
732
|
});
|
|
619
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* Decode a cookie value, or `null` when it isn't valid percent-encoding. A value
|
|
736
|
+
* we can't read is treated as no cookie at all — the caller's other checks still
|
|
737
|
+
* get their turn — rather than throwing out of the request.
|
|
738
|
+
*/
|
|
739
|
+
function decodeCookieValue(value) {
|
|
740
|
+
try {
|
|
741
|
+
return decodeURIComponent(value.trim());
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
620
747
|
function readFormOAuthToken(req) {
|
|
621
748
|
const match = (req.headers.cookie ?? '').match(/(?:^|;\s*)n8n-form-oauth=([^;]+)/);
|
|
622
|
-
return match ?
|
|
749
|
+
return match ? decodeCookieValue(match[1]) : null;
|
|
623
750
|
}
|
|
624
751
|
function clearFormOAuthToken(res, req, resourceUrl) {
|
|
625
752
|
res.clearCookie(FORM_OAUTH_COOKIE_NAME, formOAuthCookieOptions(req, resourceUrl));
|
|
626
753
|
}
|
|
754
|
+
// Carries the submitter's identity to the follow-up pages of a multi-page form.
|
|
755
|
+
// Those pages are reached by a navigation, which can present neither the
|
|
756
|
+
// `x-auth-token` header nor — from a sandboxed, opaque-origin form document —
|
|
757
|
+
// the `n8n-auth` session cookie.
|
|
758
|
+
//
|
|
759
|
+
// The name carries the token's binding, so forms open in other tabs don't
|
|
760
|
+
// overwrite each other's cookie: pages of a run use the run's id, and the
|
|
761
|
+
// trigger — rendered before any run exists — uses the workflow's. Duplicated in
|
|
762
|
+
// `packages/cli/src/constants.ts` (prefix only); keep both sides in step.
|
|
763
|
+
const FORM_AUTH_COOKIE_PREFIX = 'n8n-form-auth';
|
|
764
|
+
function getFormAuthCookieName(binding) {
|
|
765
|
+
return binding.executionId
|
|
766
|
+
? `${FORM_AUTH_COOKIE_PREFIX}-ex-${binding.executionId}`
|
|
767
|
+
: `${FORM_AUTH_COOKIE_PREFIX}-wf-${binding.workflowId ?? ''}`;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Value of the request's cookie with exactly this name, or `null`. Parses the
|
|
771
|
+
* raw header (the webhook path may bypass cookie-parser) by splitting rather
|
|
772
|
+
* than a regex, since the name embeds a workflow or execution id.
|
|
773
|
+
*/
|
|
774
|
+
function readCookieValue(req, name) {
|
|
775
|
+
for (const part of (req.headers.cookie ?? '').split(';')) {
|
|
776
|
+
const separator = part.indexOf('=');
|
|
777
|
+
if (separator === -1 || part.slice(0, separator).trim() !== name)
|
|
778
|
+
continue;
|
|
779
|
+
return decodeCookieValue(part.slice(separator + 1));
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Path the form-waiting endpoint is served under, or `null` when it can't be
|
|
785
|
+
* derived. It is configurable and the instance may sit behind a path prefix, so
|
|
786
|
+
* derive it rather than assume it: `$execution.resumeFormUrl` is
|
|
787
|
+
* `<formWaitingBaseUrl>/<executionId>`, and dropping the last segment leaves the
|
|
788
|
+
* base path.
|
|
789
|
+
*/
|
|
790
|
+
function getFormWaitingBasePath(context) {
|
|
791
|
+
try {
|
|
792
|
+
const resumeFormUrl = context.evaluateExpression('{{ $execution.resumeFormUrl }}');
|
|
793
|
+
const { pathname } = new URL(resumeFormUrl);
|
|
794
|
+
return pathname.slice(0, pathname.lastIndexOf('/')) || null;
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Hand the follow-up pages of a multi-page form their own auth token, scoped to
|
|
802
|
+
* the form-waiting path. Re-set on every page render, so a long form's later
|
|
803
|
+
* pages get a token that is still within its TTL.
|
|
804
|
+
*/
|
|
805
|
+
function setFormAuthCookie(context, token, binding) {
|
|
806
|
+
const req = context.getRequestObject();
|
|
807
|
+
context.getResponseObject().cookie(getFormAuthCookieName(binding), token, {
|
|
808
|
+
httpOnly: true,
|
|
809
|
+
// Lax, and only ever sent on a navigation the shell (or the top-level page)
|
|
810
|
+
// initiates from the real origin.
|
|
811
|
+
sameSite: 'lax',
|
|
812
|
+
secure: isSecureRequest(req),
|
|
813
|
+
// Nothing narrower is derivable when the base path is unknown; the cookie is
|
|
814
|
+
// httpOnly and expires with the token either way.
|
|
815
|
+
path: getFormWaitingBasePath(context) ?? '/',
|
|
816
|
+
maxAge: FORM_USER_AUTH_TOKEN_TTL_SECONDS * 1000,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Resolve the submitter from the form page auth cookie, or `null` when it is
|
|
821
|
+
* absent or doesn't verify for the workflow/execution being served.
|
|
822
|
+
*/
|
|
823
|
+
function readFormAuthCookieUser(context) {
|
|
824
|
+
const req = context.getRequestObject();
|
|
825
|
+
const workflowId = context.getWorkflow().id;
|
|
826
|
+
const executionId = context.getExecutionId();
|
|
827
|
+
if (!workflowId)
|
|
828
|
+
return null;
|
|
829
|
+
// The run's own cookie first; the trigger's workflow-scoped one covers the
|
|
830
|
+
// first hop into the run, made before the run existed.
|
|
831
|
+
const candidateNames = [
|
|
832
|
+
getFormAuthCookieName({ workflowId, executionId }),
|
|
833
|
+
getFormAuthCookieName({ workflowId }),
|
|
834
|
+
];
|
|
835
|
+
for (const name of candidateNames) {
|
|
836
|
+
const token = readCookieValue(req, name);
|
|
837
|
+
if (!token)
|
|
838
|
+
continue;
|
|
839
|
+
const user = verifyFormPageAuthCookie(token, workflowId, executionId);
|
|
840
|
+
if (user)
|
|
841
|
+
return user;
|
|
842
|
+
}
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* True when the request also carries a session that belongs to someone other
|
|
847
|
+
* than the form cookie's user — a sign-out and sign-in as a different person in
|
|
848
|
+
* the same browser. The live session wins: the form cookie is dropped and the
|
|
849
|
+
* request re-authenticates through the normal path. An unusable session cookie
|
|
850
|
+
* names nobody, so it leaves the form cookie alone.
|
|
851
|
+
*/
|
|
852
|
+
async function isSessionForDifferentUser(context, formCookieUser) {
|
|
853
|
+
const req = context.getRequestObject();
|
|
854
|
+
const match = (req.headers.cookie ?? '').match(/(?:^|;\s*)n8n-auth=([^;]+)/);
|
|
855
|
+
if (!match)
|
|
856
|
+
return false;
|
|
857
|
+
try {
|
|
858
|
+
const sessionUser = await context.validateCookieAuth(match[1].trim());
|
|
859
|
+
return sessionUser.id !== formCookieUser.id;
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
627
865
|
/**
|
|
628
866
|
* Authenticate an `n8nUserAuth` request via:
|
|
629
867
|
* 1. the `n8n-auth` cookie (sent on top-level GET when the user is logged in), or
|
|
@@ -634,9 +872,9 @@ function clearFormOAuthToken(res, req, resourceUrl) {
|
|
|
634
872
|
* (302 to `/signin` on GET, 401 on POST) and returns `null` — the caller
|
|
635
873
|
* must abort with `noWebhookResponse`.
|
|
636
874
|
*/
|
|
637
|
-
async function authenticateFormUserOrRespond(context,
|
|
875
|
+
async function authenticateFormUserOrRespond(context, runOAuth2Flow = false) {
|
|
638
876
|
const req = context.getRequestObject();
|
|
639
|
-
if (
|
|
877
|
+
if (runOAuth2Flow) {
|
|
640
878
|
const res = context.getResponseObject();
|
|
641
879
|
const url = context.getWebhookResourceUrl('default');
|
|
642
880
|
if (!url) {
|
|
@@ -757,7 +995,7 @@ async function authenticateFormUserOrRespond(context, oauth2Enabled = false) {
|
|
|
757
995
|
}
|
|
758
996
|
const formToken = req.headers['x-auth-token'];
|
|
759
997
|
if (typeof formToken === 'string' && formToken) {
|
|
760
|
-
const user = verifyFormUserAuthToken(formToken, context.getNode());
|
|
998
|
+
const user = verifyFormUserAuthToken(formToken, context.getNode(), context.getExecutionId());
|
|
761
999
|
if (user)
|
|
762
1000
|
return { user, token: null };
|
|
763
1001
|
}
|
|
@@ -778,12 +1016,27 @@ async function authenticateFormUserOrRespond(context, oauth2Enabled = false) {
|
|
|
778
1016
|
* Multi-step Form/Wait nodes inherit `authentication` from the upstream
|
|
779
1017
|
* Form Trigger. This wrapper short-circuits when n8nUserAuth isn't in use.
|
|
780
1018
|
*
|
|
1019
|
+
* A page is reached by navigating to it, which cannot carry an `x-auth-token`
|
|
1020
|
+
* header, so a GET is authenticated from the form page auth cookie first. Every
|
|
1021
|
+
* other request — and any GET whose cookie is missing, doesn't verify, or names
|
|
1022
|
+
* someone other than the session on the same request — falls through to the
|
|
1023
|
+
* session cookie / `x-auth-token` checks, unchanged.
|
|
1024
|
+
*
|
|
1025
|
+
* Form pages never take the OAuth2 branch: the OAuth2 token's audience is the
|
|
1026
|
+
* trigger's URL, which a page's own resource URL is not.
|
|
1027
|
+
*
|
|
781
1028
|
* Returns `{ authedUser }` on success, `{ responded: true }` after sending a
|
|
782
1029
|
* 302/401 on failure, or `{}` if the trigger doesn't require auth.
|
|
783
1030
|
*/
|
|
784
1031
|
async function validateFormPageAuth(context, triggerAuthentication) {
|
|
785
1032
|
if (triggerAuthentication !== 'n8nUserAuth')
|
|
786
1033
|
return {};
|
|
1034
|
+
if (context.getRequestObject().method === 'GET') {
|
|
1035
|
+
const cookieUser = readFormAuthCookieUser(context);
|
|
1036
|
+
if (cookieUser && !(await isSessionForDifferentUser(context, cookieUser))) {
|
|
1037
|
+
return { authedUser: cookieUser };
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
787
1040
|
const { user } = (await authenticateFormUserOrRespond(context, false)) ?? {};
|
|
788
1041
|
return user ? { authedUser: user } : { responded: true };
|
|
789
1042
|
}
|
|
@@ -797,7 +1050,7 @@ async function validateFormPageAuth(context, triggerAuthentication) {
|
|
|
797
1050
|
* links — OUTSIDE the author-scriptable iframe DOM (the security win over
|
|
798
1051
|
* connecting inside the form). It carries a live session, so it refuses framing.
|
|
799
1052
|
*/
|
|
800
|
-
function renderFormShell({ res, req, formTitle, resourceUrl, credentials, submitterEmail, }) {
|
|
1053
|
+
function renderFormShell({ res, req, formTitle, resourceUrl, credentials, submitterEmail, formWaitingPath, }) {
|
|
801
1054
|
// The iframe loads the same form via a real URL (so the form's relative POST
|
|
802
1055
|
// works exactly as it does top-level today) flagged as the inner render.
|
|
803
1056
|
const inner = new URL(buildAbsoluteFormUrl(req));
|
|
@@ -807,6 +1060,8 @@ function renderFormShell({ res, req, formTitle, resourceUrl, credentials, submit
|
|
|
807
1060
|
formTitle,
|
|
808
1061
|
iframeSrc: `${inner.pathname}${inner.search}`,
|
|
809
1062
|
resourceUrl,
|
|
1063
|
+
// The only paths the shell will move its frame to on the form's request.
|
|
1064
|
+
formWaitingPath,
|
|
810
1065
|
...buildFormShellViewModel(credentials, submitterEmail),
|
|
811
1066
|
});
|
|
812
1067
|
}
|
|
@@ -886,7 +1141,7 @@ async function formWebhook(context, authProperty = interfaces_1.FORM_TRIGGER_AUT
|
|
|
886
1141
|
let oAuth2Token;
|
|
887
1142
|
if (node.typeVersion > 1) {
|
|
888
1143
|
if (authentication === 'n8nUserAuth') {
|
|
889
|
-
const { user, token } = (await authenticateFormUserOrRespond(context,
|
|
1144
|
+
const { user, token } = (await authenticateFormUserOrRespond(context, true)) ?? {};
|
|
890
1145
|
if (!user)
|
|
891
1146
|
return { noWebhookResponse: true };
|
|
892
1147
|
authedUser = user;
|
|
@@ -954,6 +1209,14 @@ async function formWebhook(context, authProperty = interfaces_1.FORM_TRIGGER_AUT
|
|
|
954
1209
|
redirectUrl = undefined;
|
|
955
1210
|
responseMode = 'responseNode';
|
|
956
1211
|
}
|
|
1212
|
+
// A multi-page form hops to `/form-waiting/<execution>` by navigating, which
|
|
1213
|
+
// carries neither the `x-auth-token` header nor — from the hosting shell's
|
|
1214
|
+
// opaque-origin frame — the `n8n-auth` session cookie. Give the follow-up pages
|
|
1215
|
+
// their own path-scoped token so they can authenticate the same submitter.
|
|
1216
|
+
if (authentication === 'n8nUserAuth' && authedUser && hasNextPage) {
|
|
1217
|
+
const binding = { workflowId: context.getWorkflow().id };
|
|
1218
|
+
setFormAuthCookie(context, generateFormUserAuthToken(node, authedUser, binding), binding);
|
|
1219
|
+
}
|
|
957
1220
|
// The inner render is the form loaded inside the hosting-shell iframe. Honor the
|
|
958
1221
|
// flag only for iframe navigations (per Sec-Fetch-Dest, absent on older browsers)
|
|
959
1222
|
// so a hand-typed URL can't skip the connect UI; the POST gate enforces regardless.
|
|
@@ -961,15 +1224,11 @@ async function formWebhook(context, authProperty = interfaces_1.FORM_TRIGGER_AUT
|
|
|
961
1224
|
const shellInner = req.query.n8nShellInner === '1' && (secFetchDest === undefined || secFetchDest === 'iframe');
|
|
962
1225
|
// Connect-before-submit: when the workflow needs the submitter's own
|
|
963
1226
|
// (private) credentials, establish their identity from the OAuth2 token and
|
|
964
|
-
// check readiness server-side.
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
if (authentication === 'n8nUserAuth' &&
|
|
969
|
-
authedUser &&
|
|
970
|
-
isFormOAuth2Enabled() &&
|
|
971
|
-
oAuth2Token &&
|
|
972
|
-
!shellInner) {
|
|
1227
|
+
// check readiness server-side. If anything is still missing — and we're not
|
|
1228
|
+
// already rendering the inner iframe — wrap the form in the hosting shell
|
|
1229
|
+
// (form + connect panel, submit disabled). No-op unless OAuth2 form auth and
|
|
1230
|
+
// the dynamic-credentials module are both active.
|
|
1231
|
+
if (authentication === 'n8nUserAuth' && authedUser && oAuth2Token && !shellInner) {
|
|
973
1232
|
// Must name the endpoint actually being served (test vs production) — the
|
|
974
1233
|
// OAuth2 token is bound to that resource, same as in the auth path above.
|
|
975
1234
|
const resourceUrl = trimTrailingSlash(context.getWebhookResourceUrl('default') ?? '');
|
|
@@ -995,6 +1254,7 @@ async function formWebhook(context, authProperty = interfaces_1.FORM_TRIGGER_AUT
|
|
|
995
1254
|
resourceUrl,
|
|
996
1255
|
credentials: credentialStatus.credentials,
|
|
997
1256
|
submitterEmail: authedUser?.email,
|
|
1257
|
+
formWaitingPath: getFormWaitingBasePath(context) ?? undefined,
|
|
998
1258
|
});
|
|
999
1259
|
return { noWebhookResponse: true };
|
|
1000
1260
|
}
|
|
@@ -1003,15 +1263,9 @@ async function formWebhook(context, authProperty = interfaces_1.FORM_TRIGGER_AUT
|
|
|
1003
1263
|
let authToken;
|
|
1004
1264
|
if (node.typeVersion > 1) {
|
|
1005
1265
|
if (authentication === 'n8nUserAuth' && authedUser) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
// POST handler can re-authenticate the user.
|
|
1010
|
-
authToken = generateFormUserAuthToken(node, authedUser);
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
authToken = oAuth2Token;
|
|
1014
|
-
}
|
|
1266
|
+
// Cookies aren't sent on POST from the sandboxed form page (null origin +
|
|
1267
|
+
// SameSite=Lax), so the POST re-authenticates off this token.
|
|
1268
|
+
authToken = oAuth2Token;
|
|
1015
1269
|
}
|
|
1016
1270
|
else {
|
|
1017
1271
|
authToken = await (0, utils_1.generateFormPostBasicAuthToken)(context, authProperty);
|
|
@@ -1043,25 +1297,11 @@ async function formWebhook(context, authProperty = interfaces_1.FORM_TRIGGER_AUT
|
|
|
1043
1297
|
// iframe. It also works off the state at render time, so a required credential can
|
|
1044
1298
|
// be revoked — or the page simply left open — between render and submit. Re-check
|
|
1045
1299
|
// here, after identity establishment and before the execution is enqueued, so a
|
|
1046
|
-
// stale page can't spawn a run that dies at credential resolution.
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1300
|
+
// stale page can't spawn a run that dies at credential resolution. The Form node
|
|
1301
|
+
// runs the same gate on its own POST (Form.node.ts); the Wait node shares
|
|
1302
|
+
// `formWebhook`, so scope this call site to the trigger.
|
|
1049
1303
|
if (node.type === n8n_workflow_1.FORM_TRIGGER_NODE_TYPE) {
|
|
1050
|
-
|
|
1051
|
-
try {
|
|
1052
|
-
readiness = await context.checkTriggerCredentialStatus();
|
|
1053
|
-
}
|
|
1054
|
-
catch (error) {
|
|
1055
|
-
// Fail closed. Throwing here is swallowed by the webhook layer, which then
|
|
1056
|
-
// enqueues the execution anyway — exactly the doomed run we're preventing.
|
|
1057
|
-
context.logger.error('Form submit credential readiness check failed', { error });
|
|
1058
|
-
res.status(503).json({ status: 'credential_readiness_check_failed' });
|
|
1059
|
-
return { noWebhookResponse: true };
|
|
1060
|
-
}
|
|
1061
|
-
if (readiness && !readiness.readyToExecute) {
|
|
1062
|
-
// 428, matching the webhook trigger's gate (webhook-helpers.ts) so both
|
|
1063
|
-
// trigger paths answer an unconnected credential the same way.
|
|
1064
|
-
res.status(428).json((0, n8n_workflow_1.buildCredentialConnectionsRequiredResponse)(readiness));
|
|
1304
|
+
if (await respondIfCredentialsNotReady(context, res)) {
|
|
1065
1305
|
return { noWebhookResponse: true };
|
|
1066
1306
|
}
|
|
1067
1307
|
}
|