@n8n/n8n-nodes-langchain
Version:
383 lines (362 loc) • 15 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.escapeForScriptContext = escapeForScriptContext;
exports.getSanitizedInitialMessages = getSanitizedInitialMessages;
exports.getSanitizedI18nConfig = getSanitizedI18nConfig;
exports.getSanitizedCustomCss = getSanitizedCustomCss;
exports.createPage = createPage;
const sanitize_html_1 = __importDefault(require("sanitize-html"));
const SCRIPT_CONTEXT_ESCAPES = {
'<': '\\u003c',
'>': '\\u003e',
'&': '\\u0026',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
};
function escapeForScriptContext(value) {
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (c) => SCRIPT_CONTEXT_ESCAPES[c]);
}
function sanitizeUserInput(input) {
const value = typeof input === 'string' ? input : typeof input === 'number' ? String(input) : '';
let sanitized = (0, sanitize_html_1.default)(value, {
allowedTags: [],
allowedAttributes: {},
});
sanitized = sanitized.replace(/javascript:/gi, '');
sanitized = sanitized.replace(/data:/gi, '');
sanitized = sanitized.replace(/vbscript:/gi, '');
return sanitized;
}
function getSanitizedInitialMessages(initialMessages) {
const sanitizedString = sanitizeUserInput(initialMessages);
return sanitizedString
.split('\n')
.map((line) => line.trim())
.filter((line) => line !== '');
}
function getSanitizedI18nConfig(config) {
const sanitized = {};
for (const [key, value] of Object.entries(config)) {
sanitized[key] = sanitizeUserInput(value);
}
return sanitized;
}
function getSanitizedCustomCss(customCss) {
return customCss.replace(/<\/style/gi, '');
}
const WIDGET_SESSION_ID_KEY = 'n8n-chat/sessionId';
function buildCredentialGateScript(streaming) {
return `
<script>
(function () {
var STREAMING = ${!!streaming};
var NOTICE =
'Not all required accounts are connected, so your message could not be processed. Connect them below, then send it again.';
var nativeFetch = window.fetch.bind(window);
// Both the status and the discriminator must match. A 428 without this body
// belongs to something else - a proxy, or the webhook trigger's own gate.
function isGateBody(body) {
return (
!!body &&
body.status === 'credential_connections_required' &&
Array.isArray(body.credentials)
);
}
// The shell posts readiness in. The widget has its own listener for this
// message, but the published bundle the page loads does not carry it yet, so
// this page does the blocking itself. Accepted only from our own parent: the
// frame is sandboxed with no origin, so \`event.origin\` cannot be checked
// against an allowlist.
//
// Starts ready: until the shell says otherwise, nothing is blocked. That
// matches the widget, which also treats "no status yet" as no gate.
var ready = true;
window.addEventListener('message', function (event) {
if (window.parent === window || event.source !== window.parent) return;
var data = event.data;
if (!data || data.type !== 'n8n-chat:credential-status') return;
if (typeof data.ready === 'boolean') ready = data.ready;
});
// Refuse the submit before the widget sees it, so a message that cannot run is
// never sent and never appears in the transcript. Capture phase, ahead of the
// widget's own handlers. Test mode is blocked too: the server gate refuses
// builders as well, so letting it through only wastes a round trip.
function blockSubmit(event) {
event.preventDefault();
event.stopImmediatePropagation();
if (window.parent === window) return;
// Asks the shell to surface its connect panel. Never a provider popup from
// here - this frame's click doesn't hand the shell a usable gesture (it's
// an opaque origin, by design), so a popup opened off this message would
// just be blocked. The dialog's own Connect button is a real click there.
window.parent.postMessage({ type: 'n8n-chat-connect-requested' }, '*');
}
document.addEventListener(
'keydown',
function (event) {
if (ready) return;
if (event.key !== 'Enter' || event.shiftKey) return;
var target = event.target;
if (!target || String(target.tagName).toLowerCase() !== 'textarea') return;
blockSubmit(event);
},
true
);
document.addEventListener(
'click',
function (event) {
if (ready) return;
var target = event.target;
// The send button's class is part of the widget's published theming
// contract (\`--chat--input--send--button--*\`), so it is a safer hook than
// its markup.
if (!target || !target.closest || !target.closest('.chat-input-send-button')) {
return;
}
blockSubmit(event);
},
true
);
// Only a message send is ours to answer. \`loadPreviousSession\` goes down this
// same \`fetch\`, and replacing its reply with a chat notice would drop the
// restored conversation.
function isMessageSend(init) {
try {
var body = init && init.body;
if (!body) return false;
if (typeof FormData !== 'undefined' && body instanceof FormData) {
return body.get('action') === 'sendMessage';
}
if (typeof body === 'string') {
var parsed = JSON.parse(body);
return !!parsed && parsed.action === 'sendMessage';
}
} catch (error) {}
return false;
}
// Ids only: the shell must not have to trust a name or a URL from this frame.
// targetOrigin '*' because this frame is sandboxed without allow-same-origin
// and cannot know the parent's origin.
function tellShell(credentials) {
if (window.parent === window) return;
window.parent.postMessage(
{
type: 'n8n-chat-credentials-rejected',
// The body lists every required credential, connected ones included.
ids: credentials
.filter(function (credential) {
return credential.credentialStatus !== 'configured';
})
.map(function (credential) {
return credential.credentialId;
}),
},
'*'
);
}
// Answered in place of the rejection so the widget renders an ordinary bot
// message instead of the gate's JSON. The transport decides the shape:
// newline-delimited frames when streaming, a plain body otherwise.
function noticeResponse() {
var frame = { metadata: { nodeId: 'credential-gate' } };
var body = STREAMING
? JSON.stringify(Object.assign({ type: 'begin' }, frame)) +
'\\n' +
JSON.stringify(Object.assign({ type: 'item', content: NOTICE }, frame)) +
'\\n' +
JSON.stringify(Object.assign({ type: 'end' }, frame)) +
'\\n'
: JSON.stringify({ output: NOTICE });
return new Response(body, {
status: 200,
headers: { 'Content-Type': STREAMING ? 'text/plain' : 'application/json' },
});
}
window.fetch = function (input, init) {
return nativeFetch(input, init).then(function (response) {
if (response.status !== 428 || !isMessageSend(init)) return response;
return response
.clone()
.json()
.catch(function () {
return null;
})
.then(function (body) {
if (!isGateBody(body)) return response;
tellShell(body.credentials);
return noticeResponse();
});
});
};
})();
</script>`;
}
const innerBootstrapScript = `
<script>
(function () {
var store = Object.create(null);
var shim = {
getItem: function (key) { return key in store ? store[key] : null; },
setItem: function (key, value) { store[key] = String(value); },
removeItem: function (key) { delete store[key]; },
clear: function () { store = Object.create(null); },
key: function (index) {
var keys = Object.keys(store);
return index < keys.length ? keys[index] : null;
},
get length() { return Object.keys(store).length; },
};
try {
Object.defineProperty(window, 'localStorage', { value: shim, configurable: true });
} catch (error) {}
var match = /(?:^|&)sessionId=([^&]*)/.exec(window.location.hash.slice(1));
window.__n8nChatSessionId = match ? decodeURIComponent(match[1]) : '';
if (window.__n8nChatSessionId) {
shim.setItem(${escapeForScriptContext(WIDGET_SESSION_ID_KEY)}, window.__n8nChatSessionId);
}
// The widget reads this same object on every send, so writing the rotated
// token into it in place is all a refresh has to do. Created here, before
// the module script, so a token that lands early is never dropped.
window.__n8nChatAuthHeaders = {};
// A private channel rather than a window listener: the port is an object in
// this document's realm, so it dies with this document. If author script
// navigates the frame away, the replacement can't obtain the port and the
// shell's next token reaches nothing.
try {
var channel = new MessageChannel();
// Assigning onmessage implicitly starts the port. No sender check is needed
// or possible: a port has one peer, and only the shell holds it.
channel.port1.onmessage = function (event) {
var data = event.data;
if (!data || data.type !== 'n8n-chat-auth-token') return;
if (typeof data.token !== 'string' || !data.token) return;
window.__n8nChatAuthHeaders['x-auth-token'] = data.token;
};
window.parent.postMessage({ type: 'n8n-chat-frame-ready' }, '*', [channel.port2]);
} catch (error) {
// Announce anyway, with no port: that closes the shell's latch, so a document
// loaded here later cannot claim the channel we failed to open.
try { window.parent.postMessage({ type: 'n8n-chat-frame-ready' }, '*'); } catch (postError) {}
}
})();
</script>`;
function createPage({ instanceId, webhookUrl, showWelcomeScreen, loadPreviousSession, i18n: { en }, initialMessages, authentication, allowFileUploads, allowedFilesMimeTypes, customCss, enableStreaming, frameIdentity, }) {
const validAuthenticationOptions = [
'none',
'basicAuth',
'n8nUserAuth',
];
const validLoadPreviousSessionOptions = [
'manually',
'memory',
'notSupported',
];
const sanitizedAuthentication = validAuthenticationOptions.includes(authentication)
? authentication
: 'none';
const sanitizedShowWelcomeScreen = !!showWelcomeScreen;
const sanitizedAllowFileUploads = !!allowFileUploads;
const sanitizedAllowedFilesMimeTypes = sanitizeUserInput(allowedFilesMimeTypes?.toString() ?? '');
const sanitizedCustomCss = getSanitizedCustomCss(customCss?.toString() ?? '');
const sanitizedLoadPreviousSession = validLoadPreviousSessionOptions.includes(loadPreviousSession)
? loadPreviousSession
: 'notSupported';
const sanitizedInitialMessages = getSanitizedInitialMessages(initialMessages);
const sanitizedI18nConfig = getSanitizedI18nConfig(en || {});
const shellInner = frameIdentity !== undefined;
const identityBootstrap = !frameIdentity
? `const authentication = '${sanitizedAuthentication}';
const injectedVisitor = null;
let metadata;
if (injectedVisitor) {
metadata = { user: injectedVisitor };
} else if (authentication === 'n8nUserAuth') {
try {
const response = await fetch('/rest/login', {
method: 'GET',
headers: { 'browser-id': localStorage.getItem('n8n-browserId') }
});
if (response.status !== 200) {
throw new Error('Not logged in');
}
const responseData = await response.json();
metadata = {
user: {
id: responseData.data.id,
firstName: responseData.data.firstName,
lastName: responseData.data.lastName,
email: responseData.data.email,
},
};
} catch (error) {
window.location.href = '/signin?redirect=' + window.location.href;
return;
}
}`
: `const metadata = { user: ${escapeForScriptContext({
id: frameIdentity.visitor.id,
firstName: frameIdentity.visitor.firstName,
lastName: frameIdentity.visitor.lastName,
email: frameIdentity.visitor.email,
})} };`;
const headersBootstrap = frameIdentity
? `const headers = window.__n8nChatAuthHeaders || {};
headers['X-Instance-Id'] = '${instanceId}';
if (!headers['x-auth-token']) headers['x-auth-token'] = ${escapeForScriptContext(frameIdentity.authToken)};
`
: '';
const webhookConfigHeaders = frameIdentity
? 'headers: headers'
: `headers: {
'X-Instance-Id': '${instanceId}',
}`;
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chat</title>
<link href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/@n8n/chat/dist/style.css" rel="stylesheet" />
<style>
html,
body,
#n8n-chat {
width: 100%;
height: 100%;
}
</style>
<style>${sanitizedCustomCss}</style>
</head>
<body>${shellInner ? innerBootstrapScript + buildCredentialGateScript(!!enableStreaming) : ''}
<script type="module">
import { createChat } from 'https://cdn.jsdelivr.net/npm/@n8n/chat/dist/chat.bundle.es.js';
(async function () {
${identityBootstrap}
${headersBootstrap}createChat({
mode: 'fullscreen',
webhookUrl: ${escapeForScriptContext(webhookUrl ?? '')},
showWelcomeScreen: ${sanitizedShowWelcomeScreen},
loadPreviousSession: ${sanitizedLoadPreviousSession !== 'notSupported'},
metadata: metadata,
${shellInner ? 'sessionId: window.__n8nChatSessionId || undefined,' : ''}
webhookConfig: {
${webhookConfigHeaders}
},
allowFileUploads: ${sanitizedAllowFileUploads},
allowedFilesMimeTypes: ${escapeForScriptContext(sanitizedAllowedFilesMimeTypes)},
i18n: {
${Object.keys(sanitizedI18nConfig).length ? `en: ${escapeForScriptContext(sanitizedI18nConfig)},` : ''}
},
${sanitizedInitialMessages.length ? `initialMessages: ${escapeForScriptContext(sanitizedInitialMessages)},` : ''}
enableStreaming: ${!!enableStreaming},
});
})();
</script>
</body>
</html>`;
}
//# sourceMappingURL=templates.js.map