Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into N8N-7246-secure-onl…
Browse files Browse the repository at this point in the history
…y-cookie
  • Loading branch information
netroy committed Mar 5, 2024
2 parents da3eef6 + cdec7c9 commit 12cd613
Show file tree
Hide file tree
Showing 26 changed files with 1,084 additions and 746 deletions.
20 changes: 20 additions & 0 deletions cypress/e2e/5-ndv.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,4 +632,24 @@ describe('NDV', () => {
ndv.actions.changeNodeOperation('Update Row');
ndv.getters.resourceLocatorInput('documentId').find('input').should('have.value', TEST_DOC_ID);
});

it('Should open appropriate node creator after clicking on connection hint link', () => {
const nodeCreator = new NodeCreator();
const hintMapper = {
'Memory': 'AI Nodes',
'Output Parser': 'AI Nodes',
'Token Splitter': 'Document Loaders',
'Tool': 'AI Nodes',
'Embeddings': 'Vector Stores',
'Vector Store': 'Retrievers'
}
cy.createFixtureWorkflow('open_node_creator_for_connection.json', `open_node_creator_for_connection ${uuid()}`);

Object.entries(hintMapper).forEach(([node, group]) => {
workflowPage.actions.openNode(node);
cy.get('[data-action=openSelectiveNodeCreator]').contains('Insert one').click();
nodeCreator.getters.activeSubcategory().should('contain', group);
cy.realPress('Escape');
});
})
});
110 changes: 110 additions & 0 deletions cypress/fixtures/open_node_creator_for_connection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"name": "open_node_creator_for_connection",
"nodes": [
{
"parameters": {},
"id": "25ff0c17-7064-4e14-aec6-45c71d63201b",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
740,
520
]
},
{
"parameters": {},
"id": "49f376ca-845b-4737-aac0-073d0e4fa95c",
"name": "Token Splitter",
"type": "@n8n/n8n-nodes-langchain.textSplitterTokenSplitter",
"typeVersion": 1,
"position": [
1180,
540
]
},
{
"parameters": {},
"id": "d1db5111-4b01-4620-8ccb-a16ea576c363",
"name": "Memory",
"type": "@n8n/n8n-nodes-langchain.memoryXata",
"typeVersion": 1.2,
"position": [
940,
540
],
"credentials": {
"xataApi": {
"id": "q1ckaYlHTWCYDtF0",
"name": "Xata Api account"
}
}
},
{
"parameters": {},
"id": "b08b6d3a-bef8-42ac-9cef-ec9d4e5402b1",
"name": "Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.1,
"position": [
1060,
540
]
},
{
"parameters": {},
"id": "ee557938-9cf1-4b78-afef-c783c52fd307",
"name": "Tool",
"type": "@n8n/n8n-nodes-langchain.toolWikipedia",
"typeVersion": 1,
"position": [
1300,
540
]
},
{
"parameters": {
"options": {}
},
"id": "814f2e9c-cc7b-4f3c-89b4-d6eb82bc24df",
"name": "Embeddings",
"type": "@n8n/n8n-nodes-langchain.embeddingsHuggingFaceInference",
"typeVersion": 1,
"position": [
1420,
540
]
},
{
"parameters": {
"tableName": {
"__rl": true,
"mode": "list",
"value": ""
},
"options": {}
},
"id": "e8569b0b-a580-4249-9c5e-f1feed5c644e",
"name": "Vector Store",
"type": "@n8n/n8n-nodes-langchain.vectorStoreSupabase",
"typeVersion": 1,
"position": [
1540,
540
]
}
],
"pinData": {},
"connections": {},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "8e90604c-f7e9-489d-8e43-1cc699b7db04",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
},
"id": "L3tgfoW660UOSuX6",
"tags": []
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,16 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
const options = this.getNodeParameter('options', i, {});
const jsonOutput = this.getNodeParameter('jsonOutput', i, false) as boolean;

if (options.maxTokens !== undefined) {
options.max_tokens = options.maxTokens;
delete options.maxTokens;
}

if (options.topP !== undefined) {
options.top_p = options.topP;
delete options.topP;
}

let response_format;
if (jsonOutput) {
response_format = { type: 'json_object' };
Expand Down
54 changes: 28 additions & 26 deletions packages/@n8n/nodes-langchain/utils/logWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { BaseDocumentLoader } from 'langchain/document_loaders/base';
import type { BaseCallbackConfig, Callbacks } from 'langchain/dist/callbacks/manager';
import { BaseLLM } from 'langchain/llms/base';
import { BaseChatMemory } from 'langchain/memory';
import type { MemoryVariables } from 'langchain/dist/memory/base';
import type { MemoryVariables, OutputValues } from 'langchain/dist/memory/base';
import { BaseRetriever } from 'langchain/schema/retriever';
import type { FormatInstructionsOptions } from 'langchain/schema/output_parser';
import { BaseOutputParser, OutputParserException } from 'langchain/schema/output_parser';
Expand Down Expand Up @@ -148,35 +148,37 @@ export function logWrapper(
arguments: [values],
})) as MemoryVariables;

const chatHistory = (response?.chat_history as BaseMessage[]) ?? response;

executeFunctions.addOutputData(connectionType, index, [
[{ json: { action: 'loadMemoryVariables', response } }],
[{ json: { action: 'loadMemoryVariables', chatHistory } }],
]);
return response;
};
} else if (
prop === 'outputKey' &&
'outputKey' in target &&
target.constructor.name === 'BufferWindowMemory'
) {
connectionType = NodeConnectionType.AiMemory;
const { index } = executeFunctions.addInputData(connectionType, [
[{ json: { action: 'chatHistory' } }],
]);
const response = target[prop];

target.chatHistory
.getMessages()
.then((messages) => {
executeFunctions.addOutputData(NodeConnectionType.AiMemory, index, [
[{ json: { action: 'chatHistory', chatHistory: messages } }],
]);
})
.catch((error: Error) => {
executeFunctions.addOutputData(NodeConnectionType.AiMemory, index, [
[{ json: { action: 'chatHistory', error } }],
]);
});
return response;
} else if (prop === 'saveContext' && 'saveContext' in target) {
return async (input: InputValues, output: OutputValues): Promise<MemoryVariables> => {
connectionType = NodeConnectionType.AiMemory;

const { index } = executeFunctions.addInputData(connectionType, [
[{ json: { action: 'saveContext', input, output } }],
]);

const response = (await callMethodAsync.call(target, {
executeFunctions,
connectionType,
currentNodeRunIndex: index,
method: target[prop],
arguments: [input, output],
})) as MemoryVariables;

const chatHistory = await target.chatHistory.getMessages();

executeFunctions.addOutputData(connectionType, index, [
[{ json: { action: 'saveContext', chatHistory } }],
]);

return response;
};
}
}

Expand Down
14 changes: 12 additions & 2 deletions packages/@n8n/nodes-langchain/utils/sharedFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,15 @@ function determineArticle(nextWord: string): string {
const vowels = /^[aeiouAEIOU]/;
return vowels.test(nextWord) ? 'an' : 'a';
}
const getConnectionParameterString = (connectionType: string) => {
if (connectionType === '') return "data-action-parameter-creatorview='AI'";

return `data-action-parameter-connectiontype='${connectionType}'`;
};
const getAhref = (connectionType: { connection: string; locale: string }) =>
`<a data-action='openSelectiveNodeCreator' data-action-parameter-connectiontype='${connectionType.connection}'>${connectionType.locale}</a>`;
`<a class="test" data-action='openSelectiveNodeCreator'${getConnectionParameterString(
connectionType.connection,
)}'>${connectionType.locale}</a>`;

export function getConnectionHintNoticeField(
connectionTypes: AllowedConnectionTypes[],
Expand All @@ -105,12 +112,15 @@ export function getConnectionHintNoticeField(

if (groupedConnections.size === 1) {
const [[connection, locales]] = Array.from(groupedConnections);

displayName = `This node must be connected to ${determineArticle(locales[0])} ${locales[0]
.toLowerCase()
.replace(
/^ai /,
'AI ',
)}. <a data-action='openSelectiveNodeCreator' data-action-parameter-connectiontype='${connection}'>Insert one</a>`;
)}. <a data-action='openSelectiveNodeCreator' ${getConnectionParameterString(
connection,
)}>Insert one</a>`;
} else {
const ahrefs = Array.from(groupedConnections, ([connection, locales]) => {
// If there are multiple locales, join them with ' or '
Expand Down
47 changes: 21 additions & 26 deletions packages/cli/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Service } from 'typedi';
import type { NextFunction, Response } from 'express';
import { createHash } from 'crypto';
import { JsonWebTokenError, TokenExpiredError, type JwtPayload } from 'jsonwebtoken';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';

import config from '@/config';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time, inTest } from '@/constants';
Expand All @@ -18,16 +18,19 @@ import { UrlService } from '@/services/url.service';
interface AuthJwtPayload {
/** User Id */
id: string;
/** User's email */
email: string | null;
/** SHA-256 hash of bcrypt hash of the user's password */
password: string | null;
/** This hash is derived from email and bcrypt of password */
hash: string;
}

interface IssuedJWT extends AuthJwtPayload {
exp: number;
}

interface PasswordResetToken {
sub: string;
hash: string;
}

@Service()
export class AuthService {
constructor(
Expand Down Expand Up @@ -84,11 +87,9 @@ export class AuthService {
}

issueJWT(user: User) {
const { id, email, password } = user;
const payload: AuthJwtPayload = {
id,
email,
password: password ? this.createPasswordSha(user) : null,
id: user.id,
hash: this.createJWTHash(user),
};
return this.jwtService.sign(payload, {
expiresIn: this.jwtExpiration,
Expand All @@ -105,18 +106,13 @@ export class AuthService {
where: { id: jwtPayload.id },
});

// TODO: include these checks in the cache, to avoid computed this over and over again
const passwordHash = user?.password ? this.createPasswordSha(user) : null;

if (
// If not user is found
!user ||
// or, If the user has been deactivated (i.e. LDAP users)
user.disabled ||
// or, If the password has been updated
jwtPayload.password !== passwordHash ||
// or, If the email has been updated
user.email !== jwtPayload.email
// or, If the email or password has been updated
jwtPayload.hash !== this.createJWTHash(user)
) {
throw new AuthError('Unauthorized');
}
Expand All @@ -130,10 +126,8 @@ export class AuthService {
}

generatePasswordResetToken(user: User, expiresIn = '20m') {
return this.jwtService.sign(
{ sub: user.id, passwordSha: this.createPasswordSha(user) },
{ expiresIn },
);
const payload: PasswordResetToken = { sub: user.id, hash: this.createJWTHash(user) };
return this.jwtService.sign(payload, { expiresIn });
}

generatePasswordResetUrl(user: User) {
Expand All @@ -147,7 +141,7 @@ export class AuthService {
}

async resolvePasswordResetToken(token: string): Promise<User | undefined> {
let decodedToken: JwtPayload & { passwordSha: string };
let decodedToken: PasswordResetToken;
try {
decodedToken = this.jwtService.verify(token);
} catch (e) {
Expand All @@ -172,18 +166,19 @@ export class AuthService {
return;
}

if (this.createPasswordSha(user) !== decodedToken.passwordSha) {
if (decodedToken.hash !== this.createJWTHash(user)) {
this.logger.debug('Password updated since this token was generated');
return;
}

return user;
}

private createPasswordSha({ password }: User) {
return createHash('sha256')
.update(password.slice(password.length / 2))
.digest('hex');
createJWTHash({ email, password }: User) {
const hash = createHash('sha256')
.update(email + ':' + password)
.digest('base64');
return hash.substring(0, 10);
}

/** How many **milliseconds** before expiration should a JWT be renewed */
Expand Down
Loading

0 comments on commit 12cd613

Please sign in to comment.