perspectapi-ts-sdk 1.1.1 → 1.3.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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Newsletter subscription client for PerspectAPI SDK
3
+ */
4
+
5
+ import { BaseClient } from './base-client';
6
+ import type {
7
+ NewsletterSubscription,
8
+ CreateNewsletterSubscriptionRequest,
9
+ NewsletterList,
10
+ NewsletterPreferences,
11
+ NewsletterStatusResponse,
12
+ NewsletterSubscribeResponse,
13
+ NewsletterConfirmResponse,
14
+ NewsletterUnsubscribeRequest,
15
+ PaginatedResponse,
16
+ ApiResponse,
17
+ } from '../types';
18
+
19
+ export class NewsletterClient extends BaseClient {
20
+ constructor(http: any) {
21
+ super(http, '/api/v1');
22
+ }
23
+
24
+ /**
25
+ * Build a newsletter endpoint scoped to a site (without /sites prefix)
26
+ */
27
+ private newsletterEndpoint(siteName: string, endpoint: string): string {
28
+ return this.siteScopedEndpoint(siteName, endpoint, { includeSitesSegment: false });
29
+ }
30
+
31
+ /**
32
+ * Subscribe to newsletter
33
+ * @param siteName - The site to subscribe to
34
+ * @param data - Subscription data
35
+ * @param csrfToken - CSRF token (required for browser-based submissions)
36
+ */
37
+ async subscribe(
38
+ siteName: string,
39
+ data: CreateNewsletterSubscriptionRequest,
40
+ csrfToken?: string
41
+ ): Promise<ApiResponse<NewsletterSubscribeResponse>> {
42
+ // CSRF token is required for browser submissions
43
+ if (typeof window !== 'undefined' && !csrfToken && !data.turnstile_token) {
44
+ console.warn('CSRF token recommended for browser-based newsletter subscriptions');
45
+ }
46
+
47
+ return this.create<CreateNewsletterSubscriptionRequest, NewsletterSubscribeResponse>(
48
+ this.newsletterEndpoint(siteName, '/newsletter/subscribe'),
49
+ data,
50
+ csrfToken
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Confirm newsletter subscription via token
56
+ */
57
+ async confirmSubscription(
58
+ siteName: string,
59
+ token: string
60
+ ): Promise<ApiResponse<NewsletterConfirmResponse>> {
61
+ return this.getSingle(
62
+ this.newsletterEndpoint(siteName, `/newsletter/confirm/${encodeURIComponent(token)}`)
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Unsubscribe from newsletter
68
+ * @param siteName - The site to unsubscribe from
69
+ * @param data - Unsubscribe data
70
+ * @param csrfToken - CSRF token (required for browser-based submissions)
71
+ */
72
+ async unsubscribe(
73
+ siteName: string,
74
+ data: NewsletterUnsubscribeRequest,
75
+ csrfToken?: string
76
+ ): Promise<ApiResponse<{ message: string }>> {
77
+ return this.create<NewsletterUnsubscribeRequest, { message: string }>(
78
+ this.newsletterEndpoint(siteName, '/newsletter/unsubscribe'),
79
+ data,
80
+ csrfToken
81
+ );
82
+ }
83
+
84
+ /**
85
+ * One-click unsubscribe via token (GET request)
86
+ */
87
+ async unsubscribeByToken(
88
+ siteName: string,
89
+ token: string
90
+ ): Promise<ApiResponse<string>> {
91
+ // This returns HTML, so we return the raw response
92
+ return this.http.get(
93
+ this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/unsubscribe/${encodeURIComponent(token)}`))
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Update subscription preferences
99
+ * @param siteName - The site name
100
+ * @param email - Subscriber email
101
+ * @param preferences - New preferences
102
+ * @param csrfToken - CSRF token (required for browser-based submissions)
103
+ */
104
+ async updatePreferences(
105
+ siteName: string,
106
+ email: string,
107
+ preferences: NewsletterPreferences,
108
+ csrfToken?: string
109
+ ): Promise<ApiResponse<{ message: string; preferences: NewsletterPreferences }>> {
110
+ return this.patch(
111
+ this.newsletterEndpoint(siteName, '/newsletter/preferences'),
112
+ { email, ...preferences },
113
+ csrfToken
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Get available newsletter lists
119
+ */
120
+ async getLists(siteName: string): Promise<ApiResponse<{
121
+ lists: NewsletterList[];
122
+ total: number;
123
+ }>> {
124
+ return this.getSingle(
125
+ this.newsletterEndpoint(siteName, '/newsletter/lists')
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Check subscription status by email
131
+ */
132
+ async getStatus(
133
+ siteName: string,
134
+ email: string
135
+ ): Promise<ApiResponse<NewsletterStatusResponse>> {
136
+ return this.http.get(
137
+ this.buildPath(this.newsletterEndpoint(siteName, '/newsletter/status')),
138
+ { email }
139
+ );
140
+ }
141
+
142
+ // Admin methods (require authentication)
143
+
144
+ /**
145
+ * Get all newsletter subscriptions (admin only)
146
+ */
147
+ async getSubscriptions(
148
+ siteName: string,
149
+ params?: {
150
+ page?: number;
151
+ limit?: number;
152
+ status?: string;
153
+ list_id?: string;
154
+ search?: string;
155
+ startDate?: string;
156
+ endDate?: string;
157
+ }
158
+ ): Promise<PaginatedResponse<NewsletterSubscription>> {
159
+ return this.getPaginated<NewsletterSubscription>(
160
+ this.newsletterEndpoint(siteName, '/newsletter/subscriptions'),
161
+ params
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Get subscription by ID (admin only)
167
+ */
168
+ async getSubscriptionById(
169
+ siteName: string,
170
+ id: string
171
+ ): Promise<ApiResponse<NewsletterSubscription>> {
172
+ return this.getSingle(
173
+ this.newsletterEndpoint(siteName, `/newsletter/subscriptions/${encodeURIComponent(id)}`)
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Update subscription status (admin only)
179
+ */
180
+ async updateSubscriptionStatus(
181
+ siteName: string,
182
+ id: string,
183
+ status: 'confirmed' | 'unsubscribed' | 'bounced' | 'complained',
184
+ notes?: string
185
+ ): Promise<ApiResponse<{ message: string }>> {
186
+ return this.patch(
187
+ this.newsletterEndpoint(siteName, `/newsletter/subscriptions/${encodeURIComponent(id)}`),
188
+ { status, notes }
189
+ );
190
+ }
191
+
192
+ /**
193
+ * Delete subscription (admin only)
194
+ */
195
+ async deleteSubscription(
196
+ siteName: string,
197
+ id: string
198
+ ): Promise<ApiResponse<{ message: string }>> {
199
+ return this.delete<{ message: string }>(
200
+ this.newsletterEndpoint(siteName, `/newsletter/subscriptions/${encodeURIComponent(id)}`)
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Bulk update subscriptions (admin only)
206
+ */
207
+ async bulkUpdateSubscriptions(
208
+ siteName: string,
209
+ data: {
210
+ ids: string[];
211
+ action: 'confirm' | 'unsubscribe' | 'delete' | 'add_to_list' | 'remove_from_list';
212
+ list_id?: string;
213
+ }
214
+ ): Promise<ApiResponse<{
215
+ success: boolean;
216
+ updatedCount: number;
217
+ failedCount: number;
218
+ }>> {
219
+ return this.create(
220
+ this.newsletterEndpoint(siteName, '/newsletter/subscriptions/bulk-update'),
221
+ data
222
+ );
223
+ }
224
+
225
+ /**
226
+ * Create newsletter list (admin only)
227
+ */
228
+ async createList(
229
+ siteName: string,
230
+ data: {
231
+ list_name: string;
232
+ slug: string;
233
+ description?: string;
234
+ is_public?: boolean;
235
+ is_default?: boolean;
236
+ double_opt_in?: boolean;
237
+ welcome_email_enabled?: boolean;
238
+ }
239
+ ): Promise<ApiResponse<NewsletterList>> {
240
+ return this.create<any, NewsletterList>(
241
+ this.newsletterEndpoint(siteName, '/newsletter/lists'),
242
+ data
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Update newsletter list (admin only)
248
+ */
249
+ async updateList(
250
+ siteName: string,
251
+ listId: string,
252
+ data: Partial<{
253
+ list_name: string;
254
+ description: string;
255
+ is_public: boolean;
256
+ is_default: boolean;
257
+ double_opt_in: boolean;
258
+ welcome_email_enabled: boolean;
259
+ }>
260
+ ): Promise<ApiResponse<{ message: string }>> {
261
+ return this.update(
262
+ this.newsletterEndpoint(siteName, `/newsletter/lists/${encodeURIComponent(listId)}`),
263
+ data
264
+ );
265
+ }
266
+
267
+ /**
268
+ * Delete newsletter list (admin only)
269
+ */
270
+ async deleteList(
271
+ siteName: string,
272
+ listId: string
273
+ ): Promise<ApiResponse<{ message: string }>> {
274
+ return this.delete<{ message: string }>(
275
+ this.newsletterEndpoint(siteName, `/newsletter/lists/${encodeURIComponent(listId)}`)
276
+ );
277
+ }
278
+
279
+ /**
280
+ * Get newsletter statistics (admin only)
281
+ */
282
+ async getStatistics(
283
+ siteName: string,
284
+ params?: {
285
+ startDate?: string;
286
+ endDate?: string;
287
+ list_id?: string;
288
+ }
289
+ ): Promise<ApiResponse<{
290
+ totalSubscribers: number;
291
+ confirmedSubscribers: number;
292
+ pendingSubscribers: number;
293
+ unsubscribedCount: number;
294
+ bouncedCount: number;
295
+ subscribersByDay: Array<{
296
+ date: string;
297
+ count: number;
298
+ }>;
299
+ subscribersByList: Array<{
300
+ list_id: string;
301
+ list_name: string;
302
+ count: number;
303
+ }>;
304
+ engagementMetrics: {
305
+ averageOpenRate: number;
306
+ averageClickRate: number;
307
+ };
308
+ }>> {
309
+ return this.http.get(
310
+ this.buildPath(this.newsletterEndpoint(siteName, '/newsletter/statistics')),
311
+ params
312
+ );
313
+ }
314
+
315
+ /**
316
+ * Export newsletter subscriptions (admin only)
317
+ */
318
+ async exportSubscriptions(
319
+ siteName: string,
320
+ params?: {
321
+ format?: 'csv' | 'json' | 'xlsx';
322
+ status?: string;
323
+ list_id?: string;
324
+ startDate?: string;
325
+ endDate?: string;
326
+ }
327
+ ): Promise<ApiResponse<{
328
+ downloadUrl: string;
329
+ expiresAt: string;
330
+ }>> {
331
+ return this.create(
332
+ this.newsletterEndpoint(siteName, '/newsletter/export'),
333
+ params || {}
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Import newsletter subscriptions (admin only)
339
+ */
340
+ async importSubscriptions(
341
+ siteName: string,
342
+ data: {
343
+ subscriptions: Array<{
344
+ email: string;
345
+ name?: string;
346
+ status?: string;
347
+ lists?: string[];
348
+ }>;
349
+ skip_confirmation?: boolean;
350
+ update_existing?: boolean;
351
+ }
352
+ ): Promise<ApiResponse<{
353
+ imported: number;
354
+ updated: number;
355
+ failed: number;
356
+ errors?: Array<{ email: string; error: string }>;
357
+ }>> {
358
+ return this.create(
359
+ this.newsletterEndpoint(siteName, '/newsletter/import'),
360
+ data
361
+ );
362
+ }
363
+
364
+ /**
365
+ * Send test newsletter (admin only)
366
+ */
367
+ async sendTestNewsletter(
368
+ siteName: string,
369
+ data: {
370
+ to: string;
371
+ subject: string;
372
+ html_content: string;
373
+ text_content?: string;
374
+ }
375
+ ): Promise<ApiResponse<{ message: string; sent: boolean }>> {
376
+ return this.create(
377
+ this.newsletterEndpoint(siteName, '/newsletter/test'),
378
+ data
379
+ );
380
+ }
381
+ }
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export { CategoriesClient } from './client/categories-client';
18
18
  export { WebhooksClient } from './client/webhooks-client';
19
19
  export { CheckoutClient } from './client/checkout-client';
20
20
  export { ContactClient } from './client/contact-client';
21
+ export { NewsletterClient } from './client/newsletter-client';
21
22
 
22
23
  // Base classes
23
24
  export { BaseClient } from './client/base-client';
@@ -71,4 +72,12 @@ export type {
71
72
  ContactSubmission,
72
73
  CheckoutSession,
73
74
  PaymentGateway,
75
+ NewsletterSubscription,
76
+ CreateNewsletterSubscriptionRequest,
77
+ NewsletterList,
78
+ NewsletterPreferences,
79
+ NewsletterStatusResponse,
80
+ NewsletterSubscribeResponse,
81
+ NewsletterConfirmResponse,
82
+ NewsletterUnsubscribeRequest,
74
83
  } from './types';
@@ -14,6 +14,7 @@ import { CategoriesClient } from './client/categories-client';
14
14
  import { WebhooksClient } from './client/webhooks-client';
15
15
  import { CheckoutClient } from './client/checkout-client';
16
16
  import { ContactClient } from './client/contact-client';
17
+ import { NewsletterClient } from './client/newsletter-client';
17
18
 
18
19
  import type { PerspectApiConfig, ApiResponse } from './types';
19
20
 
@@ -31,6 +32,7 @@ export class PerspectApiClient {
31
32
  public readonly webhooks: WebhooksClient;
32
33
  public readonly checkout: CheckoutClient;
33
34
  public readonly contact: ContactClient;
35
+ public readonly newsletter: NewsletterClient;
34
36
 
35
37
  constructor(config: PerspectApiConfig) {
36
38
  // Validate required configuration
@@ -52,6 +54,7 @@ export class PerspectApiClient {
52
54
  this.webhooks = new WebhooksClient(this.http);
53
55
  this.checkout = new CheckoutClient(this.http);
54
56
  this.contact = new ContactClient(this.http);
57
+ this.newsletter = new NewsletterClient(this.http);
55
58
  }
56
59
 
57
60
  /**
@@ -119,6 +119,83 @@ export interface CreateSiteRequest {
119
119
  organizationId: number;
120
120
  }
121
121
 
122
+ // Newsletter Subscriptions
123
+ export interface NewsletterSubscription {
124
+ id: string;
125
+ email: string;
126
+ name?: string;
127
+ status: 'pending' | 'confirmed' | 'unsubscribed' | 'bounced' | 'complained';
128
+ frequency: 'instant' | 'daily' | 'weekly' | 'monthly';
129
+ topics?: string[];
130
+ language: string;
131
+ confirmedAt?: string;
132
+ unsubscribedAt?: string;
133
+ createdAt: string;
134
+ updatedAt: string;
135
+ }
136
+
137
+ export interface CreateNewsletterSubscriptionRequest {
138
+ email: string;
139
+ name?: string;
140
+ list_ids?: string[];
141
+ frequency?: 'instant' | 'daily' | 'weekly' | 'monthly';
142
+ topics?: string[];
143
+ language?: string;
144
+ source?: string;
145
+ source_url?: string;
146
+ double_opt_in?: boolean;
147
+ turnstile_token?: string;
148
+ metadata?: Record<string, any>;
149
+ }
150
+
151
+ export interface NewsletterList {
152
+ id: string;
153
+ list_name: string;
154
+ slug: string;
155
+ description?: string;
156
+ is_default: boolean;
157
+ subscriber_count?: number;
158
+ }
159
+
160
+ export interface NewsletterPreferences {
161
+ frequency?: 'instant' | 'daily' | 'weekly' | 'monthly';
162
+ topics?: string[];
163
+ language?: string;
164
+ email_format?: 'html' | 'text' | 'both';
165
+ timezone?: string;
166
+ track_opens?: boolean;
167
+ track_clicks?: boolean;
168
+ }
169
+
170
+ export interface NewsletterStatusResponse {
171
+ subscribed: boolean;
172
+ status: string;
173
+ frequency?: string;
174
+ created_at?: string;
175
+ confirmed_at?: string;
176
+ }
177
+
178
+ export interface NewsletterSubscribeResponse {
179
+ message: string;
180
+ status: string;
181
+ subscription_id?: string;
182
+ }
183
+
184
+ export interface NewsletterConfirmResponse {
185
+ message: string;
186
+ subscription?: {
187
+ email: string;
188
+ name?: string;
189
+ frequency: string;
190
+ };
191
+ }
192
+
193
+ export interface NewsletterUnsubscribeRequest {
194
+ token?: string;
195
+ email?: string;
196
+ reason?: string;
197
+ }
198
+
122
199
  // Content Management
123
200
  export type ContentStatus = 'draft' | 'publish' | 'private' | 'trash';
124
201
  export type ContentType = 'post' | 'page';
@@ -396,4 +473,5 @@ export interface RequestOptions {
396
473
  body?: any;
397
474
  params?: Record<string, string | number | boolean>;
398
475
  timeout?: number;
476
+ csrfToken?: string; // Optional CSRF token for protected endpoints
399
477
  }
@@ -115,29 +115,29 @@ export class HttpClient {
115
115
  /**
116
116
  * POST request
117
117
  */
118
- async post<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
119
- return this.request<T>(endpoint, { method: 'POST', body });
118
+ async post<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>> {
119
+ return this.request<T>(endpoint, { method: 'POST', body, ...options });
120
120
  }
121
121
 
122
122
  /**
123
123
  * PUT request
124
124
  */
125
- async put<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
126
- return this.request<T>(endpoint, { method: 'PUT', body });
125
+ async put<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>> {
126
+ return this.request<T>(endpoint, { method: 'PUT', body, ...options });
127
127
  }
128
128
 
129
129
  /**
130
130
  * DELETE request
131
131
  */
132
- async delete<T = any>(endpoint: string): Promise<ApiResponse<T>> {
133
- return this.request<T>(endpoint, { method: 'DELETE' });
132
+ async delete<T = any>(endpoint: string, options?: Partial<RequestOptions>): Promise<ApiResponse<T>> {
133
+ return this.request<T>(endpoint, { method: 'DELETE', ...options });
134
134
  }
135
135
 
136
136
  /**
137
137
  * PATCH request
138
138
  */
139
- async patch<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
140
- return this.request<T>(endpoint, { method: 'PATCH', body });
139
+ async patch<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>> {
140
+ return this.request<T>(endpoint, { method: 'PATCH', body, ...options });
141
141
  }
142
142
 
143
143
  /**
@@ -169,6 +169,11 @@ export class HttpClient {
169
169
  ...options.headers,
170
170
  };
171
171
 
172
+ // Add CSRF token if provided
173
+ if (options.csrfToken) {
174
+ headers['X-CSRF-Token'] = options.csrfToken;
175
+ }
176
+
172
177
  const requestOptions: RequestInit = {
173
178
  method: options.method || 'GET',
174
179
  headers,