@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.
- package/README.md +329 -0
- package/package.json +42 -0
- package/src/__tests__/jwt-refresh.test.ts +195 -0
- package/src/__tests__/query-params.test.ts +144 -0
- package/src/__tests__/url-builder.test.ts +121 -0
- package/src/constants/endpoints.ts +39 -0
- package/src/index.ts +109 -0
- package/src/lib/api-client.ts +89 -0
- package/src/lib/contacts-api.ts +111 -0
- package/src/lib/cors.ts +122 -0
- package/src/lib/entities-api.ts +123 -0
- package/src/lib/env.ts +35 -0
- package/src/lib/error-handler.ts +138 -0
- package/src/lib/errors.ts +381 -0
- package/src/lib/fetch-wrapper.ts +188 -0
- package/src/lib/llm-sanitize.ts +145 -0
- package/src/lib/messages-api.ts +273 -0
- package/src/lib/messages-api.ts.backup +273 -0
- package/src/lib/organizations-api.ts +132 -0
- package/src/lib/rate-limit.ts +91 -0
- package/src/lib/sanitize.ts +39 -0
- package/src/lib/workflows-api.ts +159 -0
- package/src/middleware/index.ts +12 -0
- package/src/middleware/with-auth.ts +90 -0
- package/src/middleware/with-error-handling.ts +83 -0
- package/src/middleware/with-validation.ts +110 -0
- package/src/types/api.ts +38 -0
- package/src/types/contact.ts +49 -0
- package/src/types/entity.ts +153 -0
- package/src/types/error.ts +129 -0
- package/src/types/funnel.ts +133 -0
- package/src/types/index.ts +95 -0
- package/src/types/organization.ts +49 -0
- package/src/types/response.ts +44 -0
- package/src/types/workflow.ts +69 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/query-params.ts +79 -0
- package/src/utils/url-builder.ts +78 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe fetch wrapper for Django REST API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FetchOptions, HttpMethod } from '../types';
|
|
6
|
+
import { buildUrl, buildQueryString } from '../utils';
|
|
7
|
+
import { ApiException, parseErrorResponse, handleFetchError } from './error-handler';
|
|
8
|
+
|
|
9
|
+
export interface FetchWrapperConfig {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
getToken?: () => Promise<string | null> | string | null;
|
|
12
|
+
onUnauthorized?: () => void;
|
|
13
|
+
onTokenRefresh?: () => Promise<string | null>;
|
|
14
|
+
defaultHeaders?: HeadersInit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class FetchWrapper {
|
|
18
|
+
private config: FetchWrapperConfig;
|
|
19
|
+
private isRefreshing = false;
|
|
20
|
+
private refreshPromise: Promise<string | null> | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(config: FetchWrapperConfig) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build headers with auth token
|
|
28
|
+
*/
|
|
29
|
+
private async buildHeaders(customHeaders?: HeadersInit): Promise<Headers> {
|
|
30
|
+
const headers = new Headers(this.config.defaultHeaders);
|
|
31
|
+
|
|
32
|
+
// Add custom headers
|
|
33
|
+
if (customHeaders) {
|
|
34
|
+
const customHeadersObj = new Headers(customHeaders);
|
|
35
|
+
customHeadersObj.forEach((value, key) => {
|
|
36
|
+
headers.set(key, value);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add auth token if available
|
|
41
|
+
if (this.config.getToken) {
|
|
42
|
+
const token = await this.config.getToken();
|
|
43
|
+
if (token) {
|
|
44
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Always set content-type for JSON
|
|
49
|
+
if (!headers.has('Content-Type')) {
|
|
50
|
+
headers.set('Content-Type', 'application/json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return headers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute fetch request
|
|
58
|
+
*/
|
|
59
|
+
private async execute<T>(
|
|
60
|
+
method: HttpMethod,
|
|
61
|
+
endpoint: string,
|
|
62
|
+
options?: FetchOptions
|
|
63
|
+
): Promise<T> {
|
|
64
|
+
try {
|
|
65
|
+
const { params, headers: customHeaders, ...fetchOptions } = options || {};
|
|
66
|
+
|
|
67
|
+
// Build URL
|
|
68
|
+
const url = buildUrl({
|
|
69
|
+
baseUrl: this.config.baseUrl,
|
|
70
|
+
endpoint,
|
|
71
|
+
params,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Build headers
|
|
75
|
+
const headers = await this.buildHeaders(customHeaders);
|
|
76
|
+
|
|
77
|
+
// Execute request
|
|
78
|
+
let response = await fetch(url, {
|
|
79
|
+
method,
|
|
80
|
+
headers,
|
|
81
|
+
credentials: 'include',
|
|
82
|
+
...fetchOptions,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Handle 401 Unauthorized - attempt token refresh
|
|
86
|
+
if (response.status === 401 && this.config.onTokenRefresh) {
|
|
87
|
+
// Prevent multiple simultaneous refresh attempts
|
|
88
|
+
if (!this.isRefreshing) {
|
|
89
|
+
this.isRefreshing = true;
|
|
90
|
+
this.refreshPromise = this.config.onTokenRefresh().finally(() => {
|
|
91
|
+
this.isRefreshing = false;
|
|
92
|
+
this.refreshPromise = null;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const newToken = await this.refreshPromise;
|
|
97
|
+
|
|
98
|
+
if (newToken) {
|
|
99
|
+
// Retry original request with new token
|
|
100
|
+
const retryHeaders = new Headers(headers);
|
|
101
|
+
retryHeaders.set('Authorization', `Bearer ${newToken}`);
|
|
102
|
+
|
|
103
|
+
response = await fetch(url, {
|
|
104
|
+
method,
|
|
105
|
+
headers: retryHeaders,
|
|
106
|
+
credentials: 'include',
|
|
107
|
+
...fetchOptions,
|
|
108
|
+
});
|
|
109
|
+
} else if (this.config.onUnauthorized) {
|
|
110
|
+
// Refresh failed - call unauthorized callback
|
|
111
|
+
this.config.onUnauthorized();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle error responses
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = await parseErrorResponse(response);
|
|
118
|
+
throw new ApiException(error.message || 'Request failed', error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle 204 No Content
|
|
122
|
+
if (response.status === 204) {
|
|
123
|
+
return undefined as T;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse JSON response
|
|
127
|
+
return await response.json();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
handleFetchError(error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* GET request
|
|
135
|
+
*/
|
|
136
|
+
async get<T>(endpoint: string, options?: FetchOptions): Promise<T> {
|
|
137
|
+
return this.execute<T>('GET', endpoint, options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* POST request
|
|
142
|
+
*/
|
|
143
|
+
async post<T, D = unknown>(
|
|
144
|
+
endpoint: string,
|
|
145
|
+
data?: D,
|
|
146
|
+
options?: FetchOptions
|
|
147
|
+
): Promise<T> {
|
|
148
|
+
return this.execute<T>('POST', endpoint, {
|
|
149
|
+
...options,
|
|
150
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* PUT request
|
|
156
|
+
*/
|
|
157
|
+
async put<T, D = unknown>(
|
|
158
|
+
endpoint: string,
|
|
159
|
+
data?: D,
|
|
160
|
+
options?: FetchOptions
|
|
161
|
+
): Promise<T> {
|
|
162
|
+
return this.execute<T>('PUT', endpoint, {
|
|
163
|
+
...options,
|
|
164
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* PATCH request
|
|
170
|
+
*/
|
|
171
|
+
async patch<T, D = unknown>(
|
|
172
|
+
endpoint: string,
|
|
173
|
+
data?: D,
|
|
174
|
+
options?: FetchOptions
|
|
175
|
+
): Promise<T> {
|
|
176
|
+
return this.execute<T>('PATCH', endpoint, {
|
|
177
|
+
...options,
|
|
178
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* DELETE request
|
|
184
|
+
*/
|
|
185
|
+
async delete<T>(endpoint: string, options?: FetchOptions): Promise<T> {
|
|
186
|
+
return this.execute<T>('DELETE', endpoint, options);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Sanitization for LLM Prompts
|
|
3
|
+
*
|
|
4
|
+
* Provides protection against prompt injection attacks by sanitizing
|
|
5
|
+
* user input before including it in LLM prompts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Maximum allowed length for user prompts (characters)
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_PROMPT_LENGTH = 2000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maximum allowed length for chat messages (characters)
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_CHAT_MESSAGE_LENGTH = 1000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Patterns that could be used for prompt injection
|
|
20
|
+
*/
|
|
21
|
+
const INJECTION_PATTERNS = [
|
|
22
|
+
// System/role injection attempts
|
|
23
|
+
/\bSYSTEM\s*:/gi,
|
|
24
|
+
/\bASSISTANT\s*:/gi,
|
|
25
|
+
/\bUSER\s*:/gi,
|
|
26
|
+
/\bHUMAN\s*:/gi,
|
|
27
|
+
/\bAI\s*:/gi,
|
|
28
|
+
// Instruction override attempts
|
|
29
|
+
/\bignore\s+(previous|above|all)\s+instructions?\b/gi,
|
|
30
|
+
/\bdisregard\s+(previous|above|all)\s+instructions?\b/gi,
|
|
31
|
+
/\bforget\s+(previous|above|all)\s+instructions?\b/gi,
|
|
32
|
+
/\bnew\s+instructions?\s*:/gi,
|
|
33
|
+
/\boverride\s*:/gi,
|
|
34
|
+
// Role-playing attempts
|
|
35
|
+
/\byou\s+are\s+now\b/gi,
|
|
36
|
+
/\bact\s+as\s+(if|a|an|the)\b/gi,
|
|
37
|
+
/\bpretend\s+(to\s+be|you\s+are)\b/gi,
|
|
38
|
+
// JSON/schema manipulation
|
|
39
|
+
/\b(output|respond|return)\s+only\s*:/gi,
|
|
40
|
+
/\bformat\s*:\s*json\b/gi,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Markdown/formatting sequences that could interfere with prompt structure
|
|
45
|
+
*/
|
|
46
|
+
const FORMATTING_MARKERS = [
|
|
47
|
+
'```', // Code blocks
|
|
48
|
+
'---', // Horizontal rules (when at line start)
|
|
49
|
+
'***', // Alternative horizontal rules
|
|
50
|
+
'##', // Headers (when at line start)
|
|
51
|
+
'>>', // Potential quote injection
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Control characters to remove (except standard whitespace)
|
|
56
|
+
*/
|
|
57
|
+
const CONTROL_CHAR_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sanitize user input for inclusion in LLM prompts
|
|
61
|
+
*
|
|
62
|
+
* @param input - Raw user input
|
|
63
|
+
* @param maxLength - Maximum allowed length (defaults to MAX_PROMPT_LENGTH)
|
|
64
|
+
* @returns Sanitized input safe for prompt inclusion
|
|
65
|
+
*/
|
|
66
|
+
export function sanitizeUserInput(
|
|
67
|
+
input: string,
|
|
68
|
+
maxLength: number = MAX_PROMPT_LENGTH
|
|
69
|
+
): string {
|
|
70
|
+
if (!input || typeof input !== 'string') {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let sanitized = input;
|
|
75
|
+
|
|
76
|
+
// 1. Remove control characters (keep newlines, tabs, spaces)
|
|
77
|
+
sanitized = sanitized.replace(CONTROL_CHAR_REGEX, '');
|
|
78
|
+
|
|
79
|
+
// 2. Normalize whitespace (collapse multiple spaces/newlines)
|
|
80
|
+
sanitized = sanitized
|
|
81
|
+
.replace(/\r\n/g, '\n') // Normalize line endings
|
|
82
|
+
.replace(/\r/g, '\n')
|
|
83
|
+
.replace(/\n{3,}/g, '\n\n') // Max 2 consecutive newlines
|
|
84
|
+
.replace(/[ \t]{3,}/g, ' ') // Max 2 consecutive spaces/tabs
|
|
85
|
+
.trim();
|
|
86
|
+
|
|
87
|
+
// 3. Strip or escape injection patterns
|
|
88
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
89
|
+
sanitized = sanitized.replace(pattern, (match) => {
|
|
90
|
+
// Replace with bracketed version to neutralize while preserving intent
|
|
91
|
+
return `[${match.replace(/:/g, '')}]`;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 4. Escape formatting markers that could break prompt structure
|
|
96
|
+
for (const marker of FORMATTING_MARKERS) {
|
|
97
|
+
// Only escape when these appear at start of line or standalone
|
|
98
|
+
const escapeRegex = new RegExp(`(^|\\n)(${escapeRegexChars(marker)})`, 'g');
|
|
99
|
+
sanitized = sanitized.replace(escapeRegex, (_, prefix, match) => {
|
|
100
|
+
return `${prefix}[${match}]`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Truncate to max length (at word boundary if possible)
|
|
105
|
+
if (sanitized.length > maxLength) {
|
|
106
|
+
sanitized = truncateAtWordBoundary(sanitized, maxLength);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return sanitized;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sanitize chat message input (shorter limit, same rules)
|
|
114
|
+
*/
|
|
115
|
+
export function sanitizeChatMessage(input: string): string {
|
|
116
|
+
return sanitizeUserInput(input, MAX_CHAT_MESSAGE_LENGTH);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Truncate string at a word boundary
|
|
121
|
+
*/
|
|
122
|
+
function truncateAtWordBoundary(text: string, maxLength: number): string {
|
|
123
|
+
if (text.length <= maxLength) {
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Find the last space before maxLength
|
|
128
|
+
const truncated = text.slice(0, maxLength);
|
|
129
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
130
|
+
|
|
131
|
+
// If we found a space in the last 20% of the string, truncate there
|
|
132
|
+
if (lastSpace > maxLength * 0.8) {
|
|
133
|
+
return truncated.slice(0, lastSpace).trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Otherwise just hard truncate
|
|
137
|
+
return truncated.trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Escape special regex characters in a string
|
|
142
|
+
*/
|
|
143
|
+
function escapeRegexChars(str: string): string {
|
|
144
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
145
|
+
}
|
|
@@ -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
|
+
channelDetails?: {
|
|
18
|
+
key: string;
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
icon: string;
|
|
22
|
+
};
|
|
23
|
+
subject: string;
|
|
24
|
+
body: string;
|
|
25
|
+
contentType: MessageContentType;
|
|
26
|
+
fromName: string | null;
|
|
27
|
+
fromEmail: string | null;
|
|
28
|
+
replyTo: string | null;
|
|
29
|
+
status: MessageStatus;
|
|
30
|
+
scheduledAt: string | null;
|
|
31
|
+
sentAt: string | null;
|
|
32
|
+
metadata: Record<string, any>;
|
|
33
|
+
totalRecipients: number;
|
|
34
|
+
recipientsSent: number;
|
|
35
|
+
recipientsDelivered: number;
|
|
36
|
+
recipientsOpened: number;
|
|
37
|
+
recipientsClicked: number;
|
|
38
|
+
recipientsBounced: number;
|
|
39
|
+
recipientsUnsubscribed: number;
|
|
40
|
+
openRate: number;
|
|
41
|
+
clickRate: number;
|
|
42
|
+
bounceRate: number;
|
|
43
|
+
errorMessage: string | null;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
updatedAt: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MessageRecipient {
|
|
49
|
+
id: string;
|
|
50
|
+
recipientType: string;
|
|
51
|
+
recipientId: string | null;
|
|
52
|
+
recipientEmail: string;
|
|
53
|
+
recipientName: string | null;
|
|
54
|
+
channelIdentifier: string | null;
|
|
55
|
+
channelProfiles: Record<string, any>;
|
|
56
|
+
status: RecipientStatus;
|
|
57
|
+
sentAt: string | null;
|
|
58
|
+
deliveredAt: string | null;
|
|
59
|
+
bouncedAt: string | null;
|
|
60
|
+
firstOpenedAt: string | null;
|
|
61
|
+
lastOpenedAt: string | null;
|
|
62
|
+
firstClickedAt: string | null;
|
|
63
|
+
lastClickedAt: string | null;
|
|
64
|
+
openCount: number;
|
|
65
|
+
clickCount: number;
|
|
66
|
+
isUnsubscribed: boolean;
|
|
67
|
+
unsubscribedAt: string | null;
|
|
68
|
+
errorMessage: string | null;
|
|
69
|
+
createdAt: 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
|
+
maxContentLength: number;
|
|
80
|
+
supportsHtml: boolean;
|
|
81
|
+
supportsMarkdown: boolean;
|
|
82
|
+
supportsAttachments: boolean;
|
|
83
|
+
maxAttachments: number;
|
|
84
|
+
maxAttachmentSize: number;
|
|
85
|
+
};
|
|
86
|
+
requirements: {
|
|
87
|
+
requiresConnection: boolean;
|
|
88
|
+
requiresOptIn: boolean;
|
|
89
|
+
authRequirements: Record<string, any>;
|
|
90
|
+
};
|
|
91
|
+
rateLimit: {
|
|
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
|
+
contentType?: MessageContentType;
|
|
103
|
+
scheduledAfter?: string;
|
|
104
|
+
scheduledBefore?: string;
|
|
105
|
+
sentAfter?: string;
|
|
106
|
+
sentBefore?: string;
|
|
107
|
+
search?: string;
|
|
108
|
+
page?: number;
|
|
109
|
+
pageSize?: number;
|
|
110
|
+
ordering?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CreateMessageInput {
|
|
114
|
+
channel: string;
|
|
115
|
+
subject: string;
|
|
116
|
+
body: string;
|
|
117
|
+
contentType?: MessageContentType;
|
|
118
|
+
fromName?: string;
|
|
119
|
+
fromEmail?: string;
|
|
120
|
+
replyTo?: string;
|
|
121
|
+
scheduledAt?: string;
|
|
122
|
+
metadata?: Record<string, any>;
|
|
123
|
+
recipients?: Array<{
|
|
124
|
+
recipientType: string;
|
|
125
|
+
recipientEmail: string;
|
|
126
|
+
recipientName?: string;
|
|
127
|
+
channelIdentifier?: string;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ScheduleMessageInput {
|
|
132
|
+
scheduledAt: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface SendTestInput {
|
|
136
|
+
testEmail?: 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?.contentType) params.append('contentType', filters.contentType);
|
|
150
|
+
if (filters?.scheduledAfter) params.append('scheduledAfter', filters.scheduledAfter);
|
|
151
|
+
if (filters?.scheduledBefore) params.append('scheduledBefore', filters.scheduledBefore);
|
|
152
|
+
if (filters?.sentAfter) params.append('sentAfter', filters.sentAfter);
|
|
153
|
+
if (filters?.sentBefore) params.append('sentBefore', filters.sentBefore);
|
|
154
|
+
if (filters?.search) params.append('search', filters.search);
|
|
155
|
+
if (filters?.page) params.append('page', String(filters.page));
|
|
156
|
+
if (filters?.pageSize) params.append('pageSize', String(filters.pageSize));
|
|
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; scheduledAt: 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; testEmail: 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
|
+
previewHtml: string;
|
|
230
|
+
fromName: string;
|
|
231
|
+
fromEmail: 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('pageSize', 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
|
+
recipientType: string;
|
|
255
|
+
recipientEmail: string;
|
|
256
|
+
recipientName?: string;
|
|
257
|
+
channelIdentifier?: string;
|
|
258
|
+
}>
|
|
259
|
+
) {
|
|
260
|
+
return this.client.post<{
|
|
261
|
+
message: string;
|
|
262
|
+
totalRecipients: 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
|
+
}
|