@startsimpli/api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +329 -0
  2. package/package.json +42 -0
  3. package/src/__tests__/jwt-refresh.test.ts +195 -0
  4. package/src/__tests__/query-params.test.ts +144 -0
  5. package/src/__tests__/url-builder.test.ts +121 -0
  6. package/src/constants/endpoints.ts +39 -0
  7. package/src/index.ts +109 -0
  8. package/src/lib/api-client.ts +89 -0
  9. package/src/lib/contacts-api.ts +111 -0
  10. package/src/lib/cors.ts +122 -0
  11. package/src/lib/entities-api.ts +123 -0
  12. package/src/lib/env.ts +35 -0
  13. package/src/lib/error-handler.ts +138 -0
  14. package/src/lib/errors.ts +381 -0
  15. package/src/lib/fetch-wrapper.ts +188 -0
  16. package/src/lib/llm-sanitize.ts +145 -0
  17. package/src/lib/messages-api.ts +273 -0
  18. package/src/lib/messages-api.ts.backup +273 -0
  19. package/src/lib/organizations-api.ts +132 -0
  20. package/src/lib/rate-limit.ts +91 -0
  21. package/src/lib/sanitize.ts +39 -0
  22. package/src/lib/workflows-api.ts +159 -0
  23. package/src/middleware/index.ts +12 -0
  24. package/src/middleware/with-auth.ts +90 -0
  25. package/src/middleware/with-error-handling.ts +83 -0
  26. package/src/middleware/with-validation.ts +110 -0
  27. package/src/types/api.ts +38 -0
  28. package/src/types/contact.ts +49 -0
  29. package/src/types/entity.ts +153 -0
  30. package/src/types/error.ts +129 -0
  31. package/src/types/funnel.ts +133 -0
  32. package/src/types/index.ts +95 -0
  33. package/src/types/organization.ts +49 -0
  34. package/src/types/response.ts +44 -0
  35. package/src/types/workflow.ts +69 -0
  36. package/src/utils/index.ts +13 -0
  37. package/src/utils/query-params.ts +79 -0
  38. package/src/utils/url-builder.ts +78 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Messages API wrapper
3
+ *
4
+ * Provides type-safe access to Django Messages API
5
+ */
6
+
7
+ import type { ApiClient } from './api-client';
8
+
9
+ export type MessageStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'failed';
10
+ export type MessageContentType = 'text/plain' | 'text/markdown' | 'text/html';
11
+ export type RecipientStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'opened' | 'clicked';
12
+
13
+ export interface Message {
14
+ id: string;
15
+ team: string;
16
+ channel: string;
17
+ channel_details?: {
18
+ key: string;
19
+ name: string;
20
+ description: string;
21
+ icon: string;
22
+ };
23
+ subject: string;
24
+ body: string;
25
+ content_type: MessageContentType;
26
+ from_name: string | null;
27
+ from_email: string | null;
28
+ reply_to: string | null;
29
+ status: MessageStatus;
30
+ scheduled_at: string | null;
31
+ sent_at: string | null;
32
+ metadata: Record<string, any>;
33
+ total_recipients: number;
34
+ recipients_sent: number;
35
+ recipients_delivered: number;
36
+ recipients_opened: number;
37
+ recipients_clicked: number;
38
+ recipients_bounced: number;
39
+ recipients_unsubscribed: number;
40
+ open_rate: number;
41
+ click_rate: number;
42
+ bounce_rate: number;
43
+ error_message: string | null;
44
+ created_at: string;
45
+ updated_at: string;
46
+ }
47
+
48
+ export interface MessageRecipient {
49
+ id: string;
50
+ recipient_type: string;
51
+ recipient_id: string | null;
52
+ recipient_email: string;
53
+ recipient_name: string | null;
54
+ channel_identifier: string | null;
55
+ channel_profiles: Record<string, any>;
56
+ status: RecipientStatus;
57
+ sent_at: string | null;
58
+ delivered_at: string | null;
59
+ bounced_at: string | null;
60
+ first_opened_at: string | null;
61
+ last_opened_at: string | null;
62
+ first_clicked_at: string | null;
63
+ last_clicked_at: string | null;
64
+ open_count: number;
65
+ click_count: number;
66
+ is_unsubscribed: boolean;
67
+ unsubscribed_at: string | null;
68
+ error_message: string | null;
69
+ created_at: string;
70
+ }
71
+
72
+ export interface MessagingChannel {
73
+ id: string;
74
+ key: string;
75
+ name: string;
76
+ description: string;
77
+ icon: string;
78
+ capabilities: {
79
+ max_content_length: number;
80
+ supports_html: boolean;
81
+ supports_markdown: boolean;
82
+ supports_attachments: boolean;
83
+ max_attachments: number;
84
+ max_attachment_size: number;
85
+ };
86
+ requirements: {
87
+ requires_connection: boolean;
88
+ requires_opt_in: boolean;
89
+ auth_requirements: Record<string, any>;
90
+ };
91
+ rate_limit: {
92
+ messages: number;
93
+ seconds: number;
94
+ scope: string;
95
+ description: string;
96
+ };
97
+ metadata: Record<string, any>;
98
+ }
99
+
100
+ export interface MessageFilters {
101
+ status?: MessageStatus;
102
+ content_type?: MessageContentType;
103
+ scheduled_after?: string;
104
+ scheduled_before?: string;
105
+ sent_after?: string;
106
+ sent_before?: string;
107
+ search?: string;
108
+ page?: number;
109
+ page_size?: number;
110
+ ordering?: string;
111
+ }
112
+
113
+ export interface CreateMessageInput {
114
+ channel: string;
115
+ subject: string;
116
+ body: string;
117
+ content_type?: MessageContentType;
118
+ from_name?: string;
119
+ from_email?: string;
120
+ reply_to?: string;
121
+ scheduled_at?: string;
122
+ metadata?: Record<string, any>;
123
+ recipients?: Array<{
124
+ recipient_type: string;
125
+ recipient_email: string;
126
+ recipient_name?: string;
127
+ channel_identifier?: string;
128
+ }>;
129
+ }
130
+
131
+ export interface ScheduleMessageInput {
132
+ scheduled_at: string;
133
+ }
134
+
135
+ export interface SendTestInput {
136
+ test_email?: string;
137
+ }
138
+
139
+ export class MessagesApi {
140
+ constructor(private client: ApiClient) {}
141
+
142
+ /**
143
+ * List messages with optional filters
144
+ */
145
+ async list(filters?: MessageFilters) {
146
+ const params = new URLSearchParams();
147
+
148
+ if (filters?.status) params.append('status', filters.status);
149
+ if (filters?.content_type) params.append('content_type', filters.content_type);
150
+ if (filters?.scheduled_after) params.append('scheduled_after', filters.scheduled_after);
151
+ if (filters?.scheduled_before) params.append('scheduled_before', filters.scheduled_before);
152
+ if (filters?.sent_after) params.append('sent_after', filters.sent_after);
153
+ if (filters?.sent_before) params.append('sent_before', filters.sent_before);
154
+ if (filters?.search) params.append('search', filters.search);
155
+ if (filters?.page) params.append('page', String(filters.page));
156
+ if (filters?.page_size) params.append('page_size', String(filters.page_size));
157
+ if (filters?.ordering) params.append('ordering', filters.ordering);
158
+
159
+ return this.client.get<{ results: Message[]; count: number; next: string | null; previous: string | null }>(
160
+ `/api/v1/messages/?${params.toString()}`
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Get message by ID
166
+ */
167
+ async get(id: string) {
168
+ return this.client.get<Message>(`/api/v1/messages/${id}/`);
169
+ }
170
+
171
+ /**
172
+ * Create a new message
173
+ */
174
+ async create(data: CreateMessageInput) {
175
+ return this.client.post<Message>('/api/v1/messages/', data);
176
+ }
177
+
178
+ /**
179
+ * Update message (draft only)
180
+ */
181
+ async update(id: string, data: Partial<CreateMessageInput>) {
182
+ return this.client.patch<Message>(`/api/v1/messages/${id}/`, data);
183
+ }
184
+
185
+ /**
186
+ * Delete message (draft only)
187
+ */
188
+ async delete(id: string) {
189
+ return this.client.delete(`/api/v1/messages/${id}/`);
190
+ }
191
+
192
+ /**
193
+ * Schedule message for future sending
194
+ */
195
+ async schedule(id: string, input: ScheduleMessageInput) {
196
+ return this.client.post<{ id: string; status: MessageStatus; scheduled_at: string; message: string }>(
197
+ `/api/v1/messages/${id}/schedule/`,
198
+ input
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Send message immediately
204
+ */
205
+ async sendNow(id: string) {
206
+ return this.client.post<{ id: string; status: MessageStatus; message: string }>(
207
+ `/api/v1/messages/${id}/send_now/`,
208
+ {}
209
+ );
210
+ }
211
+
212
+ /**
213
+ * Send test message
214
+ */
215
+ async sendTest(id: string, input?: SendTestInput) {
216
+ return this.client.post<{ id: string; test_email: string; message: string }>(
217
+ `/api/v1/messages/${id}/send_test/`,
218
+ input || {}
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Preview message rendering
224
+ */
225
+ async preview(id: string) {
226
+ return this.client.get<{
227
+ subject: string;
228
+ body: string;
229
+ preview_html: string;
230
+ from_name: string;
231
+ from_email: string;
232
+ }>(`/api/v1/messages/${id}/preview/`);
233
+ }
234
+
235
+ /**
236
+ * List recipients for a message
237
+ */
238
+ async getRecipients(id: string, page?: number, pageSize?: number) {
239
+ const params = new URLSearchParams();
240
+ if (page) params.append('page', String(page));
241
+ if (pageSize) params.append('page_size', String(pageSize));
242
+
243
+ return this.client.get<{ results: MessageRecipient[]; count: number }>(
244
+ `/api/v1/messages/${id}/recipients/?${params.toString()}`
245
+ );
246
+ }
247
+
248
+ /**
249
+ * Add recipients to a message
250
+ */
251
+ async addRecipients(
252
+ id: string,
253
+ recipients: Array<{
254
+ recipient_type: string;
255
+ recipient_email: string;
256
+ recipient_name?: string;
257
+ channel_identifier?: string;
258
+ }>
259
+ ) {
260
+ return this.client.post<{
261
+ message: string;
262
+ total_recipients: number;
263
+ recipients: MessageRecipient[];
264
+ }>(`/api/v1/messages/${id}/add_recipients/`, { recipients });
265
+ }
266
+
267
+ /**
268
+ * Get available messaging channels
269
+ */
270
+ async getChannels() {
271
+ return this.client.get<{ channels: MessagingChannel[]; count: number }>('/api/v1/messages/channels/');
272
+ }
273
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Organizations API wrapper for /api/v1/organizations/
3
+ */
4
+
5
+ import type {
6
+ Organization,
7
+ CreateOrganizationRequest,
8
+ UpdateOrganizationRequest,
9
+ OrganizationFilters,
10
+ PaginatedResponse,
11
+ PaginationParams,
12
+ SortParams,
13
+ } from '../types';
14
+ import { buildFilterParams, mergeQueryParams } from '../utils';
15
+ import { ENDPOINTS } from '../constants/endpoints';
16
+ import type { ApiClient } from './api-client';
17
+
18
+ export class OrganizationsApi {
19
+ constructor(private client: ApiClient) {}
20
+
21
+ /**
22
+ * List organizations with pagination and filters
23
+ */
24
+ async list(
25
+ filters?: OrganizationFilters,
26
+ pagination?: PaginationParams,
27
+ sorting?: SortParams
28
+ ): Promise<PaginatedResponse<Organization>> {
29
+ const params = mergeQueryParams(
30
+ pagination,
31
+ sorting,
32
+ filters ? buildFilterParams(filters) : undefined
33
+ );
34
+
35
+ return this.client.fetch.get<PaginatedResponse<Organization>>(ENDPOINTS.ORGANIZATIONS, {
36
+ params,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Get organization by ID
42
+ */
43
+ async get(id: string): Promise<Organization> {
44
+ return this.client.fetch.get<Organization>(ENDPOINTS.ORGANIZATION(id));
45
+ }
46
+
47
+ /**
48
+ * Create new organization
49
+ */
50
+ async create(data: CreateOrganizationRequest): Promise<Organization> {
51
+ return this.client.fetch.post<Organization>(ENDPOINTS.ORGANIZATIONS, data);
52
+ }
53
+
54
+ /**
55
+ * Update organization
56
+ */
57
+ async update(id: string, data: UpdateOrganizationRequest): Promise<Organization> {
58
+ return this.client.fetch.patch<Organization>(ENDPOINTS.ORGANIZATION(id), data);
59
+ }
60
+
61
+ /**
62
+ * Delete organization
63
+ */
64
+ async delete(id: string): Promise<void> {
65
+ return this.client.fetch.delete<void>(ENDPOINTS.ORGANIZATION(id));
66
+ }
67
+
68
+ /**
69
+ * Bulk create organizations
70
+ */
71
+ async bulkCreate(data: CreateOrganizationRequest[]): Promise<Organization[]> {
72
+ return this.client.fetch.post<Organization[]>(ENDPOINTS.ORGANIZATIONS_BULK, data);
73
+ }
74
+
75
+ /**
76
+ * Search organizations by name, domain, or location
77
+ */
78
+ async search(
79
+ query: string,
80
+ pagination?: PaginationParams
81
+ ): Promise<PaginatedResponse<Organization>> {
82
+ return this.list({ search: query }, pagination);
83
+ }
84
+
85
+ /**
86
+ * Get organizations by tier
87
+ */
88
+ async getByTier(
89
+ tier: number,
90
+ pagination?: PaginationParams
91
+ ): Promise<PaginatedResponse<Organization>> {
92
+ return this.list({ tier }, pagination);
93
+ }
94
+
95
+ /**
96
+ * Get organizations by stage
97
+ */
98
+ async getByStage(
99
+ stage: string,
100
+ pagination?: PaginationParams
101
+ ): Promise<PaginatedResponse<Organization>> {
102
+ return this.list({ stage }, pagination);
103
+ }
104
+
105
+ /**
106
+ * Get organizations by focus area
107
+ */
108
+ async getByFocusArea(
109
+ focusArea: string,
110
+ pagination?: PaginationParams
111
+ ): Promise<PaginatedResponse<Organization>> {
112
+ return this.list({ focusArea }, pagination);
113
+ }
114
+
115
+ /**
116
+ * Get organizations by check size range
117
+ */
118
+ async getByCheckSizeRange(
119
+ min?: number,
120
+ max?: number,
121
+ pagination?: PaginationParams
122
+ ): Promise<PaginatedResponse<Organization>> {
123
+ const filters: OrganizationFilters = {};
124
+ if (min !== undefined) {
125
+ filters.checkSizeMinGte = min;
126
+ }
127
+ if (max !== undefined) {
128
+ filters.checkSizeMaxLte = max;
129
+ }
130
+ return this.list(filters, pagination);
131
+ }
132
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * In-memory rate limiter for Next.js API routes.
3
+ *
4
+ * Fixed-window counter with periodic cleanup.
5
+ * Resets on server restart and does not share state across multiple instances.
6
+ * For multi-instance production use, back the store with Redis/Upstash instead.
7
+ */
8
+
9
+ export interface RateLimitOptions {
10
+ /** Duration of the rate limit window in milliseconds */
11
+ windowMs: number;
12
+ /** Maximum number of requests allowed per window */
13
+ maxRequests: number;
14
+ /** Optional prefix for keys (useful when sharing a single limiter across routes) */
15
+ keyPrefix?: string;
16
+ }
17
+
18
+ export interface RateLimitResult {
19
+ /** Whether the request is within the allowed limit */
20
+ success: boolean;
21
+ /** Requests remaining in the current window */
22
+ remaining: number;
23
+ /** Timestamp (ms since epoch) when the current window resets */
24
+ resetAt: number;
25
+ /** Seconds to wait before retrying, present only when success is false */
26
+ retryAfter?: number;
27
+ }
28
+
29
+ interface RateLimitEntry {
30
+ count: number;
31
+ resetAt: number;
32
+ }
33
+
34
+ const CLEANUP_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
35
+
36
+ /**
37
+ * Factory that returns a rate-check function backed by an isolated in-memory store.
38
+ *
39
+ * @example
40
+ * const check = createRateLimiter({ windowMs: 60_000, maxRequests: 10 });
41
+ * const result = check('user:123');
42
+ * if (!result.success) { // respond 429 }
43
+ */
44
+ export function createRateLimiter(options: RateLimitOptions): (key: string) => RateLimitResult {
45
+ const { windowMs, maxRequests, keyPrefix = '' } = options;
46
+ const store = new Map<string, RateLimitEntry>();
47
+ let lastCleanup = Date.now();
48
+
49
+ function cleanup(): void {
50
+ const now = Date.now();
51
+ if (now - lastCleanup < CLEANUP_INTERVAL_MS) return;
52
+ lastCleanup = now;
53
+ for (const [key, entry] of store.entries()) {
54
+ if (now > entry.resetAt) store.delete(key);
55
+ }
56
+ }
57
+
58
+ return function check(key: string): RateLimitResult {
59
+ cleanup();
60
+
61
+ const storeKey = keyPrefix ? `${keyPrefix}:${key}` : key;
62
+ const now = Date.now();
63
+ const entry = store.get(storeKey);
64
+
65
+ if (!entry || now > entry.resetAt) {
66
+ const resetAt = now + windowMs;
67
+ store.set(storeKey, { count: 1, resetAt });
68
+ return { success: true, remaining: maxRequests - 1, resetAt };
69
+ }
70
+
71
+ if (entry.count >= maxRequests) {
72
+ const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
73
+ return { success: false, remaining: 0, resetAt: entry.resetAt, retryAfter };
74
+ }
75
+
76
+ entry.count++;
77
+ return { success: true, remaining: maxRequests - entry.count, resetAt: entry.resetAt };
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Extract the client IP address from a Web API Request or NextRequest.
83
+ * Respects x-forwarded-for and x-real-ip proxy headers.
84
+ */
85
+ export function getClientIP(request: Request): string {
86
+ const forwarded = request.headers.get('x-forwarded-for');
87
+ if (forwarded) return forwarded.split(',')[0].trim();
88
+ const real = request.headers.get('x-real-ip');
89
+ if (real) return real;
90
+ return 'unknown';
91
+ }
@@ -0,0 +1,39 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
2
+
3
+ /**
4
+ * XSS Protection - sanitize HTML content
5
+ *
6
+ * Strips all tags except a safe allowlist and removes dangerous attributes.
7
+ */
8
+ export function sanitizeHtml(input: string): string {
9
+ return DOMPurify.sanitize(input, {
10
+ ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
11
+ ALLOWED_ATTR: ['href'],
12
+ ALLOW_DATA_ATTR: false,
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Validate and sanitize a search query string.
18
+ *
19
+ * Trims whitespace, escapes regex special characters, and limits length to 100 chars.
20
+ */
21
+ export function sanitizeSearchQuery(query: string): string {
22
+ if (!query || typeof query !== 'string') {
23
+ return '';
24
+ }
25
+
26
+ return query
27
+ .trim()
28
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
29
+ .substring(0, 100); // Limit length
30
+ }
31
+
32
+ /**
33
+ * Validate that a string is a safe SQL/API identifier.
34
+ *
35
+ * Only allows alphanumeric characters, underscores, and hyphens.
36
+ */
37
+ export function validateIdentifier(input: string): boolean {
38
+ return /^[a-zA-Z0-9_-]+$/.test(input);
39
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Workflows API wrapper
3
+ *
4
+ * Provides type-safe access to Django Workflows API
5
+ */
6
+
7
+ import type { ApiClient } from './api-client';
8
+
9
+ export type WorkflowStatus = 'draft' | 'active' | 'paused';
10
+
11
+ export interface Workflow {
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ team: string;
16
+ createdBy: string | null;
17
+ nodes: any[];
18
+ connections: Record<string, any>;
19
+ settings: Record<string, any>;
20
+ staticData: Record<string, any>;
21
+ isActive: boolean;
22
+ version: number;
23
+ isTemplate: boolean;
24
+ templateSource: string | null;
25
+ executionsCount: number;
26
+ createdAt: string;
27
+ updatedAt: string;
28
+ }
29
+
30
+ export interface WorkflowExecution {
31
+ id: string;
32
+ workflow: string;
33
+ workflowVersion: number;
34
+ triggeredBy: string;
35
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'paused';
36
+ mode: 'manual' | 'trigger' | 'webhook';
37
+ contextData: Record<string, any>;
38
+ state: Record<string, any>;
39
+ startedAt: string | null;
40
+ completedAt: string | null;
41
+ waitUntil: string | null;
42
+ errorMessage: string | null;
43
+ createdAt: string;
44
+ updatedAt: string;
45
+ }
46
+
47
+ export interface WorkflowFilters {
48
+ team?: string;
49
+ isActive?: boolean;
50
+ isTemplate?: boolean;
51
+ page?: number;
52
+ pageSize?: number;
53
+ ordering?: string;
54
+ }
55
+
56
+ export interface ExecuteWorkflowInput {
57
+ contextData?: Record<string, any>;
58
+ mode?: 'manual' | 'trigger' | 'webhook';
59
+ }
60
+
61
+ export interface CreateWorkflowInput {
62
+ name: string;
63
+ description?: string;
64
+ team: string;
65
+ nodes?: any[];
66
+ connections?: Record<string, any>;
67
+ settings?: Record<string, any>;
68
+ isActive?: boolean;
69
+ }
70
+
71
+ export class WorkflowsApi {
72
+ constructor(private client: ApiClient) {}
73
+
74
+ /**
75
+ * List workflows with optional filters
76
+ */
77
+ async list(filters?: WorkflowFilters) {
78
+ const params = new URLSearchParams();
79
+
80
+ if (filters?.team) params.append('team', filters.team);
81
+ if (filters?.isActive !== undefined) params.append('isActive', String(filters.isActive));
82
+ if (filters?.isTemplate !== undefined) params.append('isTemplate', String(filters.isTemplate));
83
+ if (filters?.page) params.append('page', String(filters.page));
84
+ if (filters?.pageSize) params.append('pageSize', String(filters.pageSize));
85
+ if (filters?.ordering) params.append('ordering', filters.ordering);
86
+
87
+ return this.client.get<{ results: Workflow[]; count: number; next: string | null; previous: string | null }>(
88
+ `/api/v1/workflows/?${params.toString()}`
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Get workflow by ID
94
+ */
95
+ async get(id: string) {
96
+ return this.client.get<Workflow>(`/api/v1/workflows/${id}/`);
97
+ }
98
+
99
+ /**
100
+ * Create a new workflow
101
+ */
102
+ async create(data: CreateWorkflowInput) {
103
+ return this.client.post<Workflow>('/api/v1/workflows/', data);
104
+ }
105
+
106
+ /**
107
+ * Update workflow
108
+ */
109
+ async update(id: string, data: Partial<CreateWorkflowInput>) {
110
+ return this.client.patch<Workflow>(`/api/v1/workflows/${id}/`, data);
111
+ }
112
+
113
+ /**
114
+ * Delete workflow
115
+ */
116
+ async delete(id: string) {
117
+ return this.client.delete(`/api/v1/workflows/${id}/`);
118
+ }
119
+
120
+ /**
121
+ * Execute workflow
122
+ */
123
+ async execute(id: string, input?: ExecuteWorkflowInput) {
124
+ return this.client.post<WorkflowExecution>(
125
+ `/api/v1/workflows/${id}/execute/`,
126
+ input || {}
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Get workflow templates
132
+ */
133
+ async templates() {
134
+ return this.client.get<Workflow[]>('/api/v1/workflows/templates/');
135
+ }
136
+
137
+ /**
138
+ * List workflow executions
139
+ */
140
+ async listExecutions(filters?: { workflow?: string; status?: string; page?: number; pageSize?: number }) {
141
+ const params = new URLSearchParams();
142
+
143
+ if (filters?.workflow) params.append('workflow', filters.workflow);
144
+ if (filters?.status) params.append('status', filters.status);
145
+ if (filters?.page) params.append('page', String(filters.page));
146
+ if (filters?.pageSize) params.append('pageSize', String(filters.pageSize));
147
+
148
+ return this.client.get<{ results: WorkflowExecution[]; count: number }>(
149
+ `/api/v1/workflow-executions/?${params.toString()}`
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Get workflow execution details
155
+ */
156
+ async getExecution(id: string) {
157
+ return this.client.get<WorkflowExecution>(`/api/v1/workflow-executions/${id}/`);
158
+ }
159
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Middleware exports for @startsimpli/api
3
+ */
4
+
5
+ export { withAuth, extractUserIdFromToken } from './with-auth';
6
+ export type { AuthContext, ApiHandler, WithAuthOptions } from './with-auth';
7
+
8
+ export { withErrorHandling } from './with-error-handling';
9
+ export type { ErrorApiHandler, ErrorResponseBody } from './with-error-handling';
10
+
11
+ export { withValidation, composeMiddleware } from './with-validation';
12
+ export type { ValidatedApiHandler, ValidationSchemas } from './with-validation';