perspectapi-ts-sdk 2.1.0 → 2.2.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.d.mts CHANGED
@@ -424,6 +424,34 @@ type CheckoutMetadataValue = string | number | boolean | null | CheckoutMetadata
424
424
  [key: string]: CheckoutMetadataValue;
425
425
  };
426
426
  type CheckoutMetadata = Record<string, CheckoutMetadataValue>;
427
+ type CheckoutTaxStrategy = 'disabled' | 'gateway_auto' | 'manual_rates' | 'external_service';
428
+ type CheckoutTaxExemptionStatus = 'none' | 'exempt' | 'reverse_charge';
429
+ interface CheckoutTaxCustomerExemptionRequest {
430
+ status?: CheckoutTaxExemptionStatus;
431
+ reason?: string;
432
+ tax_id?: string;
433
+ tax_id_type?: string;
434
+ certificate_url?: string;
435
+ metadata?: Record<string, any>;
436
+ expires_at?: string;
437
+ }
438
+ interface CheckoutTaxRequest {
439
+ strategy?: CheckoutTaxStrategy;
440
+ customer_identifier?: string;
441
+ customer_profile_id?: string;
442
+ customer_display_name?: string;
443
+ allow_exemption?: boolean;
444
+ require_tax_id?: boolean;
445
+ save_profile?: boolean;
446
+ customer_exemption?: CheckoutTaxCustomerExemptionRequest;
447
+ manual_rate_percent?: number;
448
+ manual_rate_map?: Record<string, number>;
449
+ external_service?: {
450
+ provider: string;
451
+ config?: Record<string, any>;
452
+ };
453
+ metadata?: Record<string, any>;
454
+ }
427
455
  interface CreateCheckoutSessionRequest {
428
456
  priceId?: string;
429
457
  quantity?: number;
@@ -455,6 +483,7 @@ interface CreateCheckoutSessionRequest {
455
483
  allowed_countries: string[];
456
484
  };
457
485
  billing_address_collection?: 'auto' | 'required';
486
+ tax?: CheckoutTaxRequest;
458
487
  }
459
488
  interface CheckoutSession {
460
489
  id: string;
@@ -2322,6 +2351,10 @@ interface CheckoutSessionOptions {
2322
2351
  * Optional resolver to convert a product record into a Stripe price ID.
2323
2352
  */
2324
2353
  priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
2354
+ /**
2355
+ * Optional tax configuration that will be forwarded to the checkout API.
2356
+ */
2357
+ tax?: CheckoutTaxRequest;
2325
2358
  }
2326
2359
  /**
2327
2360
  * Convenience helper that creates a checkout session by looking up Stripe price IDs
@@ -2331,4 +2364,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2331
2364
  error: string;
2332
2365
  }>;
2333
2366
 
2334
- export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
2367
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
package/dist/index.d.ts CHANGED
@@ -424,6 +424,34 @@ type CheckoutMetadataValue = string | number | boolean | null | CheckoutMetadata
424
424
  [key: string]: CheckoutMetadataValue;
425
425
  };
426
426
  type CheckoutMetadata = Record<string, CheckoutMetadataValue>;
427
+ type CheckoutTaxStrategy = 'disabled' | 'gateway_auto' | 'manual_rates' | 'external_service';
428
+ type CheckoutTaxExemptionStatus = 'none' | 'exempt' | 'reverse_charge';
429
+ interface CheckoutTaxCustomerExemptionRequest {
430
+ status?: CheckoutTaxExemptionStatus;
431
+ reason?: string;
432
+ tax_id?: string;
433
+ tax_id_type?: string;
434
+ certificate_url?: string;
435
+ metadata?: Record<string, any>;
436
+ expires_at?: string;
437
+ }
438
+ interface CheckoutTaxRequest {
439
+ strategy?: CheckoutTaxStrategy;
440
+ customer_identifier?: string;
441
+ customer_profile_id?: string;
442
+ customer_display_name?: string;
443
+ allow_exemption?: boolean;
444
+ require_tax_id?: boolean;
445
+ save_profile?: boolean;
446
+ customer_exemption?: CheckoutTaxCustomerExemptionRequest;
447
+ manual_rate_percent?: number;
448
+ manual_rate_map?: Record<string, number>;
449
+ external_service?: {
450
+ provider: string;
451
+ config?: Record<string, any>;
452
+ };
453
+ metadata?: Record<string, any>;
454
+ }
427
455
  interface CreateCheckoutSessionRequest {
428
456
  priceId?: string;
429
457
  quantity?: number;
@@ -455,6 +483,7 @@ interface CreateCheckoutSessionRequest {
455
483
  allowed_countries: string[];
456
484
  };
457
485
  billing_address_collection?: 'auto' | 'required';
486
+ tax?: CheckoutTaxRequest;
458
487
  }
459
488
  interface CheckoutSession {
460
489
  id: string;
@@ -2322,6 +2351,10 @@ interface CheckoutSessionOptions {
2322
2351
  * Optional resolver to convert a product record into a Stripe price ID.
2323
2352
  */
2324
2353
  priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
2354
+ /**
2355
+ * Optional tax configuration that will be forwarded to the checkout API.
2356
+ */
2357
+ tax?: CheckoutTaxRequest;
2325
2358
  }
2326
2359
  /**
2327
2360
  * Convenience helper that creates a checkout session by looking up Stripe price IDs
@@ -2331,4 +2364,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2331
2364
  error: string;
2332
2365
  }>;
2333
2366
 
2334
- export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
2367
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
package/dist/index.js CHANGED
@@ -729,6 +729,43 @@ var AuthClient = class extends BaseClient {
729
729
  }
730
730
  };
731
731
 
732
+ // src/utils/validators.ts
733
+ var MAX_API_QUERY_LIMIT = 100;
734
+ var ALLOWED_CONTENT_TYPES = ["post", "page"];
735
+ function validateLimit(limit, context) {
736
+ if (typeof limit !== "number" || Number.isNaN(limit) || !Number.isFinite(limit)) {
737
+ throw new Error(`[PerspectAPI] ${context} limit must be a finite number.`);
738
+ }
739
+ if (limit < 1) {
740
+ throw new Error(`[PerspectAPI] ${context} limit must be at least 1.`);
741
+ }
742
+ if (limit > MAX_API_QUERY_LIMIT) {
743
+ throw new Error(
744
+ `[PerspectAPI] ${context} limit ${limit} exceeds the maximum allowed value of ${MAX_API_QUERY_LIMIT}.`
745
+ );
746
+ }
747
+ return limit;
748
+ }
749
+ function validateOptionalLimit(limit, context) {
750
+ if (limit === void 0 || limit === null) {
751
+ return void 0;
752
+ }
753
+ return validateLimit(limit, context);
754
+ }
755
+ function validateOptionalContentType(pageType, context) {
756
+ if (pageType === void 0 || pageType === null) {
757
+ return void 0;
758
+ }
759
+ if (ALLOWED_CONTENT_TYPES.includes(pageType)) {
760
+ return pageType;
761
+ }
762
+ throw new Error(
763
+ `[PerspectAPI] ${context} received unsupported page_type "${pageType}". Allowed values are ${ALLOWED_CONTENT_TYPES.join(
764
+ ", "
765
+ )}.`
766
+ );
767
+ }
768
+
732
769
  // src/client/content-client.ts
733
770
  var ContentClient = class extends BaseClient {
734
771
  constructor(http, cache) {
@@ -740,12 +777,33 @@ var ContentClient = class extends BaseClient {
740
777
  async getContent(siteName, params, cachePolicy) {
741
778
  const endpoint = this.siteScopedEndpoint(siteName);
742
779
  const path = this.buildPath(endpoint);
780
+ const normalizedParams = params ? { ...params } : void 0;
781
+ if (normalizedParams) {
782
+ const validatedLimit = validateOptionalLimit(
783
+ normalizedParams.limit,
784
+ "content query"
785
+ );
786
+ if (validatedLimit !== void 0) {
787
+ normalizedParams.limit = validatedLimit;
788
+ } else {
789
+ delete normalizedParams.limit;
790
+ }
791
+ const validatedPageType = validateOptionalContentType(
792
+ normalizedParams.page_type,
793
+ "content query"
794
+ );
795
+ if (validatedPageType !== void 0) {
796
+ normalizedParams.page_type = validatedPageType;
797
+ } else {
798
+ delete normalizedParams.page_type;
799
+ }
800
+ }
743
801
  return this.fetchWithCache(
744
802
  endpoint,
745
- params,
803
+ normalizedParams,
746
804
  this.buildContentTags(siteName),
747
805
  cachePolicy,
748
- () => this.http.get(path, params)
806
+ () => this.http.get(path, normalizedParams)
749
807
  );
750
808
  }
751
809
  /**
@@ -1107,6 +1165,15 @@ var ProductsClient = class extends BaseClient {
1107
1165
  };
1108
1166
  const normalizedParams = params ? { ...params } : void 0;
1109
1167
  if (normalizedParams) {
1168
+ const validatedLimit = validateOptionalLimit(
1169
+ normalizedParams.limit,
1170
+ "products query"
1171
+ );
1172
+ if (validatedLimit !== void 0) {
1173
+ normalizedParams.limit = validatedLimit;
1174
+ } else {
1175
+ delete normalizedParams.limit;
1176
+ }
1110
1177
  const normalizedCategories = normalizeList(normalizedParams.category);
1111
1178
  if (normalizedCategories !== void 0) {
1112
1179
  normalizedParams.category = normalizedCategories;
@@ -2322,12 +2389,16 @@ async function loadProducts(options) {
2322
2389
  siteName,
2323
2390
  logger = noopLogger,
2324
2391
  fallbackProducts,
2325
- limit = 100,
2392
+ limit: requestedLimit,
2326
2393
  offset,
2327
2394
  search,
2328
2395
  category,
2329
2396
  categoryIds
2330
2397
  } = options;
2398
+ const resolvedLimit = validateLimit(
2399
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
2400
+ "products query"
2401
+ );
2331
2402
  if (!client) {
2332
2403
  log(logger, "warn", "[PerspectAPI] No client configured, using fallback products");
2333
2404
  return resolveFallbackProducts({ siteName, fallbackProducts });
@@ -2336,7 +2407,7 @@ async function loadProducts(options) {
2336
2407
  log(logger, "info", `[PerspectAPI] Loading products for site "${siteName}"`);
2337
2408
  const queryParams = {
2338
2409
  isActive: true,
2339
- limit,
2410
+ limit: resolvedLimit,
2340
2411
  offset,
2341
2412
  search
2342
2413
  };
@@ -2396,13 +2467,20 @@ async function loadPages(options) {
2396
2467
  }
2397
2468
  try {
2398
2469
  log(logger, "info", `[PerspectAPI] Loading pages for site "${siteName}"`);
2470
+ const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
2471
+ const resolvedLimit = validateLimit(
2472
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
2473
+ "pages query"
2474
+ );
2475
+ const requestedPageType = params?.page_type;
2476
+ const validatedPageType = validateOptionalContentType(requestedPageType, "pages query") ?? "page";
2399
2477
  const response = await client.content.getContent(
2400
2478
  siteName,
2401
2479
  {
2402
2480
  ...params,
2403
2481
  page_status: params?.page_status ?? "publish",
2404
- page_type: params?.page_type ?? "page",
2405
- limit: options.limit ?? params?.limit ?? 100
2482
+ page_type: validatedPageType,
2483
+ limit: resolvedLimit
2406
2484
  }
2407
2485
  );
2408
2486
  if (!response.data) {
@@ -2422,13 +2500,20 @@ async function loadPosts(options) {
2422
2500
  }
2423
2501
  try {
2424
2502
  log(logger, "info", `[PerspectAPI] Loading posts for site "${siteName}"`);
2503
+ const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
2504
+ const resolvedLimit = validateLimit(
2505
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
2506
+ "posts query"
2507
+ );
2508
+ const requestedPageType = params?.page_type;
2509
+ const validatedPageType = validateOptionalContentType(requestedPageType, "posts query") ?? "post";
2425
2510
  const response = await client.content.getContent(
2426
2511
  siteName,
2427
2512
  {
2428
2513
  ...params,
2429
2514
  page_status: params?.page_status ?? "publish",
2430
- page_type: params?.page_type ?? "post",
2431
- limit: options.limit ?? params?.limit ?? 100
2515
+ page_type: validatedPageType,
2516
+ limit: resolvedLimit
2432
2517
  }
2433
2518
  );
2434
2519
  if (!response.data) {
@@ -2488,7 +2573,8 @@ async function createCheckoutSession(options) {
2488
2573
  logger = noopLogger,
2489
2574
  fallbackProducts,
2490
2575
  metadata,
2491
- priceIdResolver
2576
+ priceIdResolver,
2577
+ tax
2492
2578
  } = options;
2493
2579
  if (!client) {
2494
2580
  log(logger, "error", "[PerspectAPI] Cannot create checkout session without SDK client");
@@ -2499,7 +2585,7 @@ async function createCheckoutSession(options) {
2499
2585
  client,
2500
2586
  siteName,
2501
2587
  logger,
2502
- limit: 200,
2588
+ limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
2503
2589
  fallbackProducts
2504
2590
  });
2505
2591
  const productMap = new Map(
@@ -2519,6 +2605,18 @@ async function createCheckoutSession(options) {
2519
2605
  quantity: item.quantity
2520
2606
  };
2521
2607
  });
2608
+ const resolvedTax = (() => {
2609
+ if (!tax && !customerEmail) {
2610
+ return void 0;
2611
+ }
2612
+ if (!tax) {
2613
+ return { customer_identifier: customerEmail };
2614
+ }
2615
+ return {
2616
+ ...tax,
2617
+ customer_identifier: tax.customer_identifier ?? customerEmail ?? void 0
2618
+ };
2619
+ })();
2522
2620
  const checkoutData = {
2523
2621
  line_items,
2524
2622
  successUrl,
@@ -2530,6 +2628,9 @@ async function createCheckoutSession(options) {
2530
2628
  mode: mode === "test" ? "payment" : "payment",
2531
2629
  metadata
2532
2630
  };
2631
+ if (resolvedTax) {
2632
+ checkoutData.tax = resolvedTax;
2633
+ }
2533
2634
  log(logger, "info", "[PerspectAPI] Creating checkout session");
2534
2635
  return client.checkout.createCheckoutSession(siteName, checkoutData);
2535
2636
  } catch (error) {
package/dist/index.mjs CHANGED
@@ -668,6 +668,43 @@ var AuthClient = class extends BaseClient {
668
668
  }
669
669
  };
670
670
 
671
+ // src/utils/validators.ts
672
+ var MAX_API_QUERY_LIMIT = 100;
673
+ var ALLOWED_CONTENT_TYPES = ["post", "page"];
674
+ function validateLimit(limit, context) {
675
+ if (typeof limit !== "number" || Number.isNaN(limit) || !Number.isFinite(limit)) {
676
+ throw new Error(`[PerspectAPI] ${context} limit must be a finite number.`);
677
+ }
678
+ if (limit < 1) {
679
+ throw new Error(`[PerspectAPI] ${context} limit must be at least 1.`);
680
+ }
681
+ if (limit > MAX_API_QUERY_LIMIT) {
682
+ throw new Error(
683
+ `[PerspectAPI] ${context} limit ${limit} exceeds the maximum allowed value of ${MAX_API_QUERY_LIMIT}.`
684
+ );
685
+ }
686
+ return limit;
687
+ }
688
+ function validateOptionalLimit(limit, context) {
689
+ if (limit === void 0 || limit === null) {
690
+ return void 0;
691
+ }
692
+ return validateLimit(limit, context);
693
+ }
694
+ function validateOptionalContentType(pageType, context) {
695
+ if (pageType === void 0 || pageType === null) {
696
+ return void 0;
697
+ }
698
+ if (ALLOWED_CONTENT_TYPES.includes(pageType)) {
699
+ return pageType;
700
+ }
701
+ throw new Error(
702
+ `[PerspectAPI] ${context} received unsupported page_type "${pageType}". Allowed values are ${ALLOWED_CONTENT_TYPES.join(
703
+ ", "
704
+ )}.`
705
+ );
706
+ }
707
+
671
708
  // src/client/content-client.ts
672
709
  var ContentClient = class extends BaseClient {
673
710
  constructor(http, cache) {
@@ -679,12 +716,33 @@ var ContentClient = class extends BaseClient {
679
716
  async getContent(siteName, params, cachePolicy) {
680
717
  const endpoint = this.siteScopedEndpoint(siteName);
681
718
  const path = this.buildPath(endpoint);
719
+ const normalizedParams = params ? { ...params } : void 0;
720
+ if (normalizedParams) {
721
+ const validatedLimit = validateOptionalLimit(
722
+ normalizedParams.limit,
723
+ "content query"
724
+ );
725
+ if (validatedLimit !== void 0) {
726
+ normalizedParams.limit = validatedLimit;
727
+ } else {
728
+ delete normalizedParams.limit;
729
+ }
730
+ const validatedPageType = validateOptionalContentType(
731
+ normalizedParams.page_type,
732
+ "content query"
733
+ );
734
+ if (validatedPageType !== void 0) {
735
+ normalizedParams.page_type = validatedPageType;
736
+ } else {
737
+ delete normalizedParams.page_type;
738
+ }
739
+ }
682
740
  return this.fetchWithCache(
683
741
  endpoint,
684
- params,
742
+ normalizedParams,
685
743
  this.buildContentTags(siteName),
686
744
  cachePolicy,
687
- () => this.http.get(path, params)
745
+ () => this.http.get(path, normalizedParams)
688
746
  );
689
747
  }
690
748
  /**
@@ -1046,6 +1104,15 @@ var ProductsClient = class extends BaseClient {
1046
1104
  };
1047
1105
  const normalizedParams = params ? { ...params } : void 0;
1048
1106
  if (normalizedParams) {
1107
+ const validatedLimit = validateOptionalLimit(
1108
+ normalizedParams.limit,
1109
+ "products query"
1110
+ );
1111
+ if (validatedLimit !== void 0) {
1112
+ normalizedParams.limit = validatedLimit;
1113
+ } else {
1114
+ delete normalizedParams.limit;
1115
+ }
1049
1116
  const normalizedCategories = normalizeList(normalizedParams.category);
1050
1117
  if (normalizedCategories !== void 0) {
1051
1118
  normalizedParams.category = normalizedCategories;
@@ -2261,12 +2328,16 @@ async function loadProducts(options) {
2261
2328
  siteName,
2262
2329
  logger = noopLogger,
2263
2330
  fallbackProducts,
2264
- limit = 100,
2331
+ limit: requestedLimit,
2265
2332
  offset,
2266
2333
  search,
2267
2334
  category,
2268
2335
  categoryIds
2269
2336
  } = options;
2337
+ const resolvedLimit = validateLimit(
2338
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
2339
+ "products query"
2340
+ );
2270
2341
  if (!client) {
2271
2342
  log(logger, "warn", "[PerspectAPI] No client configured, using fallback products");
2272
2343
  return resolveFallbackProducts({ siteName, fallbackProducts });
@@ -2275,7 +2346,7 @@ async function loadProducts(options) {
2275
2346
  log(logger, "info", `[PerspectAPI] Loading products for site "${siteName}"`);
2276
2347
  const queryParams = {
2277
2348
  isActive: true,
2278
- limit,
2349
+ limit: resolvedLimit,
2279
2350
  offset,
2280
2351
  search
2281
2352
  };
@@ -2335,13 +2406,20 @@ async function loadPages(options) {
2335
2406
  }
2336
2407
  try {
2337
2408
  log(logger, "info", `[PerspectAPI] Loading pages for site "${siteName}"`);
2409
+ const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
2410
+ const resolvedLimit = validateLimit(
2411
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
2412
+ "pages query"
2413
+ );
2414
+ const requestedPageType = params?.page_type;
2415
+ const validatedPageType = validateOptionalContentType(requestedPageType, "pages query") ?? "page";
2338
2416
  const response = await client.content.getContent(
2339
2417
  siteName,
2340
2418
  {
2341
2419
  ...params,
2342
2420
  page_status: params?.page_status ?? "publish",
2343
- page_type: params?.page_type ?? "page",
2344
- limit: options.limit ?? params?.limit ?? 100
2421
+ page_type: validatedPageType,
2422
+ limit: resolvedLimit
2345
2423
  }
2346
2424
  );
2347
2425
  if (!response.data) {
@@ -2361,13 +2439,20 @@ async function loadPosts(options) {
2361
2439
  }
2362
2440
  try {
2363
2441
  log(logger, "info", `[PerspectAPI] Loading posts for site "${siteName}"`);
2442
+ const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
2443
+ const resolvedLimit = validateLimit(
2444
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
2445
+ "posts query"
2446
+ );
2447
+ const requestedPageType = params?.page_type;
2448
+ const validatedPageType = validateOptionalContentType(requestedPageType, "posts query") ?? "post";
2364
2449
  const response = await client.content.getContent(
2365
2450
  siteName,
2366
2451
  {
2367
2452
  ...params,
2368
2453
  page_status: params?.page_status ?? "publish",
2369
- page_type: params?.page_type ?? "post",
2370
- limit: options.limit ?? params?.limit ?? 100
2454
+ page_type: validatedPageType,
2455
+ limit: resolvedLimit
2371
2456
  }
2372
2457
  );
2373
2458
  if (!response.data) {
@@ -2427,7 +2512,8 @@ async function createCheckoutSession(options) {
2427
2512
  logger = noopLogger,
2428
2513
  fallbackProducts,
2429
2514
  metadata,
2430
- priceIdResolver
2515
+ priceIdResolver,
2516
+ tax
2431
2517
  } = options;
2432
2518
  if (!client) {
2433
2519
  log(logger, "error", "[PerspectAPI] Cannot create checkout session without SDK client");
@@ -2438,7 +2524,7 @@ async function createCheckoutSession(options) {
2438
2524
  client,
2439
2525
  siteName,
2440
2526
  logger,
2441
- limit: 200,
2527
+ limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
2442
2528
  fallbackProducts
2443
2529
  });
2444
2530
  const productMap = new Map(
@@ -2458,6 +2544,18 @@ async function createCheckoutSession(options) {
2458
2544
  quantity: item.quantity
2459
2545
  };
2460
2546
  });
2547
+ const resolvedTax = (() => {
2548
+ if (!tax && !customerEmail) {
2549
+ return void 0;
2550
+ }
2551
+ if (!tax) {
2552
+ return { customer_identifier: customerEmail };
2553
+ }
2554
+ return {
2555
+ ...tax,
2556
+ customer_identifier: tax.customer_identifier ?? customerEmail ?? void 0
2557
+ };
2558
+ })();
2461
2559
  const checkoutData = {
2462
2560
  line_items,
2463
2561
  successUrl,
@@ -2469,6 +2567,9 @@ async function createCheckoutSession(options) {
2469
2567
  mode: mode === "test" ? "payment" : "payment",
2470
2568
  metadata
2471
2569
  };
2570
+ if (resolvedTax) {
2571
+ checkoutData.tax = resolvedTax;
2572
+ }
2472
2573
  log(logger, "info", "[PerspectAPI] Creating checkout session");
2473
2574
  return client.checkout.createCheckoutSession(siteName, checkoutData);
2474
2575
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perspectapi-ts-sdk",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -13,6 +13,7 @@ import type {
13
13
  PaginatedResponse,
14
14
  ApiResponse,
15
15
  } from '../types';
16
+ import { validateOptionalContentType, validateOptionalLimit } from '../utils/validators';
16
17
 
17
18
  export class ContentClient extends BaseClient {
18
19
  constructor(http: any, cache?: CacheManager) {
@@ -29,13 +30,38 @@ export class ContentClient extends BaseClient {
29
30
  ): Promise<PaginatedResponse<Content>> {
30
31
  const endpoint = this.siteScopedEndpoint(siteName);
31
32
  const path = this.buildPath(endpoint);
33
+ const normalizedParams: ContentQueryParams | undefined = params
34
+ ? { ...params }
35
+ : undefined;
36
+
37
+ if (normalizedParams) {
38
+ const validatedLimit = validateOptionalLimit(
39
+ normalizedParams.limit,
40
+ 'content query',
41
+ );
42
+ if (validatedLimit !== undefined) {
43
+ normalizedParams.limit = validatedLimit;
44
+ } else {
45
+ delete normalizedParams.limit;
46
+ }
47
+
48
+ const validatedPageType = validateOptionalContentType(
49
+ normalizedParams.page_type,
50
+ 'content query',
51
+ );
52
+ if (validatedPageType !== undefined) {
53
+ normalizedParams.page_type = validatedPageType;
54
+ } else {
55
+ delete normalizedParams.page_type;
56
+ }
57
+ }
32
58
 
33
59
  return this.fetchWithCache<PaginatedResponse<Content>>(
34
60
  endpoint,
35
- params,
61
+ normalizedParams,
36
62
  this.buildContentTags(siteName),
37
63
  cachePolicy,
38
- () => this.http.get(path, params) as Promise<PaginatedResponse<Content>>
64
+ () => this.http.get(path, normalizedParams) as Promise<PaginatedResponse<Content>>
39
65
  );
40
66
  }
41
67
 
@@ -12,6 +12,7 @@ import type {
12
12
  ApiResponse,
13
13
  ProductQueryParams,
14
14
  } from '../types';
15
+ import { validateOptionalLimit } from '../utils/validators';
15
16
 
16
17
  export class ProductsClient extends BaseClient {
17
18
  constructor(http: any, cache?: CacheManager) {
@@ -41,6 +42,16 @@ export class ProductsClient extends BaseClient {
41
42
  const normalizedParams: ProductQueryParams | undefined = params ? { ...params } : undefined;
42
43
 
43
44
  if (normalizedParams) {
45
+ const validatedLimit = validateOptionalLimit(
46
+ normalizedParams.limit,
47
+ 'products query',
48
+ );
49
+ if (validatedLimit !== undefined) {
50
+ normalizedParams.limit = validatedLimit;
51
+ } else {
52
+ delete normalizedParams.limit;
53
+ }
54
+
44
55
  const normalizedCategories = normalizeList(normalizedParams.category as any);
45
56
  if (normalizedCategories !== undefined) {
46
57
  normalizedParams.category = normalizedCategories;
package/src/loaders.ts CHANGED
@@ -17,8 +17,14 @@ import type {
17
17
  BlogPost,
18
18
  CreateCheckoutSessionRequest,
19
19
  CheckoutSession,
20
- CheckoutMetadata
20
+ CheckoutMetadata,
21
+ CheckoutTaxRequest
21
22
  } from './types';
23
+ import {
24
+ MAX_API_QUERY_LIMIT,
25
+ validateLimit,
26
+ validateOptionalContentType
27
+ } from './utils/validators';
22
28
 
23
29
  /**
24
30
  * Logger interface so consumers can supply custom logging behaviour (or noop).
@@ -311,13 +317,18 @@ export async function loadProducts(options: LoadProductsOptions): Promise<Produc
311
317
  siteName,
312
318
  logger = noopLogger,
313
319
  fallbackProducts,
314
- limit = 100,
320
+ limit: requestedLimit,
315
321
  offset,
316
322
  search,
317
323
  category,
318
324
  categoryIds
319
325
  } = options;
320
326
 
327
+ const resolvedLimit = validateLimit(
328
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
329
+ 'products query',
330
+ );
331
+
321
332
  if (!client) {
322
333
  log(logger, 'warn', '[PerspectAPI] No client configured, using fallback products');
323
334
  return resolveFallbackProducts({ siteName, fallbackProducts });
@@ -327,7 +338,7 @@ export async function loadProducts(options: LoadProductsOptions): Promise<Produc
327
338
  log(logger, 'info', `[PerspectAPI] Loading products for site "${siteName}"`);
328
339
  const queryParams: ProductQueryParams = {
329
340
  isActive: true,
330
- limit,
341
+ limit: resolvedLimit,
331
342
  offset,
332
343
  search
333
344
  };
@@ -427,13 +438,23 @@ export async function loadPages(
427
438
 
428
439
  try {
429
440
  log(logger, 'info', `[PerspectAPI] Loading pages for site "${siteName}"`);
441
+ const requestedLimit =
442
+ options.limit !== undefined ? options.limit : params?.limit;
443
+ const resolvedLimit = validateLimit(
444
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
445
+ 'pages query',
446
+ );
447
+ const requestedPageType = params?.page_type;
448
+ const validatedPageType =
449
+ validateOptionalContentType(requestedPageType, 'pages query') ?? 'page';
450
+
430
451
  const response: PaginatedResponse<Content> = await client.content.getContent(
431
452
  siteName,
432
453
  {
433
454
  ...params,
434
455
  page_status: (params?.page_status ?? 'publish') as ContentStatus,
435
- page_type: (params?.page_type ?? 'page') as ContentType,
436
- limit: options.limit ?? params?.limit ?? 100
456
+ page_type: validatedPageType as ContentType,
457
+ limit: resolvedLimit
437
458
  }
438
459
  );
439
460
 
@@ -463,13 +484,23 @@ export async function loadPosts(
463
484
 
464
485
  try {
465
486
  log(logger, 'info', `[PerspectAPI] Loading posts for site "${siteName}"`);
487
+ const requestedLimit =
488
+ options.limit !== undefined ? options.limit : params?.limit;
489
+ const resolvedLimit = validateLimit(
490
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
491
+ 'posts query',
492
+ );
493
+ const requestedPageType = params?.page_type;
494
+ const validatedPageType =
495
+ validateOptionalContentType(requestedPageType, 'posts query') ?? 'post';
496
+
466
497
  const response: PaginatedResponse<Content> = await client.content.getContent(
467
498
  siteName,
468
499
  {
469
500
  ...params,
470
501
  page_status: (params?.page_status ?? 'publish') as ContentStatus,
471
- page_type: (params?.page_type ?? 'post') as ContentType,
472
- limit: options.limit ?? params?.limit ?? 100
502
+ page_type: validatedPageType as ContentType,
503
+ limit: resolvedLimit
473
504
  }
474
505
  );
475
506
 
@@ -558,6 +589,10 @@ export interface CheckoutSessionOptions {
558
589
  * Optional resolver to convert a product record into a Stripe price ID.
559
590
  */
560
591
  priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
592
+ /**
593
+ * Optional tax configuration that will be forwarded to the checkout API.
594
+ */
595
+ tax?: CheckoutTaxRequest;
561
596
  }
562
597
 
563
598
  /**
@@ -578,7 +613,8 @@ export async function createCheckoutSession(
578
613
  logger = noopLogger,
579
614
  fallbackProducts,
580
615
  metadata,
581
- priceIdResolver
616
+ priceIdResolver,
617
+ tax
582
618
  } = options;
583
619
 
584
620
  if (!client) {
@@ -592,7 +628,7 @@ export async function createCheckoutSession(
592
628
  client,
593
629
  siteName,
594
630
  logger,
595
- limit: 200,
631
+ limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
596
632
  fallbackProducts
597
633
  });
598
634
 
@@ -623,6 +659,21 @@ export async function createCheckoutSession(
623
659
  };
624
660
  });
625
661
 
662
+ const resolvedTax: CheckoutTaxRequest | undefined = (() => {
663
+ if (!tax && !customerEmail) {
664
+ return undefined;
665
+ }
666
+
667
+ if (!tax) {
668
+ return { customer_identifier: customerEmail };
669
+ }
670
+
671
+ return {
672
+ ...tax,
673
+ customer_identifier: tax.customer_identifier ?? customerEmail ?? undefined,
674
+ };
675
+ })();
676
+
626
677
  const checkoutData: CreateCheckoutSessionRequest = {
627
678
  line_items,
628
679
  successUrl,
@@ -632,9 +683,13 @@ export async function createCheckoutSession(
632
683
  customerEmail,
633
684
  customer_email: customerEmail,
634
685
  mode: mode === 'test' ? 'payment' : 'payment',
635
- metadata
686
+ metadata,
636
687
  };
637
688
 
689
+ if (resolvedTax) {
690
+ checkoutData.tax = resolvedTax;
691
+ }
692
+
638
693
  log(logger, 'info', '[PerspectAPI] Creating checkout session');
639
694
  return client.checkout.createCheckoutSession(siteName, checkoutData);
640
695
  } catch (error) {
@@ -399,6 +399,45 @@ export type CheckoutMetadataValue =
399
399
 
400
400
  export type CheckoutMetadata = Record<string, CheckoutMetadataValue>;
401
401
 
402
+ export type CheckoutTaxStrategy =
403
+ | 'disabled'
404
+ | 'gateway_auto'
405
+ | 'manual_rates'
406
+ | 'external_service';
407
+
408
+ export type CheckoutTaxExemptionStatus =
409
+ | 'none'
410
+ | 'exempt'
411
+ | 'reverse_charge';
412
+
413
+ export interface CheckoutTaxCustomerExemptionRequest {
414
+ status?: CheckoutTaxExemptionStatus;
415
+ reason?: string;
416
+ tax_id?: string;
417
+ tax_id_type?: string;
418
+ certificate_url?: string;
419
+ metadata?: Record<string, any>;
420
+ expires_at?: string;
421
+ }
422
+
423
+ export interface CheckoutTaxRequest {
424
+ strategy?: CheckoutTaxStrategy;
425
+ customer_identifier?: string;
426
+ customer_profile_id?: string;
427
+ customer_display_name?: string;
428
+ allow_exemption?: boolean;
429
+ require_tax_id?: boolean;
430
+ save_profile?: boolean;
431
+ customer_exemption?: CheckoutTaxCustomerExemptionRequest;
432
+ manual_rate_percent?: number;
433
+ manual_rate_map?: Record<string, number>;
434
+ external_service?: {
435
+ provider: string;
436
+ config?: Record<string, any>;
437
+ };
438
+ metadata?: Record<string, any>;
439
+ }
440
+
402
441
  export interface CreateCheckoutSessionRequest {
403
442
  // Single item checkout (legacy/simple)
404
443
  priceId?: string;
@@ -437,6 +476,7 @@ export interface CreateCheckoutSessionRequest {
437
476
  allowed_countries: string[];
438
477
  };
439
478
  billing_address_collection?: 'auto' | 'required';
479
+ tax?: CheckoutTaxRequest;
440
480
  }
441
481
 
442
482
  export interface CheckoutSession {
@@ -0,0 +1,53 @@
1
+ import type { ContentType } from '../types';
2
+
3
+ export const MAX_API_QUERY_LIMIT = 100;
4
+ const ALLOWED_CONTENT_TYPES: ReadonlyArray<ContentType> = ['post', 'page'];
5
+
6
+ export function validateLimit(limit: number, context: string): number {
7
+ if (typeof limit !== 'number' || Number.isNaN(limit) || !Number.isFinite(limit)) {
8
+ throw new Error(`[PerspectAPI] ${context} limit must be a finite number.`);
9
+ }
10
+
11
+ if (limit < 1) {
12
+ throw new Error(`[PerspectAPI] ${context} limit must be at least 1.`);
13
+ }
14
+
15
+ if (limit > MAX_API_QUERY_LIMIT) {
16
+ throw new Error(
17
+ `[PerspectAPI] ${context} limit ${limit} exceeds the maximum allowed value of ${MAX_API_QUERY_LIMIT}.`,
18
+ );
19
+ }
20
+
21
+ return limit;
22
+ }
23
+
24
+ export function validateOptionalLimit(
25
+ limit: number | undefined,
26
+ context: string,
27
+ ): number | undefined {
28
+ if (limit === undefined || limit === null) {
29
+ return undefined;
30
+ }
31
+
32
+ return validateLimit(limit, context);
33
+ }
34
+
35
+ export function validateOptionalContentType(
36
+ pageType: string | undefined,
37
+ context: string,
38
+ ): ContentType | undefined {
39
+ if (pageType === undefined || pageType === null) {
40
+ return undefined;
41
+ }
42
+
43
+ if (ALLOWED_CONTENT_TYPES.includes(pageType as ContentType)) {
44
+ return pageType as ContentType;
45
+ }
46
+
47
+ throw new Error(
48
+ `[PerspectAPI] ${context} received unsupported page_type "${pageType}". Allowed values are ${ALLOWED_CONTENT_TYPES.join(
49
+ ', ',
50
+ )}.`,
51
+ );
52
+ }
53
+