perspectapi-ts-sdk 6.5.9 → 7.0.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 (57) hide show
  1. package/README.md +46 -1011
  2. package/dist/chunk-K3T2AFYA.mjs +1393 -0
  3. package/dist/index-CWvUyMt3.d.mts +2224 -0
  4. package/dist/index-CWvUyMt3.d.ts +2224 -0
  5. package/dist/index.d.mts +130 -2221
  6. package/dist/index.d.ts +130 -2221
  7. package/dist/index.js +8 -2
  8. package/dist/index.mjs +13 -1364
  9. package/dist/v2/index.d.mts +1 -0
  10. package/dist/v2/index.d.ts +1 -0
  11. package/dist/v2/index.js +1419 -0
  12. package/dist/v2/index.mjs +40 -0
  13. package/docs/README.md +15 -0
  14. package/docs/v1-deprecated/README.md +9 -0
  15. package/docs/v1-deprecated/examples/README.md +324 -0
  16. package/docs/v1-deprecated/examples/basic-usage.ts +258 -0
  17. package/docs/v1-deprecated/examples/cloudflare-worker.ts +274 -0
  18. package/docs/v1-deprecated/examples/content-query-with-slug-prefix.ts +237 -0
  19. package/docs/v1-deprecated/examples/image-transforms.ts +200 -0
  20. package/docs/v1-deprecated/examples/site-user-checkout.ts +186 -0
  21. package/docs/v1-deprecated/examples/slug-prefix-examples.ts +491 -0
  22. package/docs/v1-deprecated/legacy-docs/caching.md +667 -0
  23. package/docs/v1-deprecated/legacy-docs/contact.md +1396 -0
  24. package/docs/v1-deprecated/legacy-docs/csrf-protection.md +664 -0
  25. package/docs/v1-deprecated/legacy-docs/image-transforms.md +523 -0
  26. package/docs/v1-deprecated/legacy-docs/loaders.md +304 -0
  27. package/docs/v1-deprecated/legacy-docs/newsletter.md +811 -0
  28. package/docs/v1-deprecated/legacy-docs/site-users.md +817 -0
  29. package/docs/v1-deprecated/legacy-notes/CHANGELOG-CHECKOUT.md +143 -0
  30. package/docs/v1-deprecated/legacy-notes/CSRF-CHECKOUT.md +271 -0
  31. package/docs/v1-deprecated/legacy-notes/IMAGE_TRANSFORMS_PORT.md +298 -0
  32. package/docs/v1-deprecated/sdk-readme.md +1076 -0
  33. package/examples/README.md +19 -0
  34. package/examples/basic-v2.ts +37 -0
  35. package/llms.txt +25 -0
  36. package/package.json +18 -7
  37. package/src/client/api-keys-client.ts +4 -0
  38. package/src/client/auth-client.ts +4 -0
  39. package/src/client/base-client.ts +7 -0
  40. package/src/client/bundles-client.ts +4 -0
  41. package/src/client/categories-client.ts +4 -0
  42. package/src/client/checkout-client.ts +4 -0
  43. package/src/client/contact-client.ts +4 -0
  44. package/src/client/content-client.ts +4 -0
  45. package/src/client/newsletter-client.ts +4 -0
  46. package/src/client/newsletter-management-client.ts +4 -0
  47. package/src/client/organizations-client.ts +4 -0
  48. package/src/client/products-client.ts +4 -0
  49. package/src/client/site-users-client.ts +10 -1
  50. package/src/client/sites-client.ts +4 -0
  51. package/src/client/webhooks-client.ts +4 -0
  52. package/src/deprecation.ts +2 -1
  53. package/src/index.ts +2 -1
  54. package/src/loaders.ts +59 -0
  55. package/src/perspect-api-client.ts +2 -2
  56. package/src/v2/client/orders-client.ts +6 -1
  57. package/src/v2/types.ts +3 -0
@@ -0,0 +1,664 @@
1
+ # CSRF Protection Guide
2
+
3
+ > Deprecated v1 material. Do not copy these examples into new code. v1 sunsets
4
+ > on 2026-06-01; use `createPerspectApiV2Client` from
5
+ > `perspectapi-ts-sdk/v2` and `/api/v2`.
6
+
7
+ Cross-Site Request Forgery (CSRF) protection is critical for web applications. This guide explains how to properly handle CSRF tokens with the PerspectAPI SDK.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Understanding CSRF](#understanding-csrf)
12
+ - [When CSRF is Required](#when-csrf-is-required)
13
+ - [Implementation Patterns](#implementation-patterns)
14
+ - [Browser Applications](#browser-applications)
15
+ - [Server-Side Applications](#server-side-applications)
16
+ - [Mobile/Desktop Applications](#mobiledesktop-applications)
17
+ - [Best Practices](#best-practices)
18
+ - [Troubleshooting](#troubleshooting)
19
+
20
+ ## Understanding CSRF
21
+
22
+ CSRF tokens prevent malicious websites from making unauthorized requests on behalf of your users. The security model ensures:
23
+
24
+ 1. **Token Generation**: Server generates a unique token tied to the user's session
25
+ 2. **Token Storage**: Your app stores it securely (NOT in localStorage)
26
+ 3. **Token Submission**: Your app includes it with state-changing requests
27
+ 4. **Token Validation**: Server validates the token matches
28
+
29
+ ## When CSRF is Required
30
+
31
+ CSRF tokens are **REQUIRED** for:
32
+ - Contact form submissions
33
+ - Newsletter subscriptions
34
+ - Any state-changing operations from browsers
35
+
36
+ CSRF tokens are **NOT NEEDED** for:
37
+ - Read-only operations (GET requests)
38
+ - Server-to-server API calls (use API keys instead)
39
+ - Mobile/desktop applications (no browser = no CSRF risk)
40
+
41
+ ## Implementation Patterns
42
+
43
+ ### ✅ CORRECT: Client Provides Token
44
+
45
+ ```typescript
46
+ // GOOD: Client must obtain and provide token
47
+ async submitContact(siteName, data, csrfToken) {
48
+ if (!csrfToken) {
49
+ throw new Error('CSRF token required');
50
+ }
51
+ return this.post('/contact', data, { csrfToken });
52
+ }
53
+ ```
54
+
55
+ ## Browser Applications
56
+
57
+ ### 1. Server-Rendered HTML (Traditional)
58
+
59
+ ```html
60
+ <!-- Token rendered in meta tag by server -->
61
+ <meta name="csrf-token" content="<%= csrfToken %>" />
62
+
63
+ <script>
64
+ // Get token from meta tag
65
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
66
+
67
+ // Use with SDK
68
+ const client = createPerspectApiClient({
69
+ baseUrl: 'https://api.example.com',
70
+ apiKey: 'public-key'
71
+ });
72
+
73
+ // Submit with CSRF token
74
+ await client.contact.submitContact('my-site', formData, csrfToken);
75
+ </script>
76
+ ```
77
+
78
+ ### 2. Single Page Application (SPA)
79
+
80
+ ```typescript
81
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
82
+
83
+ class App {
84
+ private client: PerspectApiClient;
85
+ private csrfToken: string | null = null;
86
+
87
+ constructor() {
88
+ this.client = createPerspectApiClient({
89
+ baseUrl: process.env.REACT_APP_API_URL,
90
+ apiKey: process.env.REACT_APP_PUBLIC_KEY
91
+ });
92
+ }
93
+
94
+ async initialize() {
95
+ // Fetch CSRF token once on app load
96
+ // IMPORTANT: Use same-origin to ensure token is tied to user's session
97
+ const response = await fetch('/api/v1/csrf/token/my-site', {
98
+ credentials: 'same-origin', // Critical for security!
99
+ headers: {
100
+ 'Accept': 'application/json'
101
+ }
102
+ });
103
+
104
+ const data = await response.json();
105
+ this.csrfToken = data.csrf_token;
106
+
107
+ // Store in memory only, never localStorage!
108
+ // Token should be re-fetched on page refresh
109
+ }
110
+
111
+ async submitContactForm(formData: ContactFormData) {
112
+ if (!this.csrfToken) {
113
+ throw new Error('CSRF token not initialized');
114
+ }
115
+
116
+ return await this.client.contact.submitContact(
117
+ 'my-site',
118
+ formData,
119
+ this.csrfToken
120
+ );
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### 3. React Implementation
126
+
127
+ ```tsx
128
+ import React, { useState, useEffect } from 'react';
129
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
130
+
131
+ const client = createPerspectApiClient({
132
+ baseUrl: process.env.REACT_APP_API_URL!,
133
+ apiKey: process.env.REACT_APP_PUBLIC_KEY!
134
+ });
135
+
136
+ export function ContactForm() {
137
+ const [csrfToken, setCsrfToken] = useState<string | null>(null);
138
+ const [isLoading, setIsLoading] = useState(true);
139
+
140
+ useEffect(() => {
141
+ // Fetch CSRF token on component mount
142
+ async function fetchToken() {
143
+ try {
144
+ const response = await fetch(
145
+ `${process.env.REACT_APP_API_URL}/api/v1/csrf/token/${process.env.REACT_APP_SITE_NAME}`,
146
+ {
147
+ credentials: 'same-origin',
148
+ headers: { 'Accept': 'application/json' }
149
+ }
150
+ );
151
+ const data = await response.json();
152
+ setCsrfToken(data.csrf_token);
153
+ } catch (error) {
154
+ console.error('Failed to fetch CSRF token:', error);
155
+ } finally {
156
+ setIsLoading(false);
157
+ }
158
+ }
159
+
160
+ fetchToken();
161
+ }, []);
162
+
163
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
164
+ event.preventDefault();
165
+
166
+ if (!csrfToken) {
167
+ alert('Security token not loaded. Please refresh the page.');
168
+ return;
169
+ }
170
+
171
+ const formData = new FormData(event.currentTarget);
172
+
173
+ try {
174
+ const result = await client.contact.submitContact(
175
+ process.env.REACT_APP_SITE_NAME!,
176
+ {
177
+ name: formData.get('name') as string,
178
+ email: formData.get('email') as string,
179
+ message: formData.get('message') as string,
180
+ },
181
+ csrfToken // Pass CSRF token
182
+ );
183
+
184
+ alert('Message sent successfully!');
185
+ } catch (error) {
186
+ console.error('Submission failed:', error);
187
+ alert('Failed to send message. Please try again.');
188
+ }
189
+ };
190
+
191
+ if (isLoading) {
192
+ return <div>Loading...</div>;
193
+ }
194
+
195
+ return (
196
+ <form onSubmit={handleSubmit}>
197
+ <input name="name" type="text" required />
198
+ <input name="email" type="email" required />
199
+ <textarea name="message" required />
200
+ <button type="submit">Send Message</button>
201
+ </form>
202
+ );
203
+ }
204
+ ```
205
+
206
+ ### 4. Vue.js Implementation
207
+
208
+ ```vue
209
+ <template>
210
+ <form @submit.prevent="submitForm">
211
+ <input v-model="form.name" type="text" required />
212
+ <input v-model="form.email" type="email" required />
213
+ <textarea v-model="form.message" required></textarea>
214
+ <button type="submit" :disabled="!csrfToken">
215
+ Send Message
216
+ </button>
217
+ </form>
218
+ </template>
219
+
220
+ <script>
221
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
222
+
223
+ export default {
224
+ data() {
225
+ return {
226
+ csrfToken: null,
227
+ form: {
228
+ name: '',
229
+ email: '',
230
+ message: ''
231
+ },
232
+ client: null
233
+ };
234
+ },
235
+
236
+ async created() {
237
+ // Initialize client
238
+ this.client = createPerspectApiClient({
239
+ baseUrl: process.env.VUE_APP_API_URL,
240
+ apiKey: process.env.VUE_APP_PUBLIC_KEY
241
+ });
242
+
243
+ // Fetch CSRF token
244
+ await this.fetchCsrfToken();
245
+ },
246
+
247
+ methods: {
248
+ async fetchCsrfToken() {
249
+ try {
250
+ const response = await fetch(
251
+ `${process.env.VUE_APP_API_URL}/api/v1/csrf/token/${process.env.VUE_APP_SITE_NAME}`,
252
+ {
253
+ credentials: 'same-origin',
254
+ headers: { 'Accept': 'application/json' }
255
+ }
256
+ );
257
+ const data = await response.json();
258
+ this.csrfToken = data.csrf_token;
259
+ } catch (error) {
260
+ console.error('Failed to fetch CSRF token:', error);
261
+ }
262
+ },
263
+
264
+ async submitForm() {
265
+ if (!this.csrfToken) {
266
+ alert('Security token not loaded');
267
+ return;
268
+ }
269
+
270
+ try {
271
+ await this.client.contact.submitContact(
272
+ process.env.VUE_APP_SITE_NAME,
273
+ this.form,
274
+ this.csrfToken
275
+ );
276
+
277
+ alert('Message sent!');
278
+ this.form = { name: '', email: '', message: '' };
279
+ } catch (error) {
280
+ alert('Failed to send message');
281
+ }
282
+ }
283
+ }
284
+ };
285
+ </script>
286
+ ```
287
+
288
+ ## Server-Side Applications
289
+
290
+ ### Next.js (App Router)
291
+
292
+ ```typescript
293
+ // app/api/contact/route.ts
294
+ import { NextRequest, NextResponse } from 'next/server';
295
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
296
+
297
+ export async function POST(request: NextRequest) {
298
+ const body = await request.json();
299
+
300
+ // Server-side: Use API key, no CSRF needed
301
+ const client = createPerspectApiClient({
302
+ baseUrl: process.env.PERSPECT_API_URL!,
303
+ apiKey: process.env.PERSPECT_API_KEY! // Server-side API key
304
+ });
305
+
306
+ try {
307
+ // No CSRF token needed for server-to-server calls
308
+ const result = await client.contact.submitContact(
309
+ process.env.SITE_NAME!,
310
+ body
311
+ // No CSRF token parameter
312
+ );
313
+
314
+ return NextResponse.json(result);
315
+ } catch (error) {
316
+ return NextResponse.json(
317
+ { error: 'Submission failed' },
318
+ { status: 500 }
319
+ );
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### Next.js (Pages Router with SSR)
325
+
326
+ ```tsx
327
+ // pages/contact.tsx
328
+ import { GetServerSideProps } from 'next';
329
+ import { useState } from 'react';
330
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
331
+
332
+ interface Props {
333
+ csrfToken: string;
334
+ }
335
+
336
+ export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
337
+ // Server-side: Fetch CSRF token
338
+ const response = await fetch(
339
+ `${process.env.API_URL}/api/v1/csrf/token/${process.env.SITE_NAME}`
340
+ );
341
+ const data = await response.json();
342
+
343
+ return {
344
+ props: {
345
+ csrfToken: data.csrf_token
346
+ }
347
+ };
348
+ };
349
+
350
+ export default function ContactPage({ csrfToken }: Props) {
351
+ const [client] = useState(() =>
352
+ createPerspectApiClient({
353
+ baseUrl: process.env.NEXT_PUBLIC_API_URL!,
354
+ apiKey: process.env.NEXT_PUBLIC_API_KEY!
355
+ })
356
+ );
357
+
358
+ const handleSubmit = async (e: React.FormEvent) => {
359
+ e.preventDefault();
360
+ // ... get form data
361
+
362
+ await client.contact.submitContact(
363
+ process.env.NEXT_PUBLIC_SITE_NAME!,
364
+ formData,
365
+ csrfToken // Use server-provided token
366
+ );
367
+ };
368
+
369
+ return (
370
+ <form onSubmit={handleSubmit}>
371
+ {/* Token is already in memory, not in DOM */}
372
+ {/* Form fields... */}
373
+ </form>
374
+ );
375
+ }
376
+ ```
377
+
378
+ ### Remix
379
+
380
+ ```tsx
381
+ // app/routes/contact.tsx
382
+ import { json, type ActionFunctionArgs } from "@remix-run/node";
383
+ import { Form, useLoaderData } from "@remix-run/react";
384
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
385
+
386
+ export async function loader() {
387
+ // Fetch CSRF token server-side
388
+ const response = await fetch(
389
+ `${process.env.API_URL}/api/v1/csrf/token/${process.env.SITE_NAME}`
390
+ );
391
+ const data = await response.json();
392
+
393
+ return json({ csrfToken: data.csrf_token });
394
+ }
395
+
396
+ export async function action({ request }: ActionFunctionArgs) {
397
+ const formData = await request.formData();
398
+ const csrfToken = formData.get("csrfToken") as string;
399
+
400
+ const client = createPerspectApiClient({
401
+ baseUrl: process.env.API_URL!,
402
+ apiKey: process.env.API_KEY!
403
+ });
404
+
405
+ await client.contact.submitContact(
406
+ process.env.SITE_NAME!,
407
+ {
408
+ name: formData.get("name") as string,
409
+ email: formData.get("email") as string,
410
+ message: formData.get("message") as string,
411
+ },
412
+ csrfToken
413
+ );
414
+
415
+ return json({ success: true });
416
+ }
417
+
418
+ export default function Contact() {
419
+ const { csrfToken } = useLoaderData<typeof loader>();
420
+
421
+ return (
422
+ <Form method="post">
423
+ <input type="hidden" name="csrfToken" value={csrfToken} />
424
+ <input name="name" type="text" required />
425
+ <input name="email" type="email" required />
426
+ <textarea name="message" required />
427
+ <button type="submit">Send</button>
428
+ </Form>
429
+ );
430
+ }
431
+ ```
432
+
433
+ ## Mobile/Desktop Applications
434
+
435
+ CSRF tokens are **NOT NEEDED** for native applications:
436
+
437
+ ```typescript
438
+ // React Native / Electron / Tauri
439
+ const client = createPerspectApiClient({
440
+ baseUrl: 'https://api.example.com',
441
+ apiKey: 'your-api-key'
442
+ });
443
+
444
+ // No CSRF token needed - not running in a browser
445
+ await client.contact.submitContact(
446
+ 'my-site',
447
+ formData
448
+ // No CSRF parameter
449
+ );
450
+
451
+ // The SDK will detect it's not in a browser and won't warn
452
+ ```
453
+
454
+ ## Best Practices
455
+
456
+ ### 1. Token Storage
457
+
458
+ ```typescript
459
+ // ✅ GOOD: Store in memory
460
+ let csrfToken: string | null = null;
461
+
462
+ // ✅ GOOD: Store in meta tag (server-rendered)
463
+ <meta name="csrf-token" content={csrfToken} />
464
+
465
+ // ❌ BAD: Never store in localStorage
466
+ localStorage.setItem('csrf', token); // NO!
467
+
468
+ // ❌ BAD: Never store in sessionStorage
469
+ sessionStorage.setItem('csrf', token); // NO!
470
+ ```
471
+
472
+ ### 2. Token Fetching
473
+
474
+ ```typescript
475
+ // ✅ GOOD: Fetch with credentials
476
+ const response = await fetch('/api/v1/csrf/token/my-site', {
477
+ credentials: 'same-origin', // Critical!
478
+ headers: { 'Accept': 'application/json' }
479
+ });
480
+
481
+ // ❌ BAD: Fetching without credentials
482
+ const response = await fetch('/api/v1/csrf/token/my-site');
483
+ // Token won't be tied to session!
484
+ ```
485
+
486
+ ### 3. Token Refresh
487
+
488
+ ```typescript
489
+ class CSRFManager {
490
+ private token: string | null = null;
491
+ private tokenExpiry: number = 0;
492
+
493
+ async getToken(): Promise<string> {
494
+ // Refresh if expired or not set
495
+ if (!this.token || Date.now() > this.tokenExpiry) {
496
+ const response = await fetch('/api/v1/csrf/token/my-site', {
497
+ credentials: 'same-origin'
498
+ });
499
+ const data = await response.json();
500
+ this.token = data.csrf_token;
501
+ // Token expires in 1 hour
502
+ this.tokenExpiry = Date.now() + 3600000;
503
+ }
504
+
505
+ return this.token;
506
+ }
507
+
508
+ clearToken() {
509
+ this.token = null;
510
+ this.tokenExpiry = 0;
511
+ }
512
+ }
513
+ ```
514
+
515
+ ### 4. Error Handling
516
+
517
+ ```typescript
518
+ async function submitWithCSRF(client, siteName, data) {
519
+ let csrfToken;
520
+
521
+ try {
522
+ // Get CSRF token
523
+ const tokenResponse = await fetch(`/api/v1/csrf/token/${siteName}`, {
524
+ credentials: 'same-origin'
525
+ });
526
+
527
+ if (!tokenResponse.ok) {
528
+ throw new Error('Failed to fetch CSRF token');
529
+ }
530
+
531
+ const tokenData = await tokenResponse.json();
532
+ csrfToken = tokenData.csrf_token;
533
+ } catch (error) {
534
+ console.error('CSRF token fetch failed:', error);
535
+ throw new Error('Security verification failed. Please refresh and try again.');
536
+ }
537
+
538
+ try {
539
+ // Submit with token
540
+ return await client.contact.submitContact(siteName, data, csrfToken);
541
+ } catch (error) {
542
+ if (error.status === 401 && error.message.includes('CSRF')) {
543
+ // Token might be expired, clear and retry once
544
+ const newToken = await fetchNewCSRFToken(siteName);
545
+ return await client.contact.submitContact(siteName, data, newToken);
546
+ }
547
+ throw error;
548
+ }
549
+ }
550
+ ```
551
+
552
+ ## Troubleshooting
553
+
554
+ ### Common Issues
555
+
556
+ #### 1. "CSRF token required" Error
557
+
558
+ **Cause**: Not passing CSRF token to methods that require it.
559
+
560
+ **Solution**:
561
+ ```typescript
562
+ // ❌ Wrong
563
+ await client.contact.submitContact('my-site', data);
564
+
565
+ // ✅ Correct
566
+ await client.contact.submitContact('my-site', data, csrfToken);
567
+ ```
568
+
569
+ #### 2. "Invalid CSRF token" Error
570
+
571
+ **Causes**:
572
+ - Token expired
573
+ - Token not tied to user session
574
+ - Wrong site name
575
+
576
+ **Solution**:
577
+ ```typescript
578
+ // Ensure credentials are sent
579
+ fetch('/api/v1/csrf/token/my-site', {
580
+ credentials: 'same-origin' // Required!
581
+ });
582
+
583
+ // Use correct site name
584
+ const siteName = 'my-site'; // Must match your site configuration
585
+ ```
586
+
587
+ #### 3. CORS Issues
588
+
589
+ **Cause**: Cross-origin requests without proper configuration.
590
+
591
+ **Solution**:
592
+ ```typescript
593
+ // For cross-origin requests, configure CORS properly
594
+ const client = createPerspectApiClient({
595
+ baseUrl: 'https://api.example.com',
596
+ headers: {
597
+ 'X-Requested-With': 'XMLHttpRequest' // Sometimes helps with CORS
598
+ }
599
+ });
600
+
601
+ // And ensure API allows your origin
602
+ ```
603
+
604
+ #### 4. Token Not Found in Response
605
+
606
+ **Cause**: Using wrong endpoint or site not found.
607
+
608
+ **Solution**:
609
+ ```typescript
610
+ // Use site-specific endpoint for development
611
+ const response = await fetch('/api/v1/csrf/token/my-site');
612
+ const data = await response.json();
613
+ const token = data.csrf_token; // Note: csrf_token, not token
614
+
615
+ // For production with custom domain
616
+ const response = await fetch('/api/v1/csrf/token');
617
+ // Requires proper Host header
618
+ ```
619
+
620
+ ## Security Checklist
621
+
622
+ - [ ] Never auto-fetch CSRF tokens in the SDK
623
+ - [ ] Always use `credentials: 'same-origin'` when fetching tokens
624
+ - [ ] Store tokens in memory or meta tags, never localStorage
625
+ - [ ] Pass CSRF tokens explicitly to SDK methods
626
+ - [ ] Refresh tokens periodically (they expire)
627
+ - [ ] Use HTTPS in production
628
+ - [ ] Configure CORS properly for cross-origin requests
629
+ - [ ] Use API keys for server-to-server calls instead of CSRF
630
+ - [ ] Validate tokens server-side on all state-changing operations
631
+
632
+ ## Migration Guide
633
+
634
+ If you're upgrading from a version without CSRF support:
635
+
636
+ ### Before (No CSRF)
637
+ ```typescript
638
+ // Old version - no CSRF parameter
639
+ await client.contact.submitContact('my-site', formData);
640
+ ```
641
+
642
+ ### After (With CSRF)
643
+ ```typescript
644
+ // New version - CSRF token required for browser apps
645
+ const csrfToken = await fetchCSRFToken('my-site');
646
+ await client.contact.submitContact('my-site', formData, csrfToken);
647
+ ```
648
+
649
+ ### Backward Compatibility
650
+
651
+ The CSRF token parameter is optional to maintain backward compatibility:
652
+ - **Browser environments**: Will show a console warning if CSRF token is missing
653
+ - **Node.js/Server environments**: No warning, CSRF not required
654
+ - **Mobile/Desktop apps**: No warning, CSRF not required
655
+
656
+ ## Summary
657
+
658
+ 1. **Browser apps** must fetch and provide CSRF tokens
659
+ 2. **Server apps** use API keys, no CSRF needed
660
+ 3. **Mobile/Desktop apps** don't need CSRF tokens
661
+ 4. **Never** auto-fetch tokens in the SDK
662
+ 5. **Always** use `credentials: 'same-origin'`
663
+ 6. **Store** tokens in memory only
664
+ 7. **Pass** tokens explicitly to SDK methods