@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/README.md +9 -5
- package/dist/index.d.mts +6451 -4961
- package/dist/index.mjs +1201 -96
- 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
|
@@ -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
|
-
|
|
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
|
|
681
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ${
|
|
1436
|
+
Authorization: `Bearer ${accessToken}`
|
|
726
1437
|
},
|
|
727
1438
|
body: JSON.stringify({ refresh_token: refreshToken })
|
|
728
1439
|
});
|
|
729
|
-
if (!response.ok) throw new
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
782
|
-
|
|
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
|
-
|
|
800
|
-
|
|
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 -
|
|
1843
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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?.
|
|
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.
|
|
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
|