perspectapi-ts-sdk 2.7.0 → 2.8.2

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,352 @@
1
+ /**
2
+ * Site Users client for PerspectAPI SDK
3
+ * Handles per-site customer accounts (OTP-based auth, profiles, orders, subscriptions)
4
+ */
5
+
6
+ import { BaseClient } from './base-client';
7
+ import type { CacheManager } from '../cache/cache-manager';
8
+ import type {
9
+ SiteUser,
10
+ SiteUserProfile,
11
+ SiteUserSubscription,
12
+ SiteUserOrder,
13
+ RequestOtpRequest,
14
+ VerifyOtpRequest,
15
+ VerifyOtpResponse,
16
+ UpdateSiteUserRequest,
17
+ SetProfileValueRequest,
18
+ ApiResponse,
19
+ } from '../types';
20
+
21
+ export class SiteUsersClient extends BaseClient {
22
+ constructor(http: any, cache?: CacheManager) {
23
+ super(http, '/api/v1', cache);
24
+ }
25
+
26
+ /**
27
+ * Build a site user endpoint scoped to a site (without /sites prefix)
28
+ */
29
+ private siteUserEndpoint(siteName: string, endpoint: string): string {
30
+ return this.siteScopedEndpoint(siteName, endpoint, { includeSitesSegment: false });
31
+ }
32
+
33
+ // ============================================================================
34
+ // PUBLIC ENDPOINTS (OTP authentication)
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Request OTP for login/signup
39
+ * @param siteName - The site name
40
+ * @param data - Email address
41
+ * @param csrfToken - CSRF token (required for browser-based submissions)
42
+ */
43
+ async requestOtp(
44
+ siteName: string,
45
+ data: RequestOtpRequest,
46
+ csrfToken?: string
47
+ ): Promise<ApiResponse<{ success: boolean }>> {
48
+ if (typeof window !== 'undefined' && !csrfToken) {
49
+ console.warn('CSRF token recommended for browser-based OTP requests');
50
+ }
51
+
52
+ return this.create<RequestOtpRequest, { success: boolean }>(
53
+ this.siteUserEndpoint(siteName, '/users/request-otp'),
54
+ data,
55
+ csrfToken
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Verify OTP and get JWT token
61
+ *
62
+ * For cross-domain authentication, you must manually set the token after verification:
63
+ * ```typescript
64
+ * const response = await client.siteUsers.verifyOtp('mysite', { email, code });
65
+ * const { token, user } = response.data;
66
+ *
67
+ * // Store token securely (choose one):
68
+ * // Option 1: Memory (lost on refresh, most secure)
69
+ * client.setAuth(token);
70
+ *
71
+ * // Option 2: httpOnly cookie on YOUR domain (recommended for production)
72
+ * await fetch('/your-api/set-auth-cookie', {
73
+ * method: 'POST',
74
+ * body: JSON.stringify({ token })
75
+ * });
76
+ * client.setAuth(token);
77
+ *
78
+ * // Option 3: localStorage (vulnerable to XSS, not recommended)
79
+ * localStorage.setItem('site_user_token', token);
80
+ * client.setAuth(token);
81
+ * ```
82
+ *
83
+ * For convenience, use `verifyOtpAndSetAuth()` to automatically set the token in memory.
84
+ *
85
+ * @param siteName - The site name
86
+ * @param data - Email and code
87
+ * @param csrfToken - CSRF token (required for browser-based submissions)
88
+ */
89
+ async verifyOtp(
90
+ siteName: string,
91
+ data: VerifyOtpRequest,
92
+ csrfToken?: string
93
+ ): Promise<ApiResponse<VerifyOtpResponse>> {
94
+ if (typeof window !== 'undefined' && !csrfToken) {
95
+ console.warn('CSRF token recommended for browser-based OTP verification');
96
+ }
97
+
98
+ return this.create<VerifyOtpRequest, VerifyOtpResponse>(
99
+ this.siteUserEndpoint(siteName, '/users/verify-otp'),
100
+ data,
101
+ csrfToken
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Verify OTP and automatically set the token for subsequent requests
107
+ *
108
+ * Convenience method that:
109
+ * 1. Verifies the OTP
110
+ * 2. Automatically calls setAuth() with the returned token
111
+ *
112
+ * Note: Token is stored in memory only and will be lost on page refresh.
113
+ * For persistent auth, use verifyOtp() and store the token yourself.
114
+ *
115
+ * @param siteName - The site name
116
+ * @param data - Email and code
117
+ * @param csrfToken - CSRF token (required for browser-based submissions)
118
+ */
119
+ async verifyOtpAndSetAuth(
120
+ siteName: string,
121
+ data: VerifyOtpRequest,
122
+ csrfToken?: string
123
+ ): Promise<ApiResponse<VerifyOtpResponse>> {
124
+ const response = await this.verifyOtp(siteName, data, csrfToken);
125
+
126
+ if (response.data?.token) {
127
+ this.http.setAuth(response.data.token);
128
+ }
129
+
130
+ return response;
131
+ }
132
+
133
+ /**
134
+ * Logout (clear session cookie)
135
+ * @param siteName - The site name
136
+ */
137
+ async logout(siteName: string): Promise<ApiResponse<{ success: boolean }>> {
138
+ return this.create<Record<string, never>, { success: boolean }>(
139
+ this.siteUserEndpoint(siteName, '/users/logout'),
140
+ {}
141
+ );
142
+ }
143
+
144
+ // ============================================================================
145
+ // AUTHENTICATED ENDPOINTS (site user JWT required)
146
+ // ============================================================================
147
+
148
+ /**
149
+ * Get current user profile
150
+ * @param siteName - The site name
151
+ */
152
+ async getMe(siteName: string): Promise<ApiResponse<{ user: SiteUser; profile: SiteUserProfile }>> {
153
+ return this.getSingle<{ user: SiteUser; profile: SiteUserProfile }>(
154
+ this.siteUserEndpoint(siteName, '/users/me')
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Update current user profile
160
+ * @param siteName - The site name
161
+ * @param data - Fields to update
162
+ * @param csrfToken - CSRF token (required)
163
+ */
164
+ async updateMe(
165
+ siteName: string,
166
+ data: UpdateSiteUserRequest,
167
+ csrfToken?: string
168
+ ): Promise<ApiResponse<{ success: boolean }>> {
169
+ return this.patch<UpdateSiteUserRequest, { success: boolean }>(
170
+ this.siteUserEndpoint(siteName, '/users/me'),
171
+ data,
172
+ csrfToken
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Get all profile key-values
178
+ * @param siteName - The site name
179
+ */
180
+ async getProfile(siteName: string): Promise<ApiResponse<{ profile: SiteUserProfile }>> {
181
+ return this.getSingle<{ profile: SiteUserProfile }>(
182
+ this.siteUserEndpoint(siteName, '/users/me/profile')
183
+ );
184
+ }
185
+
186
+ /**
187
+ * Set a profile key-value
188
+ * @param siteName - The site name
189
+ * @param key - Profile key (e.g., 'phone', 'whatsapp', 'address_shipping')
190
+ * @param value - Profile value (string or JSON string)
191
+ * @param csrfToken - CSRF token (required)
192
+ */
193
+ async setProfileValue(
194
+ siteName: string,
195
+ key: string,
196
+ value: string,
197
+ csrfToken?: string
198
+ ): Promise<ApiResponse<{ success: boolean }>> {
199
+ return this.update<SetProfileValueRequest, { success: boolean }>(
200
+ this.siteUserEndpoint(siteName, `/users/me/profile/${encodeURIComponent(key)}`),
201
+ { value },
202
+ csrfToken
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Delete a profile key-value
208
+ * @param siteName - The site name
209
+ * @param key - Profile key to delete
210
+ * @param csrfToken - CSRF token (required)
211
+ */
212
+ async deleteProfileValue(
213
+ siteName: string,
214
+ key: string,
215
+ csrfToken?: string
216
+ ): Promise<ApiResponse<{ success: boolean }>> {
217
+ return this.delete<{ success: boolean }>(
218
+ this.siteUserEndpoint(siteName, `/users/me/profile/${encodeURIComponent(key)}`),
219
+ csrfToken
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Get transaction/order history
225
+ * @param siteName - The site name
226
+ * @param params - Pagination params
227
+ */
228
+ async getOrders(
229
+ siteName: string,
230
+ params?: { limit?: number; offset?: number }
231
+ ): Promise<ApiResponse<{ orders: SiteUserOrder[] }>> {
232
+ return this.http.get<{ orders: SiteUserOrder[] }>(
233
+ this.buildPath(this.siteUserEndpoint(siteName, '/users/me/orders')),
234
+ params
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Get single order detail
240
+ * @param siteName - The site name
241
+ * @param orderId - Order ID or session ID
242
+ */
243
+ async getOrder(siteName: string, orderId: string): Promise<ApiResponse<{ order: any }>> {
244
+ return this.getSingle<{ order: any }>(
245
+ this.siteUserEndpoint(siteName, `/users/me/orders/${encodeURIComponent(orderId)}`)
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Get payment subscriptions
251
+ * @param siteName - The site name
252
+ * @param params - Pagination params
253
+ */
254
+ async getSubscriptions(
255
+ siteName: string,
256
+ params?: { limit?: number; offset?: number }
257
+ ): Promise<ApiResponse<{ subscriptions: SiteUserSubscription[] }>> {
258
+ return this.http.get<{ subscriptions: SiteUserSubscription[] }>(
259
+ this.buildPath(this.siteUserEndpoint(siteName, '/users/me/subscriptions')),
260
+ params
261
+ );
262
+ }
263
+
264
+ /**
265
+ * Get single subscription detail
266
+ * @param siteName - The site name
267
+ * @param id - Subscription ID
268
+ */
269
+ async getSubscription(siteName: string, id: string): Promise<ApiResponse<{ subscription: SiteUserSubscription }>> {
270
+ return this.getSingle<{ subscription: SiteUserSubscription }>(
271
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(id)}`)
272
+ );
273
+ }
274
+
275
+ /**
276
+ * Cancel a subscription (marks for cancellation at period end)
277
+ * @param siteName - The site name
278
+ * @param id - Subscription ID
279
+ * @param csrfToken - CSRF token (required)
280
+ */
281
+ async cancelSubscription(
282
+ siteName: string,
283
+ id: string,
284
+ csrfToken?: string
285
+ ): Promise<ApiResponse<{ success: boolean; message: string }>> {
286
+ return this.create<Record<string, never>, { success: boolean; message: string }>(
287
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(id)}/cancel`),
288
+ {},
289
+ csrfToken
290
+ );
291
+ }
292
+
293
+ /**
294
+ * Get linked newsletter subscriptions
295
+ * @param siteName - The site name
296
+ */
297
+ async getNewsletterSubscriptions(siteName: string): Promise<ApiResponse<{ newsletters: any[] }>> {
298
+ return this.getSingle<{ newsletters: any[] }>(
299
+ this.siteUserEndpoint(siteName, '/users/me/newsletters')
300
+ );
301
+ }
302
+
303
+ // ============================================================================
304
+ // ADMIN ENDPOINTS (API key auth required)
305
+ // ============================================================================
306
+
307
+ /**
308
+ * List all site users (admin only)
309
+ * @param siteName - The site name
310
+ * @param params - Query params (limit, offset, status)
311
+ */
312
+ async listUsers(
313
+ siteName: string,
314
+ params?: { limit?: number; offset?: number; status?: string }
315
+ ): Promise<ApiResponse<{ users: SiteUser[] }>> {
316
+ return this.http.get<{ users: SiteUser[] }>(
317
+ this.buildPath(this.siteUserEndpoint(siteName, '/users')),
318
+ params
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Get user detail (admin only)
324
+ * @param siteName - The site name
325
+ * @param userId - User ID
326
+ */
327
+ async getUser(siteName: string, userId: string): Promise<ApiResponse<{ user: SiteUser; profile: SiteUserProfile }>> {
328
+ return this.getSingle<{ user: SiteUser; profile: SiteUserProfile }>(
329
+ this.siteUserEndpoint(siteName, `/users/${encodeURIComponent(userId)}`)
330
+ );
331
+ }
332
+
333
+ /**
334
+ * Update user status (admin only)
335
+ * @param siteName - The site name
336
+ * @param userId - User ID
337
+ * @param status - New status
338
+ * @param csrfToken - CSRF token (required)
339
+ */
340
+ async updateUserStatus(
341
+ siteName: string,
342
+ userId: string,
343
+ status: 'active' | 'suspended' | 'pending_verification',
344
+ csrfToken?: string
345
+ ): Promise<ApiResponse<{ success: boolean }>> {
346
+ return this.patch<{ status: string }, { success: boolean }>(
347
+ this.siteUserEndpoint(siteName, `/users/${encodeURIComponent(userId)}/status`),
348
+ { status },
349
+ csrfToken
350
+ );
351
+ }
352
+ }
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export { WebhooksClient } from './client/webhooks-client';
19
19
  export { CheckoutClient } from './client/checkout-client';
20
20
  export { ContactClient } from './client/contact-client';
21
21
  export { NewsletterClient } from './client/newsletter-client';
22
+ export { SiteUsersClient } from './client/site-users-client';
22
23
 
23
24
  // Base classes
24
25
  export { BaseClient } from './client/base-client';
@@ -106,4 +107,13 @@ export type {
106
107
  NewsletterUnsubscribeResponse,
107
108
  ContactSubmitResponse,
108
109
  ContactStatusResponse,
110
+ SiteUser,
111
+ SiteUserProfile,
112
+ SiteUserSubscription,
113
+ SiteUserOrder,
114
+ RequestOtpRequest,
115
+ VerifyOtpRequest,
116
+ VerifyOtpResponse,
117
+ UpdateSiteUserRequest,
118
+ SetProfileValueRequest,
109
119
  } from './types';
@@ -16,6 +16,7 @@ import { WebhooksClient } from './client/webhooks-client';
16
16
  import { CheckoutClient } from './client/checkout-client';
17
17
  import { ContactClient } from './client/contact-client';
18
18
  import { NewsletterClient } from './client/newsletter-client';
19
+ import { SiteUsersClient } from './client/site-users-client';
19
20
 
20
21
  import type { PerspectApiConfig, ApiResponse } from './types';
21
22
 
@@ -35,6 +36,7 @@ export class PerspectApiClient {
35
36
  public readonly checkout: CheckoutClient;
36
37
  public readonly contact: ContactClient;
37
38
  public readonly newsletter: NewsletterClient;
39
+ public readonly siteUsers: SiteUsersClient;
38
40
 
39
41
  constructor(config: PerspectApiConfig) {
40
42
  // Validate required configuration
@@ -58,6 +60,7 @@ export class PerspectApiClient {
58
60
  this.checkout = new CheckoutClient(this.http, this.cache);
59
61
  this.contact = new ContactClient(this.http, this.cache);
60
62
  this.newsletter = new NewsletterClient(this.http, this.cache);
63
+ this.siteUsers = new SiteUsersClient(this.http, this.cache);
61
64
  }
62
65
 
63
66
  /**
@@ -214,7 +214,7 @@ export interface NewsletterUnsubscribeResponse {
214
214
 
215
215
  // Content Management
216
216
  export type ContentStatus = 'draft' | 'publish' | 'private' | 'trash';
217
- export type ContentType = 'post' | 'page';
217
+ export type ContentType = 'post' | 'page' | 'block';
218
218
 
219
219
  export interface Content {
220
220
  id: number;
@@ -579,6 +579,95 @@ export interface ContactStatusResponse {
579
579
  metadata?: Record<string, any>;
580
580
  }
581
581
 
582
+ // Site Users (customer accounts)
583
+ export interface SiteUser {
584
+ id: string;
585
+ email: string;
586
+ first_name?: string;
587
+ last_name?: string;
588
+ avatar_url?: string;
589
+ status: 'active' | 'suspended' | 'pending_verification';
590
+ email_verified: boolean;
591
+ waitlist: boolean;
592
+ metadata?: Record<string, any>;
593
+ created_at: string;
594
+ last_login_at?: string;
595
+ }
596
+
597
+ export interface SiteUserProfile {
598
+ [key: string]: {
599
+ value: any;
600
+ updated_at: string;
601
+ };
602
+ }
603
+
604
+ export interface SiteUserSubscription {
605
+ id: string;
606
+ provider: string;
607
+ provider_subscription_id?: string;
608
+ plan_name?: string;
609
+ plan_id?: string;
610
+ status: 'active' | 'past_due' | 'canceled' | 'paused' | 'trialing' | 'expired';
611
+ amount?: number;
612
+ currency?: string;
613
+ billing_interval?: string;
614
+ billing_interval_count?: number;
615
+ current_period_start?: string;
616
+ current_period_end?: string;
617
+ trial_start?: string;
618
+ trial_end?: string;
619
+ canceled_at?: string;
620
+ cancel_at_period_end: boolean;
621
+ created_at: string;
622
+ updated_at: string;
623
+ }
624
+
625
+ export interface SiteUserOrder {
626
+ session_id: string;
627
+ order_id?: string;
628
+ customer_email?: string;
629
+ amount_total: number;
630
+ currency: string;
631
+ status: string;
632
+ payment_status?: string;
633
+ line_items: any[];
634
+ created_at: string;
635
+ updated_at: string;
636
+ }
637
+
638
+ export interface RequestOtpRequest {
639
+ email: string;
640
+ waitlist?: boolean; // Mark user as waitlist signup
641
+ }
642
+
643
+ export interface VerifyOtpRequest {
644
+ email: string;
645
+ code: string;
646
+ }
647
+
648
+ export interface VerifyOtpResponse {
649
+ success: boolean;
650
+ token: string; // JWT token for cross-domain authentication
651
+ user: {
652
+ id: string;
653
+ email: string;
654
+ first_name?: string;
655
+ last_name?: string;
656
+ avatar_url?: string;
657
+ };
658
+ }
659
+
660
+ export interface UpdateSiteUserRequest {
661
+ first_name?: string;
662
+ last_name?: string;
663
+ avatar_url?: string;
664
+ metadata?: Record<string, any>;
665
+ }
666
+
667
+ export interface SetProfileValueRequest {
668
+ value: string;
669
+ }
670
+
582
671
  // Error Types
583
672
  export interface ApiError {
584
673
  message: string;
@@ -1,7 +1,7 @@
1
1
  import type { ContentType } from '../types';
2
2
 
3
3
  export const MAX_API_QUERY_LIMIT = 100;
4
- const ALLOWED_CONTENT_TYPES: ReadonlyArray<ContentType> = ['post', 'page'];
4
+ const ALLOWED_CONTENT_TYPES: ReadonlyArray<ContentType> = ['post', 'page', 'block'];
5
5
 
6
6
  export function validateLimit(limit: number, context: string): number {
7
7
  if (typeof limit !== 'number' || Number.isNaN(limit) || !Number.isFinite(limit)) {
@@ -50,4 +50,3 @@ export function validateOptionalContentType(
50
50
  )}.`,
51
51
  );
52
52
  }
53
-