perspectapi-ts-sdk 5.4.5 → 6.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.
@@ -12,15 +12,57 @@ import type {
12
12
  export class ContentV2Client extends BaseV2Client {
13
13
 
14
14
  async list(siteName: string, params?: V2ContentListParams, cachePolicy?: CachePolicy): Promise<V2List<V2Content>> {
15
- return this.getList<V2Content>(this.sitePath(siteName, 'content'), params, cachePolicy);
15
+ return this.getList<V2Content>(
16
+ this.sitePath(siteName, 'content'),
17
+ params,
18
+ this.withContentTags(siteName, cachePolicy, {
19
+ category: params?.category,
20
+ slugPrefix: params?.slug_prefix,
21
+ type: params?.type,
22
+ }),
23
+ );
16
24
  }
17
25
 
18
- async *listAutoPaginated(siteName: string, params?: Omit<V2ContentListParams, 'starting_after' | 'ending_before'>) {
19
- yield* this.listAutoPaginate<V2Content>(this.sitePath(siteName, 'content'), params);
26
+ async *listAutoPaginated(
27
+ siteName: string,
28
+ params?: Omit<V2ContentListParams, 'starting_after' | 'ending_before'>,
29
+ cachePolicy?: CachePolicy,
30
+ ) {
31
+ let startingAfter: string | undefined;
32
+ let hasMore = true;
33
+
34
+ while (hasMore) {
35
+ const queryParams: V2ContentListParams = { ...(params ?? {}) };
36
+ if (startingAfter) {
37
+ queryParams.starting_after = startingAfter;
38
+ }
39
+
40
+ const page = await this.list(siteName, queryParams, cachePolicy);
41
+ for (const item of page.data) {
42
+ yield item;
43
+ }
44
+
45
+ hasMore = page.has_more;
46
+ if (page.data.length > 0) {
47
+ startingAfter = page.data[page.data.length - 1].id;
48
+ } else {
49
+ hasMore = false;
50
+ }
51
+ }
20
52
  }
21
53
 
22
54
  async get(siteName: string, idOrSlug: string, cachePolicy?: CachePolicy): Promise<V2Content> {
23
- return this.getOne<V2Content>(this.sitePath(siteName, 'content', idOrSlug), undefined, cachePolicy);
55
+ const isContentId = this.isContentId(idOrSlug);
56
+
57
+ return this.getOne<V2Content>(
58
+ this.sitePath(siteName, 'content', idOrSlug),
59
+ undefined,
60
+ this.withContentTags(siteName, cachePolicy, {
61
+ id: isContentId ? idOrSlug : undefined,
62
+ slug: isContentId ? undefined : idOrSlug,
63
+ slugPrefix: isContentId ? undefined : this.extractSlugPrefix(idOrSlug),
64
+ }),
65
+ );
24
66
  }
25
67
 
26
68
  async create(siteName: string, data: V2ContentCreateParams): Promise<V2Content> {
@@ -42,4 +84,83 @@ export class ContentV2Client extends BaseV2Client {
42
84
  async unpublish(siteName: string, id: string): Promise<V2Content> {
43
85
  return this.post<V2Content>(this.sitePath(siteName, 'content', `${id}/unpublish`));
44
86
  }
87
+
88
+ private withContentTags(
89
+ siteName: string,
90
+ cachePolicy: CachePolicy | undefined,
91
+ options: {
92
+ category?: string | null;
93
+ id?: string;
94
+ slug?: string;
95
+ slugPrefix?: string | null;
96
+ type?: string | null;
97
+ },
98
+ ): CachePolicy {
99
+ const tags = new Set<string>(cachePolicy?.tags ?? []);
100
+
101
+ for (const tag of this.buildContentTags(siteName, options)) {
102
+ tags.add(tag);
103
+ }
104
+
105
+ return {
106
+ ...cachePolicy,
107
+ tags: Array.from(tags),
108
+ };
109
+ }
110
+
111
+ private buildContentTags(
112
+ siteName: string,
113
+ options: {
114
+ category?: string | null;
115
+ id?: string;
116
+ slug?: string;
117
+ slugPrefix?: string | null;
118
+ type?: string | null;
119
+ },
120
+ ): string[] {
121
+ const tags = new Set<string>(['content', `content:site:${siteName}`]);
122
+ const normalizedPrefix = this.normalizeTagPart(options.slugPrefix);
123
+ const normalizedCategory = this.normalizeTagPart(options.category);
124
+ const normalizedType = this.normalizeTagPart(options.type);
125
+
126
+ if (options.slug) {
127
+ tags.add(`content:slug:${siteName}:${options.slug}`);
128
+ }
129
+ if (options.id) {
130
+ tags.add(`content:id:${options.id}`);
131
+ }
132
+ if (normalizedPrefix) {
133
+ tags.add(`content:prefix:${normalizedPrefix}`);
134
+ }
135
+ if (normalizedType) {
136
+ tags.add(`content:${normalizedType}`);
137
+ }
138
+ if (normalizedCategory) {
139
+ tags.add('content:category');
140
+ tags.add(`content:category:${siteName}:${normalizedCategory}`);
141
+ }
142
+
143
+ return Array.from(tags);
144
+ }
145
+
146
+ private normalizeTagPart(value?: string | null): string | undefined {
147
+ if (typeof value !== 'string') {
148
+ return undefined;
149
+ }
150
+
151
+ const normalized = value.trim().toLowerCase();
152
+ return normalized === '' ? undefined : normalized;
153
+ }
154
+
155
+ private extractSlugPrefix(slug: string): string | undefined {
156
+ const slashIndex = slug.indexOf('/');
157
+ if (slashIndex > 0) {
158
+ return slug.slice(0, slashIndex);
159
+ }
160
+ return undefined;
161
+ }
162
+
163
+ private isContentId(idOrSlug: string): boolean {
164
+ return /^cnt_/i.test(idOrSlug);
165
+ }
45
166
  }
@@ -6,7 +6,11 @@ import { BaseV2Client } from './base-v2-client';
6
6
  import type { CachePolicy } from '../../cache/types';
7
7
  import type {
8
8
  V2NewsletterSubscription, V2NewsletterList, V2NewsletterCampaign,
9
- V2PaginationParams, V2List, V2NewsletterTrackingResponse,
9
+ V2PaginationParams, V2List, V2Deleted, V2NewsletterTrackingResponse,
10
+ V2NewsletterListCreateParams, V2NewsletterListUpdateParams,
11
+ V2NewsletterSyncInput, V2NewsletterSyncResult,
12
+ V2NewsletterSubscriptionListMembershipUpdate,
13
+ V2NewsletterImportRequest, V2NewsletterImportResult,
10
14
  } from '../types';
11
15
 
12
16
  export class NewsletterV2Client extends BaseV2Client {
@@ -122,4 +126,84 @@ export class NewsletterV2Client extends BaseV2Client {
122
126
  cachePolicy,
123
127
  );
124
128
  }
129
+
130
+ // --- Admin writes: Lists CRUD ---
131
+
132
+ async createList(
133
+ siteName: string,
134
+ data: V2NewsletterListCreateParams,
135
+ ): Promise<V2NewsletterList> {
136
+ return this.post<V2NewsletterList>(
137
+ this.sitePath(siteName, 'newsletter', 'lists'),
138
+ data,
139
+ );
140
+ }
141
+
142
+ async updateList(
143
+ siteName: string,
144
+ id: string,
145
+ data: V2NewsletterListUpdateParams,
146
+ ): Promise<V2NewsletterList> {
147
+ return this.patchOne<V2NewsletterList>(
148
+ this.sitePath(siteName, 'newsletter', `lists/${id}`),
149
+ data,
150
+ );
151
+ }
152
+
153
+ async deleteList(siteName: string, id: string): Promise<V2Deleted> {
154
+ return this.deleteOne(
155
+ this.sitePath(siteName, 'newsletter', `lists/${id}`),
156
+ );
157
+ }
158
+
159
+ // --- Admin writes: Subscription sync / membership / import ---
160
+
161
+ /**
162
+ * Upsert a subscription by email and (optionally) replace its list
163
+ * memberships. Returns a `newsletter_sync_result` envelope with the
164
+ * outcome (created / updated / resubscribed / already-unsubscribed).
165
+ */
166
+ async syncSubscription(
167
+ siteName: string,
168
+ data: V2NewsletterSyncInput,
169
+ ): Promise<V2NewsletterSyncResult> {
170
+ return this.post<V2NewsletterSyncResult>(
171
+ this.sitePath(siteName, 'newsletter', 'subscriptions/sync'),
172
+ data,
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Add/remove/replace the list memberships for an existing subscription.
178
+ * Returns the refreshed subscription record.
179
+ */
180
+ async updateSubscriptionListMembership(
181
+ siteName: string,
182
+ subscriptionId: string,
183
+ data: V2NewsletterSubscriptionListMembershipUpdate,
184
+ ): Promise<V2NewsletterSubscription> {
185
+ return this.post<V2NewsletterSubscription>(
186
+ this.sitePath(
187
+ siteName,
188
+ 'newsletter',
189
+ `subscriptions/${subscriptionId}/list-membership`,
190
+ ),
191
+ data,
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Bulk import subscriptions. Each row is upserted via the same sync
197
+ * path; `refreshListCounts` is deferred until after all rows are
198
+ * processed on the server.
199
+ */
200
+ async importSubscriptions(
201
+ siteName: string,
202
+ data: V2NewsletterImportRequest,
203
+ ): Promise<V2NewsletterImportResult> {
204
+ return this.post<V2NewsletterImportResult>(
205
+ this.sitePath(siteName, 'newsletter', 'subscriptions/import'),
206
+ data,
207
+ );
208
+ }
125
209
  }
@@ -1,10 +1,19 @@
1
1
  /**
2
2
  * v2 Orders Client (checkout sessions)
3
+ *
4
+ * `create()` initiates a Stripe checkout session and returns the checkout URL.
5
+ * No CSRF token is needed — v2 uses API-key auth only.
3
6
  */
4
7
 
5
8
  import { BaseV2Client } from './base-v2-client';
6
9
  import type { CachePolicy } from '../../cache/types';
7
- import type { V2Order, V2OrderListParams, V2List } from '../types';
10
+ import type {
11
+ V2Order,
12
+ V2OrderListParams,
13
+ V2OrderCreateParams,
14
+ V2OrderCreateResult,
15
+ V2List,
16
+ } from '../types';
8
17
 
9
18
  export class OrdersV2Client extends BaseV2Client {
10
19
 
@@ -19,4 +28,21 @@ export class OrdersV2Client extends BaseV2Client {
19
28
  async get(siteName: string, id: string, cachePolicy?: CachePolicy): Promise<V2Order> {
20
29
  return this.getOne<V2Order>(this.sitePath(siteName, 'orders', id), undefined, cachePolicy);
21
30
  }
31
+
32
+ /**
33
+ * Create a checkout session via Stripe. Returns the session ID and a
34
+ * `checkout_url` that the client should redirect to (or open in a new tab).
35
+ *
36
+ * This replaces the v1 `checkout.createCheckoutSession()` + `getCsrfToken()`
37
+ * dance — v2 is API-key-only and requires no CSRF token.
38
+ */
39
+ async create(
40
+ siteName: string,
41
+ data: V2OrderCreateParams,
42
+ ): Promise<V2OrderCreateResult> {
43
+ return this.post<V2OrderCreateResult>(
44
+ this.sitePath(siteName, 'orders'),
45
+ data,
46
+ );
47
+ }
22
48
  }
@@ -1,9 +1,24 @@
1
1
  /**
2
2
  * v2 Site Users Client
3
+ *
4
+ * Two classes of endpoints:
5
+ * - Admin paths (list/get/update/OTP flows) require an API key. Use the
6
+ * usual `setApiKey(...)` on the main `PerspectApiV2Client` before calling.
7
+ * - `/me*` paths require a site-user JWT (minted by `verifyOtp`). Call
8
+ * `setAuth(jwt)` on the main client before calling these.
3
9
  */
4
10
 
5
11
  import { BaseV2Client } from './base-v2-client';
6
- import type { V2SiteUser, V2SiteUserUpdateParams, V2SiteUserListParams, V2List } from '../types';
12
+ import type {
13
+ V2SiteUser,
14
+ V2SiteUserUpdateParams,
15
+ V2SiteUserMeUpdateParams,
16
+ V2SiteUserListParams,
17
+ V2SiteUserWithProfile,
18
+ V2SiteUserProfile,
19
+ V2List,
20
+ V2Deleted,
21
+ } from '../types';
7
22
 
8
23
  export interface V2OtpRequestResponse {
9
24
  object: 'otp_request';
@@ -17,7 +32,7 @@ export interface V2OtpVerifyResponse extends V2SiteUser {
17
32
 
18
33
  export class SiteUsersV2Client extends BaseV2Client {
19
34
 
20
- // --- OTP Auth ---
35
+ // --- OTP Auth (public, API-key-scoped) ---
21
36
 
22
37
  async requestOtp(
23
38
  siteName: string,
@@ -33,7 +48,7 @@ export class SiteUsersV2Client extends BaseV2Client {
33
48
  return this.post<V2OtpVerifyResponse>(this.sitePath(siteName, 'users', 'verify-otp'), data);
34
49
  }
35
50
 
36
- // --- Admin ---
51
+ // --- Admin (API key) ---
37
52
 
38
53
  async list(siteName: string, params?: V2SiteUserListParams): Promise<V2List<V2SiteUser>> {
39
54
  return this.getList<V2SiteUser>(this.sitePath(siteName, 'users'), params);
@@ -50,4 +65,62 @@ export class SiteUsersV2Client extends BaseV2Client {
50
65
  async update(siteName: string, id: string, data: V2SiteUserUpdateParams): Promise<V2SiteUser> {
51
66
  return this.patchOne<V2SiteUser>(this.sitePath(siteName, 'users', id), data);
52
67
  }
68
+
69
+ // --- Authenticated "me" endpoints (site-user JWT) ---
70
+
71
+ /**
72
+ * Load the currently-authenticated site user's canonical record plus their
73
+ * profile KV map. Requires `client.setAuth(jwt)` to have been called with
74
+ * the token returned from `verifyOtp`.
75
+ */
76
+ async getMe(siteName: string): Promise<V2SiteUserWithProfile> {
77
+ return this.getOne<V2SiteUserWithProfile>(
78
+ this.sitePath(siteName, 'users', 'me'),
79
+ );
80
+ }
81
+
82
+ /** Update the authenticated user's own fields. */
83
+ async updateMe(
84
+ siteName: string,
85
+ data: V2SiteUserMeUpdateParams,
86
+ ): Promise<V2SiteUser> {
87
+ return this.patchOne<V2SiteUser>(
88
+ this.sitePath(siteName, 'users', 'me'),
89
+ data,
90
+ );
91
+ }
92
+
93
+ /** Fetch the profile KV map as a dedicated `site_user_profile` envelope. */
94
+ async getProfile(siteName: string): Promise<V2SiteUserProfile> {
95
+ return this.getOne<V2SiteUserProfile>(
96
+ this.sitePath(siteName, 'users', 'me/profile'),
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Set a single profile key. The value is persisted verbatim — callers wanting
102
+ * structured data should JSON-stringify their value first.
103
+ */
104
+ async setProfileValue(
105
+ siteName: string,
106
+ key: string,
107
+ value: string,
108
+ ): Promise<V2SiteUserProfile> {
109
+ const encoded = encodeURIComponent(key);
110
+ return this.putOne<V2SiteUserProfile>(
111
+ this.sitePath(siteName, 'users', `me/profile/${encoded}`),
112
+ { value },
113
+ );
114
+ }
115
+
116
+ /** Delete a single profile key. */
117
+ async deleteProfileValue(
118
+ siteName: string,
119
+ key: string,
120
+ ): Promise<V2Deleted> {
121
+ const encoded = encodeURIComponent(key);
122
+ return this.deleteOne(
123
+ this.sitePath(siteName, 'users', `me/profile/${encoded}`),
124
+ );
125
+ }
53
126
  }
package/src/v2/types.ts CHANGED
@@ -250,6 +250,90 @@ export interface V2OrderListParams extends V2PaginationParams {
250
250
  date_to?: string;
251
251
  }
252
252
 
253
+ // --- Order create (checkout session creation) ---
254
+
255
+ export interface V2OrderLineItemPriceData {
256
+ currency: string;
257
+ product_data: {
258
+ name: string;
259
+ description?: string;
260
+ images?: string[];
261
+ };
262
+ unit_amount: number;
263
+ }
264
+
265
+ export interface V2OrderLineItem {
266
+ sku_id?: number;
267
+ product_id?: number;
268
+ price_data?: V2OrderLineItemPriceData;
269
+ price?: string;
270
+ quantity: number;
271
+ }
272
+
273
+ export interface V2OrderAddress {
274
+ line1?: string;
275
+ line2?: string;
276
+ city?: string;
277
+ state?: string;
278
+ postal_code?: string;
279
+ country?: string;
280
+ }
281
+
282
+ export interface V2OrderTaxRequest {
283
+ strategy?: string;
284
+ customer_identifier?: string;
285
+ customer_profile_id?: string;
286
+ customer_display_name?: string;
287
+ allow_exemption?: boolean;
288
+ save_profile?: boolean;
289
+ customer_exemption?: {
290
+ status?: "none" | "exempt" | "reverse_charge";
291
+ reason?: string;
292
+ tax_id?: string;
293
+ tax_id_type?: string;
294
+ certificate_url?: string;
295
+ metadata?: Record<string, unknown>;
296
+ expires_at?: string;
297
+ };
298
+ }
299
+
300
+ export interface V2OrderCreateParams {
301
+ line_items: V2OrderLineItem[];
302
+ success_url: string;
303
+ cancel_url: string;
304
+ customer_email?: string;
305
+ site_user_id?: string;
306
+ mode?: "payment" | "subscription";
307
+ metadata?: Record<string, string | number | boolean>;
308
+ tax?: V2OrderTaxRequest;
309
+ shipping_amount?: number;
310
+ shipping_address?: V2OrderAddress;
311
+ billing_address?: V2OrderAddress;
312
+ currency?: string;
313
+ referral_code?: string;
314
+ referrer_site_user_id?: string;
315
+ }
316
+
317
+ export interface V2OrderCreateResult extends V2Object {
318
+ object: "checkout_session";
319
+ checkout_url: string | null;
320
+ payment_status: string | null;
321
+ tax: {
322
+ amount: number;
323
+ currency: string;
324
+ strategy: string;
325
+ exemption_applied: boolean;
326
+ exemption_status: string;
327
+ breakdown: Array<{
328
+ jurisdiction?: string;
329
+ rate_percent: number;
330
+ tax_amount: number;
331
+ taxable_amount: number;
332
+ source: string;
333
+ }>;
334
+ };
335
+ }
336
+
253
337
  // --- Site Users ---
254
338
 
255
339
  export interface V2SiteUser extends V2Object {
@@ -275,11 +359,41 @@ export interface V2SiteUserUpdateParams {
275
359
  metadata?: Record<string, unknown>;
276
360
  }
277
361
 
362
+ /**
363
+ * Patch shape for the authenticated `/me` endpoint. Unlike the admin update,
364
+ * end users cannot change their own `status`.
365
+ */
366
+ export interface V2SiteUserMeUpdateParams {
367
+ first_name?: string;
368
+ last_name?: string;
369
+ avatar_url?: string;
370
+ metadata?: Record<string, unknown>;
371
+ }
372
+
278
373
  export interface V2SiteUserListParams extends V2PaginationParams {
279
374
  status?: "active" | "suspended" | "pending_verification";
280
375
  email?: string;
281
376
  }
282
377
 
378
+ /**
379
+ * Response shape of `GET /sites/{siteName}/users/me`. Extends V2SiteUser with
380
+ * a `profile` side-channel populated from the `site_user_profiles` KV table.
381
+ */
382
+ export interface V2SiteUserWithProfile extends V2SiteUser {
383
+ profile: Record<string, unknown>;
384
+ }
385
+
386
+ /**
387
+ * Standalone profile envelope returned by
388
+ * `GET|PUT /sites/{siteName}/users/me/profile[/:key]`. Each `data` entry is a
389
+ * parsed value (arbitrary JSON).
390
+ */
391
+ export interface V2SiteUserProfile {
392
+ object: "site_user_profile";
393
+ site_user_id: string;
394
+ data: Record<string, unknown>;
395
+ }
396
+
283
397
  // --- Newsletter ---
284
398
 
285
399
  export interface V2NewsletterSubscription extends V2Object {
@@ -332,6 +446,84 @@ export interface V2NewsletterTrackingResponse {
332
446
  success: boolean;
333
447
  }
334
448
 
449
+ // --- Newsletter admin writes ---
450
+
451
+ export interface V2NewsletterListCreateParams {
452
+ list_name: string;
453
+ slug: string;
454
+ description?: string | null;
455
+ is_public?: boolean;
456
+ is_default?: boolean;
457
+ welcome_email_enabled?: boolean;
458
+ }
459
+
460
+ export interface V2NewsletterListUpdateParams {
461
+ list_name?: string;
462
+ slug?: string;
463
+ description?: string | null;
464
+ is_public?: boolean;
465
+ is_default?: boolean;
466
+ welcome_email_enabled?: boolean;
467
+ status?: "active" | "archived";
468
+ }
469
+
470
+ export interface V2NewsletterSyncInput {
471
+ email: string;
472
+ name?: string | null;
473
+ status?: "pending" | "confirmed" | "unsubscribed" | "bounced" | "complained";
474
+ list_ids?: string[];
475
+ frequency?: "instant" | "daily" | "weekly" | "monthly";
476
+ topics?: string[];
477
+ language?: string | null;
478
+ source?: string | null;
479
+ source_url?: string | null;
480
+ notes?: string | null;
481
+ tags?: string[];
482
+ metadata?: Record<string, unknown>;
483
+ resubscribe_override?: boolean;
484
+ }
485
+
486
+ export interface V2NewsletterSyncResult {
487
+ object: "newsletter_sync_result";
488
+ applied: boolean;
489
+ code: "CREATED" | "UPDATED" | "RESUBSCRIBED" | "ALREADY_UNSUBSCRIBED";
490
+ skipped_unsubscribed: boolean;
491
+ resubscribed: boolean;
492
+ created: boolean;
493
+ updated: boolean;
494
+ subscription: Record<string, unknown>;
495
+ }
496
+
497
+ export interface V2NewsletterSubscriptionListMembershipUpdate {
498
+ mode: "add" | "remove" | "replace";
499
+ list_ids: string[];
500
+ }
501
+
502
+ export interface V2NewsletterImportRequest {
503
+ rows: V2NewsletterSyncInput[];
504
+ resubscribe_override?: boolean;
505
+ }
506
+
507
+ export interface V2NewsletterImportResult {
508
+ object: "newsletter_import_result";
509
+ total: number;
510
+ processed: number;
511
+ applied: number;
512
+ created: number;
513
+ updated: number;
514
+ resubscribed: number;
515
+ skipped_unsubscribed: number;
516
+ rows: Array<{
517
+ index: number;
518
+ email: string;
519
+ applied: boolean;
520
+ code: V2NewsletterSyncResult["code"];
521
+ skipped_unsubscribed: boolean;
522
+ resubscribed: boolean;
523
+ subscription_id: string;
524
+ }>;
525
+ }
526
+
335
527
  // --- Contact ---
336
528
 
337
529
  export interface V2ContactSubmission extends V2Object {