Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/pieces/community/ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/piece-ai",
"version": "0.0.9",
"version": "0.1.0",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
Expand Down
6 changes: 5 additions & 1 deletion packages/pieces/community/amazon-s3/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"name": "@activepieces/piece-amazon-s3",
"version": "0.4.0"
"version": "0.5.0",
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.0.0",
"openpgp": "^6.3.0"
}
}
3 changes: 2 additions & 1 deletion packages/pieces/community/amazon-s3/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { generateSignedUrl } from './lib/actions/generate-signed-url';
import { moveFile } from './lib/actions/move-file';
import { deleteFile } from './lib/actions/delete-file';
import { listFiles } from './lib/actions/list-files';
import { decryptPgpFile } from './lib/actions/decrypt-pgp-file';

const description = `
This piece allows you to upload files to Amazon S3 or other S3 compatible services.
Expand Down Expand Up @@ -198,6 +199,6 @@ export const amazonS3 = createPiece({
authors: ["Willianwg","kishanprmr","MoShizzle","AbdulTheActivePiecer","khaledmashaly","abuaboud", "Kevinyu-alan"],
categories: [PieceCategory.DEVELOPER_TOOLS],
auth: amazonS3Auth,
actions: [amazons3UploadFile, readFile, generateSignedUrl, moveFile, deleteFile, listFiles],
actions: [amazons3UploadFile, readFile, generateSignedUrl, moveFile, deleteFile, listFiles, decryptPgpFile],
triggers: [newFile],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import * as openpgp from 'openpgp';
import { amazonS3Auth } from '../..';
import { createS3, createSecretsManagerClient } from '../common';

export const decryptPgpFile = createAction({
auth: amazonS3Auth,
name: 'decrypt-pgp-file',
displayName: 'Decrypt PGP File',
description: 'Decrypt a PGP encrypted file from S3 using a private key stored in AWS Secrets Manager',
props: {
key: Property.ShortText({
displayName: 'S3 File Key',
description: 'The key (path) of the encrypted file in S3',
required: true,
}),
secretArn: Property.ShortText({
displayName: 'Secret ARN',
description: 'The ARN of the secret in AWS Secrets Manager containing the PGP private key',
required: true,
}),
passphraseArn: Property.ShortText({
displayName: 'Passphrase ARN',
description: 'Optional ARN of the secret in AWS Secrets Manager containing the passphrase for the private key (if the key is encrypted)',
required: false,
}),
secretsManagerRegion: Property.ShortText({
displayName: 'Secrets Manager Region',
description: 'The AWS region where the Secrets Manager secret is stored (defaults to S3 region if not provided)',
required: false,
}),
},
async run(context) {
const { bucket } = context.auth.props;
const { key, secretArn, passphraseArn, secretsManagerRegion } = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;

// Create S3 client
const s3 = createS3(context.auth.props);

// Download the encrypted file from S3
let encryptedData: Buffer;
try {
const file = await s3.getObject({
Bucket: bucket,
Key: key,
});

const base64 = await file.Body?.transformToString('base64');
if (!base64) {
throw new Error(`Could not read file ${key} from S3`);
}
encryptedData = Buffer.from(base64, 'base64');
} catch (error: any) {
throw new Error(`Failed to download file from S3: ${error instanceof Error ? error.message : 'Unknown error'}`);
}

// Create AWS Secrets Manager client
const secretsClient = createSecretsManagerClient({
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
region: secretsManagerRegion || region,
});

// Fetch the secret key from AWS Secrets Manager
let privateKeyArmored: string;
try {
const command = new GetSecretValueCommand({
SecretId: secretArn,
});
const response = await secretsClient.send(command);

if (!response.SecretString) {
throw new Error(`Secret ${secretArn} does not contain a string value`);
}

// Trim whitespace from the key (AWS Secrets Manager might add extra whitespace)
privateKeyArmored = response.SecretString.trim();
} catch (error: any) {
// Provide more specific error messages for common issues
if (error.name === 'AccessDeniedException') {
throw new Error(
`Access denied when retrieving secret ${secretArn}. ` +
`Please ensure your AWS credentials have the secretsmanager:GetSecretValue permission for this secret.`
);
}
if (error.name === 'ResourceNotFoundException') {
throw new Error(
`Secret ${secretArn} not found. Please verify the secret ARN is correct and exists in region ${secretsManagerRegion || region}.`
);
}
if (error.name === 'InvalidParameterException') {
throw new Error(
`Invalid secret ARN: ${secretArn}. Please verify the ARN format is correct.`
);
}
throw new Error(
`Failed to retrieve secret from AWS Secrets Manager: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}

// Fetch the passphrase from AWS Secrets Manager if provided
let passphrase: string | undefined;
if (passphraseArn) {
try {
const passphraseCommand = new GetSecretValueCommand({
SecretId: passphraseArn,
});
const passphraseResponse = await secretsClient.send(passphraseCommand);

if (!passphraseResponse.SecretString) {
throw new Error(`Secret ${passphraseArn} does not contain a string value`);
}

passphrase = passphraseResponse.SecretString.trim();
} catch (error: any) {
// Provide more specific error messages for common issues
if (error.name === 'AccessDeniedException') {
throw new Error(
`Access denied when retrieving passphrase secret ${passphraseArn}. ` +
`Please ensure your AWS credentials have the secretsmanager:GetSecretValue permission for this secret.`
);
}
if (error.name === 'ResourceNotFoundException') {
throw new Error(
`Passphrase secret ${passphraseArn} not found. Please verify the secret ARN is correct and exists in region ${secretsManagerRegion || region}.`
);
}
if (error.name === 'InvalidParameterException') {
throw new Error(
`Invalid passphrase secret ARN: ${passphraseArn}. Please verify the ARN format is correct.`
);
}
throw new Error(
`Failed to retrieve passphrase from AWS Secrets Manager: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}

// Read the encrypted file data
const encryptedString = encryptedData.toString('utf8');

// Decrypt the file using OpenPGP
try {
let message;
try {
message = await openpgp.readMessage({
armoredMessage: encryptedString,
});
} catch {
// If armored fails, try as binary
message = await openpgp.readMessage({
binaryMessage: encryptedData,
});
}

// Read the private key
let privateKey = await openpgp.readPrivateKey({
armoredKey: privateKeyArmored,
});

if (!privateKey.isPrivate()) {
throw new Error('The provided key is not a private key. Please ensure you are using a private key, not a public key.');
}

// Check if the key is encrypted and needs a passphrase
if (!privateKey.isDecrypted()) {
if (!passphrase) {
throw new Error('Private key is encrypted but no passphrase was provided. Please provide a Passphrase ARN.');
}
try {
const decryptedKey = await openpgp.decryptKey({
privateKey: privateKey,
passphrase: passphrase,
});
privateKey = decryptedKey;
} catch (error) {
throw new Error(`Failed to unlock private key with passphrase: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}

// Decrypt the message
const decrypted = await openpgp.decrypt({
message,
decryptionKeys: privateKey,
format: 'binary',
config: {
allowInsecureDecryptionWithSigningKeys: false,
},
});

// Get the decrypted data
const decryptedData = new Uint8Array(await decrypted.data);

// Write the decrypted file
const fileName = key.split("/").at(-1)?.toLowerCase().replace(/\.(pgp|gpg)$/i, '');

return await context.files.write({
fileName: fileName ?? 'decrypted_file',
data: Buffer.from(decryptedData),
});
} catch (error) {
throw new Error(`Failed to decrypt file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
},
});
17 changes: 17 additions & 0 deletions packages/pieces/community/amazon-s3/src/lib/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isNil } from '@activepieces/shared';
import { S3 } from '@aws-sdk/client-s3';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';

export function createS3(auth: {
accessKeyId: string;
Expand All @@ -19,3 +20,19 @@ export function createS3(auth: {
});
return s3;
}

export function createSecretsManagerClient(auth: {
accessKeyId: string;
secretAccessKey: string;
region: string | undefined;
}) {
const client = new SecretsManagerClient({
credentials: {
accessKeyId: auth.accessKeyId,
secretAccessKey: auth.secretAccessKey,
},
region: auth.region,
endpoint: undefined,
});
return client;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
rectIntersection,
useSensor,
useSensors,
PointerSensorOptions,
} from '@dnd-kit/core';
import { ReactFlowInstance, useReactFlow } from '@xyflow/react';
import { t } from 'i18next';
import type { PointerEvent } from 'react';
import { useCallback, useRef } from 'react';
import { toast } from 'sonner';

Expand Down Expand Up @@ -110,7 +112,7 @@ const FlowDragLayer = ({ children }: { children: React.ReactNode }) => {
};

const sensors = useSensors(
useSensor(PointerSensor, {
useSensor(PointerSensorIgnoringInteractiveItems, {
activationConstraint: {
distance: 10,
},
Expand Down Expand Up @@ -214,3 +216,50 @@ function handleNoteDragEnd({
}
}
}

class PointerSensorIgnoringInteractiveItems extends PointerSensor {
static activators = [
{
eventName: 'onPointerDown' as const,
handler: (
{ nativeEvent: event }: PointerEvent,
{ onActivation }: PointerSensorOptions,
) => {
const target = event.target as HTMLElement;
if (target?.closest('[contenteditable="true"]')) {
return false;
}

if (
!event.isPrimary ||
event.button !== 0 ||
isInteractiveElement(event.target as Element)
) {
return false;
}

onActivation?.({ event });
return true;
},
},
];
}

function isInteractiveElement(element: Element | null): boolean {
const interactiveElements = [
'button',
'input',
'textarea',
'select',
'option',
];

if (
element?.tagName &&
interactiveElements.includes(element.tagName.toLowerCase())
) {
return true;
}

return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,31 @@ const ApNoteCanvasNode = (props: NodeProps & Omit<ApNoteNode, 'position'>) => {
type: flowCanvasConsts.DRAGGED_NOTE_TAG,
},
});

//because react flow only detects nowheel class, it doesn't work with focus-within:nowheel
const [isFocusWithin, setIsFocusWithin] = useState(false);
const [size, setSize] = useState(props.data.size);
if (draggedNote?.id === props.id || note === null) {
return null;
}
return (
<div className="group note-node outline-none">
<div
className={cn('group note-node outline-none', {
nowheel: isFocusWithin,
})}
onFocus={() => setIsFocusWithin(true)}
onBlur={() => setIsFocusWithin(false)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
setIsFocusWithin(false);
if (
document.activeElement instanceof HTMLElement &&
document.activeElement.closest('.note-node')
) {
document.activeElement.blur();
}
}
}}
>
<NodeResizeControl
minWidth={150}
minHeight={150}
Expand Down Expand Up @@ -121,7 +139,10 @@ const NoteContent = ({ note, isDragging }: NoteContentProps) => {
}}
>
{!isDragging && !readonly && editorRef.current && (
<div className="opacity-0 focus-within:opacity-100 group-focus-within:opacity-100 transition-opacity duration-300">
<div
className="opacity-0 focus-within:opacity-100 pointer-events-none group-focus-within:pointer-events-auto group-focus-within:opacity-100 transition-opacity duration-300"
onPointerDown={(e) => e.stopPropagation()}
>
<NoteTools
editor={editorRef.current}
currentColor={note.color}
Expand Down Expand Up @@ -162,8 +183,10 @@ const NoteContent = ({ note, isDragging }: NoteContentProps) => {
NoteColorVariantClassName[color],
)}
onChange={(value: string) => {
setLocalNote({ ...localNote, content: value });
debouncedUpdateContent(id, value);
if (value !== localNote.content) {
setLocalNote({ ...localNote, content: value });
debouncedUpdateContent(id, value);
}
}}
/>
</div>
Expand Down
Loading
Loading