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.
- package/README.md +46 -1011
- package/dist/chunk-K3T2AFYA.mjs +1393 -0
- package/dist/index-CWvUyMt3.d.mts +2224 -0
- package/dist/index-CWvUyMt3.d.ts +2224 -0
- package/dist/index.d.mts +130 -2221
- package/dist/index.d.ts +130 -2221
- package/dist/index.js +8 -2
- package/dist/index.mjs +13 -1364
- package/dist/v2/index.d.mts +1 -0
- package/dist/v2/index.d.ts +1 -0
- package/dist/v2/index.js +1419 -0
- package/dist/v2/index.mjs +40 -0
- package/docs/README.md +15 -0
- package/docs/v1-deprecated/README.md +9 -0
- package/docs/v1-deprecated/examples/README.md +324 -0
- package/docs/v1-deprecated/examples/basic-usage.ts +258 -0
- package/docs/v1-deprecated/examples/cloudflare-worker.ts +274 -0
- package/docs/v1-deprecated/examples/content-query-with-slug-prefix.ts +237 -0
- package/docs/v1-deprecated/examples/image-transforms.ts +200 -0
- package/docs/v1-deprecated/examples/site-user-checkout.ts +186 -0
- package/docs/v1-deprecated/examples/slug-prefix-examples.ts +491 -0
- package/docs/v1-deprecated/legacy-docs/caching.md +667 -0
- package/docs/v1-deprecated/legacy-docs/contact.md +1396 -0
- package/docs/v1-deprecated/legacy-docs/csrf-protection.md +664 -0
- package/docs/v1-deprecated/legacy-docs/image-transforms.md +523 -0
- package/docs/v1-deprecated/legacy-docs/loaders.md +304 -0
- package/docs/v1-deprecated/legacy-docs/newsletter.md +811 -0
- package/docs/v1-deprecated/legacy-docs/site-users.md +817 -0
- package/docs/v1-deprecated/legacy-notes/CHANGELOG-CHECKOUT.md +143 -0
- package/docs/v1-deprecated/legacy-notes/CSRF-CHECKOUT.md +271 -0
- package/docs/v1-deprecated/legacy-notes/IMAGE_TRANSFORMS_PORT.md +298 -0
- package/docs/v1-deprecated/sdk-readme.md +1076 -0
- package/examples/README.md +19 -0
- package/examples/basic-v2.ts +37 -0
- package/llms.txt +25 -0
- package/package.json +18 -7
- package/src/client/api-keys-client.ts +4 -0
- package/src/client/auth-client.ts +4 -0
- package/src/client/base-client.ts +7 -0
- package/src/client/bundles-client.ts +4 -0
- package/src/client/categories-client.ts +4 -0
- package/src/client/checkout-client.ts +4 -0
- package/src/client/contact-client.ts +4 -0
- package/src/client/content-client.ts +4 -0
- package/src/client/newsletter-client.ts +4 -0
- package/src/client/newsletter-management-client.ts +4 -0
- package/src/client/organizations-client.ts +4 -0
- package/src/client/products-client.ts +4 -0
- package/src/client/site-users-client.ts +10 -1
- package/src/client/sites-client.ts +4 -0
- package/src/client/webhooks-client.ts +4 -0
- package/src/deprecation.ts +2 -1
- package/src/index.ts +2 -1
- package/src/loaders.ts +59 -0
- package/src/perspect-api-client.ts +2 -2
- package/src/v2/client/orders-client.ts +6 -1
- 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
|