@commercengine/pos 0.4.6 → 0.6.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
@@ -427,6 +427,696 @@ function getPathnameFromUrl(url) {
427
427
  }
428
428
  }
429
429
  //#endregion
430
+ //#region src/lib/auth-token-storage.ts
431
+ const DEFAULT_PREFIX = "ce_pos_";
432
+ const AUTH_SESSION_LOCK = "ce-pos-auth-session";
433
+ const AUTH_CONTEXT_LOCK = "ce-pos-auth-context";
434
+ const SESSION_VERSION = 1;
435
+ /**
436
+ * A refresh the server answered, and refused — or one that could not even be
437
+ * asked, because the refresh token itself has expired.
438
+ *
439
+ * Only this ends a session. Everything else a refresh can run into — no
440
+ * network, a gateway timeout, a 5xx, a response that would not parse — says
441
+ * nothing about whether the session is still good, so the tokens are kept
442
+ * and the next request tries again. A refresh handler an app installs
443
+ * (`setRefreshHandler`) says "refused" by throwing this, or any error with
444
+ * the same `name` and shape: the check is by shape, not by class, so an app
445
+ * on an older package can speak the contract.
446
+ */
447
+ var RefreshRejectedError = class extends Error {
448
+ status;
449
+ reason;
450
+ constructor(detail) {
451
+ const reason = detail.reason ?? "rejected";
452
+ super(reason === "expired" ? "No valid refresh token available" : `POS token refresh rejected: ${detail.status ?? "unknown"}`);
453
+ this.name = "RefreshRejectedError";
454
+ this.status = detail.status ?? null;
455
+ this.reason = reason;
456
+ }
457
+ };
458
+ /**
459
+ * A refresh that could not complete — no network, a timeout, a 5xx, a body
460
+ * that would not parse. Wrapped so a caller can tell it from a lost lease or
461
+ * a failed store: those are not the refresh's fault, and a request that
462
+ * would otherwise go out on the API key alone must stay closed for them.
463
+ * The cause keeps its message, so callers reading the message see the
464
+ * original failure.
465
+ */
466
+ var RefreshUnavailableError = class extends Error {
467
+ cause;
468
+ constructor(cause) {
469
+ super(cause instanceof Error ? cause.message : "POS token refresh could not complete");
470
+ this.name = "RefreshUnavailableError";
471
+ this.cause = cause;
472
+ }
473
+ };
474
+ /** Whether a thrown error came out of the refresh itself, refused or not. */
475
+ function isRefreshFailure(error) {
476
+ if (!error || typeof error !== "object") return false;
477
+ const name = error.name;
478
+ return name === "RefreshRejectedError" || name === "RefreshUnavailableError";
479
+ }
480
+ const DEFINITIVE_REFRESH_STATUSES = /* @__PURE__ */ new Set([
481
+ 400,
482
+ 401,
483
+ 403
484
+ ]);
485
+ /** Whether a refresh failure means the session is over — see `RefreshRejectedError`. */
486
+ function refreshEndsSession(error) {
487
+ if (!error || typeof error !== "object") return false;
488
+ const shaped = error;
489
+ if (shaped.name !== "RefreshRejectedError") return false;
490
+ if (shaped.reason === "expired") return true;
491
+ return typeof shaped.status === "number" && DEFINITIVE_REFRESH_STATUSES.has(shaped.status);
492
+ }
493
+ /**
494
+ * How long a new page waits for the context lock before concluding that
495
+ * another page really holds it.
496
+ *
497
+ * A reload is the common case: the old document's lock is released when it is
498
+ * destroyed, and that release and the new document's request race in the
499
+ * browser process. Asking with `ifAvailable` lost that race on a slower
500
+ * WebView every time, and a lost race read as "another tab owns this till" —
501
+ * a login screen over a session that was still on the device. Queueing for
502
+ * the lock is granted the moment the old document lets go, which is
503
+ * milliseconds; only a page that is genuinely still open keeps it this long.
504
+ */
505
+ const DEFAULT_LEASE_WAIT_MS = 4e3;
506
+ const browserStorage = () => {
507
+ try {
508
+ return typeof localStorage === "undefined" ? null : localStorage;
509
+ } catch {
510
+ return null;
511
+ }
512
+ };
513
+ const browserLocks = () => {
514
+ try {
515
+ return typeof navigator === "undefined" ? null : navigator.locks ?? null;
516
+ } catch {
517
+ return null;
518
+ }
519
+ };
520
+ const newRevision = () => {
521
+ try {
522
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
523
+ } catch {}
524
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
525
+ };
526
+ const tokenNeedsRefresh = (token) => {
527
+ try {
528
+ const payloadPart = token.split(".")[1];
529
+ if (!payloadPart) return true;
530
+ let base64 = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
531
+ const padding = base64.length % 4;
532
+ if (padding) base64 += "=".repeat(4 - padding);
533
+ const payload = JSON.parse(atob(base64));
534
+ return typeof payload.exp !== "number" || Math.floor(Date.now() / 1e3) >= payload.exp - 30;
535
+ } catch {
536
+ return true;
537
+ }
538
+ };
539
+ const isNonemptyString = (value) => typeof value === "string" && value.length > 0;
540
+ const normalizeStoredTokenPair = (value) => {
541
+ if (!value || typeof value !== "object") throw new Error("Invalid POS auth token pair.");
542
+ const tokens = value;
543
+ const hasCanonicalShape = "access_token" in tokens || "refresh_token" in tokens;
544
+ const hasLegacyShape = "accessToken" in tokens || "refreshToken" in tokens;
545
+ if (hasCanonicalShape === hasLegacyShape) throw new Error("Invalid POS auth token pair.");
546
+ const accessToken = hasCanonicalShape ? tokens.access_token : tokens.accessToken;
547
+ const refreshToken = hasCanonicalShape ? tokens.refresh_token : tokens.refreshToken;
548
+ if (!isNonemptyString(accessToken) || !isNonemptyString(refreshToken)) throw new Error("Invalid POS auth token pair.");
549
+ return {
550
+ pair: {
551
+ access_token: accessToken,
552
+ refresh_token: refreshToken
553
+ },
554
+ wasLegacy: hasLegacyShape
555
+ };
556
+ };
557
+ const parseSession = (raw) => {
558
+ const parsed = JSON.parse(raw);
559
+ if (!parsed || typeof parsed !== "object") throw new Error("Invalid POS auth session record.");
560
+ const record = parsed;
561
+ if (record.version !== SESSION_VERSION || !isNonemptyString(record.revision)) throw new Error("Unsupported POS auth session record.");
562
+ if (record.tokens === null) return {
563
+ session: {
564
+ version: SESSION_VERSION,
565
+ revision: record.revision,
566
+ tokens: null
567
+ },
568
+ wasLegacy: false
569
+ };
570
+ const normalized = normalizeStoredTokenPair(record.tokens);
571
+ return {
572
+ session: {
573
+ version: SESSION_VERSION,
574
+ revision: record.revision,
575
+ tokens: normalized.pair
576
+ },
577
+ wasLegacy: normalized.wasLegacy
578
+ };
579
+ };
580
+ /**
581
+ * Browser token storage with one same-origin session owner.
582
+ *
583
+ * Revision and both tokens live in one versioned localStorage value. A Web
584
+ * Storage write is atomic, so process termination can expose either the old
585
+ * complete session or the new complete session, never a mixed token pair or an
586
+ * old pair relabelled with a new owner revision.
587
+ */
588
+ var CoordinatedBrowserTokenStorage = class {
589
+ #sessionKey;
590
+ #legacyAccessKey;
591
+ #legacyRefreshKey;
592
+ #legacyRevisionKey;
593
+ #storage;
594
+ #locks;
595
+ #createRevision;
596
+ #listeners = /* @__PURE__ */ new Set();
597
+ #onStorage = (event) => {
598
+ if (!this.#leaseHeld || event.key !== this.#sessionKey) return;
599
+ try {
600
+ if ((event.newValue === null ? null : parseSession(event.newValue).session.revision) === this.#ownedRevision) return;
601
+ } catch {}
602
+ this.#pendingAccess = null;
603
+ this.#notify("stale");
604
+ };
605
+ #ownedRevision = null;
606
+ #leaseReady;
607
+ #leaseWaitMs;
608
+ #leaseHeld = false;
609
+ #releaseLease = null;
610
+ #lastReadRefreshToken = null;
611
+ #refreshHandler = null;
612
+ #refreshInFlight = null;
613
+ #pendingAccess = null;
614
+ constructor(prefix = DEFAULT_PREFIX, options = {}) {
615
+ this.#leaseWaitMs = options.leaseWaitMs ?? DEFAULT_LEASE_WAIT_MS;
616
+ this.#sessionKey = `${prefix}auth_session`;
617
+ this.#legacyAccessKey = `${prefix}access_token`;
618
+ this.#legacyRefreshKey = `${prefix}refresh_token`;
619
+ this.#legacyRevisionKey = `${prefix}auth_revision`;
620
+ this.#storage = options.storage ?? browserStorage;
621
+ this.#locks = options.locks ?? browserLocks;
622
+ this.#createRevision = options.createRevision ?? newRevision;
623
+ this.#leaseReady = this.#acquireContextLease();
624
+ try {
625
+ if (typeof window !== "undefined") window.addEventListener("storage", this.#onStorage);
626
+ } catch {}
627
+ }
628
+ onOwnershipLost(listener) {
629
+ this.#listeners.add(listener);
630
+ return () => this.#listeners.delete(listener);
631
+ }
632
+ setRefreshHandler(handler) {
633
+ this.#refreshHandler = handler;
634
+ }
635
+ waitForContextLease() {
636
+ return this.#leaseReady;
637
+ }
638
+ /** Test/lifecycle hook; a destroyed page releases the Web Lock implicitly. */
639
+ releaseContextLease() {
640
+ this.#releaseLease?.();
641
+ this.#releaseLease = null;
642
+ }
643
+ /** Claim credentials already written by an SDK/login test boundary. */
644
+ adoptExistingSessionOwner() {
645
+ if (!this.#leaseHeld) {
646
+ this.#notify("storage");
647
+ return null;
648
+ }
649
+ return this.#beginSessionOwner(true);
650
+ }
651
+ /** Logout retains its exact bearer pair until the server request is sent. */
652
+ beginLogoutSession() {
653
+ if (!this.#leaseHeld) {
654
+ this.#notify("storage");
655
+ return null;
656
+ }
657
+ return this.#beginSessionOwner(true);
658
+ }
659
+ /** Login atomically advances ownership and retires the previous pair. */
660
+ async beginLoginSession() {
661
+ if (!this.#leaseHeld && !await this.#leaseReady) {
662
+ this.#notify("storage");
663
+ return false;
664
+ }
665
+ if (!this.#leaseHeld) {
666
+ this.#notify("storage");
667
+ return false;
668
+ }
669
+ return this.#beginSessionOwner(false) !== null;
670
+ }
671
+ #beginSessionOwner(preservePair) {
672
+ this.#pendingAccess = null;
673
+ this.#lastReadRefreshToken = null;
674
+ try {
675
+ const current = this.#readSessionOrMigrate();
676
+ const revision = this.#createRevision();
677
+ const next = {
678
+ version: SESSION_VERSION,
679
+ revision,
680
+ tokens: preservePair ? current?.tokens ?? null : null
681
+ };
682
+ this.#writeSession(next);
683
+ this.#ownedRevision = revision;
684
+ return revision;
685
+ } catch {
686
+ this.#notify("storage");
687
+ return null;
688
+ }
689
+ }
690
+ captureOwner() {
691
+ try {
692
+ if (!this.#leaseHeld) throw new Error("This browser context does not own the POS session.");
693
+ const session = this.#readSessionOrMigrate();
694
+ return {
695
+ revision: this.#ownedRevision,
696
+ accessToken: session?.tokens?.access_token ?? null,
697
+ refreshToken: session?.tokens?.refresh_token ?? null
698
+ };
699
+ } catch {
700
+ this.#notify("storage");
701
+ return {
702
+ revision: this.#ownedRevision,
703
+ accessToken: null,
704
+ refreshToken: null
705
+ };
706
+ }
707
+ }
708
+ owns(owner) {
709
+ return this.#leaseHeld && owner.revision === this.#ownedRevision && this.#ownsSharedRevision(owner.revision);
710
+ }
711
+ sharesRevision(owner) {
712
+ if (owner.revision === null) return false;
713
+ try {
714
+ return this.#readSessionOrMigrate()?.revision === owner.revision;
715
+ } catch {
716
+ this.#notify("storage");
717
+ return false;
718
+ }
719
+ }
720
+ ownsCurrentSession() {
721
+ return this.#ownsSharedRevision(this.#ownedRevision);
722
+ }
723
+ /** Pure ownership probe for transaction code that must not mutate auth. */
724
+ isCurrentSessionOwner() {
725
+ if (!this.#leaseHeld) return false;
726
+ try {
727
+ const sharedRevision = this.#readSessionOrMigrate()?.revision ?? null;
728
+ return sharedRevision === null || this.#ownedRevision !== null && sharedRevision === this.#ownedRevision;
729
+ } catch {
730
+ return false;
731
+ }
732
+ }
733
+ hasCurrentPair(accessToken, refreshToken) {
734
+ if (!this.#ownsSharedRevision(this.#ownedRevision)) return false;
735
+ try {
736
+ const pair = this.#readSessionOrMigrate()?.tokens;
737
+ return pair?.access_token === accessToken && pair.refresh_token === refreshToken;
738
+ } catch {
739
+ this.#notify("storage");
740
+ return false;
741
+ }
742
+ }
743
+ assertCurrentSession() {
744
+ const current = this.ownsCurrentSession();
745
+ if (!current) this.#notify("stale");
746
+ return current;
747
+ }
748
+ /** Used by the SDK callback after its request-owned clear attempt. */
749
+ notifySdkTokensCleared() {
750
+ this.#notify("stale");
751
+ }
752
+ async getAccessToken() {
753
+ return (await this.getRequestOwner()).accessToken;
754
+ }
755
+ /**
756
+ * Atomically capture the bearer and revision for one SDK request.
757
+ *
758
+ * Return the private operation's promise directly. That operation performs
759
+ * any proactive refresh and captures both fields in its final synchronous
760
+ * continuation, leaving no second await where a same-token revision change
761
+ * could pair an old bearer with a new owner.
762
+ */
763
+ getRequestOwner() {
764
+ const initiatingRevision = this.#ownedRevision;
765
+ return this.#readRequestOwner(initiatingRevision);
766
+ }
767
+ async #readRequestOwner(initiatingRevision) {
768
+ await this.#requireLease();
769
+ this.#assertRequestRevision(initiatingRevision);
770
+ const pair = this.#readPairOrThrow();
771
+ const accessToken = pair?.access_token ?? null;
772
+ const refreshToken = pair?.refresh_token;
773
+ if (accessToken && tokenNeedsRefresh(accessToken) && refreshToken && this.#refreshHandler) await this.#refreshOwnedPair({
774
+ revision: initiatingRevision,
775
+ accessToken,
776
+ refreshToken
777
+ });
778
+ this.#assertRequestRevision(initiatingRevision);
779
+ const current = this.#readSessionOrThrow();
780
+ return {
781
+ accessToken: current.tokens?.access_token ?? null,
782
+ refreshToken: current.tokens?.refresh_token ?? null,
783
+ revision: current.revision
784
+ };
785
+ }
786
+ /**
787
+ * Recover one request whose bearer the server rejected before its JWT expiry.
788
+ *
789
+ * The SDK's response middleware refreshes a 401 only after its own expiry
790
+ * check has elapsed. A server-side expiry can lead that clock, so long-lived
791
+ * pollers use this request-owned boundary to join the same storage
792
+ * single-flight and retry once with the rotated pair.
793
+ */
794
+ async recoverAfterUnauthorized(owner) {
795
+ try {
796
+ await this.#requireLease();
797
+ } catch {
798
+ return "failed";
799
+ }
800
+ if (this.#pairReplaced(owner)) return "refreshed";
801
+ if (!this.owns(owner) || !owner.accessToken || !owner.refreshToken || !this.#refreshHandler) return "stale";
802
+ try {
803
+ await this.#refreshOwnedPair(owner);
804
+ } catch {
805
+ return this.#pairReplaced(owner) ? "refreshed" : "failed";
806
+ }
807
+ if (this.#pairReplaced(owner)) return "refreshed";
808
+ return this.owns(owner) ? "failed" : "stale";
809
+ }
810
+ async #refreshOwnedPair(owner) {
811
+ const refreshHandler = this.#refreshHandler;
812
+ const refreshToken = owner.refreshToken;
813
+ if (!refreshHandler || !refreshToken) throw new Error("No POS refresh token is available.");
814
+ const refreshOperation = this.#refreshInFlight ?? (async () => {
815
+ try {
816
+ if (tokenNeedsRefresh(refreshToken)) throw new RefreshRejectedError({ reason: "expired" });
817
+ await this.#runSdkRefresh(owner.revision, refreshToken, (ownedAccessToken) => refreshHandler(refreshToken, ownedAccessToken));
818
+ } catch (error) {
819
+ if (this.#pairReplaced(owner)) return;
820
+ if (!refreshEndsSession(error)) throw new RefreshUnavailableError(error);
821
+ const outcome = await this.#clearOwnedPair(owner);
822
+ if (outcome === "cleared") this.#notify("stale");
823
+ if (outcome === "failed") this.#notify("storage");
824
+ if (outcome !== "stale") throw error;
825
+ }
826
+ })();
827
+ this.#refreshInFlight = refreshOperation;
828
+ try {
829
+ await refreshOperation;
830
+ } finally {
831
+ if (this.#refreshInFlight === refreshOperation) this.#refreshInFlight = null;
832
+ }
833
+ }
834
+ #pairReplaced(owner) {
835
+ try {
836
+ const current = this.#readSessionOrMigrate();
837
+ return current?.revision === owner.revision && current.tokens !== null && current.tokens.access_token !== owner.accessToken;
838
+ } catch {
839
+ this.#notify("storage");
840
+ return false;
841
+ }
842
+ }
843
+ async getRefreshToken() {
844
+ await this.#requireLease();
845
+ this.#assertRequestOwner();
846
+ const token = this.#readPairOrThrow()?.refresh_token ?? null;
847
+ this.#lastReadRefreshToken = token;
848
+ return token;
849
+ }
850
+ /** Stage only; the pair becomes visible together from setRefreshToken. */
851
+ async setAccessToken(token) {
852
+ await this.#requireLease();
853
+ if (!this.#ownsSharedRevision(this.#ownedRevision)) {
854
+ this.#pendingAccess = null;
855
+ this.#notify("stale");
856
+ return;
857
+ }
858
+ this.#pendingAccess = {
859
+ revision: this.#ownedRevision,
860
+ expectedRefreshToken: this.#lastReadRefreshToken,
861
+ token
862
+ };
863
+ }
864
+ async setRefreshToken(token) {
865
+ await this.#requireLease();
866
+ const pending = this.#pendingAccess;
867
+ this.#pendingAccess = null;
868
+ if (!pending) {
869
+ this.#notify("storage");
870
+ return;
871
+ }
872
+ await this.#commitPair(pending.revision, pending.expectedRefreshToken, pending.token, token);
873
+ }
874
+ /**
875
+ * SDK compatibility boundary.
876
+ *
877
+ * The patched POS middleware supplies the bearer that produced a 403/logout
878
+ * response. An older response is therefore inert after login or refresh. SDK
879
+ * internal cleanup without a request bearer still captures the exact current
880
+ * pair before entering the serialized clear.
881
+ */
882
+ async clearTokens(expectedAccessToken, expectedRevision) {
883
+ await this.#requireLease();
884
+ this.#pendingAccess = null;
885
+ const owner = this.captureOwner();
886
+ if ((expectedAccessToken !== void 0 || expectedRevision !== void 0) && (owner.accessToken !== (expectedAccessToken ?? null) || owner.revision !== (expectedRevision ?? null))) return false;
887
+ const outcome = await this.#clearOwnedPair(owner);
888
+ if (outcome === "failed") this.#notify("storage");
889
+ return outcome === "cleared";
890
+ }
891
+ /** Own the SDK's automatic refresh from token read through pair commit. */
892
+ async runSdkRefresh(refreshToken, execute) {
893
+ const initiatingRevision = this.#ownedRevision;
894
+ return this.#runSdkRefresh(initiatingRevision, refreshToken, execute);
895
+ }
896
+ async #runSdkRefresh(initiatingRevision, refreshToken, execute) {
897
+ await this.#requireLease();
898
+ const locks = this.#locks();
899
+ if (!locks) {
900
+ this.#notify("storage");
901
+ throw new Error("Cross-context token coordination is unavailable.");
902
+ }
903
+ return locks.request(AUTH_SESSION_LOCK, { mode: "exclusive" }, async () => {
904
+ if (initiatingRevision !== this.#ownedRevision || !this.#ownsSharedRevision(initiatingRevision)) throw new Error("The authenticated session changed before token refresh.");
905
+ const current = this.#readSessionOrThrow();
906
+ if (current.tokens?.refresh_token !== refreshToken || !current.tokens.access_token) throw new Error("A newer token refresh already completed.");
907
+ const originalPair = current.tokens;
908
+ const pair = await execute(originalPair.access_token);
909
+ if (!pair.access_token || !pair.refresh_token) throw new Error("Token refresh returned an incomplete pair.");
910
+ if (initiatingRevision !== this.#ownedRevision || !this.#ownsSharedRevision(initiatingRevision)) throw new Error("The authenticated session changed during token refresh.");
911
+ const latest = this.#readSessionOrThrow();
912
+ if (latest.tokens?.refresh_token !== refreshToken) throw new Error("A newer token refresh already completed.");
913
+ this.#writeSession({
914
+ version: SESSION_VERSION,
915
+ revision: latest.revision,
916
+ tokens: pair
917
+ });
918
+ this.#lastReadRefreshToken = pair.refresh_token;
919
+ return pair;
920
+ });
921
+ }
922
+ /** Commit a side-effect-free direct refresh against its captured pair. */
923
+ async adoptTokens(owner, accessToken, refreshToken) {
924
+ try {
925
+ await this.#requireLease();
926
+ } catch {
927
+ return "failed";
928
+ }
929
+ if (!this.owns(owner)) return "stale";
930
+ return this.#commitPair(owner.revision, owner.refreshToken, accessToken, refreshToken);
931
+ }
932
+ #notify(reason) {
933
+ const hadOwner = this.#ownedRevision !== null;
934
+ this.#ownedRevision = null;
935
+ this.#pendingAccess = null;
936
+ this.#lastReadRefreshToken = null;
937
+ if (reason === "stale" && !hadOwner) return;
938
+ for (const listener of this.#listeners) listener(reason);
939
+ }
940
+ #acquireContextLease() {
941
+ const locks = this.#locks();
942
+ if (!locks) return Promise.resolve(false);
943
+ return new Promise((resolve) => {
944
+ let readySettled = false;
945
+ const settleReady = (held) => {
946
+ if (readySettled) return;
947
+ readySettled = true;
948
+ resolve(held);
949
+ };
950
+ const waiting = new AbortController();
951
+ const deadline = setTimeout(() => waiting.abort(), this.#leaseWaitMs);
952
+ locks.request(AUTH_CONTEXT_LOCK, {
953
+ mode: "exclusive",
954
+ signal: waiting.signal
955
+ }, async (lock) => {
956
+ clearTimeout(deadline);
957
+ if (!lock) {
958
+ settleReady(false);
959
+ return;
960
+ }
961
+ try {
962
+ const session = this.#readSessionOrMigrate();
963
+ this.#leaseHeld = true;
964
+ this.#ownedRevision = session?.revision ?? null;
965
+ } catch {
966
+ this.#leaseHeld = false;
967
+ this.#notify("storage");
968
+ settleReady(false);
969
+ return;
970
+ }
971
+ settleReady(true);
972
+ await new Promise((release) => {
973
+ this.#releaseLease = release;
974
+ });
975
+ this.#releaseLease = null;
976
+ this.#leaseHeld = false;
977
+ this.#ownedRevision = null;
978
+ this.#pendingAccess = null;
979
+ }).catch(() => {
980
+ clearTimeout(deadline);
981
+ settleReady(false);
982
+ });
983
+ });
984
+ }
985
+ async #requireLease() {
986
+ if (await this.#leaseReady && this.#leaseHeld) return;
987
+ this.#notify("storage");
988
+ throw new Error("Another browser tab owns this POS session.");
989
+ }
990
+ #assertRequestOwner() {
991
+ if (this.#ownsSharedRevision(this.#ownedRevision)) return;
992
+ this.#notify("stale");
993
+ throw new Error("The authenticated session belongs to another browser context.");
994
+ }
995
+ #assertRequestRevision(revision) {
996
+ if (revision === this.#ownedRevision && this.#ownsSharedRevision(revision)) return;
997
+ if (this.#ownsSharedRevision(this.#ownedRevision)) throw new Error("The authenticated session changed before the POS request was sent.");
998
+ this.#notify("stale");
999
+ throw new Error("The authenticated session belongs to another browser context.");
1000
+ }
1001
+ #ownsSharedRevision(revision) {
1002
+ if (!this.#leaseHeld || revision === null) return false;
1003
+ try {
1004
+ return this.#readSessionOrMigrate()?.revision === revision;
1005
+ } catch {
1006
+ this.#notify("storage");
1007
+ return false;
1008
+ }
1009
+ }
1010
+ #readPairOrThrow() {
1011
+ return this.#readSessionOrThrow().tokens;
1012
+ }
1013
+ #readSessionOrThrow() {
1014
+ const session = this.#readSessionOrMigrate();
1015
+ if (!session) throw new Error("No POS auth session exists.");
1016
+ return session;
1017
+ }
1018
+ #readSessionOrMigrate() {
1019
+ const storage = this.#storageOrThrow();
1020
+ const raw = storage.getItem(this.#sessionKey);
1021
+ if (raw !== null) {
1022
+ const parsed = parseSession(raw);
1023
+ if (parsed.wasLegacy) this.#writeSession(parsed.session);
1024
+ return parsed.session;
1025
+ }
1026
+ const accessToken = storage.getItem(this.#legacyAccessKey);
1027
+ const refreshToken = storage.getItem(this.#legacyRefreshKey);
1028
+ const legacyRevision = storage.getItem(this.#legacyRevisionKey);
1029
+ if (accessToken === null && refreshToken === null && legacyRevision === null) return null;
1030
+ if (accessToken === null !== (refreshToken === null)) throw new Error("Incomplete legacy POS auth token pair.");
1031
+ if (accessToken !== null && (!accessToken || !refreshToken)) throw new Error("Invalid legacy POS auth token pair.");
1032
+ if (legacyRevision !== null && !legacyRevision) throw new Error("Invalid legacy POS auth revision.");
1033
+ const migrated = {
1034
+ version: SESSION_VERSION,
1035
+ revision: legacyRevision ?? this.#createRevision(),
1036
+ tokens: accessToken === null ? null : {
1037
+ access_token: accessToken,
1038
+ refresh_token: refreshToken
1039
+ }
1040
+ };
1041
+ this.#writeSession(migrated);
1042
+ try {
1043
+ storage.removeItem(this.#legacyAccessKey);
1044
+ storage.removeItem(this.#legacyRefreshKey);
1045
+ storage.removeItem(this.#legacyRevisionKey);
1046
+ } catch {}
1047
+ return migrated;
1048
+ }
1049
+ #writeSession(session) {
1050
+ this.#storageOrThrow().setItem(this.#sessionKey, JSON.stringify(session));
1051
+ }
1052
+ #storageOrThrow() {
1053
+ const storage = this.#storage();
1054
+ if (!storage) throw new Error("Browser storage is unavailable.");
1055
+ return storage;
1056
+ }
1057
+ async #clearOwnedPair(owner) {
1058
+ const locks = this.#locks();
1059
+ if (!locks) return "failed";
1060
+ try {
1061
+ return await locks.request(AUTH_SESSION_LOCK, { mode: "exclusive" }, () => {
1062
+ if (!this.owns(owner)) return "stale";
1063
+ const current = this.#readSessionOrThrow();
1064
+ if (current.tokens?.access_token !== owner.accessToken || current.tokens?.refresh_token !== owner.refreshToken) return "stale";
1065
+ try {
1066
+ this.#writeSession({
1067
+ ...current,
1068
+ tokens: null
1069
+ });
1070
+ } catch {
1071
+ return "failed";
1072
+ }
1073
+ this.#lastReadRefreshToken = null;
1074
+ return "cleared";
1075
+ });
1076
+ } catch {
1077
+ return "failed";
1078
+ }
1079
+ }
1080
+ async #commitPair(revision, expectedRefreshToken, accessToken, refreshToken) {
1081
+ const locks = this.#locks();
1082
+ if (!locks) {
1083
+ this.#notify("storage");
1084
+ return "failed";
1085
+ }
1086
+ try {
1087
+ return await locks.request(AUTH_SESSION_LOCK, { mode: "exclusive" }, () => {
1088
+ if (revision !== this.#ownedRevision || !this.#ownsSharedRevision(revision)) {
1089
+ this.#notify("stale");
1090
+ return "stale";
1091
+ }
1092
+ const current = this.#readSessionOrThrow();
1093
+ if ((current.tokens?.refresh_token ?? null) !== expectedRefreshToken) return "stale";
1094
+ try {
1095
+ this.#writeSession({
1096
+ version: SESSION_VERSION,
1097
+ revision: current.revision,
1098
+ tokens: {
1099
+ access_token: accessToken,
1100
+ refresh_token: refreshToken
1101
+ }
1102
+ });
1103
+ } catch {
1104
+ this.#notify("storage");
1105
+ return "failed";
1106
+ }
1107
+ this.#lastReadRefreshToken = refreshToken;
1108
+ return "adopted";
1109
+ });
1110
+ } catch {
1111
+ this.#notify("storage");
1112
+ return "failed";
1113
+ }
1114
+ }
1115
+ };
1116
+ const hotData = import.meta.hot?.data;
1117
+ const authTokenStorage = hotData?.authTokenStorage ?? new CoordinatedBrowserTokenStorage();
1118
+ if (hotData) hotData.authTokenStorage = authTokenStorage;
1119
+ //#endregion
430
1120
  //#region src/lib/utils/jwt.ts
431
1121
  /**
432
1122
  * Decode a JWT token payload without signature verification.
@@ -625,9 +1315,11 @@ var MemoryTokenStorage = class {
625
1315
  async setRefreshToken(token) {
626
1316
  this.refreshToken = token;
627
1317
  }
628
- async clearTokens() {
1318
+ async clearTokens(expectedAccessToken, _expectedRevision) {
1319
+ if (expectedAccessToken !== void 0 && this.accessToken !== expectedAccessToken) return false;
629
1320
  this.accessToken = null;
630
1321
  this.refreshToken = null;
1322
+ return true;
631
1323
  }
632
1324
  };
633
1325
  /**
@@ -654,11 +1346,13 @@ var BrowserTokenStorage = class {
654
1346
  async setRefreshToken(token) {
655
1347
  if (typeof localStorage !== "undefined") localStorage.setItem(this.refreshTokenKey, token);
656
1348
  }
657
- async clearTokens() {
1349
+ async clearTokens(expectedAccessToken, _expectedRevision) {
658
1350
  if (typeof localStorage !== "undefined") {
1351
+ if (expectedAccessToken !== void 0 && localStorage.getItem(this.accessTokenKey) !== expectedAccessToken) return false;
659
1352
  localStorage.removeItem(this.accessTokenKey);
660
1353
  localStorage.removeItem(this.refreshTokenKey);
661
1354
  }
1355
+ return true;
662
1356
  }
663
1357
  };
664
1358
  /**
@@ -668,65 +1362,82 @@ var BrowserTokenStorage = class {
668
1362
  * 1. API Key endpoints (X-Api-Key): login/email, login/phone, login/whatsapp, pair-device, verify-otp
669
1363
  * 2. Bearer token endpoints: All other endpoints
670
1364
  * 3. Token returning endpoints: verify-otp, refresh-token
1365
+ *
1366
+ * When the token storage implements `getRequestOwner()`, every destructive
1367
+ * operation (clear on failed refresh, logout, 403) is scoped to the session
1368
+ * that initiated it, and a request whose session changed between scheduling
1369
+ * and dispatch is refused instead of sent with another session's identity.
671
1370
  */
672
1371
  function createPosAuthMiddleware(config) {
673
1372
  let isRefreshing = false;
674
1373
  let refreshPromise = null;
675
1374
  let hasAssessedTokens = false;
676
- const assessTokenStateOnce = async () => {
1375
+ /** The owner each in-flight request was signed for, keyed by Request. */
1376
+ const requestOwners = /* @__PURE__ */ new WeakMap();
1377
+ /**
1378
+ * Clear tokens on behalf of `owner`. With an owner this is a
1379
+ * compare-and-swap — a session that replaced the failing one survives —
1380
+ * and `onTokensCleared` only fires when something was actually cleared.
1381
+ */
1382
+ const clearAssessedOwner = async (owner) => {
1383
+ const cleared = owner ? await config.tokenStorage.clearTokens(owner.accessToken, owner.revision) : await config.tokenStorage.clearTokens();
1384
+ if (cleared !== false) config.onTokensCleared?.();
1385
+ return cleared !== false;
1386
+ };
1387
+ const assessTokenStateOnce = async (initiatingOwner) => {
677
1388
  if (hasAssessedTokens) return;
678
1389
  hasAssessedTokens = true;
679
1390
  try {
680
- const accessToken = await config.tokenStorage.getAccessToken();
681
- const refreshToken = await config.tokenStorage.getRefreshToken();
1391
+ const owner = initiatingOwner ?? await config.tokenStorage.getRequestOwner?.();
1392
+ const accessToken = owner ? owner.accessToken : await config.tokenStorage.getAccessToken();
1393
+ const refreshToken = owner ? owner.refreshToken : await config.tokenStorage.getRefreshToken();
682
1394
  if (accessToken && !isTokenExpired(accessToken)) return;
683
1395
  if (!accessToken && refreshToken) {
684
- await config.tokenStorage.clearTokens();
685
- config.onTokensCleared?.();
1396
+ await clearAssessedOwner(owner);
686
1397
  console.info("Cleaned up orphaned refresh token in POS");
687
1398
  return;
688
1399
  }
689
1400
  if (accessToken && refreshToken && !isTokenExpired(refreshToken)) {
690
1401
  try {
691
- await refreshTokens();
1402
+ await refreshTokens(owner);
692
1403
  console.info("POS tokens refreshed proactively on startup");
693
1404
  } catch (error) {
694
- await config.tokenStorage.clearTokens();
695
- config.onTokensCleared?.();
696
- console.info("POS tokens cleared after failed refresh on startup");
1405
+ console.info(refreshEndsSession(error) ? "POS tokens cleared after a rejected refresh on startup" : "POS startup refresh could not complete; tokens kept");
697
1406
  }
698
1407
  return;
699
1408
  }
700
1409
  if (accessToken && isTokenExpired(accessToken) || refreshToken && isTokenExpired(refreshToken)) {
701
- await config.tokenStorage.clearTokens();
702
- config.onTokensCleared?.();
1410
+ await clearAssessedOwner(owner);
703
1411
  console.info("POS stale tokens cleared on startup - user needs to re-authenticate");
704
1412
  return;
705
1413
  }
706
1414
  if (!accessToken && !refreshToken) return;
707
1415
  } catch (error) {
708
1416
  console.warn("POS token state assessment failed:", error);
1417
+ if (config.tokenStorage.getRequestOwner) throw error;
709
1418
  }
710
1419
  };
711
- const refreshTokens = async () => {
1420
+ const refreshTokens = async (initiatingOwner) => {
712
1421
  if (isRefreshing && refreshPromise) return refreshPromise;
713
1422
  isRefreshing = true;
714
1423
  refreshPromise = (async () => {
715
1424
  try {
716
- const refreshToken = await config.tokenStorage.getRefreshToken();
717
- if (!refreshToken || isTokenExpired(refreshToken)) throw new Error("No valid refresh token available");
1425
+ const refreshToken = initiatingOwner ? initiatingOwner.refreshToken : await config.tokenStorage.getRefreshToken();
1426
+ if (!refreshToken || isTokenExpired(refreshToken)) throw new RefreshRejectedError({ reason: "expired" });
718
1427
  let newTokens;
719
1428
  if (config.refreshTokenFn) newTokens = await config.refreshTokenFn(refreshToken);
720
1429
  else {
1430
+ const accessToken = initiatingOwner ? initiatingOwner.accessToken : await config.tokenStorage.getAccessToken();
1431
+ if (!accessToken) throw new Error("No valid access token available for POS token refresh");
721
1432
  const response = await fetch(`${config.baseUrl}/pos/auth/refresh-token`, {
722
1433
  method: "POST",
723
1434
  headers: {
724
1435
  "Content-Type": "application/json",
725
- Authorization: `Bearer ${await config.tokenStorage.getAccessToken()}`
1436
+ Authorization: `Bearer ${accessToken}`
726
1437
  },
727
1438
  body: JSON.stringify({ refresh_token: refreshToken })
728
1439
  });
729
- if (!response.ok) throw new Error(`POS token refresh failed: ${response.status}`);
1440
+ if (!response.ok) throw new RefreshRejectedError({ status: response.status });
730
1441
  const data = await response.json();
731
1442
  newTokens = data.content || data;
732
1443
  }
@@ -734,9 +1445,10 @@ function createPosAuthMiddleware(config) {
734
1445
  await config.tokenStorage.setRefreshToken(newTokens.refresh_token);
735
1446
  config.onTokensUpdated?.(newTokens.access_token, newTokens.refresh_token);
736
1447
  } catch (error) {
737
- console.error("POS token refresh failed:", error);
738
- await config.tokenStorage.clearTokens();
739
- config.onTokensCleared?.();
1448
+ if (refreshEndsSession(error)) {
1449
+ console.error("POS token refresh rejected; tokens cleared:", error);
1450
+ await clearAssessedOwner(initiatingOwner);
1451
+ } else console.warn("POS token refresh could not complete; tokens kept:", error);
740
1452
  throw error;
741
1453
  } finally {
742
1454
  isRefreshing = false;
@@ -748,25 +1460,43 @@ function createPosAuthMiddleware(config) {
748
1460
  return {
749
1461
  async onRequest({ request }) {
750
1462
  const pathname = getPathnameFromUrl(request.url);
751
- await assessTokenStateOnce();
1463
+ let initiatingOwner;
1464
+ try {
1465
+ initiatingOwner = await config.tokenStorage.getRequestOwner?.();
1466
+ } catch (error) {
1467
+ if (!isApiKeyEndpoint(pathname) || !isRefreshFailure(error)) throw error;
1468
+ request.headers.set("X-Api-Key", config.apiKey);
1469
+ return request;
1470
+ }
1471
+ await assessTokenStateOnce(initiatingOwner);
752
1472
  if (isApiKeyEndpoint(pathname)) {
753
1473
  request.headers.set("X-Api-Key", config.apiKey);
754
1474
  return request;
755
1475
  }
756
- let accessToken = await config.tokenStorage.getAccessToken();
1476
+ let requestOwner = await config.tokenStorage.getRequestOwner?.();
1477
+ if (initiatingOwner && requestOwner?.revision !== initiatingOwner.revision) throw new Error("The authenticated session changed before the POS request was sent.");
1478
+ let accessToken = requestOwner ? requestOwner.accessToken : await config.tokenStorage.getAccessToken();
757
1479
  if (accessToken && isTokenExpired(accessToken)) try {
758
- await refreshTokens();
759
- accessToken = await config.tokenStorage.getAccessToken();
1480
+ await refreshTokens(requestOwner);
1481
+ requestOwner = await config.tokenStorage.getRequestOwner?.();
1482
+ if (initiatingOwner && requestOwner?.revision !== initiatingOwner.revision) throw new Error("The authenticated session changed before the POS request was sent.");
1483
+ accessToken = requestOwner ? requestOwner.accessToken : await config.tokenStorage.getAccessToken();
760
1484
  } catch (error) {
761
1485
  console.warn("Token refresh failed:", error);
762
1486
  accessToken = null;
763
1487
  }
764
- if (accessToken) request.headers.set("Authorization", `Bearer ${accessToken}`);
1488
+ if (accessToken) {
1489
+ request.headers.set("Authorization", `Bearer ${accessToken}`);
1490
+ requestOwners.set(request, {
1491
+ accessToken,
1492
+ revision: requestOwner?.revision
1493
+ });
1494
+ }
765
1495
  return request;
766
1496
  },
767
1497
  async onResponse({ request, response }) {
768
1498
  const pathname = getPathnameFromUrl(request.url);
769
- if (response.ok && isTokenReturningEndpoint(pathname)) try {
1499
+ if (config.storeTokenResponses !== false && response.ok && isTokenReturningEndpoint(pathname)) try {
770
1500
  const data = await response.clone().json();
771
1501
  const content = data.content || data;
772
1502
  if (content?.access_token && content?.refresh_token) {
@@ -778,8 +1508,10 @@ function createPosAuthMiddleware(config) {
778
1508
  console.warn("Failed to extract tokens from POS response:", error);
779
1509
  }
780
1510
  else if (response.ok && isLogoutEndpoint(pathname)) {
781
- await config.tokenStorage.clearTokens();
782
- config.onTokensCleared?.();
1511
+ const owner = requestOwners.get(request);
1512
+ if (owner?.accessToken) {
1513
+ if (await config.tokenStorage.clearTokens(owner.accessToken, owner.revision) !== false) config.onTokensCleared?.();
1514
+ }
783
1515
  }
784
1516
  if (response.status === 401 && !isApiKeyEndpoint(pathname)) {
785
1517
  const currentToken = await config.tokenStorage.getAccessToken();
@@ -796,8 +1528,10 @@ function createPosAuthMiddleware(config) {
796
1528
  }
797
1529
  }
798
1530
  if (response.status === 403 && !isApiKeyEndpoint(pathname)) {
799
- await config.tokenStorage.clearTokens();
800
- config.onTokensCleared?.();
1531
+ const owner = requestOwners.get(request);
1532
+ if (owner?.accessToken) {
1533
+ if (await config.tokenStorage.clearTokens(owner.accessToken, owner.revision) !== false) config.onTokensCleared?.();
1534
+ }
801
1535
  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
1536
  }
803
1537
  return response;
@@ -813,7 +1547,8 @@ function createDefaultPosAuthMiddleware(options) {
813
1547
  apiKey: options.apiKey,
814
1548
  baseUrl: options.baseUrl,
815
1549
  onTokensUpdated: options.onTokensUpdated,
816
- onTokensCleared: options.onTokensCleared
1550
+ onTokensCleared: options.onTokensCleared,
1551
+ storeTokenResponses: options.storeTokenResponses
817
1552
  });
818
1553
  }
819
1554
  //#endregion
@@ -914,7 +1649,8 @@ var PosAPIClient = class PosAPIClient extends BaseAPIClient {
914
1649
  baseUrl: this.getBaseUrl(),
915
1650
  tokenStorage: config.tokenStorage,
916
1651
  onTokensUpdated: config.onTokensUpdated,
917
- onTokensCleared: config.onTokensCleared
1652
+ onTokensCleared: config.onTokensCleared,
1653
+ storeTokenResponses: config.storeTokenResponses
918
1654
  });
919
1655
  this.client.use(authMiddleware);
920
1656
  if (config.accessToken) {
@@ -998,20 +1734,6 @@ var PosAPIClient = class PosAPIClient extends BaseAPIClient {
998
1734
  console.warn("Failed to initialize tokens in storage:", error);
999
1735
  }
1000
1736
  }
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
1737
  };
1016
1738
  //#endregion
1017
1739
  //#region src/lib/pos.ts
@@ -1805,7 +2527,7 @@ var PosClient = class extends PosAPIClient {
1805
2527
  * ```
1806
2528
  */
1807
2529
  async getFulfillmentOptions(body) {
1808
- return this.executeRequest(() => this.client.POST("/pos/fulfillment-options", { body }));
2530
+ return this.executeRequest(() => this.client.POST("/pos/carts/fulfillment-options", { body }));
1809
2531
  }
1810
2532
  /**
1811
2533
  * Update cart customer information
@@ -1839,24 +2561,60 @@ var PosClient = class extends PosAPIClient {
1839
2561
  }
1840
2562
  /**
1841
2563
  * Create order from cart
1842
- * @param body - Order creation data
1843
- * @returns Promise with created order
2564
+ * @param body - Cart ID, plus an optional payment method to create the
2565
+ * initial payment request in the same call
2566
+ * @returns Promise with the created order, `payment_required`, and gateway
2567
+ * initiation details (`payment_info`) when an auto payment method was used
1844
2568
  * @example
1845
2569
  * ```typescript
1846
2570
  * const { data, error } = await pos.createOrder({
1847
- * cart_id: "01H9CART12345ABCDE"
2571
+ * cart_id: "01H9CART12345ABCDE",
2572
+ * payment_method: { payment_provider_slug: "cash" },
1848
2573
  * });
1849
2574
  *
1850
2575
  * if (error) {
1851
2576
  * console.error("Failed to create order:", error.message);
1852
2577
  * } else {
1853
- * console.log("Order created:", data.order.id);
2578
+ * console.log("Order created:", data.order.order_number);
1854
2579
  * console.log("Payment required:", data.payment_required);
1855
2580
  * }
1856
2581
  * ```
1857
2582
  */
1858
2583
  async createOrder(body) {
1859
- return this.executeRequest(() => this.storefrontClient.POST("/pos/orders", { body }));
2584
+ return this.executeRequest(() => this.client.POST("/pos/orders", { body }));
2585
+ }
2586
+ /**
2587
+ * Create a payment request for an order
2588
+ *
2589
+ * Use this when the order was created without a payment method, or to
2590
+ * collect the remaining `to_be_paid` amount with another method (split
2591
+ * tender). The total requested across payment requests cannot exceed the
2592
+ * order's `to_be_paid`.
2593
+ * @param pathParams - Order number
2594
+ * @param body - Amount to request and the payment method to use
2595
+ * @returns Promise with all payment records for the order, the remaining
2596
+ * `pending_amount`, and gateway initiation details (`payment_info`) when
2597
+ * an auto payment method was used
2598
+ * @example
2599
+ * ```typescript
2600
+ * const { data, error } = await pos.createOrderPaymentRequest(
2601
+ * { order_number: "1234567890" },
2602
+ * { amount: 500, payment_method: { payment_provider_slug: "cash" } }
2603
+ * );
2604
+ *
2605
+ * if (error) {
2606
+ * console.error("Failed to create payment request:", error.message);
2607
+ * } else {
2608
+ * console.log("Pending amount:", data.pending_amount);
2609
+ * console.log("Payments:", data.payments.length);
2610
+ * }
2611
+ * ```
2612
+ */
2613
+ async createOrderPaymentRequest(pathParams, body) {
2614
+ return this.executeRequest(() => this.client.POST("/pos/orders/{order_number}/payments", {
2615
+ params: { path: pathParams },
2616
+ body
2617
+ }));
1860
2618
  }
1861
2619
  /**
1862
2620
  * Get payment status
@@ -1880,6 +2638,124 @@ var PosClient = class extends PosAPIClient {
1880
2638
  return this.executeRequest(() => this.client.GET("/pos/orders/{order_number}/payment-status", { params: { path: pathParams } }));
1881
2639
  }
1882
2640
  /**
2641
+ * List available payment methods
2642
+ * @param query - Optional query parameters (e.g. amount for method filtering)
2643
+ * @returns Promise with the payment methods enabled for the store
2644
+ * @example
2645
+ * ```typescript
2646
+ * const { data, error } = await pos.listPaymentMethods();
2647
+ *
2648
+ * if (error) {
2649
+ * console.error("Failed to list payment methods:", error.message);
2650
+ * } else {
2651
+ * data.payment_methods.forEach(method => {
2652
+ * console.log(`${method.name} (${method.code})`);
2653
+ * });
2654
+ * }
2655
+ * ```
2656
+ */
2657
+ async listPaymentMethods(query) {
2658
+ return this.executeRequest(() => this.client.GET("/pos/payments/payment-methods", { params: { query } }));
2659
+ }
2660
+ /**
2661
+ * Verify a UPI VPA (virtual payment address)
2662
+ * @param query - The VPA to verify
2663
+ * @returns Promise with the verification result and account holder name
2664
+ * @example
2665
+ * ```typescript
2666
+ * const { data, error } = await pos.verifyVpa({ vpa: "customer@upi" });
2667
+ *
2668
+ * if (error) {
2669
+ * console.error("VPA verification failed:", error.message);
2670
+ * } else if (data.is_valid) {
2671
+ * console.log("Paying to:", data.customer_name);
2672
+ * }
2673
+ * ```
2674
+ */
2675
+ async verifyVpa(query) {
2676
+ return this.executeRequest(() => this.client.GET("/pos/payments/verify-vpa", { params: { query } }));
2677
+ }
2678
+ /**
2679
+ * Get card metadata for a card number prefix
2680
+ * @param query - Card number prefix (BIN) to look up
2681
+ * @returns Promise with card brand, type, and issuer details
2682
+ * @example
2683
+ * ```typescript
2684
+ * const { data, error } = await pos.getCardInfo({ card_number: "411111" });
2685
+ *
2686
+ * if (error) {
2687
+ * console.error("Card lookup failed:", error.message);
2688
+ * } else {
2689
+ * console.log(`${data.card_brand} ${data.card_type}`);
2690
+ * }
2691
+ * ```
2692
+ */
2693
+ async getCardInfo(query) {
2694
+ return this.executeRequest(() => this.client.GET("/pos/payments/card-info", { params: { query } }));
2695
+ }
2696
+ /**
2697
+ * Authenticate a direct (headless) card payment with an OTP
2698
+ * @param body - OTP authentication payload from the payment flow
2699
+ * @returns Promise with the authentication result
2700
+ * @example
2701
+ * ```typescript
2702
+ * const { data, error } = await pos.authenticateDirectOtp({
2703
+ * transaction_id: "txn_123",
2704
+ * otp: "123456"
2705
+ * });
2706
+ *
2707
+ * if (error) {
2708
+ * console.error("OTP authentication failed:", error.message);
2709
+ * }
2710
+ * ```
2711
+ */
2712
+ async authenticateDirectOtp(body) {
2713
+ return this.executeRequest(() => this.client.POST("/pos/payments/authenticate-direct-otp", { body }));
2714
+ }
2715
+ /**
2716
+ * Resend the OTP for a direct (headless) card payment
2717
+ * @param body - The transaction whose OTP should be resent
2718
+ * @returns Promise with the resend confirmation
2719
+ * @example
2720
+ * ```typescript
2721
+ * const { data, error } = await pos.resendDirectOtp({
2722
+ * transaction_id: "txn_123"
2723
+ * });
2724
+ *
2725
+ * if (error) {
2726
+ * console.error("Failed to resend OTP:", error.message);
2727
+ * }
2728
+ * ```
2729
+ */
2730
+ async resendDirectOtp(body) {
2731
+ return this.executeRequest(() => this.client.POST("/pos/payments/resend-direct-otp", { body }));
2732
+ }
2733
+ /**
2734
+ * Retry payment for an order whose previous payment failed or is unpaid
2735
+ * @param pathParams - Order number
2736
+ * @param body - Payment method details for the retry
2737
+ * @returns Promise with fresh payment info for the retried payment
2738
+ * @example
2739
+ * ```typescript
2740
+ * const { data, error } = await pos.retryOrderPayment(
2741
+ * { order_number: "ORD-2024-001" },
2742
+ * { payment_method: "upi" }
2743
+ * );
2744
+ *
2745
+ * if (error) {
2746
+ * console.error("Payment retry failed:", error.message);
2747
+ * } else {
2748
+ * console.log("New payment initiated:", data.payment_info);
2749
+ * }
2750
+ * ```
2751
+ */
2752
+ async retryOrderPayment(pathParams, body) {
2753
+ return this.executeRequest(() => this.client.POST("/pos/orders/{order_number}/retry-payment", {
2754
+ params: { path: pathParams },
2755
+ body
2756
+ }));
2757
+ }
2758
+ /**
1883
2759
  * List all categories
1884
2760
  * @param query - Optional query parameters for filtering categories
1885
2761
  * @returns Promise with list of categories
@@ -2229,43 +3105,6 @@ var PosClient = class extends PosAPIClient {
2229
3105
  } }));
2230
3106
  }
2231
3107
  /**
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
3108
  * List product variants
2270
3109
  * @param pathParams - The path parameters. Accepts product ID or product slug.
2271
3110
  * @param headers - Optional header parameters
@@ -2560,6 +3399,142 @@ var PosClient = class extends PosAPIClient {
2560
3399
  return this.executeRequest(() => this.client.GET("/pos/customers/{id}", { params: { path: pathParams } }));
2561
3400
  }
2562
3401
  /**
3402
+ * List a customer's saved addresses (Admin)
3403
+ * @param pathParams - Customer ID
3404
+ * @returns Promise with the customer's addresses
3405
+ * @example
3406
+ * ```typescript
3407
+ * const { data, error } = await pos.listAddresses({ id: "cust_123" });
3408
+ *
3409
+ * if (error) {
3410
+ * console.error("Failed to list addresses:", error.message);
3411
+ * } else {
3412
+ * data.addresses?.forEach(address => {
3413
+ * console.log(`${address.name}: ${address.city}, ${address.state}`);
3414
+ * });
3415
+ * }
3416
+ * ```
3417
+ */
3418
+ async listAddresses(pathParams) {
3419
+ return this.executeRequest(() => this.client.GET("/pos/customers/{id}/addresses", { params: { path: pathParams } }));
3420
+ }
3421
+ /**
3422
+ * Create an address on a customer's profile (Admin)
3423
+ * @param pathParams - Customer ID
3424
+ * @param body - The address to save
3425
+ * @returns Promise with the created address
3426
+ * @example
3427
+ * ```typescript
3428
+ * const { data, error } = await pos.createAddress(
3429
+ * { id: "cust_123" },
3430
+ * {
3431
+ * name: "Home",
3432
+ * address_line_1: "123 Main St",
3433
+ * city: "Mumbai",
3434
+ * state: "Maharashtra",
3435
+ * country: "India",
3436
+ * pincode: "400001",
3437
+ * phone: "9876543210"
3438
+ * }
3439
+ * );
3440
+ *
3441
+ * if (error) {
3442
+ * console.error("Failed to create address:", error.message);
3443
+ * }
3444
+ * ```
3445
+ */
3446
+ async createAddress(pathParams, body) {
3447
+ return this.executeRequest(() => this.client.POST("/pos/customers/{id}/addresses", {
3448
+ params: { path: pathParams },
3449
+ body
3450
+ }));
3451
+ }
3452
+ /**
3453
+ * Update a customer's saved address (Admin)
3454
+ * @param pathParams - Customer ID and address ID
3455
+ * @param body - The address fields to update
3456
+ * @returns Promise with the updated address
3457
+ * @example
3458
+ * ```typescript
3459
+ * const { data, error } = await pos.updateAddress(
3460
+ * { id: "cust_123", address_id: "addr_456" },
3461
+ * { phone: "9876543210" }
3462
+ * );
3463
+ *
3464
+ * if (error) {
3465
+ * console.error("Failed to update address:", error.message);
3466
+ * }
3467
+ * ```
3468
+ */
3469
+ async updateAddress(pathParams, body) {
3470
+ return this.executeRequest(() => this.client.PUT("/pos/customers/{id}/addresses/{address_id}", {
3471
+ params: { path: pathParams },
3472
+ body
3473
+ }));
3474
+ }
3475
+ /**
3476
+ * Delete a customer's saved address (Admin)
3477
+ * @param pathParams - Customer ID and address ID
3478
+ * @returns Promise with the deletion confirmation
3479
+ * @example
3480
+ * ```typescript
3481
+ * const { error } = await pos.deleteAddress({
3482
+ * id: "cust_123",
3483
+ * address_id: "addr_456"
3484
+ * });
3485
+ *
3486
+ * if (error) {
3487
+ * console.error("Failed to delete address:", error.message);
3488
+ * }
3489
+ * ```
3490
+ */
3491
+ async deleteAddress(pathParams) {
3492
+ return this.executeRequest(() => this.client.DELETE("/pos/customers/{id}/addresses/{address_id}", { params: { path: pathParams } }));
3493
+ }
3494
+ /**
3495
+ * List states for a country
3496
+ * @param pathParams - ISO country code
3497
+ * @returns Promise with the country's states
3498
+ * @example
3499
+ * ```typescript
3500
+ * const { data, error } = await pos.listCountryStates({
3501
+ * country_iso_code: "IN"
3502
+ * });
3503
+ *
3504
+ * if (error) {
3505
+ * console.error("Failed to list states:", error.message);
3506
+ * } else {
3507
+ * data.states?.forEach(state => console.log(state.name));
3508
+ * }
3509
+ * ```
3510
+ */
3511
+ async listCountryStates(pathParams) {
3512
+ return this.executeRequest(() => this.client.GET("/pos/common/countries/{country_iso_code}/states", { params: { path: pathParams } }));
3513
+ }
3514
+ /**
3515
+ * List pincodes for a country
3516
+ * @param pathParams - ISO country code
3517
+ * @param query - Optional query parameters for search and pagination
3518
+ * @returns Promise with the country's serviceable pincodes
3519
+ * @example
3520
+ * ```typescript
3521
+ * const { data, error } = await pos.listCountryPincodes(
3522
+ * { country_iso_code: "IN" },
3523
+ * { search: "4000" }
3524
+ * );
3525
+ *
3526
+ * if (error) {
3527
+ * console.error("Failed to list pincodes:", error.message);
3528
+ * }
3529
+ * ```
3530
+ */
3531
+ async listCountryPincodes(pathParams, query) {
3532
+ return this.executeRequest(() => this.client.GET("/pos/common/countries/{country_iso_code}/pincodes", { params: {
3533
+ path: pathParams,
3534
+ query
3535
+ } }));
3536
+ }
3537
+ /**
2563
3538
  * List all orders (Admin)
2564
3539
  * @param query - Optional query parameters for filtering orders
2565
3540
  * @returns Promise with list of orders
@@ -2589,7 +3564,7 @@ var PosClient = class extends PosAPIClient {
2589
3564
  * ```
2590
3565
  */
2591
3566
  async listOrders(query) {
2592
- return this.executeRequest(() => this.adminClient.GET("/pos/orders", { params: { query } }));
3567
+ return this.executeRequest(() => this.client.GET("/pos/orders", { params: { query } }));
2593
3568
  }
2594
3569
  /**
2595
3570
  * Get order details (Admin)
@@ -2778,9 +3753,13 @@ var PosClient = class extends PosAPIClient {
2778
3753
  /**
2779
3754
  * Check inventory for order (Admin)
2780
3755
  * @param pathParams - Order number
3756
+ * @param query - Optional shipment number and fulfillment type to evaluate against.
3757
+ * Defaults to the order's `unscheduled` shipment and that shipment's own
3758
+ * `fulfillment_type` when omitted.
2781
3759
  * @returns Promise with inventory check results
2782
3760
  * @example
2783
3761
  * ```typescript
3762
+ * // Check the order's unscheduled shipment
2784
3763
  * const { data, error } = await pos.checkInventory({
2785
3764
  * order_number: "ORD-2024-001234"
2786
3765
  * });
@@ -2788,17 +3767,31 @@ var PosClient = class extends PosAPIClient {
2788
3767
  * if (error) {
2789
3768
  * console.error("Failed to check inventory:", error.message);
2790
3769
  * } else {
2791
- * const inventory = data.content;
3770
+ * const inventory = data.inventory;
2792
3771
  * console.log(`Inventory Status: ${inventory?.inventory_status}`);
2793
3772
  * console.log(`Shipment Items:`, inventory?.shipment_items);
2794
3773
  * console.log(`Recommended Warehouses:`, inventory?.recommended_warehouses);
2795
3774
  * console.log(`Inventory Detail:`, inventory?.inventory_detail);
2796
- * console.log(`Allowed Actions:`, inventory?.allowed_action);
3775
+ * console.log(`Allowed Actions:`, inventory?.allowed_actions);
2797
3776
  * }
3777
+ *
3778
+ * // Check a specific shipment, evaluated as an in-store collection
3779
+ * const { data: pickupData } = await pos.checkInventory(
3780
+ * { order_number: "ORD-2024-001234" },
3781
+ * {
3782
+ * shipment_number: "SHIP-2024-001234",
3783
+ * fulfillment_type: "collect-in-store"
3784
+ * }
3785
+ * );
3786
+ *
3787
+ * console.log(`Can fulfill from store:`, pickupData?.inventory?.inventory_status);
2798
3788
  * ```
2799
3789
  */
2800
- async checkInventory(pathParams) {
2801
- return this.executeRequest(() => this.adminClient.GET("/pos/orders/{order_number}/check-inventory", { params: { path: pathParams } }));
3790
+ async checkInventory(pathParams, query) {
3791
+ return this.executeRequest(() => this.client.GET("/pos/orders/{order_number}/check-inventory", { params: {
3792
+ path: pathParams,
3793
+ query
3794
+ } }));
2802
3795
  }
2803
3796
  /**
2804
3797
  * Refund shortfall for order (Admin)
@@ -2873,6 +3866,30 @@ var PosClient = class extends PosAPIClient {
2873
3866
  return this.executeRequest(() => this.client.GET("/pos/shipping/shipments/{reference_number}", { params: { path: pathParams } }));
2874
3867
  }
2875
3868
  /**
3869
+ * Get shipment activities (Admin)
3870
+ * @param pathParams - Shipment reference number
3871
+ * @returns Promise with the shipment's activity trail
3872
+ * @example
3873
+ * ```typescript
3874
+ * const { data, error } = await pos.getShipmentActivities({
3875
+ * reference_number: "SHIP-2024-001234"
3876
+ * });
3877
+ *
3878
+ * if (error) {
3879
+ * console.error("Failed to get shipment activities:", error.message);
3880
+ * } else {
3881
+ * data.activities?.forEach(activity => {
3882
+ * console.log(`[${activity.created_at}] ${activity.activity_type} - ${activity.status}`);
3883
+ * console.log(` ${activity.comment} (by ${activity.user_name}, ${activity.user_type})`);
3884
+ * });
3885
+ * console.log(`Total activities: ${data.pagination?.total_records}`);
3886
+ * }
3887
+ * ```
3888
+ */
3889
+ async getShipmentActivities(pathParams) {
3890
+ return this.executeRequest(() => this.client.GET("/pos/shipping/shipments/{reference_number}/activities", { params: { path: pathParams } }));
3891
+ }
3892
+ /**
2876
3893
  * Get shipment invoice (Admin)
2877
3894
  * @param pathParams - Shipment reference number
2878
3895
  * @param query - Optional format parameter
@@ -2952,6 +3969,93 @@ var PosClient = class extends PosAPIClient {
2952
3969
  body
2953
3970
  }));
2954
3971
  }
3972
+ /**
3973
+ * Create a replacement shipment against an existing shipment (Admin)
3974
+ * @param pathParams - Reference number of the original shipment being replaced
3975
+ * @param body - Replacement payload. Use `shipping_option: "manual"` to dispatch
3976
+ * outside an integrated carrier, `shipping_option: "auto"` to fulfill through an
3977
+ * integrated carrier with system-managed rates, or `fulfillment_type:
3978
+ * "collect-in-store"` when the customer picks the items up in store.
3979
+ * @returns Promise with replacement confirmation
3980
+ * @example
3981
+ * ```typescript
3982
+ * // Replacement shipped through an integrated carrier, system-packed
3983
+ * const { data, error } = await pos.createReplacementShipment(
3984
+ * { reference_number: "SHIP-2024-001234" },
3985
+ * {
3986
+ * fulfillment_type: "delivery",
3987
+ * warehouse_id: "WH-001",
3988
+ * shipment_items: [
3989
+ * { product_id: "PROD-123", variant_id: "VAR-456", quantity: 1 }
3990
+ * ],
3991
+ * shipping_option: "auto",
3992
+ * shipping_provider_id: "SP-001",
3993
+ * packing_option: "auto",
3994
+ * total_weight: 1.5,
3995
+ * reason: "Damaged on delivery"
3996
+ * }
3997
+ * );
3998
+ *
3999
+ * if (error) {
4000
+ * console.error("Failed to create replacement:", error.message);
4001
+ * } else {
4002
+ * console.log("Replacement created:", data.message);
4003
+ * }
4004
+ *
4005
+ * // Replacement dispatched manually, with explicit box packing
4006
+ * const { data: manualData } = await pos.createReplacementShipment(
4007
+ * { reference_number: "SHIP-2024-001234" },
4008
+ * {
4009
+ * fulfillment_type: "delivery",
4010
+ * warehouse_id: "WH-001",
4011
+ * shipment_items: [
4012
+ * { product_id: "PROD-123", variant_id: null, quantity: 2 }
4013
+ * ],
4014
+ * shipping_option: "manual",
4015
+ * shipping_provider_id: "SP-MANUAL-01",
4016
+ * packing_option: "manual",
4017
+ * boxes: [
4018
+ * {
4019
+ * box_name: "Medium",
4020
+ * box_length: 30,
4021
+ * box_width: 20,
4022
+ * box_height: 15,
4023
+ * box_weight: 2,
4024
+ * items_count: 2,
4025
+ * box_count: 1
4026
+ * }
4027
+ * ],
4028
+ * total_weight: 2,
4029
+ * expected_delivery_date: "2024-01-20",
4030
+ * tracking_link: "https://tracking.example.com/AWB123456789",
4031
+ * manual_shipping_charges: 120,
4032
+ * reason: "Wrong item shipped"
4033
+ * }
4034
+ * );
4035
+ *
4036
+ * // Replacement collected by the customer in store
4037
+ * const { data: pickupData } = await pos.createReplacementShipment(
4038
+ * { reference_number: "SHIP-2024-001234" },
4039
+ * {
4040
+ * fulfillment_type: "collect-in-store",
4041
+ * warehouse_id: "STORE-042",
4042
+ * shipment_items: [
4043
+ * { product_id: "PROD-123", variant_id: "VAR-456", quantity: 1 }
4044
+ * ],
4045
+ * shipping_option: "manual",
4046
+ * shipping_provider_id: "SP-MANUAL-01",
4047
+ * packing_option: "auto",
4048
+ * reason: "Size exchange"
4049
+ * }
4050
+ * );
4051
+ * ```
4052
+ */
4053
+ async createReplacementShipment(pathParams, body) {
4054
+ return this.executeRequest(() => this.client.POST("/pos/shipping/shipments/{reference_number}/replacement", {
4055
+ params: { path: pathParams },
4056
+ body
4057
+ }));
4058
+ }
2955
4059
  };
2956
4060
  //#endregion
2957
4061
  //#region src/index.ts
@@ -2981,6 +4085,7 @@ var PosSDK = class {
2981
4085
  tokenStorage: options.tokenStorage,
2982
4086
  onTokensUpdated: options.onTokensUpdated,
2983
4087
  onTokensCleared: options.onTokensCleared,
4088
+ storeTokenResponses: options.storeTokenResponses,
2984
4089
  defaultHeaders: options.defaultHeaders,
2985
4090
  debug: options.debug,
2986
4091
  logger: options.logger
@@ -3113,6 +4218,6 @@ var PosSDK = class {
3113
4218
  }
3114
4219
  };
3115
4220
  //#endregion
3116
- export { BrowserTokenStorage, Environment, MemoryTokenStorage, PosAPIClient, PosClient, PosSDK, PosSDK as default, ResponseUtils };
4221
+ export { BrowserTokenStorage, CoordinatedBrowserTokenStorage, Environment, MemoryTokenStorage, PosAPIClient, PosClient, PosSDK, PosSDK as default, RefreshRejectedError, RefreshUnavailableError, ResponseUtils, authTokenStorage, isRefreshFailure, refreshEndsSession };
3117
4222
 
3118
4223
  //# sourceMappingURL=index.mjs.map