perspectapi-ts-sdk 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -537,7 +537,29 @@ const session = await client.checkout.createCheckoutSession({
537
537
  priceId: 'price_1234567890',
538
538
  successUrl: 'https://myapp.com/success',
539
539
  cancelUrl: 'https://myapp.com/cancel',
540
- customerEmail: 'customer@example.com'
540
+ customerEmail: 'customer@example.com',
541
+ currency: 'usd',
542
+ shipping_amount: 500,
543
+ shipping_address: {
544
+ country: 'US',
545
+ state: 'CA',
546
+ postal_code: '94110'
547
+ },
548
+ billing_address: {
549
+ country: 'US',
550
+ state: 'CA',
551
+ postal_code: '94110'
552
+ },
553
+ tax: {
554
+ strategy: 'manual_rates',
555
+ customer_identifier: 'acct-123',
556
+ customer_display_name: 'Acme Corp',
557
+ customer_exemption: {
558
+ status: 'exempt',
559
+ tax_id: '99-1234567',
560
+ tax_id_type: 'ein'
561
+ }
562
+ }
541
563
  });
542
564
 
543
565
  // Redirect user to checkout
@@ -545,6 +567,7 @@ window.location.href = session.data.url;
545
567
 
546
568
  // Get checkout session status
547
569
  const sessionStatus = await client.checkout.getCheckoutSession('cs_test_123');
570
+ console.log(sessionStatus.data.tax?.amount); // Display assessed tax (if calculated)
548
571
  ```
549
572
 
550
573
  ### Contact Forms
package/dist/index.d.mts CHANGED
@@ -424,6 +424,42 @@ 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 CheckoutAddress {
430
+ line1?: string;
431
+ line2?: string;
432
+ city?: string;
433
+ state?: string;
434
+ postal_code?: string;
435
+ country?: string;
436
+ }
437
+ interface CheckoutTaxCustomerExemptionRequest {
438
+ status?: CheckoutTaxExemptionStatus;
439
+ reason?: string;
440
+ tax_id?: string;
441
+ tax_id_type?: string;
442
+ certificate_url?: string;
443
+ metadata?: Record<string, any>;
444
+ expires_at?: string;
445
+ }
446
+ interface CheckoutTaxRequest {
447
+ strategy?: CheckoutTaxStrategy;
448
+ customer_identifier?: string;
449
+ customer_profile_id?: string;
450
+ customer_display_name?: string;
451
+ allow_exemption?: boolean;
452
+ require_tax_id?: boolean;
453
+ save_profile?: boolean;
454
+ customer_exemption?: CheckoutTaxCustomerExemptionRequest;
455
+ manual_rate_percent?: number;
456
+ manual_rate_map?: Record<string, number>;
457
+ external_service?: {
458
+ provider: string;
459
+ config?: Record<string, any>;
460
+ };
461
+ metadata?: Record<string, any>;
462
+ }
427
463
  interface CreateCheckoutSessionRequest {
428
464
  priceId?: string;
429
465
  quantity?: number;
@@ -446,6 +482,7 @@ interface CreateCheckoutSessionRequest {
446
482
  cancelUrl?: string;
447
483
  customer_email?: string;
448
484
  customerEmail?: string;
485
+ currency?: string;
449
486
  metadata?: CheckoutMetadata;
450
487
  mode?: 'payment' | 'subscription' | 'setup';
451
488
  automatic_tax?: {
@@ -455,11 +492,35 @@ interface CreateCheckoutSessionRequest {
455
492
  allowed_countries: string[];
456
493
  };
457
494
  billing_address_collection?: 'auto' | 'required';
495
+ shipping_amount?: number;
496
+ shippingAmount?: number;
497
+ shipping_address?: CheckoutAddress;
498
+ shippingAddress?: CheckoutAddress;
499
+ billing_address?: CheckoutAddress;
500
+ billingAddress?: CheckoutAddress;
501
+ tax?: CheckoutTaxRequest;
502
+ }
503
+ interface CheckoutTaxBreakdownItem {
504
+ jurisdiction: string;
505
+ rate_percent: number;
506
+ tax_amount: number;
507
+ taxable_amount: number;
508
+ source: 'manual_map' | 'manual_percent' | 'gateway' | 'external';
509
+ }
510
+ interface CheckoutSessionTax {
511
+ amount: number;
512
+ currency: string;
513
+ strategy: CheckoutTaxStrategy;
514
+ exemption_applied: boolean;
515
+ exemption_status: CheckoutTaxExemptionStatus;
516
+ breakdown?: CheckoutTaxBreakdownItem[] | null;
458
517
  }
459
518
  interface CheckoutSession {
460
519
  id: string;
461
520
  url: string;
462
521
  status: string;
522
+ payment_status?: string;
523
+ tax?: CheckoutSessionTax | null;
463
524
  }
464
525
  interface ContactSubmission {
465
526
  id: string;
@@ -2318,10 +2379,30 @@ interface CheckoutSessionOptions {
2318
2379
  logger?: LoaderLogger;
2319
2380
  fallbackProducts?: Product[];
2320
2381
  metadata?: CheckoutMetadata;
2382
+ /**
2383
+ * Optional currency override; defaults to the currency defined on each Stripe price.
2384
+ */
2385
+ currency?: string;
2386
+ /**
2387
+ * Optional shipping amount (in the smallest currency unit).
2388
+ */
2389
+ shippingAmount?: number;
2390
+ /**
2391
+ * Shipping address forwarded to the checkout API for tax estimation.
2392
+ */
2393
+ shippingAddress?: CheckoutAddress;
2394
+ /**
2395
+ * Billing address forwarded to the checkout API for tax estimation.
2396
+ */
2397
+ billingAddress?: CheckoutAddress;
2321
2398
  /**
2322
2399
  * Optional resolver to convert a product record into a Stripe price ID.
2323
2400
  */
2324
2401
  priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
2402
+ /**
2403
+ * Optional tax configuration that will be forwarded to the checkout API.
2404
+ */
2405
+ tax?: CheckoutTaxRequest;
2325
2406
  }
2326
2407
  /**
2327
2408
  * Convenience helper that creates a checkout session by looking up Stripe price IDs
@@ -2331,4 +2412,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2331
2412
  error: string;
2332
2413
  }>;
2333
2414
 
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 };
2415
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, 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,42 @@ 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 CheckoutAddress {
430
+ line1?: string;
431
+ line2?: string;
432
+ city?: string;
433
+ state?: string;
434
+ postal_code?: string;
435
+ country?: string;
436
+ }
437
+ interface CheckoutTaxCustomerExemptionRequest {
438
+ status?: CheckoutTaxExemptionStatus;
439
+ reason?: string;
440
+ tax_id?: string;
441
+ tax_id_type?: string;
442
+ certificate_url?: string;
443
+ metadata?: Record<string, any>;
444
+ expires_at?: string;
445
+ }
446
+ interface CheckoutTaxRequest {
447
+ strategy?: CheckoutTaxStrategy;
448
+ customer_identifier?: string;
449
+ customer_profile_id?: string;
450
+ customer_display_name?: string;
451
+ allow_exemption?: boolean;
452
+ require_tax_id?: boolean;
453
+ save_profile?: boolean;
454
+ customer_exemption?: CheckoutTaxCustomerExemptionRequest;
455
+ manual_rate_percent?: number;
456
+ manual_rate_map?: Record<string, number>;
457
+ external_service?: {
458
+ provider: string;
459
+ config?: Record<string, any>;
460
+ };
461
+ metadata?: Record<string, any>;
462
+ }
427
463
  interface CreateCheckoutSessionRequest {
428
464
  priceId?: string;
429
465
  quantity?: number;
@@ -446,6 +482,7 @@ interface CreateCheckoutSessionRequest {
446
482
  cancelUrl?: string;
447
483
  customer_email?: string;
448
484
  customerEmail?: string;
485
+ currency?: string;
449
486
  metadata?: CheckoutMetadata;
450
487
  mode?: 'payment' | 'subscription' | 'setup';
451
488
  automatic_tax?: {
@@ -455,11 +492,35 @@ interface CreateCheckoutSessionRequest {
455
492
  allowed_countries: string[];
456
493
  };
457
494
  billing_address_collection?: 'auto' | 'required';
495
+ shipping_amount?: number;
496
+ shippingAmount?: number;
497
+ shipping_address?: CheckoutAddress;
498
+ shippingAddress?: CheckoutAddress;
499
+ billing_address?: CheckoutAddress;
500
+ billingAddress?: CheckoutAddress;
501
+ tax?: CheckoutTaxRequest;
502
+ }
503
+ interface CheckoutTaxBreakdownItem {
504
+ jurisdiction: string;
505
+ rate_percent: number;
506
+ tax_amount: number;
507
+ taxable_amount: number;
508
+ source: 'manual_map' | 'manual_percent' | 'gateway' | 'external';
509
+ }
510
+ interface CheckoutSessionTax {
511
+ amount: number;
512
+ currency: string;
513
+ strategy: CheckoutTaxStrategy;
514
+ exemption_applied: boolean;
515
+ exemption_status: CheckoutTaxExemptionStatus;
516
+ breakdown?: CheckoutTaxBreakdownItem[] | null;
458
517
  }
459
518
  interface CheckoutSession {
460
519
  id: string;
461
520
  url: string;
462
521
  status: string;
522
+ payment_status?: string;
523
+ tax?: CheckoutSessionTax | null;
463
524
  }
464
525
  interface ContactSubmission {
465
526
  id: string;
@@ -2318,10 +2379,30 @@ interface CheckoutSessionOptions {
2318
2379
  logger?: LoaderLogger;
2319
2380
  fallbackProducts?: Product[];
2320
2381
  metadata?: CheckoutMetadata;
2382
+ /**
2383
+ * Optional currency override; defaults to the currency defined on each Stripe price.
2384
+ */
2385
+ currency?: string;
2386
+ /**
2387
+ * Optional shipping amount (in the smallest currency unit).
2388
+ */
2389
+ shippingAmount?: number;
2390
+ /**
2391
+ * Shipping address forwarded to the checkout API for tax estimation.
2392
+ */
2393
+ shippingAddress?: CheckoutAddress;
2394
+ /**
2395
+ * Billing address forwarded to the checkout API for tax estimation.
2396
+ */
2397
+ billingAddress?: CheckoutAddress;
2321
2398
  /**
2322
2399
  * Optional resolver to convert a product record into a Stripe price ID.
2323
2400
  */
2324
2401
  priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
2402
+ /**
2403
+ * Optional tax configuration that will be forwarded to the checkout API.
2404
+ */
2405
+ tax?: CheckoutTaxRequest;
2325
2406
  }
2326
2407
  /**
2327
2408
  * Convenience helper that creates a checkout session by looking up Stripe price IDs
@@ -2331,4 +2412,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2331
2412
  error: string;
2332
2413
  }>;
2333
2414
 
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 };
2415
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, 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,12 @@ async function createCheckoutSession(options) {
2488
2573
  logger = noopLogger,
2489
2574
  fallbackProducts,
2490
2575
  metadata,
2491
- priceIdResolver
2576
+ currency,
2577
+ shippingAmount,
2578
+ shippingAddress,
2579
+ billingAddress,
2580
+ priceIdResolver,
2581
+ tax
2492
2582
  } = options;
2493
2583
  if (!client) {
2494
2584
  log(logger, "error", "[PerspectAPI] Cannot create checkout session without SDK client");
@@ -2499,7 +2589,7 @@ async function createCheckoutSession(options) {
2499
2589
  client,
2500
2590
  siteName,
2501
2591
  logger,
2502
- limit: 200,
2592
+ limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
2503
2593
  fallbackProducts
2504
2594
  });
2505
2595
  const productMap = new Map(
@@ -2519,6 +2609,18 @@ async function createCheckoutSession(options) {
2519
2609
  quantity: item.quantity
2520
2610
  };
2521
2611
  });
2612
+ const resolvedTax = (() => {
2613
+ if (!tax && !customerEmail) {
2614
+ return void 0;
2615
+ }
2616
+ if (!tax) {
2617
+ return { customer_identifier: customerEmail };
2618
+ }
2619
+ return {
2620
+ ...tax,
2621
+ customer_identifier: tax.customer_identifier ?? customerEmail ?? void 0
2622
+ };
2623
+ })();
2522
2624
  const checkoutData = {
2523
2625
  line_items,
2524
2626
  successUrl,
@@ -2530,6 +2632,24 @@ async function createCheckoutSession(options) {
2530
2632
  mode: mode === "test" ? "payment" : "payment",
2531
2633
  metadata
2532
2634
  };
2635
+ if (currency) {
2636
+ checkoutData.currency = currency;
2637
+ }
2638
+ if (typeof shippingAmount === "number") {
2639
+ checkoutData.shipping_amount = shippingAmount;
2640
+ checkoutData.shippingAmount = shippingAmount;
2641
+ }
2642
+ if (shippingAddress) {
2643
+ checkoutData.shipping_address = shippingAddress;
2644
+ checkoutData.shippingAddress = shippingAddress;
2645
+ }
2646
+ if (billingAddress) {
2647
+ checkoutData.billing_address = billingAddress;
2648
+ checkoutData.billingAddress = billingAddress;
2649
+ }
2650
+ if (resolvedTax) {
2651
+ checkoutData.tax = resolvedTax;
2652
+ }
2533
2653
  log(logger, "info", "[PerspectAPI] Creating checkout session");
2534
2654
  return client.checkout.createCheckoutSession(siteName, checkoutData);
2535
2655
  } 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,12 @@ async function createCheckoutSession(options) {
2427
2512
  logger = noopLogger,
2428
2513
  fallbackProducts,
2429
2514
  metadata,
2430
- priceIdResolver
2515
+ currency,
2516
+ shippingAmount,
2517
+ shippingAddress,
2518
+ billingAddress,
2519
+ priceIdResolver,
2520
+ tax
2431
2521
  } = options;
2432
2522
  if (!client) {
2433
2523
  log(logger, "error", "[PerspectAPI] Cannot create checkout session without SDK client");
@@ -2438,7 +2528,7 @@ async function createCheckoutSession(options) {
2438
2528
  client,
2439
2529
  siteName,
2440
2530
  logger,
2441
- limit: 200,
2531
+ limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
2442
2532
  fallbackProducts
2443
2533
  });
2444
2534
  const productMap = new Map(
@@ -2458,6 +2548,18 @@ async function createCheckoutSession(options) {
2458
2548
  quantity: item.quantity
2459
2549
  };
2460
2550
  });
2551
+ const resolvedTax = (() => {
2552
+ if (!tax && !customerEmail) {
2553
+ return void 0;
2554
+ }
2555
+ if (!tax) {
2556
+ return { customer_identifier: customerEmail };
2557
+ }
2558
+ return {
2559
+ ...tax,
2560
+ customer_identifier: tax.customer_identifier ?? customerEmail ?? void 0
2561
+ };
2562
+ })();
2461
2563
  const checkoutData = {
2462
2564
  line_items,
2463
2565
  successUrl,
@@ -2469,6 +2571,24 @@ async function createCheckoutSession(options) {
2469
2571
  mode: mode === "test" ? "payment" : "payment",
2470
2572
  metadata
2471
2573
  };
2574
+ if (currency) {
2575
+ checkoutData.currency = currency;
2576
+ }
2577
+ if (typeof shippingAmount === "number") {
2578
+ checkoutData.shipping_amount = shippingAmount;
2579
+ checkoutData.shippingAmount = shippingAmount;
2580
+ }
2581
+ if (shippingAddress) {
2582
+ checkoutData.shipping_address = shippingAddress;
2583
+ checkoutData.shippingAddress = shippingAddress;
2584
+ }
2585
+ if (billingAddress) {
2586
+ checkoutData.billing_address = billingAddress;
2587
+ checkoutData.billingAddress = billingAddress;
2588
+ }
2589
+ if (resolvedTax) {
2590
+ checkoutData.tax = resolvedTax;
2591
+ }
2472
2592
  log(logger, "info", "[PerspectAPI] Creating checkout session");
2473
2593
  return client.checkout.createCheckoutSession(siteName, checkoutData);
2474
2594
  } 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.3.0",
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,15 @@ import type {
17
17
  BlogPost,
18
18
  CreateCheckoutSessionRequest,
19
19
  CheckoutSession,
20
- CheckoutMetadata
20
+ CheckoutMetadata,
21
+ CheckoutTaxRequest,
22
+ CheckoutAddress
21
23
  } from './types';
24
+ import {
25
+ MAX_API_QUERY_LIMIT,
26
+ validateLimit,
27
+ validateOptionalContentType
28
+ } from './utils/validators';
22
29
 
23
30
  /**
24
31
  * Logger interface so consumers can supply custom logging behaviour (or noop).
@@ -311,13 +318,18 @@ export async function loadProducts(options: LoadProductsOptions): Promise<Produc
311
318
  siteName,
312
319
  logger = noopLogger,
313
320
  fallbackProducts,
314
- limit = 100,
321
+ limit: requestedLimit,
315
322
  offset,
316
323
  search,
317
324
  category,
318
325
  categoryIds
319
326
  } = options;
320
327
 
328
+ const resolvedLimit = validateLimit(
329
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
330
+ 'products query',
331
+ );
332
+
321
333
  if (!client) {
322
334
  log(logger, 'warn', '[PerspectAPI] No client configured, using fallback products');
323
335
  return resolveFallbackProducts({ siteName, fallbackProducts });
@@ -327,7 +339,7 @@ export async function loadProducts(options: LoadProductsOptions): Promise<Produc
327
339
  log(logger, 'info', `[PerspectAPI] Loading products for site "${siteName}"`);
328
340
  const queryParams: ProductQueryParams = {
329
341
  isActive: true,
330
- limit,
342
+ limit: resolvedLimit,
331
343
  offset,
332
344
  search
333
345
  };
@@ -427,13 +439,23 @@ export async function loadPages(
427
439
 
428
440
  try {
429
441
  log(logger, 'info', `[PerspectAPI] Loading pages for site "${siteName}"`);
442
+ const requestedLimit =
443
+ options.limit !== undefined ? options.limit : params?.limit;
444
+ const resolvedLimit = validateLimit(
445
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
446
+ 'pages query',
447
+ );
448
+ const requestedPageType = params?.page_type;
449
+ const validatedPageType =
450
+ validateOptionalContentType(requestedPageType, 'pages query') ?? 'page';
451
+
430
452
  const response: PaginatedResponse<Content> = await client.content.getContent(
431
453
  siteName,
432
454
  {
433
455
  ...params,
434
456
  page_status: (params?.page_status ?? 'publish') as ContentStatus,
435
- page_type: (params?.page_type ?? 'page') as ContentType,
436
- limit: options.limit ?? params?.limit ?? 100
457
+ page_type: validatedPageType as ContentType,
458
+ limit: resolvedLimit
437
459
  }
438
460
  );
439
461
 
@@ -463,13 +485,23 @@ export async function loadPosts(
463
485
 
464
486
  try {
465
487
  log(logger, 'info', `[PerspectAPI] Loading posts for site "${siteName}"`);
488
+ const requestedLimit =
489
+ options.limit !== undefined ? options.limit : params?.limit;
490
+ const resolvedLimit = validateLimit(
491
+ requestedLimit ?? MAX_API_QUERY_LIMIT,
492
+ 'posts query',
493
+ );
494
+ const requestedPageType = params?.page_type;
495
+ const validatedPageType =
496
+ validateOptionalContentType(requestedPageType, 'posts query') ?? 'post';
497
+
466
498
  const response: PaginatedResponse<Content> = await client.content.getContent(
467
499
  siteName,
468
500
  {
469
501
  ...params,
470
502
  page_status: (params?.page_status ?? 'publish') as ContentStatus,
471
- page_type: (params?.page_type ?? 'post') as ContentType,
472
- limit: options.limit ?? params?.limit ?? 100
503
+ page_type: validatedPageType as ContentType,
504
+ limit: resolvedLimit
473
505
  }
474
506
  );
475
507
 
@@ -554,10 +586,30 @@ export interface CheckoutSessionOptions {
554
586
  logger?: LoaderLogger;
555
587
  fallbackProducts?: Product[];
556
588
  metadata?: CheckoutMetadata;
589
+ /**
590
+ * Optional currency override; defaults to the currency defined on each Stripe price.
591
+ */
592
+ currency?: string;
593
+ /**
594
+ * Optional shipping amount (in the smallest currency unit).
595
+ */
596
+ shippingAmount?: number;
597
+ /**
598
+ * Shipping address forwarded to the checkout API for tax estimation.
599
+ */
600
+ shippingAddress?: CheckoutAddress;
601
+ /**
602
+ * Billing address forwarded to the checkout API for tax estimation.
603
+ */
604
+ billingAddress?: CheckoutAddress;
557
605
  /**
558
606
  * Optional resolver to convert a product record into a Stripe price ID.
559
607
  */
560
608
  priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
609
+ /**
610
+ * Optional tax configuration that will be forwarded to the checkout API.
611
+ */
612
+ tax?: CheckoutTaxRequest;
561
613
  }
562
614
 
563
615
  /**
@@ -578,7 +630,12 @@ export async function createCheckoutSession(
578
630
  logger = noopLogger,
579
631
  fallbackProducts,
580
632
  metadata,
581
- priceIdResolver
633
+ currency,
634
+ shippingAmount,
635
+ shippingAddress,
636
+ billingAddress,
637
+ priceIdResolver,
638
+ tax
582
639
  } = options;
583
640
 
584
641
  if (!client) {
@@ -592,7 +649,7 @@ export async function createCheckoutSession(
592
649
  client,
593
650
  siteName,
594
651
  logger,
595
- limit: 200,
652
+ limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
596
653
  fallbackProducts
597
654
  });
598
655
 
@@ -623,6 +680,21 @@ export async function createCheckoutSession(
623
680
  };
624
681
  });
625
682
 
683
+ const resolvedTax: CheckoutTaxRequest | undefined = (() => {
684
+ if (!tax && !customerEmail) {
685
+ return undefined;
686
+ }
687
+
688
+ if (!tax) {
689
+ return { customer_identifier: customerEmail };
690
+ }
691
+
692
+ return {
693
+ ...tax,
694
+ customer_identifier: tax.customer_identifier ?? customerEmail ?? undefined,
695
+ };
696
+ })();
697
+
626
698
  const checkoutData: CreateCheckoutSessionRequest = {
627
699
  line_items,
628
700
  successUrl,
@@ -632,9 +704,32 @@ export async function createCheckoutSession(
632
704
  customerEmail,
633
705
  customer_email: customerEmail,
634
706
  mode: mode === 'test' ? 'payment' : 'payment',
635
- metadata
707
+ metadata,
636
708
  };
637
709
 
710
+ if (currency) {
711
+ checkoutData.currency = currency;
712
+ }
713
+
714
+ if (typeof shippingAmount === 'number') {
715
+ checkoutData.shipping_amount = shippingAmount;
716
+ checkoutData.shippingAmount = shippingAmount;
717
+ }
718
+
719
+ if (shippingAddress) {
720
+ checkoutData.shipping_address = shippingAddress;
721
+ checkoutData.shippingAddress = shippingAddress;
722
+ }
723
+
724
+ if (billingAddress) {
725
+ checkoutData.billing_address = billingAddress;
726
+ checkoutData.billingAddress = billingAddress;
727
+ }
728
+
729
+ if (resolvedTax) {
730
+ checkoutData.tax = resolvedTax;
731
+ }
732
+
638
733
  log(logger, 'info', '[PerspectAPI] Creating checkout session');
639
734
  return client.checkout.createCheckoutSession(siteName, checkoutData);
640
735
  } catch (error) {
@@ -399,6 +399,54 @@ 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 CheckoutAddress {
414
+ line1?: string;
415
+ line2?: string;
416
+ city?: string;
417
+ state?: string;
418
+ postal_code?: string;
419
+ country?: string;
420
+ }
421
+
422
+ export interface CheckoutTaxCustomerExemptionRequest {
423
+ status?: CheckoutTaxExemptionStatus;
424
+ reason?: string;
425
+ tax_id?: string;
426
+ tax_id_type?: string;
427
+ certificate_url?: string;
428
+ metadata?: Record<string, any>;
429
+ expires_at?: string;
430
+ }
431
+
432
+ export interface CheckoutTaxRequest {
433
+ strategy?: CheckoutTaxStrategy;
434
+ customer_identifier?: string;
435
+ customer_profile_id?: string;
436
+ customer_display_name?: string;
437
+ allow_exemption?: boolean;
438
+ require_tax_id?: boolean;
439
+ save_profile?: boolean;
440
+ customer_exemption?: CheckoutTaxCustomerExemptionRequest;
441
+ manual_rate_percent?: number;
442
+ manual_rate_map?: Record<string, number>;
443
+ external_service?: {
444
+ provider: string;
445
+ config?: Record<string, any>;
446
+ };
447
+ metadata?: Record<string, any>;
448
+ }
449
+
402
450
  export interface CreateCheckoutSessionRequest {
403
451
  // Single item checkout (legacy/simple)
404
452
  priceId?: string;
@@ -428,6 +476,7 @@ export interface CreateCheckoutSessionRequest {
428
476
  cancelUrl?: string; // Alternative naming
429
477
  customer_email?: string;
430
478
  customerEmail?: string; // Alternative naming
479
+ currency?: string;
431
480
  metadata?: CheckoutMetadata;
432
481
  mode?: 'payment' | 'subscription' | 'setup';
433
482
  automatic_tax?: {
@@ -437,12 +486,38 @@ export interface CreateCheckoutSessionRequest {
437
486
  allowed_countries: string[];
438
487
  };
439
488
  billing_address_collection?: 'auto' | 'required';
489
+ shipping_amount?: number;
490
+ shippingAmount?: number;
491
+ shipping_address?: CheckoutAddress;
492
+ shippingAddress?: CheckoutAddress;
493
+ billing_address?: CheckoutAddress;
494
+ billingAddress?: CheckoutAddress;
495
+ tax?: CheckoutTaxRequest;
496
+ }
497
+
498
+ export interface CheckoutTaxBreakdownItem {
499
+ jurisdiction: string;
500
+ rate_percent: number;
501
+ tax_amount: number;
502
+ taxable_amount: number;
503
+ source: 'manual_map' | 'manual_percent' | 'gateway' | 'external';
504
+ }
505
+
506
+ export interface CheckoutSessionTax {
507
+ amount: number;
508
+ currency: string;
509
+ strategy: CheckoutTaxStrategy;
510
+ exemption_applied: boolean;
511
+ exemption_status: CheckoutTaxExemptionStatus;
512
+ breakdown?: CheckoutTaxBreakdownItem[] | null;
440
513
  }
441
514
 
442
515
  export interface CheckoutSession {
443
516
  id: string;
444
517
  url: string;
445
518
  status: string;
519
+ payment_status?: string;
520
+ tax?: CheckoutSessionTax | null;
446
521
  }
447
522
 
448
523
  // Contact Forms
@@ -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
+