@commercengine/pos 0.4.6 → 0.5.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/dist/index.mjs CHANGED
@@ -625,9 +625,11 @@ var MemoryTokenStorage = class {
625
625
  async setRefreshToken(token) {
626
626
  this.refreshToken = token;
627
627
  }
628
- async clearTokens() {
628
+ async clearTokens(expectedAccessToken, _expectedRevision) {
629
+ if (expectedAccessToken !== void 0 && this.accessToken !== expectedAccessToken) return false;
629
630
  this.accessToken = null;
630
631
  this.refreshToken = null;
632
+ return true;
631
633
  }
632
634
  };
633
635
  /**
@@ -654,11 +656,13 @@ var BrowserTokenStorage = class {
654
656
  async setRefreshToken(token) {
655
657
  if (typeof localStorage !== "undefined") localStorage.setItem(this.refreshTokenKey, token);
656
658
  }
657
- async clearTokens() {
659
+ async clearTokens(expectedAccessToken, _expectedRevision) {
658
660
  if (typeof localStorage !== "undefined") {
661
+ if (expectedAccessToken !== void 0 && localStorage.getItem(this.accessTokenKey) !== expectedAccessToken) return false;
659
662
  localStorage.removeItem(this.accessTokenKey);
660
663
  localStorage.removeItem(this.refreshTokenKey);
661
664
  }
665
+ return true;
662
666
  }
663
667
  };
664
668
  /**
@@ -668,61 +672,78 @@ var BrowserTokenStorage = class {
668
672
  * 1. API Key endpoints (X-Api-Key): login/email, login/phone, login/whatsapp, pair-device, verify-otp
669
673
  * 2. Bearer token endpoints: All other endpoints
670
674
  * 3. Token returning endpoints: verify-otp, refresh-token
675
+ *
676
+ * When the token storage implements `getRequestOwner()`, every destructive
677
+ * operation (clear on failed refresh, logout, 403) is scoped to the session
678
+ * that initiated it, and a request whose session changed between scheduling
679
+ * and dispatch is refused instead of sent with another session's identity.
671
680
  */
672
681
  function createPosAuthMiddleware(config) {
673
682
  let isRefreshing = false;
674
683
  let refreshPromise = null;
675
684
  let hasAssessedTokens = false;
676
- const assessTokenStateOnce = async () => {
685
+ /** The owner each in-flight request was signed for, keyed by Request. */
686
+ const requestOwners = /* @__PURE__ */ new WeakMap();
687
+ /**
688
+ * Clear tokens on behalf of `owner`. With an owner this is a
689
+ * compare-and-swap — a session that replaced the failing one survives —
690
+ * and `onTokensCleared` only fires when something was actually cleared.
691
+ */
692
+ const clearAssessedOwner = async (owner) => {
693
+ const cleared = owner ? await config.tokenStorage.clearTokens(owner.accessToken, owner.revision) : await config.tokenStorage.clearTokens();
694
+ if (cleared !== false) config.onTokensCleared?.();
695
+ return cleared !== false;
696
+ };
697
+ const assessTokenStateOnce = async (initiatingOwner) => {
677
698
  if (hasAssessedTokens) return;
678
699
  hasAssessedTokens = true;
679
700
  try {
680
- const accessToken = await config.tokenStorage.getAccessToken();
681
- const refreshToken = await config.tokenStorage.getRefreshToken();
701
+ const owner = initiatingOwner ?? await config.tokenStorage.getRequestOwner?.();
702
+ const accessToken = owner ? owner.accessToken : await config.tokenStorage.getAccessToken();
703
+ const refreshToken = owner ? owner.refreshToken : await config.tokenStorage.getRefreshToken();
682
704
  if (accessToken && !isTokenExpired(accessToken)) return;
683
705
  if (!accessToken && refreshToken) {
684
- await config.tokenStorage.clearTokens();
685
- config.onTokensCleared?.();
706
+ await clearAssessedOwner(owner);
686
707
  console.info("Cleaned up orphaned refresh token in POS");
687
708
  return;
688
709
  }
689
710
  if (accessToken && refreshToken && !isTokenExpired(refreshToken)) {
690
711
  try {
691
- await refreshTokens();
712
+ await refreshTokens(owner);
692
713
  console.info("POS tokens refreshed proactively on startup");
693
714
  } catch (error) {
694
- await config.tokenStorage.clearTokens();
695
- config.onTokensCleared?.();
696
715
  console.info("POS tokens cleared after failed refresh on startup");
697
716
  }
698
717
  return;
699
718
  }
700
719
  if (accessToken && isTokenExpired(accessToken) || refreshToken && isTokenExpired(refreshToken)) {
701
- await config.tokenStorage.clearTokens();
702
- config.onTokensCleared?.();
720
+ await clearAssessedOwner(owner);
703
721
  console.info("POS stale tokens cleared on startup - user needs to re-authenticate");
704
722
  return;
705
723
  }
706
724
  if (!accessToken && !refreshToken) return;
707
725
  } catch (error) {
708
726
  console.warn("POS token state assessment failed:", error);
727
+ if (config.tokenStorage.getRequestOwner) throw error;
709
728
  }
710
729
  };
711
- const refreshTokens = async () => {
730
+ const refreshTokens = async (initiatingOwner) => {
712
731
  if (isRefreshing && refreshPromise) return refreshPromise;
713
732
  isRefreshing = true;
714
733
  refreshPromise = (async () => {
715
734
  try {
716
- const refreshToken = await config.tokenStorage.getRefreshToken();
735
+ const refreshToken = initiatingOwner ? initiatingOwner.refreshToken : await config.tokenStorage.getRefreshToken();
717
736
  if (!refreshToken || isTokenExpired(refreshToken)) throw new Error("No valid refresh token available");
718
737
  let newTokens;
719
738
  if (config.refreshTokenFn) newTokens = await config.refreshTokenFn(refreshToken);
720
739
  else {
740
+ const accessToken = initiatingOwner ? initiatingOwner.accessToken : await config.tokenStorage.getAccessToken();
741
+ if (!accessToken) throw new Error("No valid access token available for POS token refresh");
721
742
  const response = await fetch(`${config.baseUrl}/pos/auth/refresh-token`, {
722
743
  method: "POST",
723
744
  headers: {
724
745
  "Content-Type": "application/json",
725
- Authorization: `Bearer ${await config.tokenStorage.getAccessToken()}`
746
+ Authorization: `Bearer ${accessToken}`
726
747
  },
727
748
  body: JSON.stringify({ refresh_token: refreshToken })
728
749
  });
@@ -735,8 +756,7 @@ function createPosAuthMiddleware(config) {
735
756
  config.onTokensUpdated?.(newTokens.access_token, newTokens.refresh_token);
736
757
  } catch (error) {
737
758
  console.error("POS token refresh failed:", error);
738
- await config.tokenStorage.clearTokens();
739
- config.onTokensCleared?.();
759
+ await clearAssessedOwner(initiatingOwner);
740
760
  throw error;
741
761
  } finally {
742
762
  isRefreshing = false;
@@ -748,25 +768,36 @@ function createPosAuthMiddleware(config) {
748
768
  return {
749
769
  async onRequest({ request }) {
750
770
  const pathname = getPathnameFromUrl(request.url);
751
- await assessTokenStateOnce();
771
+ const initiatingOwner = await config.tokenStorage.getRequestOwner?.();
772
+ await assessTokenStateOnce(initiatingOwner);
752
773
  if (isApiKeyEndpoint(pathname)) {
753
774
  request.headers.set("X-Api-Key", config.apiKey);
754
775
  return request;
755
776
  }
756
- let accessToken = await config.tokenStorage.getAccessToken();
777
+ let requestOwner = await config.tokenStorage.getRequestOwner?.();
778
+ if (initiatingOwner && requestOwner?.revision !== initiatingOwner.revision) throw new Error("The authenticated session changed before the POS request was sent.");
779
+ let accessToken = requestOwner ? requestOwner.accessToken : await config.tokenStorage.getAccessToken();
757
780
  if (accessToken && isTokenExpired(accessToken)) try {
758
- await refreshTokens();
759
- accessToken = await config.tokenStorage.getAccessToken();
781
+ await refreshTokens(requestOwner);
782
+ requestOwner = await config.tokenStorage.getRequestOwner?.();
783
+ if (initiatingOwner && requestOwner?.revision !== initiatingOwner.revision) throw new Error("The authenticated session changed before the POS request was sent.");
784
+ accessToken = requestOwner ? requestOwner.accessToken : await config.tokenStorage.getAccessToken();
760
785
  } catch (error) {
761
786
  console.warn("Token refresh failed:", error);
762
787
  accessToken = null;
763
788
  }
764
- if (accessToken) request.headers.set("Authorization", `Bearer ${accessToken}`);
789
+ if (accessToken) {
790
+ request.headers.set("Authorization", `Bearer ${accessToken}`);
791
+ requestOwners.set(request, {
792
+ accessToken,
793
+ revision: requestOwner?.revision
794
+ });
795
+ }
765
796
  return request;
766
797
  },
767
798
  async onResponse({ request, response }) {
768
799
  const pathname = getPathnameFromUrl(request.url);
769
- if (response.ok && isTokenReturningEndpoint(pathname)) try {
800
+ if (config.storeTokenResponses !== false && response.ok && isTokenReturningEndpoint(pathname)) try {
770
801
  const data = await response.clone().json();
771
802
  const content = data.content || data;
772
803
  if (content?.access_token && content?.refresh_token) {
@@ -778,8 +809,10 @@ function createPosAuthMiddleware(config) {
778
809
  console.warn("Failed to extract tokens from POS response:", error);
779
810
  }
780
811
  else if (response.ok && isLogoutEndpoint(pathname)) {
781
- await config.tokenStorage.clearTokens();
782
- config.onTokensCleared?.();
812
+ const owner = requestOwners.get(request);
813
+ if (owner?.accessToken) {
814
+ if (await config.tokenStorage.clearTokens(owner.accessToken, owner.revision) !== false) config.onTokensCleared?.();
815
+ }
783
816
  }
784
817
  if (response.status === 401 && !isApiKeyEndpoint(pathname)) {
785
818
  const currentToken = await config.tokenStorage.getAccessToken();
@@ -796,8 +829,10 @@ function createPosAuthMiddleware(config) {
796
829
  }
797
830
  }
798
831
  if (response.status === 403 && !isApiKeyEndpoint(pathname)) {
799
- await config.tokenStorage.clearTokens();
800
- config.onTokensCleared?.();
832
+ const owner = requestOwners.get(request);
833
+ if (owner?.accessToken) {
834
+ if (await config.tokenStorage.clearTokens(owner.accessToken, owner.revision) !== false) config.onTokensCleared?.();
835
+ }
801
836
  console.info("POS tokens cleared due to 403 - session revoked. This can happen when a user has more than 5 active sessions. Please re-authenticate.");
802
837
  }
803
838
  return response;
@@ -813,7 +848,8 @@ function createDefaultPosAuthMiddleware(options) {
813
848
  apiKey: options.apiKey,
814
849
  baseUrl: options.baseUrl,
815
850
  onTokensUpdated: options.onTokensUpdated,
816
- onTokensCleared: options.onTokensCleared
851
+ onTokensCleared: options.onTokensCleared,
852
+ storeTokenResponses: options.storeTokenResponses
817
853
  });
818
854
  }
819
855
  //#endregion
@@ -914,7 +950,8 @@ var PosAPIClient = class PosAPIClient extends BaseAPIClient {
914
950
  baseUrl: this.getBaseUrl(),
915
951
  tokenStorage: config.tokenStorage,
916
952
  onTokensUpdated: config.onTokensUpdated,
917
- onTokensCleared: config.onTokensCleared
953
+ onTokensCleared: config.onTokensCleared,
954
+ storeTokenResponses: config.storeTokenResponses
918
955
  });
919
956
  this.client.use(authMiddleware);
920
957
  if (config.accessToken) {
@@ -998,20 +1035,6 @@ var PosAPIClient = class PosAPIClient extends BaseAPIClient {
998
1035
  console.warn("Failed to initialize tokens in storage:", error);
999
1036
  }
1000
1037
  }
1001
- /**
1002
- * Get client typed for storefront POS operations (paths schema)
1003
- * This provides proper typing for storefront POS endpoints
1004
- */
1005
- get storefrontClient() {
1006
- return this.client;
1007
- }
1008
- /**
1009
- * Get client typed for admin POS operations (AdminPaths schema)
1010
- * This provides proper typing for admin POS endpoints
1011
- */
1012
- get adminClient() {
1013
- return this.client;
1014
- }
1015
1038
  };
1016
1039
  //#endregion
1017
1040
  //#region src/lib/pos.ts
@@ -1805,7 +1828,7 @@ var PosClient = class extends PosAPIClient {
1805
1828
  * ```
1806
1829
  */
1807
1830
  async getFulfillmentOptions(body) {
1808
- return this.executeRequest(() => this.client.POST("/pos/fulfillment-options", { body }));
1831
+ return this.executeRequest(() => this.client.POST("/pos/carts/fulfillment-options", { body }));
1809
1832
  }
1810
1833
  /**
1811
1834
  * Update cart customer information
@@ -1839,24 +1862,60 @@ var PosClient = class extends PosAPIClient {
1839
1862
  }
1840
1863
  /**
1841
1864
  * Create order from cart
1842
- * @param body - Order creation data
1843
- * @returns Promise with created order
1865
+ * @param body - Cart ID, plus an optional payment method to create the
1866
+ * initial payment request in the same call
1867
+ * @returns Promise with the created order, `payment_required`, and gateway
1868
+ * initiation details (`payment_info`) when an auto payment method was used
1844
1869
  * @example
1845
1870
  * ```typescript
1846
1871
  * const { data, error } = await pos.createOrder({
1847
- * cart_id: "01H9CART12345ABCDE"
1872
+ * cart_id: "01H9CART12345ABCDE",
1873
+ * payment_method: { payment_provider_slug: "cash" },
1848
1874
  * });
1849
1875
  *
1850
1876
  * if (error) {
1851
1877
  * console.error("Failed to create order:", error.message);
1852
1878
  * } else {
1853
- * console.log("Order created:", data.order.id);
1879
+ * console.log("Order created:", data.order.order_number);
1854
1880
  * console.log("Payment required:", data.payment_required);
1855
1881
  * }
1856
1882
  * ```
1857
1883
  */
1858
1884
  async createOrder(body) {
1859
- return this.executeRequest(() => this.storefrontClient.POST("/pos/orders", { body }));
1885
+ return this.executeRequest(() => this.client.POST("/pos/orders", { body }));
1886
+ }
1887
+ /**
1888
+ * Create a payment request for an order
1889
+ *
1890
+ * Use this when the order was created without a payment method, or to
1891
+ * collect the remaining `to_be_paid` amount with another method (split
1892
+ * tender). The total requested across payment requests cannot exceed the
1893
+ * order's `to_be_paid`.
1894
+ * @param pathParams - Order number
1895
+ * @param body - Amount to request and the payment method to use
1896
+ * @returns Promise with all payment records for the order, the remaining
1897
+ * `pending_amount`, and gateway initiation details (`payment_info`) when
1898
+ * an auto payment method was used
1899
+ * @example
1900
+ * ```typescript
1901
+ * const { data, error } = await pos.createOrderPaymentRequest(
1902
+ * { order_number: "1234567890" },
1903
+ * { amount: 500, payment_method: { payment_provider_slug: "cash" } }
1904
+ * );
1905
+ *
1906
+ * if (error) {
1907
+ * console.error("Failed to create payment request:", error.message);
1908
+ * } else {
1909
+ * console.log("Pending amount:", data.pending_amount);
1910
+ * console.log("Payments:", data.payments.length);
1911
+ * }
1912
+ * ```
1913
+ */
1914
+ async createOrderPaymentRequest(pathParams, body) {
1915
+ return this.executeRequest(() => this.client.POST("/pos/orders/{order_number}/payments", {
1916
+ params: { path: pathParams },
1917
+ body
1918
+ }));
1860
1919
  }
1861
1920
  /**
1862
1921
  * Get payment status
@@ -1880,6 +1939,124 @@ var PosClient = class extends PosAPIClient {
1880
1939
  return this.executeRequest(() => this.client.GET("/pos/orders/{order_number}/payment-status", { params: { path: pathParams } }));
1881
1940
  }
1882
1941
  /**
1942
+ * List available payment methods
1943
+ * @param query - Optional query parameters (e.g. amount for method filtering)
1944
+ * @returns Promise with the payment methods enabled for the store
1945
+ * @example
1946
+ * ```typescript
1947
+ * const { data, error } = await pos.listPaymentMethods();
1948
+ *
1949
+ * if (error) {
1950
+ * console.error("Failed to list payment methods:", error.message);
1951
+ * } else {
1952
+ * data.payment_methods.forEach(method => {
1953
+ * console.log(`${method.name} (${method.code})`);
1954
+ * });
1955
+ * }
1956
+ * ```
1957
+ */
1958
+ async listPaymentMethods(query) {
1959
+ return this.executeRequest(() => this.client.GET("/pos/payments/payment-methods", { params: { query } }));
1960
+ }
1961
+ /**
1962
+ * Verify a UPI VPA (virtual payment address)
1963
+ * @param query - The VPA to verify
1964
+ * @returns Promise with the verification result and account holder name
1965
+ * @example
1966
+ * ```typescript
1967
+ * const { data, error } = await pos.verifyVpa({ vpa: "customer@upi" });
1968
+ *
1969
+ * if (error) {
1970
+ * console.error("VPA verification failed:", error.message);
1971
+ * } else if (data.is_valid) {
1972
+ * console.log("Paying to:", data.customer_name);
1973
+ * }
1974
+ * ```
1975
+ */
1976
+ async verifyVpa(query) {
1977
+ return this.executeRequest(() => this.client.GET("/pos/payments/verify-vpa", { params: { query } }));
1978
+ }
1979
+ /**
1980
+ * Get card metadata for a card number prefix
1981
+ * @param query - Card number prefix (BIN) to look up
1982
+ * @returns Promise with card brand, type, and issuer details
1983
+ * @example
1984
+ * ```typescript
1985
+ * const { data, error } = await pos.getCardInfo({ card_number: "411111" });
1986
+ *
1987
+ * if (error) {
1988
+ * console.error("Card lookup failed:", error.message);
1989
+ * } else {
1990
+ * console.log(`${data.card_brand} ${data.card_type}`);
1991
+ * }
1992
+ * ```
1993
+ */
1994
+ async getCardInfo(query) {
1995
+ return this.executeRequest(() => this.client.GET("/pos/payments/card-info", { params: { query } }));
1996
+ }
1997
+ /**
1998
+ * Authenticate a direct (headless) card payment with an OTP
1999
+ * @param body - OTP authentication payload from the payment flow
2000
+ * @returns Promise with the authentication result
2001
+ * @example
2002
+ * ```typescript
2003
+ * const { data, error } = await pos.authenticateDirectOtp({
2004
+ * transaction_id: "txn_123",
2005
+ * otp: "123456"
2006
+ * });
2007
+ *
2008
+ * if (error) {
2009
+ * console.error("OTP authentication failed:", error.message);
2010
+ * }
2011
+ * ```
2012
+ */
2013
+ async authenticateDirectOtp(body) {
2014
+ return this.executeRequest(() => this.client.POST("/pos/payments/authenticate-direct-otp", { body }));
2015
+ }
2016
+ /**
2017
+ * Resend the OTP for a direct (headless) card payment
2018
+ * @param body - The transaction whose OTP should be resent
2019
+ * @returns Promise with the resend confirmation
2020
+ * @example
2021
+ * ```typescript
2022
+ * const { data, error } = await pos.resendDirectOtp({
2023
+ * transaction_id: "txn_123"
2024
+ * });
2025
+ *
2026
+ * if (error) {
2027
+ * console.error("Failed to resend OTP:", error.message);
2028
+ * }
2029
+ * ```
2030
+ */
2031
+ async resendDirectOtp(body) {
2032
+ return this.executeRequest(() => this.client.POST("/pos/payments/resend-direct-otp", { body }));
2033
+ }
2034
+ /**
2035
+ * Retry payment for an order whose previous payment failed or is unpaid
2036
+ * @param pathParams - Order number
2037
+ * @param body - Payment method details for the retry
2038
+ * @returns Promise with fresh payment info for the retried payment
2039
+ * @example
2040
+ * ```typescript
2041
+ * const { data, error } = await pos.retryOrderPayment(
2042
+ * { order_number: "ORD-2024-001" },
2043
+ * { payment_method: "upi" }
2044
+ * );
2045
+ *
2046
+ * if (error) {
2047
+ * console.error("Payment retry failed:", error.message);
2048
+ * } else {
2049
+ * console.log("New payment initiated:", data.payment_info);
2050
+ * }
2051
+ * ```
2052
+ */
2053
+ async retryOrderPayment(pathParams, body) {
2054
+ return this.executeRequest(() => this.client.POST("/pos/orders/{order_number}/retry-payment", {
2055
+ params: { path: pathParams },
2056
+ body
2057
+ }));
2058
+ }
2059
+ /**
1883
2060
  * List all categories
1884
2061
  * @param query - Optional query parameters for filtering categories
1885
2062
  * @returns Promise with list of categories
@@ -2229,43 +2406,6 @@ var PosClient = class extends PosAPIClient {
2229
2406
  } }));
2230
2407
  }
2231
2408
  /**
2232
- * List product reviews
2233
- * @param pathParams - Product ID
2234
- * @param query - Optional query parameters for filtering reviews
2235
- * @returns Promise with product reviews
2236
- * @example
2237
- * ```typescript
2238
- * const { data, error } = await pos.listProductReviews(
2239
- * { product_id: "prod_123" }
2240
- * );
2241
- *
2242
- * if (error) {
2243
- * console.error("Failed to list product reviews:", error.message);
2244
- * } else {
2245
- * console.log("Reviews found:", data.reviews?.length || 0);
2246
- * data.reviews?.forEach(review => {
2247
- * console.log(`Review by ${review.customer_name}: ${review.rating}/5`);
2248
- * console.log("Comment:", review.comment);
2249
- * });
2250
- * }
2251
- *
2252
- * // With pagination
2253
- * const { data: reviewData, error: reviewError } = await pos.listProductReviews(
2254
- * { product_id: "prod_123" },
2255
- * {
2256
- * page: 1,
2257
- * limit: 5
2258
- * }
2259
- * );
2260
- * ```
2261
- */
2262
- async listProductReviews(pathParams, query) {
2263
- return this.executeRequest(() => this.client.GET("/pos/catalog/products/{product_id}/reviews", { params: {
2264
- path: pathParams,
2265
- query
2266
- } }));
2267
- }
2268
- /**
2269
2409
  * List product variants
2270
2410
  * @param pathParams - The path parameters. Accepts product ID or product slug.
2271
2411
  * @param headers - Optional header parameters
@@ -2560,6 +2700,142 @@ var PosClient = class extends PosAPIClient {
2560
2700
  return this.executeRequest(() => this.client.GET("/pos/customers/{id}", { params: { path: pathParams } }));
2561
2701
  }
2562
2702
  /**
2703
+ * List a customer's saved addresses (Admin)
2704
+ * @param pathParams - Customer ID
2705
+ * @returns Promise with the customer's addresses
2706
+ * @example
2707
+ * ```typescript
2708
+ * const { data, error } = await pos.listAddresses({ id: "cust_123" });
2709
+ *
2710
+ * if (error) {
2711
+ * console.error("Failed to list addresses:", error.message);
2712
+ * } else {
2713
+ * data.addresses?.forEach(address => {
2714
+ * console.log(`${address.name}: ${address.city}, ${address.state}`);
2715
+ * });
2716
+ * }
2717
+ * ```
2718
+ */
2719
+ async listAddresses(pathParams) {
2720
+ return this.executeRequest(() => this.client.GET("/pos/customers/{id}/addresses", { params: { path: pathParams } }));
2721
+ }
2722
+ /**
2723
+ * Create an address on a customer's profile (Admin)
2724
+ * @param pathParams - Customer ID
2725
+ * @param body - The address to save
2726
+ * @returns Promise with the created address
2727
+ * @example
2728
+ * ```typescript
2729
+ * const { data, error } = await pos.createAddress(
2730
+ * { id: "cust_123" },
2731
+ * {
2732
+ * name: "Home",
2733
+ * address_line_1: "123 Main St",
2734
+ * city: "Mumbai",
2735
+ * state: "Maharashtra",
2736
+ * country: "India",
2737
+ * pincode: "400001",
2738
+ * phone: "9876543210"
2739
+ * }
2740
+ * );
2741
+ *
2742
+ * if (error) {
2743
+ * console.error("Failed to create address:", error.message);
2744
+ * }
2745
+ * ```
2746
+ */
2747
+ async createAddress(pathParams, body) {
2748
+ return this.executeRequest(() => this.client.POST("/pos/customers/{id}/addresses", {
2749
+ params: { path: pathParams },
2750
+ body
2751
+ }));
2752
+ }
2753
+ /**
2754
+ * Update a customer's saved address (Admin)
2755
+ * @param pathParams - Customer ID and address ID
2756
+ * @param body - The address fields to update
2757
+ * @returns Promise with the updated address
2758
+ * @example
2759
+ * ```typescript
2760
+ * const { data, error } = await pos.updateAddress(
2761
+ * { id: "cust_123", address_id: "addr_456" },
2762
+ * { phone: "9876543210" }
2763
+ * );
2764
+ *
2765
+ * if (error) {
2766
+ * console.error("Failed to update address:", error.message);
2767
+ * }
2768
+ * ```
2769
+ */
2770
+ async updateAddress(pathParams, body) {
2771
+ return this.executeRequest(() => this.client.PUT("/pos/customers/{id}/addresses/{address_id}", {
2772
+ params: { path: pathParams },
2773
+ body
2774
+ }));
2775
+ }
2776
+ /**
2777
+ * Delete a customer's saved address (Admin)
2778
+ * @param pathParams - Customer ID and address ID
2779
+ * @returns Promise with the deletion confirmation
2780
+ * @example
2781
+ * ```typescript
2782
+ * const { error } = await pos.deleteAddress({
2783
+ * id: "cust_123",
2784
+ * address_id: "addr_456"
2785
+ * });
2786
+ *
2787
+ * if (error) {
2788
+ * console.error("Failed to delete address:", error.message);
2789
+ * }
2790
+ * ```
2791
+ */
2792
+ async deleteAddress(pathParams) {
2793
+ return this.executeRequest(() => this.client.DELETE("/pos/customers/{id}/addresses/{address_id}", { params: { path: pathParams } }));
2794
+ }
2795
+ /**
2796
+ * List states for a country
2797
+ * @param pathParams - ISO country code
2798
+ * @returns Promise with the country's states
2799
+ * @example
2800
+ * ```typescript
2801
+ * const { data, error } = await pos.listCountryStates({
2802
+ * country_iso_code: "IN"
2803
+ * });
2804
+ *
2805
+ * if (error) {
2806
+ * console.error("Failed to list states:", error.message);
2807
+ * } else {
2808
+ * data.states?.forEach(state => console.log(state.name));
2809
+ * }
2810
+ * ```
2811
+ */
2812
+ async listCountryStates(pathParams) {
2813
+ return this.executeRequest(() => this.client.GET("/pos/common/countries/{country_iso_code}/states", { params: { path: pathParams } }));
2814
+ }
2815
+ /**
2816
+ * List pincodes for a country
2817
+ * @param pathParams - ISO country code
2818
+ * @param query - Optional query parameters for search and pagination
2819
+ * @returns Promise with the country's serviceable pincodes
2820
+ * @example
2821
+ * ```typescript
2822
+ * const { data, error } = await pos.listCountryPincodes(
2823
+ * { country_iso_code: "IN" },
2824
+ * { search: "4000" }
2825
+ * );
2826
+ *
2827
+ * if (error) {
2828
+ * console.error("Failed to list pincodes:", error.message);
2829
+ * }
2830
+ * ```
2831
+ */
2832
+ async listCountryPincodes(pathParams, query) {
2833
+ return this.executeRequest(() => this.client.GET("/pos/common/countries/{country_iso_code}/pincodes", { params: {
2834
+ path: pathParams,
2835
+ query
2836
+ } }));
2837
+ }
2838
+ /**
2563
2839
  * List all orders (Admin)
2564
2840
  * @param query - Optional query parameters for filtering orders
2565
2841
  * @returns Promise with list of orders
@@ -2589,7 +2865,7 @@ var PosClient = class extends PosAPIClient {
2589
2865
  * ```
2590
2866
  */
2591
2867
  async listOrders(query) {
2592
- return this.executeRequest(() => this.adminClient.GET("/pos/orders", { params: { query } }));
2868
+ return this.executeRequest(() => this.client.GET("/pos/orders", { params: { query } }));
2593
2869
  }
2594
2870
  /**
2595
2871
  * Get order details (Admin)
@@ -2778,9 +3054,13 @@ var PosClient = class extends PosAPIClient {
2778
3054
  /**
2779
3055
  * Check inventory for order (Admin)
2780
3056
  * @param pathParams - Order number
3057
+ * @param query - Optional shipment number and fulfillment type to evaluate against.
3058
+ * Defaults to the order's `unscheduled` shipment and that shipment's own
3059
+ * `fulfillment_type` when omitted.
2781
3060
  * @returns Promise with inventory check results
2782
3061
  * @example
2783
3062
  * ```typescript
3063
+ * // Check the order's unscheduled shipment
2784
3064
  * const { data, error } = await pos.checkInventory({
2785
3065
  * order_number: "ORD-2024-001234"
2786
3066
  * });
@@ -2788,17 +3068,31 @@ var PosClient = class extends PosAPIClient {
2788
3068
  * if (error) {
2789
3069
  * console.error("Failed to check inventory:", error.message);
2790
3070
  * } else {
2791
- * const inventory = data.content;
3071
+ * const inventory = data.inventory;
2792
3072
  * console.log(`Inventory Status: ${inventory?.inventory_status}`);
2793
3073
  * console.log(`Shipment Items:`, inventory?.shipment_items);
2794
3074
  * console.log(`Recommended Warehouses:`, inventory?.recommended_warehouses);
2795
3075
  * console.log(`Inventory Detail:`, inventory?.inventory_detail);
2796
- * console.log(`Allowed Actions:`, inventory?.allowed_action);
3076
+ * console.log(`Allowed Actions:`, inventory?.allowed_actions);
2797
3077
  * }
3078
+ *
3079
+ * // Check a specific shipment, evaluated as an in-store collection
3080
+ * const { data: pickupData } = await pos.checkInventory(
3081
+ * { order_number: "ORD-2024-001234" },
3082
+ * {
3083
+ * shipment_number: "SHIP-2024-001234",
3084
+ * fulfillment_type: "collect-in-store"
3085
+ * }
3086
+ * );
3087
+ *
3088
+ * console.log(`Can fulfill from store:`, pickupData?.inventory?.inventory_status);
2798
3089
  * ```
2799
3090
  */
2800
- async checkInventory(pathParams) {
2801
- return this.executeRequest(() => this.adminClient.GET("/pos/orders/{order_number}/check-inventory", { params: { path: pathParams } }));
3091
+ async checkInventory(pathParams, query) {
3092
+ return this.executeRequest(() => this.client.GET("/pos/orders/{order_number}/check-inventory", { params: {
3093
+ path: pathParams,
3094
+ query
3095
+ } }));
2802
3096
  }
2803
3097
  /**
2804
3098
  * Refund shortfall for order (Admin)
@@ -2873,6 +3167,30 @@ var PosClient = class extends PosAPIClient {
2873
3167
  return this.executeRequest(() => this.client.GET("/pos/shipping/shipments/{reference_number}", { params: { path: pathParams } }));
2874
3168
  }
2875
3169
  /**
3170
+ * Get shipment activities (Admin)
3171
+ * @param pathParams - Shipment reference number
3172
+ * @returns Promise with the shipment's activity trail
3173
+ * @example
3174
+ * ```typescript
3175
+ * const { data, error } = await pos.getShipmentActivities({
3176
+ * reference_number: "SHIP-2024-001234"
3177
+ * });
3178
+ *
3179
+ * if (error) {
3180
+ * console.error("Failed to get shipment activities:", error.message);
3181
+ * } else {
3182
+ * data.activities?.forEach(activity => {
3183
+ * console.log(`[${activity.created_at}] ${activity.activity_type} - ${activity.status}`);
3184
+ * console.log(` ${activity.comment} (by ${activity.user_name}, ${activity.user_type})`);
3185
+ * });
3186
+ * console.log(`Total activities: ${data.pagination?.total_records}`);
3187
+ * }
3188
+ * ```
3189
+ */
3190
+ async getShipmentActivities(pathParams) {
3191
+ return this.executeRequest(() => this.client.GET("/pos/shipping/shipments/{reference_number}/activities", { params: { path: pathParams } }));
3192
+ }
3193
+ /**
2876
3194
  * Get shipment invoice (Admin)
2877
3195
  * @param pathParams - Shipment reference number
2878
3196
  * @param query - Optional format parameter
@@ -2952,7 +3270,703 @@ var PosClient = class extends PosAPIClient {
2952
3270
  body
2953
3271
  }));
2954
3272
  }
3273
+ /**
3274
+ * Create a replacement shipment against an existing shipment (Admin)
3275
+ * @param pathParams - Reference number of the original shipment being replaced
3276
+ * @param body - Replacement payload. Use `shipping_option: "manual"` to dispatch
3277
+ * outside an integrated carrier, `shipping_option: "auto"` to fulfill through an
3278
+ * integrated carrier with system-managed rates, or `fulfillment_type:
3279
+ * "collect-in-store"` when the customer picks the items up in store.
3280
+ * @returns Promise with replacement confirmation
3281
+ * @example
3282
+ * ```typescript
3283
+ * // Replacement shipped through an integrated carrier, system-packed
3284
+ * const { data, error } = await pos.createReplacementShipment(
3285
+ * { reference_number: "SHIP-2024-001234" },
3286
+ * {
3287
+ * fulfillment_type: "delivery",
3288
+ * warehouse_id: "WH-001",
3289
+ * shipment_items: [
3290
+ * { product_id: "PROD-123", variant_id: "VAR-456", quantity: 1 }
3291
+ * ],
3292
+ * shipping_option: "auto",
3293
+ * shipping_provider_id: "SP-001",
3294
+ * packing_option: "auto",
3295
+ * total_weight: 1.5,
3296
+ * reason: "Damaged on delivery"
3297
+ * }
3298
+ * );
3299
+ *
3300
+ * if (error) {
3301
+ * console.error("Failed to create replacement:", error.message);
3302
+ * } else {
3303
+ * console.log("Replacement created:", data.message);
3304
+ * }
3305
+ *
3306
+ * // Replacement dispatched manually, with explicit box packing
3307
+ * const { data: manualData } = await pos.createReplacementShipment(
3308
+ * { reference_number: "SHIP-2024-001234" },
3309
+ * {
3310
+ * fulfillment_type: "delivery",
3311
+ * warehouse_id: "WH-001",
3312
+ * shipment_items: [
3313
+ * { product_id: "PROD-123", variant_id: null, quantity: 2 }
3314
+ * ],
3315
+ * shipping_option: "manual",
3316
+ * shipping_provider_id: "SP-MANUAL-01",
3317
+ * packing_option: "manual",
3318
+ * boxes: [
3319
+ * {
3320
+ * box_name: "Medium",
3321
+ * box_length: 30,
3322
+ * box_width: 20,
3323
+ * box_height: 15,
3324
+ * box_weight: 2,
3325
+ * items_count: 2,
3326
+ * box_count: 1
3327
+ * }
3328
+ * ],
3329
+ * total_weight: 2,
3330
+ * expected_delivery_date: "2024-01-20",
3331
+ * tracking_link: "https://tracking.example.com/AWB123456789",
3332
+ * manual_shipping_charges: 120,
3333
+ * reason: "Wrong item shipped"
3334
+ * }
3335
+ * );
3336
+ *
3337
+ * // Replacement collected by the customer in store
3338
+ * const { data: pickupData } = await pos.createReplacementShipment(
3339
+ * { reference_number: "SHIP-2024-001234" },
3340
+ * {
3341
+ * fulfillment_type: "collect-in-store",
3342
+ * warehouse_id: "STORE-042",
3343
+ * shipment_items: [
3344
+ * { product_id: "PROD-123", variant_id: "VAR-456", quantity: 1 }
3345
+ * ],
3346
+ * shipping_option: "manual",
3347
+ * shipping_provider_id: "SP-MANUAL-01",
3348
+ * packing_option: "auto",
3349
+ * reason: "Size exchange"
3350
+ * }
3351
+ * );
3352
+ * ```
3353
+ */
3354
+ async createReplacementShipment(pathParams, body) {
3355
+ return this.executeRequest(() => this.client.POST("/pos/shipping/shipments/{reference_number}/replacement", {
3356
+ params: { path: pathParams },
3357
+ body
3358
+ }));
3359
+ }
3360
+ };
3361
+ //#endregion
3362
+ //#region src/lib/auth-token-storage.ts
3363
+ const DEFAULT_PREFIX = "ce_pos_";
3364
+ const AUTH_SESSION_LOCK = "ce-pos-auth-session";
3365
+ const AUTH_CONTEXT_LOCK = "ce-pos-auth-context";
3366
+ const SESSION_VERSION = 1;
3367
+ const browserStorage = () => {
3368
+ try {
3369
+ return typeof localStorage === "undefined" ? null : localStorage;
3370
+ } catch {
3371
+ return null;
3372
+ }
3373
+ };
3374
+ const browserLocks = () => {
3375
+ try {
3376
+ return typeof navigator === "undefined" ? null : navigator.locks ?? null;
3377
+ } catch {
3378
+ return null;
3379
+ }
3380
+ };
3381
+ const newRevision = () => {
3382
+ try {
3383
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
3384
+ } catch {}
3385
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3386
+ };
3387
+ const tokenNeedsRefresh = (token) => {
3388
+ try {
3389
+ const payloadPart = token.split(".")[1];
3390
+ if (!payloadPart) return true;
3391
+ let base64 = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
3392
+ const padding = base64.length % 4;
3393
+ if (padding) base64 += "=".repeat(4 - padding);
3394
+ const payload = JSON.parse(atob(base64));
3395
+ return typeof payload.exp !== "number" || Math.floor(Date.now() / 1e3) >= payload.exp - 30;
3396
+ } catch {
3397
+ return true;
3398
+ }
3399
+ };
3400
+ const isNonemptyString = (value) => typeof value === "string" && value.length > 0;
3401
+ const normalizeStoredTokenPair = (value) => {
3402
+ if (!value || typeof value !== "object") throw new Error("Invalid POS auth token pair.");
3403
+ const tokens = value;
3404
+ const hasCanonicalShape = "access_token" in tokens || "refresh_token" in tokens;
3405
+ const hasLegacyShape = "accessToken" in tokens || "refreshToken" in tokens;
3406
+ if (hasCanonicalShape === hasLegacyShape) throw new Error("Invalid POS auth token pair.");
3407
+ const accessToken = hasCanonicalShape ? tokens.access_token : tokens.accessToken;
3408
+ const refreshToken = hasCanonicalShape ? tokens.refresh_token : tokens.refreshToken;
3409
+ if (!isNonemptyString(accessToken) || !isNonemptyString(refreshToken)) throw new Error("Invalid POS auth token pair.");
3410
+ return {
3411
+ pair: {
3412
+ access_token: accessToken,
3413
+ refresh_token: refreshToken
3414
+ },
3415
+ wasLegacy: hasLegacyShape
3416
+ };
3417
+ };
3418
+ const parseSession = (raw) => {
3419
+ const parsed = JSON.parse(raw);
3420
+ if (!parsed || typeof parsed !== "object") throw new Error("Invalid POS auth session record.");
3421
+ const record = parsed;
3422
+ if (record.version !== SESSION_VERSION || !isNonemptyString(record.revision)) throw new Error("Unsupported POS auth session record.");
3423
+ if (record.tokens === null) return {
3424
+ session: {
3425
+ version: SESSION_VERSION,
3426
+ revision: record.revision,
3427
+ tokens: null
3428
+ },
3429
+ wasLegacy: false
3430
+ };
3431
+ const normalized = normalizeStoredTokenPair(record.tokens);
3432
+ return {
3433
+ session: {
3434
+ version: SESSION_VERSION,
3435
+ revision: record.revision,
3436
+ tokens: normalized.pair
3437
+ },
3438
+ wasLegacy: normalized.wasLegacy
3439
+ };
3440
+ };
3441
+ /**
3442
+ * Browser token storage with one same-origin session owner.
3443
+ *
3444
+ * Revision and both tokens live in one versioned localStorage value. A Web
3445
+ * Storage write is atomic, so process termination can expose either the old
3446
+ * complete session or the new complete session, never a mixed token pair or an
3447
+ * old pair relabelled with a new owner revision.
3448
+ */
3449
+ var CoordinatedBrowserTokenStorage = class {
3450
+ #sessionKey;
3451
+ #legacyAccessKey;
3452
+ #legacyRefreshKey;
3453
+ #legacyRevisionKey;
3454
+ #storage;
3455
+ #locks;
3456
+ #createRevision;
3457
+ #listeners = /* @__PURE__ */ new Set();
3458
+ #onStorage = (event) => {
3459
+ if (!this.#leaseHeld || event.key !== this.#sessionKey) return;
3460
+ try {
3461
+ if ((event.newValue === null ? null : parseSession(event.newValue).session.revision) === this.#ownedRevision) return;
3462
+ } catch {}
3463
+ this.#pendingAccess = null;
3464
+ this.#notify("stale");
3465
+ };
3466
+ #ownedRevision = null;
3467
+ #leaseReady;
3468
+ #leaseHeld = false;
3469
+ #releaseLease = null;
3470
+ #lastReadRefreshToken = null;
3471
+ #refreshHandler = null;
3472
+ #refreshInFlight = null;
3473
+ #pendingAccess = null;
3474
+ constructor(prefix = DEFAULT_PREFIX, options = {}) {
3475
+ this.#sessionKey = `${prefix}auth_session`;
3476
+ this.#legacyAccessKey = `${prefix}access_token`;
3477
+ this.#legacyRefreshKey = `${prefix}refresh_token`;
3478
+ this.#legacyRevisionKey = `${prefix}auth_revision`;
3479
+ this.#storage = options.storage ?? browserStorage;
3480
+ this.#locks = options.locks ?? browserLocks;
3481
+ this.#createRevision = options.createRevision ?? newRevision;
3482
+ this.#leaseReady = this.#acquireContextLease();
3483
+ try {
3484
+ if (typeof window !== "undefined") window.addEventListener("storage", this.#onStorage);
3485
+ } catch {}
3486
+ }
3487
+ onOwnershipLost(listener) {
3488
+ this.#listeners.add(listener);
3489
+ return () => this.#listeners.delete(listener);
3490
+ }
3491
+ setRefreshHandler(handler) {
3492
+ this.#refreshHandler = handler;
3493
+ }
3494
+ waitForContextLease() {
3495
+ return this.#leaseReady;
3496
+ }
3497
+ /** Test/lifecycle hook; a destroyed page releases the Web Lock implicitly. */
3498
+ releaseContextLease() {
3499
+ this.#releaseLease?.();
3500
+ this.#releaseLease = null;
3501
+ }
3502
+ /** Claim credentials already written by an SDK/login test boundary. */
3503
+ adoptExistingSessionOwner() {
3504
+ if (!this.#leaseHeld) {
3505
+ this.#notify("storage");
3506
+ return null;
3507
+ }
3508
+ return this.#beginSessionOwner(true);
3509
+ }
3510
+ /** Logout retains its exact bearer pair until the server request is sent. */
3511
+ beginLogoutSession() {
3512
+ if (!this.#leaseHeld) {
3513
+ this.#notify("storage");
3514
+ return null;
3515
+ }
3516
+ return this.#beginSessionOwner(true);
3517
+ }
3518
+ /** Login atomically advances ownership and retires the previous pair. */
3519
+ async beginLoginSession() {
3520
+ if (!this.#leaseHeld && !await this.#leaseReady) {
3521
+ this.#notify("storage");
3522
+ return false;
3523
+ }
3524
+ if (!this.#leaseHeld) {
3525
+ this.#notify("storage");
3526
+ return false;
3527
+ }
3528
+ return this.#beginSessionOwner(false) !== null;
3529
+ }
3530
+ #beginSessionOwner(preservePair) {
3531
+ this.#pendingAccess = null;
3532
+ this.#lastReadRefreshToken = null;
3533
+ try {
3534
+ const current = this.#readSessionOrMigrate();
3535
+ const revision = this.#createRevision();
3536
+ const next = {
3537
+ version: SESSION_VERSION,
3538
+ revision,
3539
+ tokens: preservePair ? current?.tokens ?? null : null
3540
+ };
3541
+ this.#writeSession(next);
3542
+ this.#ownedRevision = revision;
3543
+ return revision;
3544
+ } catch {
3545
+ this.#notify("storage");
3546
+ return null;
3547
+ }
3548
+ }
3549
+ captureOwner() {
3550
+ try {
3551
+ if (!this.#leaseHeld) throw new Error("This browser context does not own the POS session.");
3552
+ const session = this.#readSessionOrMigrate();
3553
+ return {
3554
+ revision: this.#ownedRevision,
3555
+ accessToken: session?.tokens?.access_token ?? null,
3556
+ refreshToken: session?.tokens?.refresh_token ?? null
3557
+ };
3558
+ } catch {
3559
+ this.#notify("storage");
3560
+ return {
3561
+ revision: this.#ownedRevision,
3562
+ accessToken: null,
3563
+ refreshToken: null
3564
+ };
3565
+ }
3566
+ }
3567
+ owns(owner) {
3568
+ return this.#leaseHeld && owner.revision === this.#ownedRevision && this.#ownsSharedRevision(owner.revision);
3569
+ }
3570
+ sharesRevision(owner) {
3571
+ if (owner.revision === null) return false;
3572
+ try {
3573
+ return this.#readSessionOrMigrate()?.revision === owner.revision;
3574
+ } catch {
3575
+ this.#notify("storage");
3576
+ return false;
3577
+ }
3578
+ }
3579
+ ownsCurrentSession() {
3580
+ return this.#ownsSharedRevision(this.#ownedRevision);
3581
+ }
3582
+ /** Pure ownership probe for transaction code that must not mutate auth. */
3583
+ isCurrentSessionOwner() {
3584
+ if (!this.#leaseHeld) return false;
3585
+ try {
3586
+ const sharedRevision = this.#readSessionOrMigrate()?.revision ?? null;
3587
+ return sharedRevision === null || this.#ownedRevision !== null && sharedRevision === this.#ownedRevision;
3588
+ } catch {
3589
+ return false;
3590
+ }
3591
+ }
3592
+ hasCurrentPair(accessToken, refreshToken) {
3593
+ if (!this.#ownsSharedRevision(this.#ownedRevision)) return false;
3594
+ try {
3595
+ const pair = this.#readSessionOrMigrate()?.tokens;
3596
+ return pair?.access_token === accessToken && pair.refresh_token === refreshToken;
3597
+ } catch {
3598
+ this.#notify("storage");
3599
+ return false;
3600
+ }
3601
+ }
3602
+ assertCurrentSession() {
3603
+ const current = this.ownsCurrentSession();
3604
+ if (!current) this.#notify("stale");
3605
+ return current;
3606
+ }
3607
+ /** Used by the SDK callback after its request-owned clear attempt. */
3608
+ notifySdkTokensCleared() {
3609
+ this.#notify("stale");
3610
+ }
3611
+ async getAccessToken() {
3612
+ return (await this.getRequestOwner()).accessToken;
3613
+ }
3614
+ /**
3615
+ * Atomically capture the bearer and revision for one SDK request.
3616
+ *
3617
+ * Return the private operation's promise directly. That operation performs
3618
+ * any proactive refresh and captures both fields in its final synchronous
3619
+ * continuation, leaving no second await where a same-token revision change
3620
+ * could pair an old bearer with a new owner.
3621
+ */
3622
+ getRequestOwner() {
3623
+ const initiatingRevision = this.#ownedRevision;
3624
+ return this.#readRequestOwner(initiatingRevision);
3625
+ }
3626
+ async #readRequestOwner(initiatingRevision) {
3627
+ await this.#requireLease();
3628
+ this.#assertRequestRevision(initiatingRevision);
3629
+ const pair = this.#readPairOrThrow();
3630
+ const accessToken = pair?.access_token ?? null;
3631
+ const refreshToken = pair?.refresh_token;
3632
+ if (accessToken && tokenNeedsRefresh(accessToken) && refreshToken && this.#refreshHandler) await this.#refreshOwnedPair({
3633
+ revision: initiatingRevision,
3634
+ accessToken,
3635
+ refreshToken
3636
+ });
3637
+ this.#assertRequestRevision(initiatingRevision);
3638
+ const current = this.#readSessionOrThrow();
3639
+ return {
3640
+ accessToken: current.tokens?.access_token ?? null,
3641
+ refreshToken: current.tokens?.refresh_token ?? null,
3642
+ revision: current.revision
3643
+ };
3644
+ }
3645
+ /**
3646
+ * Recover one request whose bearer the server rejected before its JWT expiry.
3647
+ *
3648
+ * The SDK's response middleware refreshes a 401 only after its own expiry
3649
+ * check has elapsed. A server-side expiry can lead that clock, so long-lived
3650
+ * pollers use this request-owned boundary to join the same storage
3651
+ * single-flight and retry once with the rotated pair.
3652
+ */
3653
+ async recoverAfterUnauthorized(owner) {
3654
+ try {
3655
+ await this.#requireLease();
3656
+ } catch {
3657
+ return "failed";
3658
+ }
3659
+ if (this.#pairReplaced(owner)) return "refreshed";
3660
+ if (!this.owns(owner) || !owner.accessToken || !owner.refreshToken || !this.#refreshHandler) return "stale";
3661
+ try {
3662
+ await this.#refreshOwnedPair(owner);
3663
+ } catch {
3664
+ return this.#pairReplaced(owner) ? "refreshed" : "failed";
3665
+ }
3666
+ if (this.#pairReplaced(owner)) return "refreshed";
3667
+ return this.owns(owner) ? "failed" : "stale";
3668
+ }
3669
+ async #refreshOwnedPair(owner) {
3670
+ const refreshHandler = this.#refreshHandler;
3671
+ const refreshToken = owner.refreshToken;
3672
+ if (!refreshHandler || !refreshToken) throw new Error("No POS refresh token is available.");
3673
+ const refreshOperation = this.#refreshInFlight ?? (async () => {
3674
+ try {
3675
+ await this.#runSdkRefresh(owner.revision, refreshToken, (ownedAccessToken) => refreshHandler(refreshToken, ownedAccessToken));
3676
+ } catch (error) {
3677
+ if (this.#pairReplaced(owner)) return;
3678
+ const outcome = await this.#clearOwnedPair(owner);
3679
+ if (outcome === "cleared") this.#notify("stale");
3680
+ if (outcome === "failed") this.#notify("storage");
3681
+ if (outcome !== "stale") throw error;
3682
+ }
3683
+ })();
3684
+ this.#refreshInFlight = refreshOperation;
3685
+ try {
3686
+ await refreshOperation;
3687
+ } finally {
3688
+ if (this.#refreshInFlight === refreshOperation) this.#refreshInFlight = null;
3689
+ }
3690
+ }
3691
+ #pairReplaced(owner) {
3692
+ try {
3693
+ const current = this.#readSessionOrMigrate();
3694
+ return current?.revision === owner.revision && current.tokens !== null && current.tokens.access_token !== owner.accessToken;
3695
+ } catch {
3696
+ this.#notify("storage");
3697
+ return false;
3698
+ }
3699
+ }
3700
+ async getRefreshToken() {
3701
+ await this.#requireLease();
3702
+ this.#assertRequestOwner();
3703
+ const token = this.#readPairOrThrow()?.refresh_token ?? null;
3704
+ this.#lastReadRefreshToken = token;
3705
+ return token;
3706
+ }
3707
+ /** Stage only; the pair becomes visible together from setRefreshToken. */
3708
+ async setAccessToken(token) {
3709
+ await this.#requireLease();
3710
+ if (!this.#ownsSharedRevision(this.#ownedRevision)) {
3711
+ this.#pendingAccess = null;
3712
+ this.#notify("stale");
3713
+ return;
3714
+ }
3715
+ this.#pendingAccess = {
3716
+ revision: this.#ownedRevision,
3717
+ expectedRefreshToken: this.#lastReadRefreshToken,
3718
+ token
3719
+ };
3720
+ }
3721
+ async setRefreshToken(token) {
3722
+ await this.#requireLease();
3723
+ const pending = this.#pendingAccess;
3724
+ this.#pendingAccess = null;
3725
+ if (!pending) {
3726
+ this.#notify("storage");
3727
+ return;
3728
+ }
3729
+ await this.#commitPair(pending.revision, pending.expectedRefreshToken, pending.token, token);
3730
+ }
3731
+ /**
3732
+ * SDK compatibility boundary.
3733
+ *
3734
+ * The patched POS middleware supplies the bearer that produced a 403/logout
3735
+ * response. An older response is therefore inert after login or refresh. SDK
3736
+ * internal cleanup without a request bearer still captures the exact current
3737
+ * pair before entering the serialized clear.
3738
+ */
3739
+ async clearTokens(expectedAccessToken, expectedRevision) {
3740
+ await this.#requireLease();
3741
+ this.#pendingAccess = null;
3742
+ const owner = this.captureOwner();
3743
+ if ((expectedAccessToken !== void 0 || expectedRevision !== void 0) && (owner.accessToken !== (expectedAccessToken ?? null) || owner.revision !== (expectedRevision ?? null))) return false;
3744
+ const outcome = await this.#clearOwnedPair(owner);
3745
+ if (outcome === "failed") this.#notify("storage");
3746
+ return outcome === "cleared";
3747
+ }
3748
+ /** Own the SDK's automatic refresh from token read through pair commit. */
3749
+ async runSdkRefresh(refreshToken, execute) {
3750
+ const initiatingRevision = this.#ownedRevision;
3751
+ return this.#runSdkRefresh(initiatingRevision, refreshToken, execute);
3752
+ }
3753
+ async #runSdkRefresh(initiatingRevision, refreshToken, execute) {
3754
+ await this.#requireLease();
3755
+ const locks = this.#locks();
3756
+ if (!locks) {
3757
+ this.#notify("storage");
3758
+ throw new Error("Cross-context token coordination is unavailable.");
3759
+ }
3760
+ return locks.request(AUTH_SESSION_LOCK, { mode: "exclusive" }, async () => {
3761
+ if (initiatingRevision !== this.#ownedRevision || !this.#ownsSharedRevision(initiatingRevision)) throw new Error("The authenticated session changed before token refresh.");
3762
+ const current = this.#readSessionOrThrow();
3763
+ if (current.tokens?.refresh_token !== refreshToken || !current.tokens.access_token) throw new Error("A newer token refresh already completed.");
3764
+ const originalPair = current.tokens;
3765
+ const pair = await execute(originalPair.access_token);
3766
+ if (!pair.access_token || !pair.refresh_token) throw new Error("Token refresh returned an incomplete pair.");
3767
+ if (initiatingRevision !== this.#ownedRevision || !this.#ownsSharedRevision(initiatingRevision)) throw new Error("The authenticated session changed during token refresh.");
3768
+ const latest = this.#readSessionOrThrow();
3769
+ if (latest.tokens?.refresh_token !== refreshToken) throw new Error("A newer token refresh already completed.");
3770
+ this.#writeSession({
3771
+ version: SESSION_VERSION,
3772
+ revision: latest.revision,
3773
+ tokens: pair
3774
+ });
3775
+ this.#lastReadRefreshToken = pair.refresh_token;
3776
+ return pair;
3777
+ });
3778
+ }
3779
+ /** Commit a side-effect-free direct refresh against its captured pair. */
3780
+ async adoptTokens(owner, accessToken, refreshToken) {
3781
+ try {
3782
+ await this.#requireLease();
3783
+ } catch {
3784
+ return "failed";
3785
+ }
3786
+ if (!this.owns(owner)) return "stale";
3787
+ return this.#commitPair(owner.revision, owner.refreshToken, accessToken, refreshToken);
3788
+ }
3789
+ #notify(reason) {
3790
+ const hadOwner = this.#ownedRevision !== null;
3791
+ this.#ownedRevision = null;
3792
+ this.#pendingAccess = null;
3793
+ this.#lastReadRefreshToken = null;
3794
+ if (reason === "stale" && !hadOwner) return;
3795
+ for (const listener of this.#listeners) listener(reason);
3796
+ }
3797
+ #acquireContextLease() {
3798
+ const locks = this.#locks();
3799
+ if (!locks) return Promise.resolve(false);
3800
+ return new Promise((resolve) => {
3801
+ let readySettled = false;
3802
+ const settleReady = (held) => {
3803
+ if (readySettled) return;
3804
+ readySettled = true;
3805
+ resolve(held);
3806
+ };
3807
+ locks.request(AUTH_CONTEXT_LOCK, {
3808
+ mode: "exclusive",
3809
+ ifAvailable: true
3810
+ }, async (lock) => {
3811
+ if (!lock) {
3812
+ settleReady(false);
3813
+ return;
3814
+ }
3815
+ try {
3816
+ const session = this.#readSessionOrMigrate();
3817
+ this.#leaseHeld = true;
3818
+ this.#ownedRevision = session?.revision ?? null;
3819
+ } catch {
3820
+ this.#leaseHeld = false;
3821
+ this.#notify("storage");
3822
+ settleReady(false);
3823
+ return;
3824
+ }
3825
+ settleReady(true);
3826
+ await new Promise((release) => {
3827
+ this.#releaseLease = release;
3828
+ });
3829
+ this.#releaseLease = null;
3830
+ this.#leaseHeld = false;
3831
+ this.#ownedRevision = null;
3832
+ this.#pendingAccess = null;
3833
+ }).catch(() => settleReady(false));
3834
+ });
3835
+ }
3836
+ async #requireLease() {
3837
+ if (await this.#leaseReady && this.#leaseHeld) return;
3838
+ this.#notify("storage");
3839
+ throw new Error("Another browser tab owns this POS session.");
3840
+ }
3841
+ #assertRequestOwner() {
3842
+ if (this.#ownsSharedRevision(this.#ownedRevision)) return;
3843
+ this.#notify("stale");
3844
+ throw new Error("The authenticated session belongs to another browser context.");
3845
+ }
3846
+ #assertRequestRevision(revision) {
3847
+ if (revision === this.#ownedRevision && this.#ownsSharedRevision(revision)) return;
3848
+ if (this.#ownsSharedRevision(this.#ownedRevision)) throw new Error("The authenticated session changed before the POS request was sent.");
3849
+ this.#notify("stale");
3850
+ throw new Error("The authenticated session belongs to another browser context.");
3851
+ }
3852
+ #ownsSharedRevision(revision) {
3853
+ if (!this.#leaseHeld || revision === null) return false;
3854
+ try {
3855
+ return this.#readSessionOrMigrate()?.revision === revision;
3856
+ } catch {
3857
+ this.#notify("storage");
3858
+ return false;
3859
+ }
3860
+ }
3861
+ #readPairOrThrow() {
3862
+ return this.#readSessionOrThrow().tokens;
3863
+ }
3864
+ #readSessionOrThrow() {
3865
+ const session = this.#readSessionOrMigrate();
3866
+ if (!session) throw new Error("No POS auth session exists.");
3867
+ return session;
3868
+ }
3869
+ #readSessionOrMigrate() {
3870
+ const storage = this.#storageOrThrow();
3871
+ const raw = storage.getItem(this.#sessionKey);
3872
+ if (raw !== null) {
3873
+ const parsed = parseSession(raw);
3874
+ if (parsed.wasLegacy) this.#writeSession(parsed.session);
3875
+ return parsed.session;
3876
+ }
3877
+ const accessToken = storage.getItem(this.#legacyAccessKey);
3878
+ const refreshToken = storage.getItem(this.#legacyRefreshKey);
3879
+ const legacyRevision = storage.getItem(this.#legacyRevisionKey);
3880
+ if (accessToken === null && refreshToken === null && legacyRevision === null) return null;
3881
+ if (accessToken === null !== (refreshToken === null)) throw new Error("Incomplete legacy POS auth token pair.");
3882
+ if (accessToken !== null && (!accessToken || !refreshToken)) throw new Error("Invalid legacy POS auth token pair.");
3883
+ if (legacyRevision !== null && !legacyRevision) throw new Error("Invalid legacy POS auth revision.");
3884
+ const migrated = {
3885
+ version: SESSION_VERSION,
3886
+ revision: legacyRevision ?? this.#createRevision(),
3887
+ tokens: accessToken === null ? null : {
3888
+ access_token: accessToken,
3889
+ refresh_token: refreshToken
3890
+ }
3891
+ };
3892
+ this.#writeSession(migrated);
3893
+ try {
3894
+ storage.removeItem(this.#legacyAccessKey);
3895
+ storage.removeItem(this.#legacyRefreshKey);
3896
+ storage.removeItem(this.#legacyRevisionKey);
3897
+ } catch {}
3898
+ return migrated;
3899
+ }
3900
+ #writeSession(session) {
3901
+ this.#storageOrThrow().setItem(this.#sessionKey, JSON.stringify(session));
3902
+ }
3903
+ #storageOrThrow() {
3904
+ const storage = this.#storage();
3905
+ if (!storage) throw new Error("Browser storage is unavailable.");
3906
+ return storage;
3907
+ }
3908
+ async #clearOwnedPair(owner) {
3909
+ const locks = this.#locks();
3910
+ if (!locks) return "failed";
3911
+ try {
3912
+ return await locks.request(AUTH_SESSION_LOCK, { mode: "exclusive" }, () => {
3913
+ if (!this.owns(owner)) return "stale";
3914
+ const current = this.#readSessionOrThrow();
3915
+ if (current.tokens?.access_token !== owner.accessToken || current.tokens?.refresh_token !== owner.refreshToken) return "stale";
3916
+ try {
3917
+ this.#writeSession({
3918
+ ...current,
3919
+ tokens: null
3920
+ });
3921
+ } catch {
3922
+ return "failed";
3923
+ }
3924
+ this.#lastReadRefreshToken = null;
3925
+ return "cleared";
3926
+ });
3927
+ } catch {
3928
+ return "failed";
3929
+ }
3930
+ }
3931
+ async #commitPair(revision, expectedRefreshToken, accessToken, refreshToken) {
3932
+ const locks = this.#locks();
3933
+ if (!locks) {
3934
+ this.#notify("storage");
3935
+ return "failed";
3936
+ }
3937
+ try {
3938
+ return await locks.request(AUTH_SESSION_LOCK, { mode: "exclusive" }, () => {
3939
+ if (revision !== this.#ownedRevision || !this.#ownsSharedRevision(revision)) {
3940
+ this.#notify("stale");
3941
+ return "stale";
3942
+ }
3943
+ const current = this.#readSessionOrThrow();
3944
+ if ((current.tokens?.refresh_token ?? null) !== expectedRefreshToken) return "stale";
3945
+ try {
3946
+ this.#writeSession({
3947
+ version: SESSION_VERSION,
3948
+ revision: current.revision,
3949
+ tokens: {
3950
+ access_token: accessToken,
3951
+ refresh_token: refreshToken
3952
+ }
3953
+ });
3954
+ } catch {
3955
+ this.#notify("storage");
3956
+ return "failed";
3957
+ }
3958
+ this.#lastReadRefreshToken = refreshToken;
3959
+ return "adopted";
3960
+ });
3961
+ } catch {
3962
+ this.#notify("storage");
3963
+ return "failed";
3964
+ }
3965
+ }
2955
3966
  };
3967
+ const hotData = import.meta.hot?.data;
3968
+ const authTokenStorage = hotData?.authTokenStorage ?? new CoordinatedBrowserTokenStorage();
3969
+ if (hotData) hotData.authTokenStorage = authTokenStorage;
2956
3970
  //#endregion
2957
3971
  //#region src/index.ts
2958
3972
  /**
@@ -2981,6 +3995,7 @@ var PosSDK = class {
2981
3995
  tokenStorage: options.tokenStorage,
2982
3996
  onTokensUpdated: options.onTokensUpdated,
2983
3997
  onTokensCleared: options.onTokensCleared,
3998
+ storeTokenResponses: options.storeTokenResponses,
2984
3999
  defaultHeaders: options.defaultHeaders,
2985
4000
  debug: options.debug,
2986
4001
  logger: options.logger
@@ -3113,6 +4128,6 @@ var PosSDK = class {
3113
4128
  }
3114
4129
  };
3115
4130
  //#endregion
3116
- export { BrowserTokenStorage, Environment, MemoryTokenStorage, PosAPIClient, PosClient, PosSDK, PosSDK as default, ResponseUtils };
4131
+ export { BrowserTokenStorage, CoordinatedBrowserTokenStorage, Environment, MemoryTokenStorage, PosAPIClient, PosClient, PosSDK, PosSDK as default, ResponseUtils, authTokenStorage };
3117
4132
 
3118
4133
  //# sourceMappingURL=index.mjs.map