Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient, unwrapResource } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { Highlight } from '../../common/types';
|
||||
|
||||
export const getHighlight = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'get-highlight',
|
||||
displayName: 'Get Highlight',
|
||||
description: 'Retrieve a specific highlight by ID.',
|
||||
props: {
|
||||
highlightId: commonProps.highlightId,
|
||||
},
|
||||
async run(context) {
|
||||
const highlightId = context.propsValue.highlightId as string;
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const response = await client.request<Highlight>({
|
||||
method: HttpMethod.GET,
|
||||
path: `/highlights/${highlightId}`,
|
||||
});
|
||||
|
||||
return unwrapResource(response);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './get-highlight';
|
||||
export * from './list-highlights';
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { topicDropdown } from '../../common/load-options';
|
||||
import { Highlight } from '../../common/types';
|
||||
import { assertLimit } from '../../common/validation';
|
||||
|
||||
export const listHighlights = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'list-highlights',
|
||||
displayName: 'List Highlights',
|
||||
description: 'Retrieve highlights with optional topic filtering and pagination.',
|
||||
props: {
|
||||
returnAll: commonProps.returnAll,
|
||||
limit: commonProps.limit,
|
||||
format: commonProps.format,
|
||||
topicId: topicDropdown,
|
||||
after: commonProps.afterCursor,
|
||||
before: commonProps.beforeCursor,
|
||||
},
|
||||
async run(context) {
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const { returnAll, limit, format, topicId, after, before } = context.propsValue as {
|
||||
returnAll?: boolean;
|
||||
limit?: number;
|
||||
format?: 'standard' | 'zapier';
|
||||
topicId?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
};
|
||||
|
||||
return client.paginate<Highlight>('/highlights', {
|
||||
returnAll: Boolean(returnAll),
|
||||
limit: assertLimit(limit),
|
||||
format,
|
||||
topicId,
|
||||
after,
|
||||
before,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './sessions';
|
||||
export * from './highlights';
|
||||
export * from './todos';
|
||||
export * from './topics';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient, unwrapResource } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { Session } from '../../common/types';
|
||||
|
||||
export const getSession = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'get-session',
|
||||
displayName: 'Get Session',
|
||||
description: 'Retrieve a specific session by ID.',
|
||||
props: {
|
||||
sessionId: commonProps.sessionId,
|
||||
},
|
||||
async run(context) {
|
||||
const sessionId = context.propsValue.sessionId as string;
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const response = await client.request<Session>({
|
||||
method: HttpMethod.GET,
|
||||
path: `/sessions/${sessionId}`,
|
||||
});
|
||||
|
||||
return unwrapResource(response);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './get-session';
|
||||
export * from './list-sessions';
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { topicDropdown } from '../../common/load-options';
|
||||
import { Session } from '../../common/types';
|
||||
import { assertLimit } from '../../common/validation';
|
||||
|
||||
export const listSessions = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'list-sessions',
|
||||
displayName: 'List Sessions',
|
||||
description: 'Retrieve multiple sessions with optional topic filtering and pagination.',
|
||||
props: {
|
||||
returnAll: commonProps.returnAll,
|
||||
limit: commonProps.limit,
|
||||
format: commonProps.format,
|
||||
topicId: topicDropdown,
|
||||
after: commonProps.afterCursor,
|
||||
before: commonProps.beforeCursor,
|
||||
},
|
||||
async run(context) {
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const { returnAll, limit, format, topicId, after, before } = context.propsValue as {
|
||||
returnAll?: boolean;
|
||||
limit?: number;
|
||||
format?: 'standard' | 'zapier';
|
||||
topicId?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
};
|
||||
|
||||
return client.paginate<Session>('/sessions', {
|
||||
returnAll: Boolean(returnAll),
|
||||
limit: assertLimit(limit),
|
||||
format,
|
||||
topicId,
|
||||
after,
|
||||
before,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './list-todos';
|
||||
export * from './list-session-todos';
|
||||
@@ -0,0 +1,56 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { PaginatedResponse, Todo } from '../../common/types';
|
||||
import { assertLimit } from '../../common/validation';
|
||||
|
||||
function toTodoArray(result: unknown): Todo[] {
|
||||
if (Array.isArray(result)) {
|
||||
return result as Todo[];
|
||||
}
|
||||
|
||||
if (result && typeof result === 'object' && 'data' in result) {
|
||||
const data = (result as PaginatedResponse<Todo>).data;
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export const listSessionTodos = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'list-session-todos',
|
||||
displayName: 'List Session Todos',
|
||||
description: 'Retrieve todos generated for a specific session.',
|
||||
props: {
|
||||
sessionId: commonProps.sessionId,
|
||||
returnAll: commonProps.returnAll,
|
||||
limit: commonProps.limit,
|
||||
},
|
||||
async run(context) {
|
||||
const sessionId = context.propsValue.sessionId as string;
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const { returnAll, limit } = context.propsValue as {
|
||||
returnAll?: boolean;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
const response = await client.request<Todo[]>({
|
||||
method: HttpMethod.GET,
|
||||
path: `/sessions/${sessionId}/todos`,
|
||||
});
|
||||
|
||||
const todos = toTodoArray(response);
|
||||
|
||||
if (!returnAll) {
|
||||
const limited = assertLimit(limit);
|
||||
return limited ? todos.slice(0, limited) : todos.slice(0, 50);
|
||||
}
|
||||
|
||||
return todos;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { PaginatedResponse, Todo } from '../../common/types';
|
||||
import { assertLimit } from '../../common/validation';
|
||||
|
||||
function extractTodos(result: unknown): Todo[] {
|
||||
if (Array.isArray(result)) {
|
||||
return result as Todo[];
|
||||
}
|
||||
|
||||
if (result && typeof result === 'object' && 'data' in result) {
|
||||
const data = (result as PaginatedResponse<Todo>).data;
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export const listTodos = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'list-todos',
|
||||
displayName: 'List Todos',
|
||||
description: 'Retrieve todos assigned to you in Hedy.',
|
||||
props: {
|
||||
returnAll: commonProps.returnAll,
|
||||
limit: commonProps.limit,
|
||||
},
|
||||
async run(context) {
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const { returnAll, limit } = context.propsValue as {
|
||||
returnAll?: boolean;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
const response = await client.request<Todo[]>({
|
||||
method: HttpMethod.GET,
|
||||
path: '/todos',
|
||||
});
|
||||
|
||||
const todos = extractTodos(response);
|
||||
|
||||
if (!returnAll) {
|
||||
const limited = assertLimit(limit);
|
||||
return limited ? todos.slice(0, limited) : todos.slice(0, 50);
|
||||
}
|
||||
|
||||
return todos;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient, unwrapResource } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { Topic } from '../../common/types';
|
||||
|
||||
export const getTopic = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'get-topic',
|
||||
displayName: 'Get Topic',
|
||||
description: 'Retrieve details for a specific topic.',
|
||||
props: {
|
||||
topicId: commonProps.topicId,
|
||||
},
|
||||
async run(context) {
|
||||
const topicId = context.propsValue.topicId as string;
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const response = await client.request<Topic>({
|
||||
method: HttpMethod.GET,
|
||||
path: `/topics/${topicId}`,
|
||||
});
|
||||
|
||||
return unwrapResource(response);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './get-topic';
|
||||
export * from './list-topics';
|
||||
export * from './list-topic-sessions';
|
||||
@@ -0,0 +1,56 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { PaginatedResponse, Session } from '../../common/types';
|
||||
import { assertLimit } from '../../common/validation';
|
||||
|
||||
function toSessionArray(result: unknown): Session[] {
|
||||
if (Array.isArray(result)) {
|
||||
return result as Session[];
|
||||
}
|
||||
|
||||
if (result && typeof result === 'object' && 'data' in result) {
|
||||
const data = (result as PaginatedResponse<Session>).data;
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export const listTopicSessions = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'list-topic-sessions',
|
||||
displayName: 'List Topic Sessions',
|
||||
description: 'Retrieve sessions associated with a specific topic.',
|
||||
props: {
|
||||
topicId: commonProps.topicId,
|
||||
returnAll: commonProps.returnAll,
|
||||
limit: commonProps.limit,
|
||||
},
|
||||
async run(context) {
|
||||
const topicId = context.propsValue.topicId as string;
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const { returnAll, limit } = context.propsValue as {
|
||||
returnAll?: boolean;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
const response = await client.request<Session[]>({
|
||||
method: HttpMethod.GET,
|
||||
path: `/topics/${topicId}/sessions`,
|
||||
});
|
||||
|
||||
const sessions = toSessionArray(response);
|
||||
|
||||
if (!returnAll) {
|
||||
const limited = assertLimit(limit);
|
||||
return limited ? sessions.slice(0, limited) : sessions.slice(0, 50);
|
||||
}
|
||||
|
||||
return sessions;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient } from '../../common/client';
|
||||
import { commonProps } from '../../common/props';
|
||||
import { PaginatedResponse, Topic } from '../../common/types';
|
||||
import { assertLimit } from '../../common/validation';
|
||||
|
||||
function toTopicArray(result: unknown): Topic[] {
|
||||
if (Array.isArray(result)) {
|
||||
return result as Topic[];
|
||||
}
|
||||
|
||||
if (result && typeof result === 'object' && 'data' in result) {
|
||||
const data = (result as PaginatedResponse<Topic>).data;
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export const listTopics = createAction({
|
||||
auth: hedyAuth,
|
||||
name: 'list-topics',
|
||||
displayName: 'List Topics',
|
||||
description: 'Retrieve all topics from your Hedy workspace.',
|
||||
props: {
|
||||
returnAll: commonProps.returnAll,
|
||||
limit: commonProps.limit,
|
||||
},
|
||||
async run(context) {
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const { returnAll, limit } = context.propsValue as {
|
||||
returnAll?: boolean;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
const response = await client.request<Topic[]>({
|
||||
method: HttpMethod.GET,
|
||||
path: '/topics',
|
||||
});
|
||||
|
||||
const topics = toTopicArray(response);
|
||||
|
||||
if (!returnAll) {
|
||||
const limited = assertLimit(limit);
|
||||
return limited ? topics.slice(0, limited) : topics.slice(0, 50);
|
||||
}
|
||||
|
||||
return topics;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { PieceAuth } from '@activepieces/pieces-framework';
|
||||
import { HedyApiClient } from '../common/client';
|
||||
|
||||
export const hedyAuth = PieceAuth.SecretText({
|
||||
displayName: 'API Key',
|
||||
description:
|
||||
'Generate an API key from your Hedy dashboard under Settings → API, then paste the key here (it begins with `hedy_live_`).',
|
||||
required: true,
|
||||
validate: async ({ auth }) => {
|
||||
if (!auth || typeof auth !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Please provide a valid API key.',
|
||||
};
|
||||
}
|
||||
|
||||
const client = new HedyApiClient(auth);
|
||||
try {
|
||||
await client.request({
|
||||
method: HttpMethod.GET,
|
||||
path: '/sessions',
|
||||
queryParams: {
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Invalid API key. Please verify the key in your Hedy dashboard and try again.',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
AuthenticationType,
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
HttpRequest,
|
||||
QueryParams,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { HedyApiError } from './errors';
|
||||
import {
|
||||
ApiErrorPayload,
|
||||
HedyResponse,
|
||||
PaginatedResponse,
|
||||
PaginationInfo,
|
||||
} from './types';
|
||||
|
||||
const BASE_URL = 'https://api.hedy.bot';
|
||||
const DEFAULT_LIMIT = 50;
|
||||
const MAX_RESULTS = 1000;
|
||||
const MAX_RETRIES = 3;
|
||||
const INITIAL_BACKOFF_MS = 500;
|
||||
|
||||
export interface PaginationOptions {
|
||||
returnAll?: boolean;
|
||||
limit?: number;
|
||||
after?: string;
|
||||
before?: string;
|
||||
topicId?: string;
|
||||
format?: 'standard' | 'zapier';
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
method: HttpMethod;
|
||||
path: string;
|
||||
body?: unknown;
|
||||
queryParams?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class HedyApiClient {
|
||||
constructor(private readonly apiKey: string) {}
|
||||
|
||||
async request<T>(options: RequestOptions): Promise<HedyResponse<T>> {
|
||||
return this.withRetry(() => this.performRequest<T>(options));
|
||||
}
|
||||
|
||||
async paginate<T>(path: string, options: PaginationOptions = {}): Promise<T[]> {
|
||||
const { returnAll = false, limit = DEFAULT_LIMIT, ...rest } = options;
|
||||
const collected: T[] = [];
|
||||
let cursor = rest.after;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const query = {
|
||||
...rest,
|
||||
limit: returnAll ? Math.min(limit, 100) : limit,
|
||||
after: cursor,
|
||||
};
|
||||
|
||||
const response = await this.request<T>({
|
||||
method: HttpMethod.GET,
|
||||
path,
|
||||
queryParams: query,
|
||||
});
|
||||
|
||||
const { data, pagination } = normalizeListResult(response);
|
||||
collected.push(...data);
|
||||
|
||||
if (!returnAll && collected.length >= limit) {
|
||||
// When NOT returning all, stop when we hit the limit
|
||||
hasMore = false;
|
||||
} else if (pagination?.hasMore && pagination.next) {
|
||||
// Continue if there's more data available
|
||||
cursor = pagination.next;
|
||||
hasMore = true;
|
||||
} else {
|
||||
// No more data available
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
if (collected.length >= MAX_RESULTS) {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
return returnAll ? collected : collected.slice(0, limit);
|
||||
}
|
||||
|
||||
private async performRequest<T>({ method, path, body, queryParams }: RequestOptions): Promise<HedyResponse<T>> {
|
||||
const qs: QueryParams = {};
|
||||
|
||||
if (queryParams) {
|
||||
for (const [key, value] of Object.entries(queryParams)) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
continue;
|
||||
}
|
||||
qs[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
const request: HttpRequest = {
|
||||
method,
|
||||
url: `${BASE_URL}${path}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: this.apiKey,
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': 'activepieces-hedy/1.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
queryParams: qs,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest<HedyResponse<T>>(request);
|
||||
const { body: responseBody } = response;
|
||||
|
||||
if (isErrorPayload(responseBody)) {
|
||||
throw HedyApiError.fromPayload(responseBody, undefined, response.status);
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (error: any) {
|
||||
const apiErrorPayload: ApiErrorPayload | undefined = error?.response?.body;
|
||||
const statusCode: number | undefined = error?.response?.status;
|
||||
throw HedyApiError.fromPayload(apiErrorPayload, error, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async withRetry<T>(operation: () => Promise<T>): Promise<T> {
|
||||
let attempt = 0;
|
||||
let backoff = INITIAL_BACKOFF_MS;
|
||||
|
||||
while (attempt < MAX_RETRIES) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
const isLastAttempt = attempt === MAX_RETRIES - 1;
|
||||
if (!(error instanceof HedyApiError) || !this.shouldRetry(error) || isLastAttempt) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.delay(backoff);
|
||||
backoff *= 2;
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The loop above either returns or throws, but TypeScript expects a return.
|
||||
throw new HedyApiError('unknown_error', 'Request failed after multiple retries.');
|
||||
}
|
||||
|
||||
private shouldRetry(error: HedyApiError): boolean {
|
||||
return error.code === 'rate_limit_exceeded';
|
||||
}
|
||||
|
||||
private async delay(duration: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, duration));
|
||||
}
|
||||
}
|
||||
|
||||
function isErrorPayload<T>(body: HedyResponse<T>): body is ApiErrorPayload {
|
||||
return Boolean(body && typeof body === 'object' && 'error' in body);
|
||||
}
|
||||
|
||||
function normalizeListResult<T>(result: HedyResponse<T>): {
|
||||
data: T[];
|
||||
pagination?: PaginationInfo;
|
||||
} {
|
||||
if (Array.isArray(result)) {
|
||||
return { data: result };
|
||||
}
|
||||
|
||||
if (result && typeof result === 'object') {
|
||||
const maybePaginated = result as PaginatedResponse<T>;
|
||||
if (Array.isArray(maybePaginated.data)) {
|
||||
return {
|
||||
data: maybePaginated.data,
|
||||
pagination: maybePaginated.pagination,
|
||||
};
|
||||
}
|
||||
|
||||
const maybeData = (result as { data?: unknown }).data;
|
||||
if (Array.isArray(maybeData)) {
|
||||
return { data: maybeData as T[] };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: result ? [result as T] : [],
|
||||
};
|
||||
}
|
||||
|
||||
export function unwrapResource<T>(result: HedyResponse<T>): T {
|
||||
if (result && typeof result === 'object') {
|
||||
const data = (result as { data?: unknown }).data;
|
||||
if (data && !Array.isArray(data)) {
|
||||
return data as T;
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ApiErrorPayload } from './types';
|
||||
|
||||
const DEFAULT_ERROR_MESSAGE = 'An unknown error occurred while communicating with Hedy.';
|
||||
|
||||
function deriveCode(initialCode: string | undefined, statusCode?: number): string {
|
||||
if (initialCode) {
|
||||
return initialCode;
|
||||
}
|
||||
|
||||
if (statusCode === 429) {
|
||||
return 'rate_limit_exceeded';
|
||||
}
|
||||
|
||||
return 'unknown_error';
|
||||
}
|
||||
|
||||
const FRIENDLY_MESSAGES: Record<string, string> = {
|
||||
webhook_limit_exceeded:
|
||||
'Maximum webhook limit (10) reached. Please delete unused webhooks in your Hedy dashboard.',
|
||||
authentication_failed:
|
||||
'Invalid API key. Please check your Hedy dashboard for the correct API key.',
|
||||
invalid_event:
|
||||
'Invalid event type. Valid events: session.created, session.ended, highlight.created, todo.exported.',
|
||||
invalid_webhook_url:
|
||||
'Webhook URL must be publicly accessible. For local testing, use a tunneling service like ngrok.',
|
||||
invalid_parameter:
|
||||
'Invalid request parameter. Please review your configuration and try again.',
|
||||
rate_limit_exceeded:
|
||||
'Rate limit reached. Please wait a moment before trying again.',
|
||||
unknown_error: DEFAULT_ERROR_MESSAGE,
|
||||
};
|
||||
|
||||
export class HedyApiError extends Error {
|
||||
constructor(
|
||||
public readonly code: string,
|
||||
message: string,
|
||||
public readonly details?: unknown,
|
||||
) {
|
||||
super(message || DEFAULT_ERROR_MESSAGE);
|
||||
this.name = 'HedyApiError';
|
||||
}
|
||||
|
||||
static fromPayload(
|
||||
payload: ApiErrorPayload | undefined,
|
||||
fallback?: unknown,
|
||||
statusCode?: number,
|
||||
): HedyApiError {
|
||||
if (payload && payload.error) {
|
||||
const { code, message } = payload.error;
|
||||
const resolvedCode = deriveCode(code, statusCode);
|
||||
return new HedyApiError(
|
||||
resolvedCode,
|
||||
FRIENDLY_MESSAGES[resolvedCode] ?? message ?? DEFAULT_ERROR_MESSAGE,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
if (fallback instanceof HedyApiError) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (fallback instanceof Error) {
|
||||
const derivedCode = deriveCode(undefined, statusCode);
|
||||
return new HedyApiError(
|
||||
derivedCode,
|
||||
FRIENDLY_MESSAGES[derivedCode] ?? fallback.message ?? DEFAULT_ERROR_MESSAGE,
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
|
||||
const finalCode = deriveCode(undefined, statusCode);
|
||||
return new HedyApiError(
|
||||
finalCode,
|
||||
FRIENDLY_MESSAGES[finalCode] ?? DEFAULT_ERROR_MESSAGE,
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
import { HedyApiClient } from './client';
|
||||
import { PaginatedResponse, Topic } from './types';
|
||||
import { hedyAuth } from '../auth';
|
||||
|
||||
function toTopicArray(result: unknown): Topic[] {
|
||||
if (Array.isArray(result)) {
|
||||
return result as Topic[];
|
||||
}
|
||||
|
||||
if (result && typeof result === 'object' && 'data' in result) {
|
||||
const data = (result as PaginatedResponse<Topic>).data;
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export const topicDropdown = Property.Dropdown({
|
||||
auth: hedyAuth,
|
||||
displayName: 'Topic',
|
||||
description: 'Optionally filter results by a specific topic.',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Connect your Hedy account first.',
|
||||
};
|
||||
}
|
||||
|
||||
const client = new HedyApiClient(auth.secret_text);
|
||||
try {
|
||||
const response = await client.request<Topic[]>({
|
||||
method: HttpMethod.GET,
|
||||
path: '/topics',
|
||||
});
|
||||
|
||||
const topics = toTopicArray(response);
|
||||
|
||||
if (topics.length === 0) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: [],
|
||||
placeholder: 'No topics found in your Hedy workspace.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: topics.map((topic) => ({
|
||||
label: topic.name,
|
||||
value: topic.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder:
|
||||
error instanceof Error ? error.message : 'Failed to load topics. Check your connection.',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
|
||||
export const commonProps = {
|
||||
sessionId: Property.ShortText({
|
||||
displayName: 'Session ID',
|
||||
description: 'The session ID as shown in the Hedy dashboard.',
|
||||
required: true,
|
||||
}),
|
||||
|
||||
highlightId: Property.ShortText({
|
||||
displayName: 'Highlight ID',
|
||||
description: 'The highlight ID as shown in the Hedy dashboard.',
|
||||
required: true,
|
||||
}),
|
||||
|
||||
topicId: Property.ShortText({
|
||||
displayName: 'Topic ID',
|
||||
description: 'The topic ID as shown in the Hedy dashboard.',
|
||||
required: true,
|
||||
}),
|
||||
|
||||
returnAll: Property.Checkbox({
|
||||
displayName: 'Return All',
|
||||
description: 'Return all results instead of using the limit.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
|
||||
limit: Property.Number({
|
||||
displayName: 'Limit',
|
||||
description: 'Maximum number of results to return (default 50).',
|
||||
required: false,
|
||||
defaultValue: 50,
|
||||
}),
|
||||
|
||||
format: Property.StaticDropdown({
|
||||
displayName: 'Response Format',
|
||||
description: 'Select the response format to use.',
|
||||
required: false,
|
||||
defaultValue: 'standard',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Standard', value: 'standard' },
|
||||
{ label: 'Zapier Compatible', value: 'zapier' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
afterCursor: Property.ShortText({
|
||||
displayName: 'After Cursor',
|
||||
description: 'Pagination cursor used to fetch results after a specific item.',
|
||||
required: false,
|
||||
}),
|
||||
|
||||
beforeCursor: Property.ShortText({
|
||||
displayName: 'Before Cursor',
|
||||
description: 'Pagination cursor used to fetch results before a specific item.',
|
||||
required: false,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
export interface Topic {
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
iconName?: string;
|
||||
}
|
||||
|
||||
export interface Todo {
|
||||
id: string;
|
||||
text: string;
|
||||
dueDate?: string;
|
||||
completed: boolean;
|
||||
topic?: Topic;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
question: string;
|
||||
answer: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
title: string;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
duration?: number;
|
||||
transcript?: string;
|
||||
conversations?: Conversation[] | string;
|
||||
meeting_minutes?: string;
|
||||
meetingMinutes?: string;
|
||||
recap?: string;
|
||||
user_todos?: Todo[];
|
||||
userTodos?: Todo[];
|
||||
topic?: Topic;
|
||||
}
|
||||
|
||||
export interface Highlight {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
timestamp?: string;
|
||||
title?: string;
|
||||
rawQuote?: string;
|
||||
cleanedQuote?: string;
|
||||
mainIdea?: string;
|
||||
aiInsights?: string;
|
||||
}
|
||||
|
||||
export interface TodoExportedPayload {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
text: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
export interface PaginationInfo {
|
||||
hasMore: boolean;
|
||||
next?: string;
|
||||
previous?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
success?: boolean;
|
||||
data: T[];
|
||||
pagination?: PaginationInfo;
|
||||
}
|
||||
|
||||
export interface ApiSuccessResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface ApiErrorPayload {
|
||||
success?: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type HedyResponse<T> =
|
||||
| T
|
||||
| T[]
|
||||
| PaginatedResponse<T>
|
||||
| ApiSuccessResponse<T>
|
||||
| ApiErrorPayload;
|
||||
|
||||
export interface WebhookRegistration {
|
||||
id?: string;
|
||||
url: string;
|
||||
events: string[];
|
||||
signingSecret?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export enum HedyWebhookEvent {
|
||||
SessionCreated = 'session.created',
|
||||
SessionEnded = 'session.ended',
|
||||
HighlightCreated = 'highlight.created',
|
||||
TodoExported = 'todo.exported',
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function assertLimit(limit?: number): number | undefined {
|
||||
if (limit === undefined || limit === null) {
|
||||
return limit ?? undefined;
|
||||
}
|
||||
|
||||
if (Number.isNaN(limit)) {
|
||||
throw new Error('Limit must be a number between 1 and 100.');
|
||||
}
|
||||
|
||||
if (limit < 1 || limit > 100) {
|
||||
throw new Error('Limit must be between 1 and 100.');
|
||||
}
|
||||
|
||||
return limit;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './webhook';
|
||||
@@ -0,0 +1,157 @@
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { hedyAuth } from '../../auth';
|
||||
import { HedyApiClient, unwrapResource } from '../../common/client';
|
||||
import { HedyWebhookEvent, WebhookRegistration } from '../../common/types';
|
||||
|
||||
interface TriggerConfig {
|
||||
event: HedyWebhookEvent;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
sampleData?: unknown;
|
||||
}
|
||||
|
||||
export function createHedyWebhookTrigger(config: TriggerConfig) {
|
||||
return createTrigger({
|
||||
auth: hedyAuth,
|
||||
name: config.name,
|
||||
displayName: config.displayName,
|
||||
description: config.description,
|
||||
type: TriggerStrategy.WEBHOOK,
|
||||
props: {
|
||||
verifySignature: Property.Checkbox({
|
||||
displayName: 'Verify Signature',
|
||||
description:
|
||||
'Verify the webhook signature using the secret returned by Hedy. Disable this option if Hedy does not provide a signing secret for your account.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
sampleData: config.sampleData,
|
||||
async onEnable(context) {
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
const webhookUrl = context.webhookUrl;
|
||||
|
||||
if (!webhookUrl) {
|
||||
throw new Error('Webhook URL is unavailable. Please try again.');
|
||||
}
|
||||
|
||||
const response = await client.request<WebhookRegistration>({
|
||||
method: HttpMethod.POST,
|
||||
path: '/webhooks',
|
||||
body: {
|
||||
url: webhookUrl,
|
||||
events: [config.event],
|
||||
},
|
||||
});
|
||||
|
||||
const webhook = unwrapResource<WebhookRegistration>(response);
|
||||
|
||||
if (!webhook?.id) {
|
||||
throw new Error('Failed to register webhook with Hedy. No webhook ID was returned.');
|
||||
}
|
||||
|
||||
await context.store.put('webhookId', webhook.id);
|
||||
|
||||
if (webhook.signingSecret) {
|
||||
await context.store.put('signingSecret', webhook.signingSecret);
|
||||
} else {
|
||||
await context.store.delete('signingSecret');
|
||||
}
|
||||
},
|
||||
async onDisable(context) {
|
||||
const webhookId = (await context.store.get<string>('webhookId')) ?? undefined;
|
||||
if (!webhookId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new HedyApiClient(context.auth.secret_text);
|
||||
try {
|
||||
await client.request({
|
||||
method: HttpMethod.DELETE,
|
||||
path: `/webhooks/${webhookId}`,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore deletion errors – webhook may already be removed.
|
||||
} finally {
|
||||
await context.store.delete('webhookId');
|
||||
await context.store.delete('signingSecret');
|
||||
}
|
||||
},
|
||||
async run(context) {
|
||||
const props = context.propsValue as Record<string, unknown>;
|
||||
const verifySignatureEnabled = Boolean(props['verifySignature']);
|
||||
const payload = (context.payload.body ?? {}) as Record<string, unknown>;
|
||||
|
||||
if (verifySignatureEnabled) {
|
||||
const signatureHeader = getSignatureHeader(context.payload.headers ?? {});
|
||||
if (!signatureHeader) {
|
||||
throw new Error('Hedy signature header is missing from the webhook request.');
|
||||
}
|
||||
|
||||
const signingSecret = await context.store.get<string>('signingSecret');
|
||||
if (!signingSecret) {
|
||||
throw new Error(
|
||||
'Hedy did not return a signing secret during webhook registration. Disable signature verification or re-register your webhook.',
|
||||
);
|
||||
}
|
||||
|
||||
const rawBody = extractRawBody(context.payload.rawBody as RawBody, payload);
|
||||
const expectedSignature = createHmac('sha256', signingSecret).update(rawBody).digest('hex');
|
||||
|
||||
if (!secureCompare(expectedSignature, signatureHeader)) {
|
||||
throw new Error('Webhook signature verification failed. This request may not be from Hedy.');
|
||||
}
|
||||
}
|
||||
|
||||
const eventType = payload['event'] as string | undefined;
|
||||
if (eventType && eventType !== config.event) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [payload];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type HeadersMap = Record<string, string | string[] | undefined>;
|
||||
|
||||
type RawBody = string | Buffer | undefined;
|
||||
|
||||
function getSignatureHeader(headers: HeadersMap): string | undefined {
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (typeof value === 'string') {
|
||||
normalized[key.toLowerCase()] = value;
|
||||
} else if (Array.isArray(value) && value.length > 0) {
|
||||
normalized[key.toLowerCase()] = value[0];
|
||||
}
|
||||
}
|
||||
|
||||
return normalized['x-hedy-signature'];
|
||||
}
|
||||
|
||||
function extractRawBody(rawBody: RawBody, payload: Record<string, unknown>): Buffer {
|
||||
if (typeof rawBody === 'string') {
|
||||
return Buffer.from(rawBody, 'utf8');
|
||||
}
|
||||
|
||||
if (rawBody instanceof Buffer) {
|
||||
return rawBody;
|
||||
}
|
||||
|
||||
return Buffer.from(JSON.stringify(payload ?? {}), 'utf8');
|
||||
}
|
||||
|
||||
function secureCompare(expected: string, candidate: string): boolean {
|
||||
const expectedBuffer = Buffer.from(expected, 'hex');
|
||||
const candidateBuffer = Buffer.from(candidate, 'hex');
|
||||
|
||||
if (expectedBuffer.length !== candidateBuffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return timingSafeEqual(expectedBuffer, candidateBuffer);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { HedyWebhookEvent } from '../../common/types';
|
||||
import { createHedyWebhookTrigger } from './factory';
|
||||
|
||||
export const highlightCreated = createHedyWebhookTrigger({
|
||||
event: HedyWebhookEvent.HighlightCreated,
|
||||
name: 'highlight-created',
|
||||
displayName: 'Highlight Created',
|
||||
description: 'Triggers when a highlight is created during a session.',
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './session-created';
|
||||
export * from './session-ended';
|
||||
export * from './highlight-created';
|
||||
export * from './todo-exported';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { HedyWebhookEvent } from '../../common/types';
|
||||
import { createHedyWebhookTrigger } from './factory';
|
||||
|
||||
export const sessionCreated = createHedyWebhookTrigger({
|
||||
event: HedyWebhookEvent.SessionCreated,
|
||||
name: 'session-created',
|
||||
displayName: 'Session Created',
|
||||
description: 'Triggers when a new session is created in Hedy.',
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { HedyWebhookEvent } from '../../common/types';
|
||||
import { createHedyWebhookTrigger } from './factory';
|
||||
|
||||
export const sessionEnded = createHedyWebhookTrigger({
|
||||
event: HedyWebhookEvent.SessionEnded,
|
||||
name: 'session-ended',
|
||||
displayName: 'Session Ended',
|
||||
description: 'Triggers when a session is completed in Hedy.',
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { HedyWebhookEvent } from '../../common/types';
|
||||
import { createHedyWebhookTrigger } from './factory';
|
||||
|
||||
export const todoExported = createHedyWebhookTrigger({
|
||||
event: HedyWebhookEvent.TodoExported,
|
||||
name: 'todo-exported',
|
||||
displayName: 'Todo Exported',
|
||||
description: 'Triggers when a todo item is exported from Hedy.',
|
||||
});
|
||||
Reference in New Issue
Block a user