perspectapi-ts-sdk 1.2.0 → 1.3.1

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 CHANGED
@@ -12,6 +12,7 @@ A comprehensive TypeScript SDK for PerspectAPI, designed to work seamlessly with
12
12
  - 🔑 **Multiple Auth Methods** - Support for JWT tokens and API keys
13
13
  - 📊 **Comprehensive Coverage** - All PerspectAPI endpoints supported
14
14
  - 🧩 **High-Level Loaders** - Drop-in helpers for products, content, and checkout flows with fallbacks
15
+ - 📧 **Newsletter Management** - Complete newsletter subscription system with double opt-in, preferences, and lists
15
16
 
16
17
  ## Installation
17
18
 
@@ -376,6 +377,132 @@ const submissions = await client.contact.getContactSubmissions(siteName, {
376
377
  await client.contact.updateContactStatus(siteName, submission.data.id, 'read');
377
378
  ```
378
379
 
380
+ ### Newsletter Subscriptions
381
+
382
+ ```typescript
383
+ const siteName = 'your-site-name';
384
+
385
+ // Subscribe to newsletter (with double opt-in)
386
+ const subscription = await client.newsletter.subscribe(siteName, {
387
+ email: 'subscriber@example.com',
388
+ name: 'Jane Doe',
389
+ list_ids: ['list_default'], // Optional: subscribe to specific lists
390
+ frequency: 'weekly', // instant, daily, weekly, monthly
391
+ topics: ['news', 'updates'], // Optional: topic preferences
392
+ double_opt_in: true, // Default: true for GDPR compliance
393
+ turnstile_token: 'token' // Optional: Turnstile verification
394
+ });
395
+
396
+ // Confirm subscription via email token
397
+ const confirmed = await client.newsletter.confirmSubscription(
398
+ siteName,
399
+ 'confirmation-token-from-email'
400
+ );
401
+
402
+ // Unsubscribe
403
+ const unsubscribed = await client.newsletter.unsubscribe(siteName, {
404
+ email: 'subscriber@example.com',
405
+ reason: 'Too many emails' // Optional feedback
406
+ });
407
+
408
+ // One-click unsubscribe (from email link)
409
+ await client.newsletter.unsubscribeByToken(siteName, 'unsubscribe-token');
410
+
411
+ // Update preferences
412
+ await client.newsletter.updatePreferences(siteName, 'subscriber@example.com', {
413
+ frequency: 'monthly',
414
+ topics: ['product-updates'],
415
+ email_format: 'html', // html, text, or both
416
+ timezone: 'America/New_York',
417
+ track_opens: false,
418
+ track_clicks: false
419
+ });
420
+
421
+ // Check subscription status
422
+ const status = await client.newsletter.getStatus(siteName, 'subscriber@example.com');
423
+ console.log('Subscribed:', status.data.subscribed);
424
+ console.log('Status:', status.data.status); // pending, confirmed, unsubscribed
425
+
426
+ // Get available newsletter lists
427
+ const lists = await client.newsletter.getLists(siteName);
428
+ console.log('Available lists:', lists.data.lists);
429
+ ```
430
+
431
+ #### Newsletter Admin Functions
432
+
433
+ ```typescript
434
+ // Get all subscriptions (admin only)
435
+ const subscriptions = await client.newsletter.getSubscriptions(siteName, {
436
+ page: 1,
437
+ limit: 100,
438
+ status: 'confirmed',
439
+ list_id: 'list_weekly',
440
+ search: 'john',
441
+ startDate: '2024-01-01',
442
+ endDate: '2024-12-31'
443
+ });
444
+
445
+ // Update subscription status
446
+ await client.newsletter.updateSubscriptionStatus(
447
+ siteName,
448
+ 'sub_123',
449
+ 'unsubscribed',
450
+ 'Admin action: User requested via support'
451
+ );
452
+
453
+ // Bulk operations
454
+ await client.newsletter.bulkUpdateSubscriptions(siteName, {
455
+ ids: ['sub_123', 'sub_456'],
456
+ action: 'add_to_list',
457
+ list_id: 'list_special_offers'
458
+ });
459
+
460
+ // List management
461
+ const newList = await client.newsletter.createList(siteName, {
462
+ list_name: 'VIP Customers',
463
+ slug: 'vip-customers',
464
+ description: 'Exclusive updates for VIP customers',
465
+ is_public: false,
466
+ is_default: false,
467
+ double_opt_in: true,
468
+ welcome_email_enabled: true
469
+ });
470
+
471
+ await client.newsletter.updateList(siteName, 'list_123', {
472
+ description: 'Updated description',
473
+ is_public: true
474
+ });
475
+
476
+ // Get statistics
477
+ const stats = await client.newsletter.getStatistics(siteName, {
478
+ startDate: '2024-01-01',
479
+ endDate: '2024-12-31',
480
+ list_id: 'list_weekly'
481
+ });
482
+ console.log('Total subscribers:', stats.data.totalSubscribers);
483
+ console.log('Open rate:', stats.data.engagementMetrics.averageOpenRate);
484
+
485
+ // Export subscriptions
486
+ const exportData = await client.newsletter.exportSubscriptions(siteName, {
487
+ format: 'csv', // csv, json, or xlsx
488
+ status: 'confirmed',
489
+ list_id: 'list_weekly'
490
+ });
491
+ console.log('Download URL:', exportData.data.downloadUrl);
492
+
493
+ // Import subscriptions
494
+ const importResult = await client.newsletter.importSubscriptions(siteName, {
495
+ subscriptions: [
496
+ { email: 'user1@example.com', name: 'User One', lists: ['list_weekly'] },
497
+ { email: 'user2@example.com', name: 'User Two', lists: ['list_daily'] }
498
+ ],
499
+ skip_confirmation: false, // Skip double opt-in for imported users
500
+ update_existing: true // Update if email already exists
501
+ });
502
+ console.log('Imported:', importResult.data.imported);
503
+ console.log('Failed:', importResult.data.failed);
504
+ ```
505
+
379
506
  ## Configuration Options
380
507
 
381
508
  ```typescript
package/dist/index.d.mts CHANGED
@@ -121,7 +121,6 @@ interface CreateNewsletterSubscriptionRequest {
121
121
  source?: string;
122
122
  source_url?: string;
123
123
  double_opt_in?: boolean;
124
- turnstile_token?: string;
125
124
  metadata?: Record<string, any>;
126
125
  }
127
126
  interface NewsletterList {
@@ -374,7 +373,6 @@ interface CreateContactRequest {
374
373
  email: string;
375
374
  subject?: string;
376
375
  message: string;
377
- turnstileToken?: string;
378
376
  }
379
377
  interface ApiError {
380
378
  message: string;
@@ -397,6 +395,7 @@ interface RequestOptions {
397
395
  body?: any;
398
396
  params?: Record<string, string | number | boolean>;
399
397
  timeout?: number;
398
+ csrfToken?: string;
400
399
  }
401
400
 
402
401
  /**
@@ -433,19 +432,19 @@ declare class HttpClient {
433
432
  /**
434
433
  * POST request
435
434
  */
436
- post<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>>;
435
+ post<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
437
436
  /**
438
437
  * PUT request
439
438
  */
440
- put<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>>;
439
+ put<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
441
440
  /**
442
441
  * DELETE request
443
442
  */
444
- delete<T = any>(endpoint: string): Promise<ApiResponse<T>>;
443
+ delete<T = any>(endpoint: string, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
445
444
  /**
446
445
  * PATCH request
447
446
  */
448
- patch<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>>;
447
+ patch<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
449
448
  /**
450
449
  * Build full URL with query parameters
451
450
  */
@@ -501,19 +500,19 @@ declare abstract class BaseClient {
501
500
  /**
502
501
  * Handle create operations
503
502
  */
504
- protected create<T, R = T>(endpoint: string, data: T): Promise<ApiResponse<R>>;
503
+ protected create<T, R = T>(endpoint: string, data: T, csrfToken?: string): Promise<ApiResponse<R>>;
505
504
  /**
506
505
  * Handle update operations
507
506
  */
508
- protected update<T, R = T>(endpoint: string, data: T): Promise<ApiResponse<R>>;
507
+ protected update<T, R = T>(endpoint: string, data: T, csrfToken?: string): Promise<ApiResponse<R>>;
509
508
  /**
510
509
  * Handle partial update operations
511
510
  */
512
- protected patch<T, R = T>(endpoint: string, data: T): Promise<ApiResponse<R>>;
511
+ protected patch<T, R = T>(endpoint: string, data: T, csrfToken?: string): Promise<ApiResponse<R>>;
513
512
  /**
514
513
  * Handle delete operations
515
514
  */
516
- protected delete<T = any>(endpoint: string): Promise<ApiResponse<T>>;
515
+ protected delete<T = any>(endpoint: string, csrfToken?: string): Promise<ApiResponse<T>>;
517
516
  }
518
517
 
519
518
  /**
@@ -1457,8 +1456,11 @@ declare class ContactClient extends BaseClient {
1457
1456
  private contactEndpoint;
1458
1457
  /**
1459
1458
  * Submit contact form
1459
+ * @param siteName - The site to submit contact form to
1460
+ * @param data - Contact form data
1461
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1460
1462
  */
1461
- submitContact(siteName: string, data: CreateContactRequest): Promise<ApiResponse<{
1463
+ submitContact(siteName: string, data: CreateContactRequest, csrfToken?: string): Promise<ApiResponse<{
1462
1464
  id: string;
1463
1465
  message: string;
1464
1466
  status: string;
@@ -1568,7 +1570,6 @@ declare class ContactClient extends BaseClient {
1568
1570
  * Get contact form configuration
1569
1571
  */
1570
1572
  getContactConfig(siteName: string): Promise<ApiResponse<{
1571
- turnstileEnabled: boolean;
1572
1573
  rateLimitEnabled: boolean;
1573
1574
  rateLimitWindow: number;
1574
1575
  rateLimitMax: number;
@@ -1584,7 +1585,6 @@ declare class ContactClient extends BaseClient {
1584
1585
  * Update contact form configuration (admin only)
1585
1586
  */
1586
1587
  updateContactConfig(siteName: string, data: {
1587
- turnstileEnabled?: boolean;
1588
1588
  rateLimitEnabled?: boolean;
1589
1589
  rateLimitWindow?: number;
1590
1590
  rateLimitMax?: number;
@@ -1612,16 +1612,22 @@ declare class NewsletterClient extends BaseClient {
1612
1612
  private newsletterEndpoint;
1613
1613
  /**
1614
1614
  * Subscribe to newsletter
1615
+ * @param siteName - The site to subscribe to
1616
+ * @param data - Subscription data
1617
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1615
1618
  */
1616
- subscribe(siteName: string, data: CreateNewsletterSubscriptionRequest): Promise<ApiResponse<NewsletterSubscribeResponse>>;
1619
+ subscribe(siteName: string, data: CreateNewsletterSubscriptionRequest, csrfToken?: string): Promise<ApiResponse<NewsletterSubscribeResponse>>;
1617
1620
  /**
1618
1621
  * Confirm newsletter subscription via token
1619
1622
  */
1620
1623
  confirmSubscription(siteName: string, token: string): Promise<ApiResponse<NewsletterConfirmResponse>>;
1621
1624
  /**
1622
1625
  * Unsubscribe from newsletter
1626
+ * @param siteName - The site to unsubscribe from
1627
+ * @param data - Unsubscribe data
1628
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1623
1629
  */
1624
- unsubscribe(siteName: string, data: NewsletterUnsubscribeRequest): Promise<ApiResponse<{
1630
+ unsubscribe(siteName: string, data: NewsletterUnsubscribeRequest, csrfToken?: string): Promise<ApiResponse<{
1625
1631
  message: string;
1626
1632
  }>>;
1627
1633
  /**
@@ -1630,8 +1636,12 @@ declare class NewsletterClient extends BaseClient {
1630
1636
  unsubscribeByToken(siteName: string, token: string): Promise<ApiResponse<string>>;
1631
1637
  /**
1632
1638
  * Update subscription preferences
1639
+ * @param siteName - The site name
1640
+ * @param email - Subscriber email
1641
+ * @param preferences - New preferences
1642
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1633
1643
  */
1634
- updatePreferences(siteName: string, email: string, preferences: NewsletterPreferences): Promise<ApiResponse<{
1644
+ updatePreferences(siteName: string, email: string, preferences: NewsletterPreferences, csrfToken?: string): Promise<ApiResponse<{
1635
1645
  message: string;
1636
1646
  preferences: NewsletterPreferences;
1637
1647
  }>>;
package/dist/index.d.ts CHANGED
@@ -121,7 +121,6 @@ interface CreateNewsletterSubscriptionRequest {
121
121
  source?: string;
122
122
  source_url?: string;
123
123
  double_opt_in?: boolean;
124
- turnstile_token?: string;
125
124
  metadata?: Record<string, any>;
126
125
  }
127
126
  interface NewsletterList {
@@ -374,7 +373,6 @@ interface CreateContactRequest {
374
373
  email: string;
375
374
  subject?: string;
376
375
  message: string;
377
- turnstileToken?: string;
378
376
  }
379
377
  interface ApiError {
380
378
  message: string;
@@ -397,6 +395,7 @@ interface RequestOptions {
397
395
  body?: any;
398
396
  params?: Record<string, string | number | boolean>;
399
397
  timeout?: number;
398
+ csrfToken?: string;
400
399
  }
401
400
 
402
401
  /**
@@ -433,19 +432,19 @@ declare class HttpClient {
433
432
  /**
434
433
  * POST request
435
434
  */
436
- post<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>>;
435
+ post<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
437
436
  /**
438
437
  * PUT request
439
438
  */
440
- put<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>>;
439
+ put<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
441
440
  /**
442
441
  * DELETE request
443
442
  */
444
- delete<T = any>(endpoint: string): Promise<ApiResponse<T>>;
443
+ delete<T = any>(endpoint: string, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
445
444
  /**
446
445
  * PATCH request
447
446
  */
448
- patch<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>>;
447
+ patch<T = any>(endpoint: string, body?: any, options?: Partial<RequestOptions>): Promise<ApiResponse<T>>;
449
448
  /**
450
449
  * Build full URL with query parameters
451
450
  */
@@ -501,19 +500,19 @@ declare abstract class BaseClient {
501
500
  /**
502
501
  * Handle create operations
503
502
  */
504
- protected create<T, R = T>(endpoint: string, data: T): Promise<ApiResponse<R>>;
503
+ protected create<T, R = T>(endpoint: string, data: T, csrfToken?: string): Promise<ApiResponse<R>>;
505
504
  /**
506
505
  * Handle update operations
507
506
  */
508
- protected update<T, R = T>(endpoint: string, data: T): Promise<ApiResponse<R>>;
507
+ protected update<T, R = T>(endpoint: string, data: T, csrfToken?: string): Promise<ApiResponse<R>>;
509
508
  /**
510
509
  * Handle partial update operations
511
510
  */
512
- protected patch<T, R = T>(endpoint: string, data: T): Promise<ApiResponse<R>>;
511
+ protected patch<T, R = T>(endpoint: string, data: T, csrfToken?: string): Promise<ApiResponse<R>>;
513
512
  /**
514
513
  * Handle delete operations
515
514
  */
516
- protected delete<T = any>(endpoint: string): Promise<ApiResponse<T>>;
515
+ protected delete<T = any>(endpoint: string, csrfToken?: string): Promise<ApiResponse<T>>;
517
516
  }
518
517
 
519
518
  /**
@@ -1457,8 +1456,11 @@ declare class ContactClient extends BaseClient {
1457
1456
  private contactEndpoint;
1458
1457
  /**
1459
1458
  * Submit contact form
1459
+ * @param siteName - The site to submit contact form to
1460
+ * @param data - Contact form data
1461
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1460
1462
  */
1461
- submitContact(siteName: string, data: CreateContactRequest): Promise<ApiResponse<{
1463
+ submitContact(siteName: string, data: CreateContactRequest, csrfToken?: string): Promise<ApiResponse<{
1462
1464
  id: string;
1463
1465
  message: string;
1464
1466
  status: string;
@@ -1568,7 +1570,6 @@ declare class ContactClient extends BaseClient {
1568
1570
  * Get contact form configuration
1569
1571
  */
1570
1572
  getContactConfig(siteName: string): Promise<ApiResponse<{
1571
- turnstileEnabled: boolean;
1572
1573
  rateLimitEnabled: boolean;
1573
1574
  rateLimitWindow: number;
1574
1575
  rateLimitMax: number;
@@ -1584,7 +1585,6 @@ declare class ContactClient extends BaseClient {
1584
1585
  * Update contact form configuration (admin only)
1585
1586
  */
1586
1587
  updateContactConfig(siteName: string, data: {
1587
- turnstileEnabled?: boolean;
1588
1588
  rateLimitEnabled?: boolean;
1589
1589
  rateLimitWindow?: number;
1590
1590
  rateLimitMax?: number;
@@ -1612,16 +1612,22 @@ declare class NewsletterClient extends BaseClient {
1612
1612
  private newsletterEndpoint;
1613
1613
  /**
1614
1614
  * Subscribe to newsletter
1615
+ * @param siteName - The site to subscribe to
1616
+ * @param data - Subscription data
1617
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1615
1618
  */
1616
- subscribe(siteName: string, data: CreateNewsletterSubscriptionRequest): Promise<ApiResponse<NewsletterSubscribeResponse>>;
1619
+ subscribe(siteName: string, data: CreateNewsletterSubscriptionRequest, csrfToken?: string): Promise<ApiResponse<NewsletterSubscribeResponse>>;
1617
1620
  /**
1618
1621
  * Confirm newsletter subscription via token
1619
1622
  */
1620
1623
  confirmSubscription(siteName: string, token: string): Promise<ApiResponse<NewsletterConfirmResponse>>;
1621
1624
  /**
1622
1625
  * Unsubscribe from newsletter
1626
+ * @param siteName - The site to unsubscribe from
1627
+ * @param data - Unsubscribe data
1628
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1623
1629
  */
1624
- unsubscribe(siteName: string, data: NewsletterUnsubscribeRequest): Promise<ApiResponse<{
1630
+ unsubscribe(siteName: string, data: NewsletterUnsubscribeRequest, csrfToken?: string): Promise<ApiResponse<{
1625
1631
  message: string;
1626
1632
  }>>;
1627
1633
  /**
@@ -1630,8 +1636,12 @@ declare class NewsletterClient extends BaseClient {
1630
1636
  unsubscribeByToken(siteName: string, token: string): Promise<ApiResponse<string>>;
1631
1637
  /**
1632
1638
  * Update subscription preferences
1639
+ * @param siteName - The site name
1640
+ * @param email - Subscriber email
1641
+ * @param preferences - New preferences
1642
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1633
1643
  */
1634
- updatePreferences(siteName: string, email: string, preferences: NewsletterPreferences): Promise<ApiResponse<{
1644
+ updatePreferences(siteName: string, email: string, preferences: NewsletterPreferences, csrfToken?: string): Promise<ApiResponse<{
1635
1645
  message: string;
1636
1646
  preferences: NewsletterPreferences;
1637
1647
  }>>;
package/dist/index.js CHANGED
@@ -130,26 +130,26 @@ var HttpClient = class {
130
130
  /**
131
131
  * POST request
132
132
  */
133
- async post(endpoint, body) {
134
- return this.request(endpoint, { method: "POST", body });
133
+ async post(endpoint, body, options) {
134
+ return this.request(endpoint, { method: "POST", body, ...options });
135
135
  }
136
136
  /**
137
137
  * PUT request
138
138
  */
139
- async put(endpoint, body) {
140
- return this.request(endpoint, { method: "PUT", body });
139
+ async put(endpoint, body, options) {
140
+ return this.request(endpoint, { method: "PUT", body, ...options });
141
141
  }
142
142
  /**
143
143
  * DELETE request
144
144
  */
145
- async delete(endpoint) {
146
- return this.request(endpoint, { method: "DELETE" });
145
+ async delete(endpoint, options) {
146
+ return this.request(endpoint, { method: "DELETE", ...options });
147
147
  }
148
148
  /**
149
149
  * PATCH request
150
150
  */
151
- async patch(endpoint, body) {
152
- return this.request(endpoint, { method: "PATCH", body });
151
+ async patch(endpoint, body, options) {
152
+ return this.request(endpoint, { method: "PATCH", body, ...options });
153
153
  }
154
154
  /**
155
155
  * Build full URL with query parameters
@@ -175,6 +175,9 @@ var HttpClient = class {
175
175
  ...this.defaultHeaders,
176
176
  ...options.headers
177
177
  };
178
+ if (options.csrfToken) {
179
+ headers["X-CSRF-Token"] = options.csrfToken;
180
+ }
178
181
  const requestOptions = {
179
182
  method: options.method || "GET",
180
183
  headers
@@ -298,26 +301,26 @@ var BaseClient = class {
298
301
  /**
299
302
  * Handle create operations
300
303
  */
301
- async create(endpoint, data) {
302
- return this.http.post(this.buildPath(endpoint), data);
304
+ async create(endpoint, data, csrfToken) {
305
+ return this.http.post(this.buildPath(endpoint), data, { csrfToken });
303
306
  }
304
307
  /**
305
308
  * Handle update operations
306
309
  */
307
- async update(endpoint, data) {
308
- return this.http.put(this.buildPath(endpoint), data);
310
+ async update(endpoint, data, csrfToken) {
311
+ return this.http.put(this.buildPath(endpoint), data, { csrfToken });
309
312
  }
310
313
  /**
311
314
  * Handle partial update operations
312
315
  */
313
- async patch(endpoint, data) {
314
- return this.http.patch(this.buildPath(endpoint), data);
316
+ async patch(endpoint, data, csrfToken) {
317
+ return this.http.patch(this.buildPath(endpoint), data, { csrfToken });
315
318
  }
316
319
  /**
317
320
  * Handle delete operations
318
321
  */
319
- async delete(endpoint) {
320
- return this.http.delete(this.buildPath(endpoint));
322
+ async delete(endpoint, csrfToken) {
323
+ return this.http.delete(this.buildPath(endpoint), { csrfToken });
321
324
  }
322
325
  };
323
326
 
@@ -1184,9 +1187,15 @@ var ContactClient = class extends BaseClient {
1184
1187
  }
1185
1188
  /**
1186
1189
  * Submit contact form
1190
+ * @param siteName - The site to submit contact form to
1191
+ * @param data - Contact form data
1192
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1187
1193
  */
1188
- async submitContact(siteName, data) {
1189
- return this.create(this.contactEndpoint(siteName, "/contact/submit"), data);
1194
+ async submitContact(siteName, data, csrfToken) {
1195
+ if (typeof window !== "undefined" && !csrfToken) {
1196
+ console.warn("CSRF token recommended for browser-based contact form submissions");
1197
+ }
1198
+ return this.create(this.contactEndpoint(siteName, "/contact/submit"), data, csrfToken);
1190
1199
  }
1191
1200
  /**
1192
1201
  * Get contact submission status
@@ -1281,11 +1290,18 @@ var NewsletterClient = class extends BaseClient {
1281
1290
  }
1282
1291
  /**
1283
1292
  * Subscribe to newsletter
1293
+ * @param siteName - The site to subscribe to
1294
+ * @param data - Subscription data
1295
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1284
1296
  */
1285
- async subscribe(siteName, data) {
1297
+ async subscribe(siteName, data, csrfToken) {
1298
+ if (typeof window !== "undefined" && !csrfToken) {
1299
+ console.warn("CSRF token recommended for browser-based newsletter subscriptions");
1300
+ }
1286
1301
  return this.create(
1287
1302
  this.newsletterEndpoint(siteName, "/newsletter/subscribe"),
1288
- data
1303
+ data,
1304
+ csrfToken
1289
1305
  );
1290
1306
  }
1291
1307
  /**
@@ -1298,11 +1314,15 @@ var NewsletterClient = class extends BaseClient {
1298
1314
  }
1299
1315
  /**
1300
1316
  * Unsubscribe from newsletter
1317
+ * @param siteName - The site to unsubscribe from
1318
+ * @param data - Unsubscribe data
1319
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1301
1320
  */
1302
- async unsubscribe(siteName, data) {
1321
+ async unsubscribe(siteName, data, csrfToken) {
1303
1322
  return this.create(
1304
1323
  this.newsletterEndpoint(siteName, "/newsletter/unsubscribe"),
1305
- data
1324
+ data,
1325
+ csrfToken
1306
1326
  );
1307
1327
  }
1308
1328
  /**
@@ -1315,11 +1335,16 @@ var NewsletterClient = class extends BaseClient {
1315
1335
  }
1316
1336
  /**
1317
1337
  * Update subscription preferences
1338
+ * @param siteName - The site name
1339
+ * @param email - Subscriber email
1340
+ * @param preferences - New preferences
1341
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1318
1342
  */
1319
- async updatePreferences(siteName, email, preferences) {
1343
+ async updatePreferences(siteName, email, preferences, csrfToken) {
1320
1344
  return this.patch(
1321
1345
  this.newsletterEndpoint(siteName, "/newsletter/preferences"),
1322
- { email, ...preferences }
1346
+ { email, ...preferences },
1347
+ csrfToken
1323
1348
  );
1324
1349
  }
1325
1350
  /**
package/dist/index.mjs CHANGED
@@ -79,26 +79,26 @@ var HttpClient = class {
79
79
  /**
80
80
  * POST request
81
81
  */
82
- async post(endpoint, body) {
83
- return this.request(endpoint, { method: "POST", body });
82
+ async post(endpoint, body, options) {
83
+ return this.request(endpoint, { method: "POST", body, ...options });
84
84
  }
85
85
  /**
86
86
  * PUT request
87
87
  */
88
- async put(endpoint, body) {
89
- return this.request(endpoint, { method: "PUT", body });
88
+ async put(endpoint, body, options) {
89
+ return this.request(endpoint, { method: "PUT", body, ...options });
90
90
  }
91
91
  /**
92
92
  * DELETE request
93
93
  */
94
- async delete(endpoint) {
95
- return this.request(endpoint, { method: "DELETE" });
94
+ async delete(endpoint, options) {
95
+ return this.request(endpoint, { method: "DELETE", ...options });
96
96
  }
97
97
  /**
98
98
  * PATCH request
99
99
  */
100
- async patch(endpoint, body) {
101
- return this.request(endpoint, { method: "PATCH", body });
100
+ async patch(endpoint, body, options) {
101
+ return this.request(endpoint, { method: "PATCH", body, ...options });
102
102
  }
103
103
  /**
104
104
  * Build full URL with query parameters
@@ -124,6 +124,9 @@ var HttpClient = class {
124
124
  ...this.defaultHeaders,
125
125
  ...options.headers
126
126
  };
127
+ if (options.csrfToken) {
128
+ headers["X-CSRF-Token"] = options.csrfToken;
129
+ }
127
130
  const requestOptions = {
128
131
  method: options.method || "GET",
129
132
  headers
@@ -247,26 +250,26 @@ var BaseClient = class {
247
250
  /**
248
251
  * Handle create operations
249
252
  */
250
- async create(endpoint, data) {
251
- return this.http.post(this.buildPath(endpoint), data);
253
+ async create(endpoint, data, csrfToken) {
254
+ return this.http.post(this.buildPath(endpoint), data, { csrfToken });
252
255
  }
253
256
  /**
254
257
  * Handle update operations
255
258
  */
256
- async update(endpoint, data) {
257
- return this.http.put(this.buildPath(endpoint), data);
259
+ async update(endpoint, data, csrfToken) {
260
+ return this.http.put(this.buildPath(endpoint), data, { csrfToken });
258
261
  }
259
262
  /**
260
263
  * Handle partial update operations
261
264
  */
262
- async patch(endpoint, data) {
263
- return this.http.patch(this.buildPath(endpoint), data);
265
+ async patch(endpoint, data, csrfToken) {
266
+ return this.http.patch(this.buildPath(endpoint), data, { csrfToken });
264
267
  }
265
268
  /**
266
269
  * Handle delete operations
267
270
  */
268
- async delete(endpoint) {
269
- return this.http.delete(this.buildPath(endpoint));
271
+ async delete(endpoint, csrfToken) {
272
+ return this.http.delete(this.buildPath(endpoint), { csrfToken });
270
273
  }
271
274
  };
272
275
 
@@ -1133,9 +1136,15 @@ var ContactClient = class extends BaseClient {
1133
1136
  }
1134
1137
  /**
1135
1138
  * Submit contact form
1139
+ * @param siteName - The site to submit contact form to
1140
+ * @param data - Contact form data
1141
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1136
1142
  */
1137
- async submitContact(siteName, data) {
1138
- return this.create(this.contactEndpoint(siteName, "/contact/submit"), data);
1143
+ async submitContact(siteName, data, csrfToken) {
1144
+ if (typeof window !== "undefined" && !csrfToken) {
1145
+ console.warn("CSRF token recommended for browser-based contact form submissions");
1146
+ }
1147
+ return this.create(this.contactEndpoint(siteName, "/contact/submit"), data, csrfToken);
1139
1148
  }
1140
1149
  /**
1141
1150
  * Get contact submission status
@@ -1230,11 +1239,18 @@ var NewsletterClient = class extends BaseClient {
1230
1239
  }
1231
1240
  /**
1232
1241
  * Subscribe to newsletter
1242
+ * @param siteName - The site to subscribe to
1243
+ * @param data - Subscription data
1244
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1233
1245
  */
1234
- async subscribe(siteName, data) {
1246
+ async subscribe(siteName, data, csrfToken) {
1247
+ if (typeof window !== "undefined" && !csrfToken) {
1248
+ console.warn("CSRF token recommended for browser-based newsletter subscriptions");
1249
+ }
1235
1250
  return this.create(
1236
1251
  this.newsletterEndpoint(siteName, "/newsletter/subscribe"),
1237
- data
1252
+ data,
1253
+ csrfToken
1238
1254
  );
1239
1255
  }
1240
1256
  /**
@@ -1247,11 +1263,15 @@ var NewsletterClient = class extends BaseClient {
1247
1263
  }
1248
1264
  /**
1249
1265
  * Unsubscribe from newsletter
1266
+ * @param siteName - The site to unsubscribe from
1267
+ * @param data - Unsubscribe data
1268
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1250
1269
  */
1251
- async unsubscribe(siteName, data) {
1270
+ async unsubscribe(siteName, data, csrfToken) {
1252
1271
  return this.create(
1253
1272
  this.newsletterEndpoint(siteName, "/newsletter/unsubscribe"),
1254
- data
1273
+ data,
1274
+ csrfToken
1255
1275
  );
1256
1276
  }
1257
1277
  /**
@@ -1264,11 +1284,16 @@ var NewsletterClient = class extends BaseClient {
1264
1284
  }
1265
1285
  /**
1266
1286
  * Update subscription preferences
1287
+ * @param siteName - The site name
1288
+ * @param email - Subscriber email
1289
+ * @param preferences - New preferences
1290
+ * @param csrfToken - CSRF token (required for browser-based submissions)
1267
1291
  */
1268
- async updatePreferences(siteName, email, preferences) {
1292
+ async updatePreferences(siteName, email, preferences, csrfToken) {
1269
1293
  return this.patch(
1270
1294
  this.newsletterEndpoint(siteName, "/newsletter/preferences"),
1271
- { email, ...preferences }
1295
+ { email, ...preferences },
1296
+ csrfToken
1272
1297
  );
1273
1298
  }
1274
1299
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perspectapi-ts-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -66,9 +66,10 @@ export abstract class BaseClient {
66
66
  */
67
67
  protected async create<T, R = T>(
68
68
  endpoint: string,
69
- data: T
69
+ data: T,
70
+ csrfToken?: string
70
71
  ): Promise<ApiResponse<R>> {
71
- return this.http.post<R>(this.buildPath(endpoint), data);
72
+ return this.http.post<R>(this.buildPath(endpoint), data, { csrfToken });
72
73
  }
73
74
 
74
75
  /**
@@ -76,9 +77,10 @@ export abstract class BaseClient {
76
77
  */
77
78
  protected async update<T, R = T>(
78
79
  endpoint: string,
79
- data: T
80
+ data: T,
81
+ csrfToken?: string
80
82
  ): Promise<ApiResponse<R>> {
81
- return this.http.put<R>(this.buildPath(endpoint), data);
83
+ return this.http.put<R>(this.buildPath(endpoint), data, { csrfToken });
82
84
  }
83
85
 
84
86
  /**
@@ -86,15 +88,16 @@ export abstract class BaseClient {
86
88
  */
87
89
  protected async patch<T, R = T>(
88
90
  endpoint: string,
89
- data: T
91
+ data: T,
92
+ csrfToken?: string
90
93
  ): Promise<ApiResponse<R>> {
91
- return this.http.patch<R>(this.buildPath(endpoint), data);
94
+ return this.http.patch<R>(this.buildPath(endpoint), data, { csrfToken });
92
95
  }
93
96
 
94
97
  /**
95
98
  * Handle delete operations
96
99
  */
97
- protected async delete<T = any>(endpoint: string): Promise<ApiResponse<T>> {
98
- return this.http.delete<T>(this.buildPath(endpoint));
100
+ protected async delete<T = any>(endpoint: string, csrfToken?: string): Promise<ApiResponse<T>> {
101
+ return this.http.delete<T>(this.buildPath(endpoint), { csrfToken });
99
102
  }
100
103
  }
@@ -24,17 +24,29 @@ export class ContactClient extends BaseClient {
24
24
 
25
25
  /**
26
26
  * Submit contact form
27
+ * @param siteName - The site to submit contact form to
28
+ * @param data - Contact form data
29
+ * @param csrfToken - CSRF token (required for browser-based submissions)
27
30
  */
28
- async submitContact(siteName: string, data: CreateContactRequest): Promise<ApiResponse<{
31
+ async submitContact(
32
+ siteName: string,
33
+ data: CreateContactRequest,
34
+ csrfToken?: string
35
+ ): Promise<ApiResponse<{
29
36
  id: string;
30
37
  message: string;
31
38
  status: string;
32
39
  }>> {
40
+ // CSRF token is required for browser submissions
41
+ if (typeof window !== 'undefined' && !csrfToken) {
42
+ console.warn('CSRF token recommended for browser-based contact form submissions');
43
+ }
44
+
33
45
  return this.create<CreateContactRequest, {
34
46
  id: string;
35
47
  message: string;
36
48
  status: string;
37
- }>(this.contactEndpoint(siteName, '/contact/submit'), data);
49
+ }>(this.contactEndpoint(siteName, '/contact/submit'), data, csrfToken);
38
50
  }
39
51
 
40
52
  /**
@@ -167,7 +179,6 @@ export class ContactClient extends BaseClient {
167
179
  * Get contact form configuration
168
180
  */
169
181
  async getContactConfig(siteName: string): Promise<ApiResponse<{
170
- turnstileEnabled: boolean;
171
182
  rateLimitEnabled: boolean;
172
183
  rateLimitWindow: number;
173
184
  rateLimitMax: number;
@@ -186,7 +197,6 @@ export class ContactClient extends BaseClient {
186
197
  * Update contact form configuration (admin only)
187
198
  */
188
199
  async updateContactConfig(siteName: string, data: {
189
- turnstileEnabled?: boolean;
190
200
  rateLimitEnabled?: boolean;
191
201
  rateLimitWindow?: number;
192
202
  rateLimitMax?: number;
@@ -30,14 +30,24 @@ export class NewsletterClient extends BaseClient {
30
30
 
31
31
  /**
32
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)
33
36
  */
34
37
  async subscribe(
35
38
  siteName: string,
36
- data: CreateNewsletterSubscriptionRequest
39
+ data: CreateNewsletterSubscriptionRequest,
40
+ csrfToken?: string
37
41
  ): Promise<ApiResponse<NewsletterSubscribeResponse>> {
42
+ // CSRF token is required for browser submissions
43
+ if (typeof window !== 'undefined' && !csrfToken) {
44
+ console.warn('CSRF token recommended for browser-based newsletter subscriptions');
45
+ }
46
+
38
47
  return this.create<CreateNewsletterSubscriptionRequest, NewsletterSubscribeResponse>(
39
48
  this.newsletterEndpoint(siteName, '/newsletter/subscribe'),
40
- data
49
+ data,
50
+ csrfToken
41
51
  );
42
52
  }
43
53
 
@@ -55,14 +65,19 @@ export class NewsletterClient extends BaseClient {
55
65
 
56
66
  /**
57
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)
58
71
  */
59
72
  async unsubscribe(
60
73
  siteName: string,
61
- data: NewsletterUnsubscribeRequest
74
+ data: NewsletterUnsubscribeRequest,
75
+ csrfToken?: string
62
76
  ): Promise<ApiResponse<{ message: string }>> {
63
77
  return this.create<NewsletterUnsubscribeRequest, { message: string }>(
64
78
  this.newsletterEndpoint(siteName, '/newsletter/unsubscribe'),
65
- data
79
+ data,
80
+ csrfToken
66
81
  );
67
82
  }
68
83
 
@@ -81,15 +96,21 @@ export class NewsletterClient extends BaseClient {
81
96
 
82
97
  /**
83
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)
84
103
  */
85
104
  async updatePreferences(
86
105
  siteName: string,
87
106
  email: string,
88
- preferences: NewsletterPreferences
107
+ preferences: NewsletterPreferences,
108
+ csrfToken?: string
89
109
  ): Promise<ApiResponse<{ message: string; preferences: NewsletterPreferences }>> {
90
110
  return this.patch(
91
111
  this.newsletterEndpoint(siteName, '/newsletter/preferences'),
92
- { email, ...preferences }
112
+ { email, ...preferences },
113
+ csrfToken
93
114
  );
94
115
  }
95
116
 
@@ -144,7 +144,6 @@ export interface CreateNewsletterSubscriptionRequest {
144
144
  source?: string;
145
145
  source_url?: string;
146
146
  double_opt_in?: boolean;
147
- turnstile_token?: string;
148
147
  metadata?: Record<string, any>;
149
148
  }
150
149
 
@@ -442,7 +441,6 @@ export interface CreateContactRequest {
442
441
  email: string;
443
442
  subject?: string;
444
443
  message: string;
445
- turnstileToken?: string;
446
444
  }
447
445
 
448
446
  // Error Types
@@ -473,4 +471,5 @@ export interface RequestOptions {
473
471
  body?: any;
474
472
  params?: Record<string, string | number | boolean>;
475
473
  timeout?: number;
474
+ csrfToken?: string; // Optional CSRF token for protected endpoints
476
475
  }
@@ -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,