mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Add includeData parameter to GET /credentials
(#12220)
Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
parent
096329db51
commit
f56ad8cf49
|
@ -27,6 +27,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"xss": "catalog:",
|
"xss": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
"zod-class": "0.0.15"
|
"zod-class": "0.0.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { CredentialsGetManyRequestQuery } from '../credentials-get-many-request.dto';
|
||||||
|
|
||||||
|
describe('CredentialsGetManyRequestQuery', () => {
|
||||||
|
describe('should pass validation', () => {
|
||||||
|
it('with empty object', () => {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
const result = CredentialsGetManyRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ field: 'includeScopes', value: 'true' },
|
||||||
|
{ field: 'includeScopes', value: 'false' },
|
||||||
|
{ field: 'includeData', value: 'true' },
|
||||||
|
{ field: 'includeData', value: 'false' },
|
||||||
|
])('with $field set to $value', ({ field, value }) => {
|
||||||
|
const data = { [field]: value };
|
||||||
|
|
||||||
|
const result = CredentialsGetManyRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with both parameters set', () => {
|
||||||
|
const data = {
|
||||||
|
includeScopes: 'true',
|
||||||
|
includeData: 'true',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = CredentialsGetManyRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should fail validation', () => {
|
||||||
|
test.each([
|
||||||
|
{ field: 'includeScopes', value: true },
|
||||||
|
{ field: 'includeScopes', value: false },
|
||||||
|
{ field: 'includeScopes', value: 'invalid' },
|
||||||
|
{ field: 'includeData', value: true },
|
||||||
|
{ field: 'includeData', value: false },
|
||||||
|
{ field: 'includeData', value: 'invalid' },
|
||||||
|
])('with invalid value $value for $field', ({ field, value }) => {
|
||||||
|
const data = { [field]: value };
|
||||||
|
|
||||||
|
const result = CredentialsGetManyRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error?.issues[0].path[0]).toBe(field);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { CredentialsGetOneRequestQuery } from '../credentials-get-one-request.dto';
|
||||||
|
|
||||||
|
describe('CredentialsGetManyRequestQuery', () => {
|
||||||
|
describe('should pass validation', () => {
|
||||||
|
it('with empty object', () => {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
const result = CredentialsGetOneRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
// defaults to false
|
||||||
|
expect(result.data?.includeData).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ field: 'includeData', value: 'true' },
|
||||||
|
{ field: 'includeData', value: 'false' },
|
||||||
|
])('with $field set to $value', ({ field, value }) => {
|
||||||
|
const data = { [field]: value };
|
||||||
|
|
||||||
|
const result = CredentialsGetOneRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with both parameters set', () => {
|
||||||
|
const data = {
|
||||||
|
includeScopes: 'true',
|
||||||
|
includeData: 'true',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = CredentialsGetOneRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should fail validation', () => {
|
||||||
|
test.each([
|
||||||
|
{ field: 'includeData', value: true },
|
||||||
|
{ field: 'includeData', value: false },
|
||||||
|
{ field: 'includeData', value: 'invalid' },
|
||||||
|
])('with invalid value $value for $field', ({ field, value }) => {
|
||||||
|
const data = { [field]: value };
|
||||||
|
|
||||||
|
const result = CredentialsGetOneRequestQuery.safeParse(data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error?.issues[0].path[0]).toBe(field);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Z } from 'zod-class';
|
||||||
|
|
||||||
|
import { booleanFromString } from '../../schemas/booleanFromString';
|
||||||
|
|
||||||
|
export class CredentialsGetManyRequestQuery extends Z.class({
|
||||||
|
/**
|
||||||
|
* Adds the `scopes` field to each credential which includes all scopes the
|
||||||
|
* requesting user has in relation to the credential, e.g.
|
||||||
|
* ['credential:read', 'credential:update']
|
||||||
|
*/
|
||||||
|
includeScopes: booleanFromString.optional(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the decrypted `data` field to each credential.
|
||||||
|
*
|
||||||
|
* It only does this for credentials for which the user has the
|
||||||
|
* `credential:update` scope.
|
||||||
|
*
|
||||||
|
* This switches `includeScopes` to true to be able to check for the scopes
|
||||||
|
*/
|
||||||
|
includeData: booleanFromString.optional(),
|
||||||
|
}) {}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Z } from 'zod-class';
|
||||||
|
|
||||||
|
import { booleanFromString } from '../../schemas/booleanFromString';
|
||||||
|
|
||||||
|
export class CredentialsGetOneRequestQuery extends Z.class({
|
||||||
|
/**
|
||||||
|
* Adds the decrypted `data` field to each credential.
|
||||||
|
*
|
||||||
|
* It only does this for credentials for which the user has the
|
||||||
|
* `credential:update` scope.
|
||||||
|
*/
|
||||||
|
includeData: booleanFromString.optional().default('false'),
|
||||||
|
}) {}
|
|
@ -29,3 +29,5 @@ export { UserUpdateRequestDto } from './user/user-update-request.dto';
|
||||||
export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto';
|
export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto';
|
||||||
|
|
||||||
export { VariableListRequestDto } from './variables/variables-list-request.dto';
|
export { VariableListRequestDto } from './variables/variables-list-request.dto';
|
||||||
|
export { CredentialsGetOneRequestQuery } from './credentials/credentials-get-one-request.dto';
|
||||||
|
export { CredentialsGetManyRequestQuery } from './credentials/credentials-get-many-request.dto';
|
||||||
|
|
3
packages/@n8n/api-types/src/schemas/booleanFromString.ts
Normal file
3
packages/@n8n/api-types/src/schemas/booleanFromString.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const booleanFromString = z.enum(['true', 'false']).transform((value) => value === 'true');
|
|
@ -1,5 +1,6 @@
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { nanoId, date } from 'minifaker';
|
import { nanoId, date } from 'minifaker';
|
||||||
|
import { Credentials } from 'n8n-core';
|
||||||
import { CREDENTIAL_EMPTY_VALUE, type ICredentialType } from 'n8n-workflow';
|
import { CREDENTIAL_EMPTY_VALUE, type ICredentialType } from 'n8n-workflow';
|
||||||
|
|
||||||
import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
|
import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
|
||||||
|
@ -30,6 +31,7 @@ describe('CredentialsService', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const credentialTypes = mock<CredentialTypes>();
|
const credentialTypes = mock<CredentialTypes>();
|
||||||
const service = new CredentialsService(
|
const service = new CredentialsService(
|
||||||
mock(),
|
mock(),
|
||||||
|
@ -61,7 +63,7 @@ describe('CredentialsService', () => {
|
||||||
csrfSecret: 'super-secret',
|
csrfSecret: 'super-secret',
|
||||||
};
|
};
|
||||||
|
|
||||||
credentialTypes.getByName.calledWith(credential.type).mockReturnValue(credType);
|
credentialTypes.getByName.calledWith(credential.type).mockReturnValueOnce(credType);
|
||||||
|
|
||||||
const redactedData = service.redact(decryptedData, credential);
|
const redactedData = service.redact(decryptedData, credential);
|
||||||
|
|
||||||
|
@ -137,4 +139,60 @@ describe('CredentialsService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('decrypt', () => {
|
||||||
|
it('should redact sensitive values by default', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const data = {
|
||||||
|
clientId: 'abc123',
|
||||||
|
clientSecret: 'sensitiveSecret',
|
||||||
|
accessToken: '',
|
||||||
|
oauthTokenData: 'super-secret',
|
||||||
|
csrfSecret: 'super-secret',
|
||||||
|
};
|
||||||
|
const credential = mock<CredentialsEntity>({
|
||||||
|
id: '123',
|
||||||
|
name: 'Test Credential',
|
||||||
|
type: 'oauth2',
|
||||||
|
});
|
||||||
|
jest.spyOn(Credentials.prototype, 'getData').mockReturnValueOnce(data);
|
||||||
|
credentialTypes.getByName.calledWith(credential.type).mockReturnValueOnce(credType);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const redactedData = service.decrypt(credential);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(redactedData).toEqual({
|
||||||
|
clientId: 'abc123',
|
||||||
|
clientSecret: CREDENTIAL_BLANKING_VALUE,
|
||||||
|
accessToken: CREDENTIAL_EMPTY_VALUE,
|
||||||
|
oauthTokenData: CREDENTIAL_BLANKING_VALUE,
|
||||||
|
csrfSecret: CREDENTIAL_BLANKING_VALUE,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sensitive values if `includeRawData` is true', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const data = {
|
||||||
|
clientId: 'abc123',
|
||||||
|
clientSecret: 'sensitiveSecret',
|
||||||
|
accessToken: '',
|
||||||
|
oauthTokenData: 'super-secret',
|
||||||
|
csrfSecret: 'super-secret',
|
||||||
|
};
|
||||||
|
const credential = mock<CredentialsEntity>({
|
||||||
|
id: '123',
|
||||||
|
name: 'Test Credential',
|
||||||
|
type: 'oauth2',
|
||||||
|
});
|
||||||
|
jest.spyOn(Credentials.prototype, 'getData').mockReturnValueOnce(data);
|
||||||
|
credentialTypes.getByName.calledWith(credential.type).mockReturnValueOnce(credType);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const redactedData = service.decrypt(credential, true);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(redactedData).toEqual(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { CredentialsGetManyRequestQuery, CredentialsGetOneRequestQuery } from '@n8n/api-types';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
|
@ -19,6 +20,7 @@ import {
|
||||||
RestController,
|
RestController,
|
||||||
ProjectScope,
|
ProjectScope,
|
||||||
} from '@/decorators';
|
} from '@/decorators';
|
||||||
|
import { Param, Query } from '@/decorators/args';
|
||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
@ -49,10 +51,15 @@ export class CredentialsController {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/', { middlewares: listQueryMiddleware })
|
@Get('/', { middlewares: listQueryMiddleware })
|
||||||
async getMany(req: CredentialRequest.GetMany) {
|
async getMany(
|
||||||
|
req: CredentialRequest.GetMany,
|
||||||
|
_res: unknown,
|
||||||
|
@Query query: CredentialsGetManyRequestQuery,
|
||||||
|
) {
|
||||||
const credentials = await this.credentialsService.getMany(req.user, {
|
const credentials = await this.credentialsService.getMany(req.user, {
|
||||||
listQueryOptions: req.listQueryOptions,
|
listQueryOptions: req.listQueryOptions,
|
||||||
includeScopes: req.query.includeScopes,
|
includeScopes: query.includeScopes,
|
||||||
|
includeData: query.includeData,
|
||||||
});
|
});
|
||||||
credentials.forEach((c) => {
|
credentials.forEach((c) => {
|
||||||
// @ts-expect-error: This is to emulate the old behavior of removing the shared
|
// @ts-expect-error: This is to emulate the old behavior of removing the shared
|
||||||
|
@ -82,21 +89,22 @@ export class CredentialsController {
|
||||||
|
|
||||||
@Get('/:credentialId')
|
@Get('/:credentialId')
|
||||||
@ProjectScope('credential:read')
|
@ProjectScope('credential:read')
|
||||||
async getOne(req: CredentialRequest.Get) {
|
async getOne(
|
||||||
|
req: CredentialRequest.Get,
|
||||||
|
_res: unknown,
|
||||||
|
@Param('credentialId') credentialId: string,
|
||||||
|
@Query query: CredentialsGetOneRequestQuery,
|
||||||
|
) {
|
||||||
const { shared, ...credential } = this.license.isSharingEnabled()
|
const { shared, ...credential } = this.license.isSharingEnabled()
|
||||||
? await this.enterpriseCredentialsService.getOne(
|
? await this.enterpriseCredentialsService.getOne(
|
||||||
req.user,
|
req.user,
|
||||||
req.params.credentialId,
|
credentialId,
|
||||||
// TODO: editor-ui is always sending this, maybe we can just rely on the
|
// TODO: editor-ui is always sending this, maybe we can just rely on the
|
||||||
// the scopes and always decrypt the data if the user has the permissions
|
// the scopes and always decrypt the data if the user has the permissions
|
||||||
// to do so.
|
// to do so.
|
||||||
req.query.includeData === 'true',
|
query.includeData,
|
||||||
)
|
)
|
||||||
: await this.credentialsService.getOne(
|
: await this.credentialsService.getOne(req.user, credentialId, query.includeData);
|
||||||
req.user,
|
|
||||||
req.params.credentialId,
|
|
||||||
req.query.includeData === 'true',
|
|
||||||
);
|
|
||||||
|
|
||||||
const scopes = await this.credentialsService.getCredentialScopes(
|
const scopes = await this.credentialsService.getCredentialScopes(
|
||||||
req.user,
|
req.user,
|
||||||
|
|
|
@ -87,10 +87,7 @@ export class EnterpriseCredentialsService {
|
||||||
if (credential) {
|
if (credential) {
|
||||||
// Decrypt the data if we found the credential with the `credential:update`
|
// Decrypt the data if we found the credential with the `credential:update`
|
||||||
// scope.
|
// scope.
|
||||||
decryptedData = this.credentialsService.redact(
|
decryptedData = this.credentialsService.decrypt(credential);
|
||||||
this.credentialsService.decrypt(credential),
|
|
||||||
credential,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise try to find them with only the `credential:read` scope. In
|
// Otherwise try to find them with only the `credential:read` scope. In
|
||||||
// that case we return them without the decrypted data.
|
// that case we return them without the decrypted data.
|
||||||
|
|
|
@ -38,6 +38,7 @@ import type { CredentialRequest, ListQuery } from '@/requests';
|
||||||
import { CredentialsTester } from '@/services/credentials-tester.service';
|
import { CredentialsTester } from '@/services/credentials-tester.service';
|
||||||
import { OwnershipService } from '@/services/ownership.service';
|
import { OwnershipService } from '@/services/ownership.service';
|
||||||
import { ProjectService } from '@/services/project.service.ee';
|
import { ProjectService } from '@/services/project.service.ee';
|
||||||
|
import type { ScopesField } from '@/services/role.service';
|
||||||
import { RoleService } from '@/services/role.service';
|
import { RoleService } from '@/services/role.service';
|
||||||
|
|
||||||
export type CredentialsGetSharedOptions =
|
export type CredentialsGetSharedOptions =
|
||||||
|
@ -62,33 +63,47 @@ export class CredentialsService {
|
||||||
|
|
||||||
async getMany(
|
async getMany(
|
||||||
user: User,
|
user: User,
|
||||||
options: {
|
{
|
||||||
listQueryOptions?: ListQuery.Options;
|
listQueryOptions = {},
|
||||||
includeScopes?: string;
|
includeScopes = false,
|
||||||
|
includeData = false,
|
||||||
|
}: {
|
||||||
|
listQueryOptions?: ListQuery.Options & { includeData?: boolean };
|
||||||
|
includeScopes?: boolean;
|
||||||
|
includeData?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const returnAll = user.hasGlobalScope('credential:list');
|
const returnAll = user.hasGlobalScope('credential:list');
|
||||||
const isDefaultSelect = !options.listQueryOptions?.select;
|
const isDefaultSelect = !listQueryOptions.select;
|
||||||
|
|
||||||
|
if (includeData) {
|
||||||
|
// We need the scopes to check if we're allowed to include the decrypted
|
||||||
|
// data.
|
||||||
|
// Only if the user has the `credential:update` scope the user is allowed
|
||||||
|
// to get the data.
|
||||||
|
includeScopes = true;
|
||||||
|
listQueryOptions.includeData = true;
|
||||||
|
}
|
||||||
|
|
||||||
let projectRelations: ProjectRelation[] | undefined = undefined;
|
let projectRelations: ProjectRelation[] | undefined = undefined;
|
||||||
if (options.includeScopes) {
|
if (includeScopes) {
|
||||||
projectRelations = await this.projectService.getProjectRelationsForUser(user);
|
projectRelations = await this.projectService.getProjectRelationsForUser(user);
|
||||||
if (options.listQueryOptions?.filter?.projectId && user.hasGlobalScope('credential:list')) {
|
if (listQueryOptions.filter?.projectId && user.hasGlobalScope('credential:list')) {
|
||||||
// Only instance owners and admins have the credential:list scope
|
// Only instance owners and admins have the credential:list scope
|
||||||
// Those users should be able to use _all_ credentials within their workflows.
|
// Those users should be able to use _all_ credentials within their workflows.
|
||||||
// TODO: Change this so we filter by `workflowId` in this case. Require a slight FE change
|
// TODO: Change this so we filter by `workflowId` in this case. Require a slight FE change
|
||||||
const projectRelation = projectRelations.find(
|
const projectRelation = projectRelations.find(
|
||||||
(relation) => relation.projectId === options.listQueryOptions?.filter?.projectId,
|
(relation) => relation.projectId === listQueryOptions.filter?.projectId,
|
||||||
);
|
);
|
||||||
if (projectRelation?.role === 'project:personalOwner') {
|
if (projectRelation?.role === 'project:personalOwner') {
|
||||||
// Will not affect team projects as these have admins, not owners.
|
// Will not affect team projects as these have admins, not owners.
|
||||||
delete options.listQueryOptions?.filter?.projectId;
|
delete listQueryOptions.filter?.projectId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnAll) {
|
if (returnAll) {
|
||||||
let credentials = await this.credentialsRepository.findMany(options.listQueryOptions);
|
let credentials = await this.credentialsRepository.findMany(listQueryOptions);
|
||||||
|
|
||||||
if (isDefaultSelect) {
|
if (isDefaultSelect) {
|
||||||
// Since we're filtering using project ID as part of the relation,
|
// Since we're filtering using project ID as part of the relation,
|
||||||
|
@ -96,7 +111,7 @@ export class CredentialsService {
|
||||||
// it's shared to a project, it won't be able to find the home project.
|
// it's shared to a project, it won't be able to find the home project.
|
||||||
// To solve this, we have to get all the relation now, even though
|
// To solve this, we have to get all the relation now, even though
|
||||||
// we're deleting them later.
|
// we're deleting them later.
|
||||||
if ((options.listQueryOptions?.filter?.shared as { projectId?: string })?.projectId) {
|
if ((listQueryOptions.filter?.shared as { projectId?: string })?.projectId) {
|
||||||
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
||||||
credentials.map((c) => c.id),
|
credentials.map((c) => c.id),
|
||||||
);
|
);
|
||||||
|
@ -107,23 +122,32 @@ export class CredentialsService {
|
||||||
credentials = credentials.map((c) => this.ownershipService.addOwnedByAndSharedWith(c));
|
credentials = credentials.map((c) => this.ownershipService.addOwnedByAndSharedWith(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.includeScopes) {
|
if (includeScopes) {
|
||||||
credentials = credentials.map((c) =>
|
credentials = credentials.map((c) =>
|
||||||
this.roleService.addScopes(c, user, projectRelations!),
|
this.roleService.addScopes(c, user, projectRelations!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeData) {
|
||||||
|
credentials = credentials.map((c: CredentialsEntity & ScopesField) => {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
data: c.scopes.includes('credential:update') ? this.decrypt(c) : undefined,
|
||||||
|
} as unknown as CredentialsEntity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the workflow is part of a personal project we want to show the credentials the user making the request has access to, not the credentials the user owning the workflow has access to.
|
// If the workflow is part of a personal project we want to show the
|
||||||
if (typeof options.listQueryOptions?.filter?.projectId === 'string') {
|
// credentials the user making the request has access to, not the
|
||||||
const project = await this.projectService.getProject(
|
// credentials the user owning the workflow has access to.
|
||||||
options.listQueryOptions.filter.projectId,
|
if (typeof listQueryOptions.filter?.projectId === 'string') {
|
||||||
);
|
const project = await this.projectService.getProject(listQueryOptions.filter.projectId);
|
||||||
if (project?.type === 'personal') {
|
if (project?.type === 'personal') {
|
||||||
const currentUsersPersonalProject = await this.projectService.getPersonalProject(user);
|
const currentUsersPersonalProject = await this.projectService.getPersonalProject(user);
|
||||||
options.listQueryOptions.filter.projectId = currentUsersPersonalProject?.id;
|
listQueryOptions.filter.projectId = currentUsersPersonalProject?.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +156,7 @@ export class CredentialsService {
|
||||||
});
|
});
|
||||||
|
|
||||||
let credentials = await this.credentialsRepository.findMany(
|
let credentials = await this.credentialsRepository.findMany(
|
||||||
options.listQueryOptions,
|
listQueryOptions,
|
||||||
ids, // only accessible credentials
|
ids, // only accessible credentials
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -142,7 +166,7 @@ export class CredentialsService {
|
||||||
// it's shared to a project, it won't be able to find the home project.
|
// it's shared to a project, it won't be able to find the home project.
|
||||||
// To solve this, we have to get all the relation now, even though
|
// To solve this, we have to get all the relation now, even though
|
||||||
// we're deleting them later.
|
// we're deleting them later.
|
||||||
if ((options.listQueryOptions?.filter?.shared as { projectId?: string })?.projectId) {
|
if ((listQueryOptions.filter?.shared as { projectId?: string })?.projectId) {
|
||||||
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
||||||
credentials.map((c) => c.id),
|
credentials.map((c) => c.id),
|
||||||
);
|
);
|
||||||
|
@ -154,10 +178,19 @@ export class CredentialsService {
|
||||||
credentials = credentials.map((c) => this.ownershipService.addOwnedByAndSharedWith(c));
|
credentials = credentials.map((c) => this.ownershipService.addOwnedByAndSharedWith(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.includeScopes) {
|
if (includeScopes) {
|
||||||
credentials = credentials.map((c) => this.roleService.addScopes(c, user, projectRelations!));
|
credentials = credentials.map((c) => this.roleService.addScopes(c, user, projectRelations!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeData) {
|
||||||
|
credentials = credentials.map((c: CredentialsEntity & ScopesField) => {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
data: c.scopes.includes('credential:update') ? this.decrypt(c) : undefined,
|
||||||
|
} as unknown as CredentialsEntity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,9 +341,18 @@ export class CredentialsService {
|
||||||
return newCredentialData;
|
return newCredentialData;
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypt(credential: CredentialsEntity) {
|
/**
|
||||||
|
* Decrypts the credentials data and redacts the content by default.
|
||||||
|
*
|
||||||
|
* If `includeRawData` is set to true it will not redact the data.
|
||||||
|
*/
|
||||||
|
decrypt(credential: CredentialsEntity, includeRawData = false) {
|
||||||
const coreCredential = createCredentialsFromCredentialsEntity(credential);
|
const coreCredential = createCredentialsFromCredentialsEntity(credential);
|
||||||
return coreCredential.getData();
|
const data = coreCredential.getData();
|
||||||
|
if (includeRawData) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return this.redact(data, credential);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(credentialId: string, newCredentialData: ICredentialsDb) {
|
async update(credentialId: string, newCredentialData: ICredentialsDb) {
|
||||||
|
@ -500,7 +542,7 @@ export class CredentialsService {
|
||||||
if (sharing) {
|
if (sharing) {
|
||||||
// Decrypt the data if we found the credential with the `credential:update`
|
// Decrypt the data if we found the credential with the `credential:update`
|
||||||
// scope.
|
// scope.
|
||||||
decryptedData = this.redact(this.decrypt(sharing.credentials), sharing.credentials);
|
decryptedData = this.decrypt(sharing.credentials);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise try to find them with only the `credential:read` scope. In
|
// Otherwise try to find them with only the `credential:read` scope. In
|
||||||
// that case we return them without the decrypted data.
|
// that case we return them without the decrypted data.
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
|
import { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
||||||
|
import { mockEntityManager } from '@test/mocking';
|
||||||
|
|
||||||
|
import { CredentialsRepository } from '../credentials.repository';
|
||||||
|
|
||||||
|
const entityManager = mockEntityManager(CredentialsEntity);
|
||||||
|
const repository = Container.get(CredentialsRepository);
|
||||||
|
|
||||||
|
describe('findMany', () => {
|
||||||
|
const credentialsId = 'cred_123';
|
||||||
|
const credential = mock<CredentialsEntity>({ id: credentialsId });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('return `data` property if `includeData:true` and select is using the record syntax', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
entityManager.find.mockResolvedValueOnce([credential]);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const credentials = await repository.findMany({ includeData: true, select: { id: true } });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(credentials).toHaveLength(1);
|
||||||
|
expect(credentials[0]).toHaveProperty('data');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('return `data` property if `includeData:true` and select is using the record syntax', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
entityManager.find.mockResolvedValueOnce([credential]);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const credentials = await repository.findMany({
|
||||||
|
includeData: true,
|
||||||
|
//TODO: fix this
|
||||||
|
// The function's type does not support this but this is what it
|
||||||
|
// actually gets from the service because the middlewares are typed
|
||||||
|
// loosely.
|
||||||
|
select: ['id'] as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(credentials).toHaveLength(1);
|
||||||
|
expect(credentials[0]).toHaveProperty('data');
|
||||||
|
});
|
||||||
|
});
|
|
@ -25,7 +25,10 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMany(listQueryOptions?: ListQuery.Options, credentialIds?: string[]) {
|
async findMany(
|
||||||
|
listQueryOptions?: ListQuery.Options & { includeData?: boolean },
|
||||||
|
credentialIds?: string[],
|
||||||
|
) {
|
||||||
const findManyOptions = this.toFindManyOptions(listQueryOptions);
|
const findManyOptions = this.toFindManyOptions(listQueryOptions);
|
||||||
|
|
||||||
if (credentialIds) {
|
if (credentialIds) {
|
||||||
|
@ -35,7 +38,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
||||||
return await this.find(findManyOptions);
|
return await this.find(findManyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toFindManyOptions(listQueryOptions?: ListQuery.Options) {
|
private toFindManyOptions(listQueryOptions?: ListQuery.Options & { includeData?: boolean }) {
|
||||||
const findManyOptions: FindManyOptions<CredentialsEntity> = {};
|
const findManyOptions: FindManyOptions<CredentialsEntity> = {};
|
||||||
|
|
||||||
type Select = Array<keyof CredentialsEntity>;
|
type Select = Array<keyof CredentialsEntity>;
|
||||||
|
@ -74,6 +77,14 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
||||||
findManyOptions.relations = defaultRelations;
|
findManyOptions.relations = defaultRelations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (listQueryOptions.includeData) {
|
||||||
|
if (Array.isArray(findManyOptions.select)) {
|
||||||
|
findManyOptions.select.push('data');
|
||||||
|
} else {
|
||||||
|
findManyOptions.select.data = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return findManyOptions;
|
return findManyOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,6 +226,161 @@ describe('GET /credentials', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return data when ?includeData=true', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const [actor, otherMember] = await createManyUsers(2, {
|
||||||
|
role: 'global:member',
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamProjectViewer = await createTeamProject(undefined);
|
||||||
|
await linkUserToProject(actor, teamProjectViewer, 'project:viewer');
|
||||||
|
const teamProjectEditor = await createTeamProject(undefined);
|
||||||
|
await linkUserToProject(actor, teamProjectEditor, 'project:editor');
|
||||||
|
|
||||||
|
const [
|
||||||
|
// should have data
|
||||||
|
ownedCredential,
|
||||||
|
// should not have
|
||||||
|
sharedCredential,
|
||||||
|
// should not have data
|
||||||
|
teamCredentialAsViewer,
|
||||||
|
// should have data
|
||||||
|
teamCredentialAsEditor,
|
||||||
|
] = await Promise.all([
|
||||||
|
saveCredential(randomCredentialPayload(), { user: actor, role: 'credential:owner' }),
|
||||||
|
saveCredential(randomCredentialPayload(), { user: otherMember, role: 'credential:owner' }),
|
||||||
|
saveCredential(randomCredentialPayload(), {
|
||||||
|
project: teamProjectViewer,
|
||||||
|
role: 'credential:owner',
|
||||||
|
}),
|
||||||
|
saveCredential(randomCredentialPayload(), {
|
||||||
|
project: teamProjectEditor,
|
||||||
|
role: 'credential:owner',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
await shareCredentialWithUsers(sharedCredential, [actor]);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(actor)
|
||||||
|
.get('/credentials')
|
||||||
|
.query({ includeData: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data.length).toBe(4);
|
||||||
|
|
||||||
|
const creds = response.body.data as Array<Credentials & { scopes: Scope[] }>;
|
||||||
|
const ownedCred = creds.find((c) => c.id === ownedCredential.id)!;
|
||||||
|
const sharedCred = creds.find((c) => c.id === sharedCredential.id)!;
|
||||||
|
const teamCredAsViewer = creds.find((c) => c.id === teamCredentialAsViewer.id)!;
|
||||||
|
const teamCredAsEditor = creds.find((c) => c.id === teamCredentialAsEditor.id)!;
|
||||||
|
|
||||||
|
expect(ownedCred.id).toBe(ownedCredential.id);
|
||||||
|
expect(ownedCred.data).toBeDefined();
|
||||||
|
expect(ownedCred.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:update',
|
||||||
|
'credential:share',
|
||||||
|
'credential:delete',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sharedCred.id).toBe(sharedCredential.id);
|
||||||
|
expect(sharedCred.data).not.toBeDefined();
|
||||||
|
expect(sharedCred.scopes).toEqual(['credential:read'].sort());
|
||||||
|
|
||||||
|
expect(teamCredAsViewer.id).toBe(teamCredentialAsViewer.id);
|
||||||
|
expect(teamCredAsViewer.data).not.toBeDefined();
|
||||||
|
expect(teamCredAsViewer.scopes).toEqual(['credential:read'].sort());
|
||||||
|
|
||||||
|
expect(teamCredAsEditor.id).toBe(teamCredentialAsEditor.id);
|
||||||
|
expect(teamCredAsEditor.data).toBeDefined();
|
||||||
|
expect(teamCredAsEditor.scopes).toEqual(
|
||||||
|
['credential:read', 'credential:update', 'credential:delete'].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return data when ?includeData=true for owners', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const teamProjectViewer = await createTeamProject(undefined);
|
||||||
|
|
||||||
|
const [
|
||||||
|
// should have data
|
||||||
|
ownedCredential,
|
||||||
|
// should have data
|
||||||
|
sharedCredential,
|
||||||
|
// should have data
|
||||||
|
teamCredentialAsViewer,
|
||||||
|
] = await Promise.all([
|
||||||
|
saveCredential(randomCredentialPayload(), { user: owner, role: 'credential:owner' }),
|
||||||
|
saveCredential(randomCredentialPayload(), { user: member, role: 'credential:owner' }),
|
||||||
|
saveCredential(randomCredentialPayload(), {
|
||||||
|
project: teamProjectViewer,
|
||||||
|
role: 'credential:owner',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(owner)
|
||||||
|
.get('/credentials')
|
||||||
|
.query({ includeData: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data.length).toBe(3);
|
||||||
|
|
||||||
|
const creds = response.body.data as Array<Credentials & { scopes: Scope[] }>;
|
||||||
|
const ownedCred = creds.find((c) => c.id === ownedCredential.id)!;
|
||||||
|
const sharedCred = creds.find((c) => c.id === sharedCredential.id)!;
|
||||||
|
const teamCredAsViewer = creds.find((c) => c.id === teamCredentialAsViewer.id)!;
|
||||||
|
|
||||||
|
expect(ownedCred.id).toBe(ownedCredential.id);
|
||||||
|
expect(ownedCred.data).toBeDefined();
|
||||||
|
expect(ownedCred.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:update',
|
||||||
|
'credential:share',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:create',
|
||||||
|
'credential:list',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sharedCred.id).toBe(sharedCredential.id);
|
||||||
|
expect(sharedCred.data).toBeDefined();
|
||||||
|
expect(sharedCred.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:update',
|
||||||
|
'credential:share',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:create',
|
||||||
|
'credential:list',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(teamCredAsViewer.id).toBe(teamCredentialAsViewer.id);
|
||||||
|
expect(teamCredAsViewer.data).toBeDefined();
|
||||||
|
expect(teamCredAsViewer.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:update',
|
||||||
|
'credential:share',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:create',
|
||||||
|
'credential:list',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('should return', () => {
|
describe('should return', () => {
|
||||||
test('all credentials for owner', async () => {
|
test('all credentials for owner', async () => {
|
||||||
const { id: id1 } = await saveCredential(payload(), {
|
const { id: id1 } = await saveCredential(payload(), {
|
||||||
|
|
|
@ -272,8 +272,8 @@ importers:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.23.8
|
version: 3.23.8
|
||||||
zod-class:
|
zod-class:
|
||||||
specifier: 0.0.15
|
specifier: 0.0.16
|
||||||
version: 0.0.15(zod@3.23.8)
|
version: 0.0.16(zod@3.23.8)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@n8n/config':
|
'@n8n/config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
@ -434,7 +434,7 @@ importers:
|
||||||
version: 3.666.0(@aws-sdk/client-sts@3.666.0)
|
version: 3.666.0(@aws-sdk/client-sts@3.666.0)
|
||||||
'@getzep/zep-cloud':
|
'@getzep/zep-cloud':
|
||||||
specifier: 1.0.12
|
specifier: 1.0.12
|
||||||
version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))
|
version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))
|
||||||
'@getzep/zep-js':
|
'@getzep/zep-js':
|
||||||
specifier: 0.9.0
|
specifier: 0.9.0
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
|
@ -461,7 +461,7 @@ importers:
|
||||||
version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
|
version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
|
||||||
'@langchain/community':
|
'@langchain/community':
|
||||||
specifier: 0.3.15
|
specifier: 0.3.15
|
||||||
version: 0.3.15(vc5hvyy27o4cmm4jplsptc2fqm)
|
version: 0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe)
|
||||||
'@langchain/core':
|
'@langchain/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
||||||
|
@ -548,7 +548,7 @@ importers:
|
||||||
version: 23.0.1
|
version: 23.0.1
|
||||||
langchain:
|
langchain:
|
||||||
specifier: 0.3.6
|
specifier: 0.3.6
|
||||||
version: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)
|
version: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)
|
||||||
lodash:
|
lodash:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
@ -13393,8 +13393,8 @@ packages:
|
||||||
resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==}
|
resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
zod-class@0.0.15:
|
zod-class@0.0.16:
|
||||||
resolution: {integrity: sha512-CD5B4e9unKPj1hiy7JOSwRV01WqbEBkFOlhws0C9s9wB0FSpECOnlKXOAkjo9tKYX2enQsXWyyOIBNPPNUHWRA==}
|
resolution: {integrity: sha512-3A1l81VEUOxvSTGoNPsU4fTUY9CKin/HSySnXT3bIc+TJTDGCPbzSPE8W1VvwXqyzHEIWK608eFZja2uew9Ivw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3
|
zod: ^3
|
||||||
|
|
||||||
|
@ -15690,7 +15690,7 @@ snapshots:
|
||||||
'@gar/promisify@1.1.3':
|
'@gar/promisify@1.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))':
|
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))':
|
||||||
dependencies:
|
dependencies:
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
node-fetch: 2.7.0(encoding@0.1.13)
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
|
@ -15699,7 +15699,7 @@ snapshots:
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
'@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
||||||
langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)
|
langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
@ -16163,7 +16163,7 @@ snapshots:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
'@langchain/community@0.3.15(vc5hvyy27o4cmm4jplsptc2fqm)':
|
'@langchain/community@0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ibm-cloud/watsonx-ai': 1.1.2
|
'@ibm-cloud/watsonx-ai': 1.1.2
|
||||||
'@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
'@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
||||||
|
@ -16173,7 +16173,7 @@ snapshots:
|
||||||
flat: 5.0.2
|
flat: 5.0.2
|
||||||
ibm-cloud-sdk-core: 5.1.0
|
ibm-cloud-sdk-core: 5.1.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)
|
langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)
|
||||||
langsmith: 0.2.3(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
langsmith: 0.2.3(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
||||||
uuid: 10.0.0
|
uuid: 10.0.0
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
@ -16186,7 +16186,7 @@ snapshots:
|
||||||
'@aws-sdk/client-s3': 3.666.0
|
'@aws-sdk/client-s3': 3.666.0
|
||||||
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
||||||
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
||||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))
|
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))
|
||||||
'@getzep/zep-js': 0.9.0
|
'@getzep/zep-js': 0.9.0
|
||||||
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
||||||
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
||||||
|
@ -19470,14 +19470,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
axios@1.7.4(debug@4.3.7):
|
|
||||||
dependencies:
|
|
||||||
follow-redirects: 1.15.6(debug@4.3.7)
|
|
||||||
form-data: 4.0.0
|
|
||||||
proxy-from-env: 1.1.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
|
|
||||||
axios@1.7.7:
|
axios@1.7.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.6(debug@4.3.6)
|
follow-redirects: 1.15.6(debug@4.3.6)
|
||||||
|
@ -22329,7 +22321,7 @@ snapshots:
|
||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 18.16.16
|
'@types/node': 18.16.16
|
||||||
'@types/tough-cookie': 4.0.2
|
'@types/tough-cookie': 4.0.2
|
||||||
axios: 1.7.4(debug@4.3.7)
|
axios: 1.7.4
|
||||||
camelcase: 6.3.0
|
camelcase: 6.3.0
|
||||||
debug: 4.3.7
|
debug: 4.3.7
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.5
|
||||||
|
@ -22339,7 +22331,7 @@ snapshots:
|
||||||
isstream: 0.1.2
|
isstream: 0.1.2
|
||||||
jsonwebtoken: 9.0.2
|
jsonwebtoken: 9.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
retry-axios: 2.6.0(axios@1.7.4)
|
retry-axios: 2.6.0(axios@1.7.4(debug@4.3.7))
|
||||||
tough-cookie: 4.1.3
|
tough-cookie: 4.1.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -23340,7 +23332,7 @@ snapshots:
|
||||||
|
|
||||||
kuler@2.0.0: {}
|
kuler@2.0.0: {}
|
||||||
|
|
||||||
langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu):
|
langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
'@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
|
||||||
'@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
|
'@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
|
||||||
|
@ -25710,7 +25702,7 @@ snapshots:
|
||||||
|
|
||||||
ret@0.1.15: {}
|
ret@0.1.15: {}
|
||||||
|
|
||||||
retry-axios@2.6.0(axios@1.7.4):
|
retry-axios@2.6.0(axios@1.7.4(debug@4.3.7)):
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.7.4
|
axios: 1.7.4
|
||||||
|
|
||||||
|
@ -27783,7 +27775,7 @@ snapshots:
|
||||||
property-expr: 2.0.5
|
property-expr: 2.0.5
|
||||||
toposort: 2.0.2
|
toposort: 2.0.2
|
||||||
|
|
||||||
zod-class@0.0.15(zod@3.23.8):
|
zod-class@0.0.16(zod@3.23.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 4.26.1
|
type-fest: 4.26.1
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
Loading…
Reference in a new issue