@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/README.md +9 -5
- package/dist/index.d.mts +6471 -5020
- package/dist/index.mjs +1107 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -10
- package/dist/admin-types.d.mts +0 -2239
- package/dist/admin-types.mjs +0 -1
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
|
-
|
|
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
|
|
681
|
-
const
|
|
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
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
782
|
-
|
|
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
|
-
|
|
800
|
-
|
|
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 -
|
|
1843
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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?.
|
|
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.
|
|
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
|