perspectapi-ts-sdk 5.4.5 → 6.0.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/dist/index.mjs CHANGED
@@ -331,11 +331,7 @@ var CacheManager = class {
331
331
  async getOrSet(key, resolveValue, policy) {
332
332
  if (!this.enabled || policy?.skipCache) {
333
333
  console.log("[Cache] Cache disabled or skipped", { key, enabled: this.enabled, skipCache: policy?.skipCache });
334
- const value2 = await resolveValue();
335
- if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
336
- await this.set(key, value2, policy);
337
- }
338
- return value2;
334
+ return resolveValue();
339
335
  }
340
336
  const namespacedKey = this.namespacedKey(key);
341
337
  const cachedRaw = await this.adapter.get(namespacedKey);
@@ -358,7 +354,7 @@ var CacheManager = class {
358
354
  return value;
359
355
  }
360
356
  async set(key, value, options) {
361
- if (!this.enabled || options?.ttlSeconds === 0) {
357
+ if (!this.enabled) {
362
358
  return;
363
359
  }
364
360
  const namespacedKey = this.namespacedKey(key);
@@ -3354,6 +3350,11 @@ var BaseV2Client = class {
3354
3350
  const response = await this.http.patch(path, body);
3355
3351
  return this.extractData(response);
3356
3352
  }
3353
+ /** PUT to upsert a resource. */
3354
+ async putOne(path, body) {
3355
+ const response = await this.http.put(path, body);
3356
+ return this.extractData(response);
3357
+ }
3357
3358
  /** DELETE a resource. */
3358
3359
  async deleteOne(path) {
3359
3360
  const response = await this.http.delete(path);
@@ -3427,13 +3428,47 @@ var BaseV2Client = class {
3427
3428
  // src/v2/client/content-client.ts
3428
3429
  var ContentV2Client = class extends BaseV2Client {
3429
3430
  async list(siteName, params, cachePolicy) {
3430
- return this.getList(this.sitePath(siteName, "content"), params, cachePolicy);
3431
+ return this.getList(
3432
+ this.sitePath(siteName, "content"),
3433
+ params,
3434
+ this.withContentTags(siteName, cachePolicy, {
3435
+ category: params?.category,
3436
+ slugPrefix: params?.slug_prefix,
3437
+ type: params?.type
3438
+ })
3439
+ );
3431
3440
  }
3432
- async *listAutoPaginated(siteName, params) {
3433
- yield* this.listAutoPaginate(this.sitePath(siteName, "content"), params);
3441
+ async *listAutoPaginated(siteName, params, cachePolicy) {
3442
+ let startingAfter;
3443
+ let hasMore = true;
3444
+ while (hasMore) {
3445
+ const queryParams = { ...params ?? {} };
3446
+ if (startingAfter) {
3447
+ queryParams.starting_after = startingAfter;
3448
+ }
3449
+ const page = await this.list(siteName, queryParams, cachePolicy);
3450
+ for (const item of page.data) {
3451
+ yield item;
3452
+ }
3453
+ hasMore = page.has_more;
3454
+ if (page.data.length > 0) {
3455
+ startingAfter = page.data[page.data.length - 1].id;
3456
+ } else {
3457
+ hasMore = false;
3458
+ }
3459
+ }
3434
3460
  }
3435
3461
  async get(siteName, idOrSlug, cachePolicy) {
3436
- return this.getOne(this.sitePath(siteName, "content", idOrSlug), void 0, cachePolicy);
3462
+ const isContentId = this.isContentId(idOrSlug);
3463
+ return this.getOne(
3464
+ this.sitePath(siteName, "content", idOrSlug),
3465
+ void 0,
3466
+ this.withContentTags(siteName, cachePolicy, {
3467
+ id: isContentId ? idOrSlug : void 0,
3468
+ slug: isContentId ? void 0 : idOrSlug,
3469
+ slugPrefix: isContentId ? void 0 : this.extractSlugPrefix(idOrSlug)
3470
+ })
3471
+ );
3437
3472
  }
3438
3473
  async create(siteName, data) {
3439
3474
  return this.post(this.sitePath(siteName, "content"), data);
@@ -3450,6 +3485,56 @@ var ContentV2Client = class extends BaseV2Client {
3450
3485
  async unpublish(siteName, id) {
3451
3486
  return this.post(this.sitePath(siteName, "content", `${id}/unpublish`));
3452
3487
  }
3488
+ withContentTags(siteName, cachePolicy, options) {
3489
+ const tags = new Set(cachePolicy?.tags ?? []);
3490
+ for (const tag of this.buildContentTags(siteName, options)) {
3491
+ tags.add(tag);
3492
+ }
3493
+ return {
3494
+ ...cachePolicy,
3495
+ tags: Array.from(tags)
3496
+ };
3497
+ }
3498
+ buildContentTags(siteName, options) {
3499
+ const tags = /* @__PURE__ */ new Set(["content", `content:site:${siteName}`]);
3500
+ const normalizedPrefix = this.normalizeTagPart(options.slugPrefix);
3501
+ const normalizedCategory = this.normalizeTagPart(options.category);
3502
+ const normalizedType = this.normalizeTagPart(options.type);
3503
+ if (options.slug) {
3504
+ tags.add(`content:slug:${siteName}:${options.slug}`);
3505
+ }
3506
+ if (options.id) {
3507
+ tags.add(`content:id:${options.id}`);
3508
+ }
3509
+ if (normalizedPrefix) {
3510
+ tags.add(`content:prefix:${normalizedPrefix}`);
3511
+ }
3512
+ if (normalizedType) {
3513
+ tags.add(`content:${normalizedType}`);
3514
+ }
3515
+ if (normalizedCategory) {
3516
+ tags.add("content:category");
3517
+ tags.add(`content:category:${siteName}:${normalizedCategory}`);
3518
+ }
3519
+ return Array.from(tags);
3520
+ }
3521
+ normalizeTagPart(value) {
3522
+ if (typeof value !== "string") {
3523
+ return void 0;
3524
+ }
3525
+ const normalized = value.trim().toLowerCase();
3526
+ return normalized === "" ? void 0 : normalized;
3527
+ }
3528
+ extractSlugPrefix(slug) {
3529
+ const slashIndex = slug.indexOf("/");
3530
+ if (slashIndex > 0) {
3531
+ return slug.slice(0, slashIndex);
3532
+ }
3533
+ return void 0;
3534
+ }
3535
+ isContentId(idOrSlug) {
3536
+ return /^cnt_/i.test(idOrSlug);
3537
+ }
3453
3538
  };
3454
3539
 
3455
3540
  // src/v2/client/products-client.ts
@@ -3546,18 +3631,38 @@ var OrdersV2Client = class extends BaseV2Client {
3546
3631
  async get(siteName, id, cachePolicy) {
3547
3632
  return this.getOne(this.sitePath(siteName, "orders", id), void 0, cachePolicy);
3548
3633
  }
3634
+ /**
3635
+ * Create a checkout session via Stripe. Returns the session ID and a
3636
+ * `checkout_url` that the client should redirect to (or open in a new tab).
3637
+ *
3638
+ * This replaces the v1 `checkout.createCheckoutSession()` + `getCsrfToken()`
3639
+ * dance — v2 is API-key-only and requires no CSRF token.
3640
+ */
3641
+ async create(siteName, data) {
3642
+ return this.post(
3643
+ this.sitePath(siteName, "orders"),
3644
+ data
3645
+ );
3646
+ }
3647
+ /** Update fulfillment status, tracking number, and/or notes on an order. */
3648
+ async updateFulfillment(siteName, id, data) {
3649
+ return this.patchOne(
3650
+ this.sitePath(siteName, "orders", `${id}/fulfillment`),
3651
+ data
3652
+ );
3653
+ }
3549
3654
  };
3550
3655
 
3551
3656
  // src/v2/client/site-users-client.ts
3552
3657
  var SiteUsersV2Client = class extends BaseV2Client {
3553
- // --- OTP Auth ---
3658
+ // --- OTP Auth (public, API-key-scoped) ---
3554
3659
  async requestOtp(siteName, data) {
3555
3660
  return this.post(this.sitePath(siteName, "users", "request-otp"), data);
3556
3661
  }
3557
3662
  async verifyOtp(siteName, data) {
3558
3663
  return this.post(this.sitePath(siteName, "users", "verify-otp"), data);
3559
3664
  }
3560
- // --- Admin ---
3665
+ // --- Admin (API key) ---
3561
3666
  async list(siteName, params) {
3562
3667
  return this.getList(this.sitePath(siteName, "users"), params);
3563
3668
  }
@@ -3570,6 +3675,48 @@ var SiteUsersV2Client = class extends BaseV2Client {
3570
3675
  async update(siteName, id, data) {
3571
3676
  return this.patchOne(this.sitePath(siteName, "users", id), data);
3572
3677
  }
3678
+ // --- Authenticated "me" endpoints (site-user JWT) ---
3679
+ /**
3680
+ * Load the currently-authenticated site user's canonical record plus their
3681
+ * profile KV map. Requires `client.setAuth(jwt)` to have been called with
3682
+ * the token returned from `verifyOtp`.
3683
+ */
3684
+ async getMe(siteName) {
3685
+ return this.getOne(
3686
+ this.sitePath(siteName, "users", "me")
3687
+ );
3688
+ }
3689
+ /** Update the authenticated user's own fields. */
3690
+ async updateMe(siteName, data) {
3691
+ return this.patchOne(
3692
+ this.sitePath(siteName, "users", "me"),
3693
+ data
3694
+ );
3695
+ }
3696
+ /** Fetch the profile KV map as a dedicated `site_user_profile` envelope. */
3697
+ async getProfile(siteName) {
3698
+ return this.getOne(
3699
+ this.sitePath(siteName, "users", "me/profile")
3700
+ );
3701
+ }
3702
+ /**
3703
+ * Set a single profile key. The value is persisted verbatim — callers wanting
3704
+ * structured data should JSON-stringify their value first.
3705
+ */
3706
+ async setProfileValue(siteName, key, value) {
3707
+ const encoded = encodeURIComponent(key);
3708
+ return this.putOne(
3709
+ this.sitePath(siteName, "users", `me/profile/${encoded}`),
3710
+ { value }
3711
+ );
3712
+ }
3713
+ /** Delete a single profile key. */
3714
+ async deleteProfileValue(siteName, key) {
3715
+ const encoded = encodeURIComponent(key);
3716
+ return this.deleteOne(
3717
+ this.sitePath(siteName, "users", `me/profile/${encoded}`)
3718
+ );
3719
+ }
3573
3720
  };
3574
3721
 
3575
3722
  // src/v2/client/newsletter-client.ts
@@ -3641,6 +3788,61 @@ var NewsletterV2Client = class extends BaseV2Client {
3641
3788
  cachePolicy
3642
3789
  );
3643
3790
  }
3791
+ // --- Admin writes: Lists CRUD ---
3792
+ async createList(siteName, data) {
3793
+ return this.post(
3794
+ this.sitePath(siteName, "newsletter", "lists"),
3795
+ data
3796
+ );
3797
+ }
3798
+ async updateList(siteName, id, data) {
3799
+ return this.patchOne(
3800
+ this.sitePath(siteName, "newsletter", `lists/${id}`),
3801
+ data
3802
+ );
3803
+ }
3804
+ async deleteList(siteName, id) {
3805
+ return this.deleteOne(
3806
+ this.sitePath(siteName, "newsletter", `lists/${id}`)
3807
+ );
3808
+ }
3809
+ // --- Admin writes: Subscription sync / membership / import ---
3810
+ /**
3811
+ * Upsert a subscription by email and (optionally) replace its list
3812
+ * memberships. Returns a `newsletter_sync_result` envelope with the
3813
+ * outcome (created / updated / resubscribed / already-unsubscribed).
3814
+ */
3815
+ async syncSubscription(siteName, data) {
3816
+ return this.post(
3817
+ this.sitePath(siteName, "newsletter", "subscriptions/sync"),
3818
+ data
3819
+ );
3820
+ }
3821
+ /**
3822
+ * Add/remove/replace the list memberships for an existing subscription.
3823
+ * Returns the refreshed subscription record.
3824
+ */
3825
+ async updateSubscriptionListMembership(siteName, subscriptionId, data) {
3826
+ return this.post(
3827
+ this.sitePath(
3828
+ siteName,
3829
+ "newsletter",
3830
+ `subscriptions/${subscriptionId}/list-membership`
3831
+ ),
3832
+ data
3833
+ );
3834
+ }
3835
+ /**
3836
+ * Bulk import subscriptions. Each row is upserted via the same sync
3837
+ * path; `refreshListCounts` is deferred until after all rows are
3838
+ * processed on the server.
3839
+ */
3840
+ async importSubscriptions(siteName, data) {
3841
+ return this.post(
3842
+ this.sitePath(siteName, "newsletter", "subscriptions/import"),
3843
+ data
3844
+ );
3845
+ }
3644
3846
  };
3645
3847
 
3646
3848
  // src/v2/client/contacts-client.ts
@@ -3708,6 +3910,102 @@ var WebhooksV2Client = class extends BaseV2Client {
3708
3910
  }
3709
3911
  };
3710
3912
 
3913
+ // src/v2/client/subscriptions-client.ts
3914
+ var SubscriptionsV2Client = class extends BaseV2Client {
3915
+ // --- Authenticated "me" endpoints (site-user JWT) ---
3916
+ /** List all subscriptions for the authenticated user. */
3917
+ async listMySubscriptions(siteName) {
3918
+ return this.getList(
3919
+ this.sitePath(siteName, "users", "me/subscriptions")
3920
+ );
3921
+ }
3922
+ /** Pause a subscription. */
3923
+ async pauseSubscription(siteName, subId, params) {
3924
+ return this.post(
3925
+ this.sitePath(siteName, "users", `me/subscriptions/${subId}/pause`),
3926
+ params ?? {}
3927
+ );
3928
+ }
3929
+ /** Resume a paused subscription. */
3930
+ async resumeSubscription(siteName, subId) {
3931
+ return this.post(
3932
+ this.sitePath(siteName, "users", `me/subscriptions/${subId}/resume`)
3933
+ );
3934
+ }
3935
+ /** Cancel a subscription. */
3936
+ async cancelSubscription(siteName, subId, params) {
3937
+ return this.post(
3938
+ this.sitePath(siteName, "users", `me/subscriptions/${subId}/cancel`),
3939
+ params ?? {}
3940
+ );
3941
+ }
3942
+ /** Change the plan (price) of a subscription. */
3943
+ async changeSubscriptionPlan(siteName, subId, params) {
3944
+ return this.post(
3945
+ this.sitePath(siteName, "users", `me/subscriptions/${subId}/change-plan`),
3946
+ params
3947
+ );
3948
+ }
3949
+ // --- Admin endpoints (API key) ---
3950
+ /** List subscriptions for a specific user (admin). */
3951
+ async listUserSubscriptions(siteName, userId) {
3952
+ return this.getList(
3953
+ this.sitePath(siteName, "users", `${userId}/subscriptions`)
3954
+ );
3955
+ }
3956
+ /** Pause a user's subscription (admin). */
3957
+ async pauseUserSubscription(siteName, userId, subId, params) {
3958
+ return this.post(
3959
+ this.sitePath(siteName, "users", `${userId}/subscriptions/${subId}/pause`),
3960
+ params ?? {}
3961
+ );
3962
+ }
3963
+ /** Resume a user's paused subscription (admin). */
3964
+ async resumeUserSubscription(siteName, userId, subId) {
3965
+ return this.post(
3966
+ this.sitePath(siteName, "users", `${userId}/subscriptions/${subId}/resume`)
3967
+ );
3968
+ }
3969
+ /** Cancel a user's subscription (admin). */
3970
+ async cancelUserSubscription(siteName, userId, subId, params) {
3971
+ return this.post(
3972
+ this.sitePath(siteName, "users", `${userId}/subscriptions/${subId}/cancel`),
3973
+ params ?? {}
3974
+ );
3975
+ }
3976
+ };
3977
+
3978
+ // src/v2/client/credits-client.ts
3979
+ var CreditsV2Client = class extends BaseV2Client {
3980
+ // --- Authenticated "me" endpoints (site-user JWT) ---
3981
+ /** Get the current credit balance for the authenticated user. */
3982
+ async getMyBalance(siteName) {
3983
+ return this.getOne(
3984
+ this.sitePath(siteName, "users", "me/credits/balance")
3985
+ );
3986
+ }
3987
+ /** Get credit balance and transaction history for the authenticated user. */
3988
+ async getMyCredits(siteName) {
3989
+ return this.getOne(
3990
+ this.sitePath(siteName, "users", "me/credits")
3991
+ );
3992
+ }
3993
+ // --- Admin endpoints (API key) ---
3994
+ /** Get the credit balance for a specific user (admin). */
3995
+ async getUserBalance(siteName, userId) {
3996
+ return this.getOne(
3997
+ this.sitePath(siteName, "users", `${userId}/credits/balance`)
3998
+ );
3999
+ }
4000
+ /** Grant credit to a specific user (admin). */
4001
+ async grantCredit(siteName, userId, data) {
4002
+ return this.post(
4003
+ this.sitePath(siteName, "users", `${userId}/credits/grant`),
4004
+ data
4005
+ );
4006
+ }
4007
+ };
4008
+
3711
4009
  // src/v2/index.ts
3712
4010
  var PerspectApiV2Client = class {
3713
4011
  http;
@@ -3724,6 +4022,8 @@ var PerspectApiV2Client = class {
3724
4022
  sites;
3725
4023
  apiKeys;
3726
4024
  webhooks;
4025
+ subscriptions;
4026
+ credits;
3727
4027
  constructor(config) {
3728
4028
  const baseUrl = config.baseUrl.replace(/\/+$/, "");
3729
4029
  const v2BaseUrl = baseUrl.endsWith("/api/v2") ? baseUrl : `${baseUrl}/api/v2`;
@@ -3743,6 +4043,8 @@ var PerspectApiV2Client = class {
3743
4043
  this.sites = new SitesV2Client(this.http, basePath, cache);
3744
4044
  this.apiKeys = new ApiKeysV2Client(this.http, basePath, cache);
3745
4045
  this.webhooks = new WebhooksV2Client(this.http, basePath, cache);
4046
+ this.subscriptions = new SubscriptionsV2Client(this.http, basePath, cache);
4047
+ this.credits = new CreditsV2Client(this.http, basePath, cache);
3746
4048
  }
3747
4049
  /** Update the JWT token for authenticated requests. */
3748
4050
  setAuth(jwt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perspectapi-ts-sdk",
3
- "version": "5.4.5",
3
+ "version": "6.0.1",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -76,13 +76,7 @@ export class CacheManager {
76
76
  ): Promise<T> {
77
77
  if (!this.enabled || policy?.skipCache) {
78
78
  console.log('[Cache] Cache disabled or skipped', { key, enabled: this.enabled, skipCache: policy?.skipCache });
79
- const value = await resolveValue();
80
-
81
- if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
82
- await this.set(key, value, policy);
83
- }
84
-
85
- return value;
79
+ return resolveValue();
86
80
  }
87
81
 
88
82
  const namespacedKey = this.namespacedKey(key);
@@ -113,7 +107,7 @@ export class CacheManager {
113
107
  }
114
108
 
115
109
  async set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void> {
116
- if (!this.enabled || options?.ttlSeconds === 0) {
110
+ if (!this.enabled) {
117
111
  return;
118
112
  }
119
113
 
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export { default } from './perspect-api-client';
10
10
  // v2 client
11
11
  export { PerspectApiV2Client, createPerspectApiV2Client } from './v2';
12
12
  export { PerspectV2Error } from './v2/client/base-v2-client';
13
+ export type * from './v2/types';
13
14
 
14
15
  // Individual clients (for advanced usage)
15
16
  export { AuthClient } from './client/auth-client';
@@ -129,6 +129,12 @@ export abstract class BaseV2Client {
129
129
  return this.extractData<T>(response);
130
130
  }
131
131
 
132
+ /** PUT to upsert a resource. */
133
+ protected async putOne<T>(path: string, body?: unknown): Promise<T> {
134
+ const response = await this.http.put<T>(path, body);
135
+ return this.extractData<T>(response);
136
+ }
137
+
132
138
  /** DELETE a resource. */
133
139
  protected async deleteOne(path: string): Promise<V2Deleted> {
134
140
  const response = await this.http.delete<V2Deleted>(path);
@@ -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
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * v2 Credits Client — balance queries and admin grant operations.
3
+ *
4
+ * Two classes of endpoints:
5
+ * - /me paths require a site-user JWT (call `setAuth(jwt)` first)
6
+ * - Admin paths require an API key (call `setApiKey(key)` first)
7
+ */
8
+
9
+ import { BaseV2Client } from './base-v2-client';
10
+ import type {
11
+ V2CreditBalance,
12
+ V2GrantCreditParams,
13
+ V2GrantCreditResult,
14
+ } from '../types';
15
+
16
+ export class CreditsV2Client extends BaseV2Client {
17
+
18
+ // --- Authenticated "me" endpoints (site-user JWT) ---
19
+
20
+ /** Get the current credit balance for the authenticated user. */
21
+ async getMyBalance(siteName: string): Promise<V2CreditBalance> {
22
+ return this.getOne<V2CreditBalance>(
23
+ this.sitePath(siteName, 'users', 'me/credits/balance'),
24
+ );
25
+ }
26
+
27
+ /** Get credit balance and transaction history for the authenticated user. */
28
+ async getMyCredits(siteName: string): Promise<V2CreditBalance> {
29
+ return this.getOne<V2CreditBalance>(
30
+ this.sitePath(siteName, 'users', 'me/credits'),
31
+ );
32
+ }
33
+
34
+ // --- Admin endpoints (API key) ---
35
+
36
+ /** Get the credit balance for a specific user (admin). */
37
+ async getUserBalance(
38
+ siteName: string,
39
+ userId: string,
40
+ ): Promise<V2CreditBalance> {
41
+ return this.getOne<V2CreditBalance>(
42
+ this.sitePath(siteName, 'users', `${userId}/credits/balance`),
43
+ );
44
+ }
45
+
46
+ /** Grant credit to a specific user (admin). */
47
+ async grantCredit(
48
+ siteName: string,
49
+ userId: string,
50
+ data: V2GrantCreditParams,
51
+ ): Promise<V2GrantCreditResult> {
52
+ return this.post<V2GrantCreditResult>(
53
+ this.sitePath(siteName, 'users', `${userId}/credits/grant`),
54
+ data,
55
+ );
56
+ }
57
+ }