@moonbase.sh/storefront-api 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -122,7 +122,17 @@ host — is returned untouched and logs a warning, since it will be served at fu
122
122
 
123
123
  ## Token storage and logging
124
124
 
125
- By default, credentials are stored in localStorage (or in-memory if localStorage is unavailable).
125
+ By default, credentials are stored in localStorage, falling back to memory only when localStorage
126
+ cannot be reached at all. Even that check sits inside a `try`, because reading
127
+ `window.localStorage` itself throws in Firefox when the buyer has blocked cookies and site data.
128
+
129
+ Reachability, not writability, is the test on purpose: an origin that is merely full is still
130
+ holding the buyer's existing session, so it is used rather than discarded. Individual reads and
131
+ writes that fail degrade to an in-memory copy and warn through the configured logger. The session
132
+ stays valid for that page load; it just doesn't survive a reload, and
133
+ `client.tokenStore.isPersistent` goes false once that has happened if you want to tell the buyer.
134
+ A failure to *persist* credentials is never reported as a failure to sign in, so a locked-down
135
+ browser can't strand someone in a sign-in loop.
126
136
 
127
137
  You can inject your own storage/logger implementations:
128
138
 
package/dist/index.cjs CHANGED
@@ -49,6 +49,7 @@ __export(index_exports, {
49
49
  SubscriptionStatus: () => SubscriptionStatus,
50
50
  TokenStore: () => TokenStore,
51
51
  imageWidths: () => imageWidths,
52
+ isLocalStorageAvailable: () => isLocalStorageAvailable,
52
53
  isPromotionDisplay: () => isPromotionDisplay,
53
54
  isTransformableImageUrl: () => isTransformableImageUrl,
54
55
  maxImageDimension: () => maxImageDimension,
@@ -757,15 +758,15 @@ var IdentityEndpoints = class {
757
758
  throw new NotFoundError("User not found");
758
759
  if (response.status >= 400)
759
760
  await handleResponseProblem(response, this.logger);
761
+ let identity;
760
762
  try {
761
- const data = await response.json();
762
- const user = identitySchema.parse(data);
763
- this.tokenStore.setUser(user);
764
- return userSchema.parse(user);
763
+ identity = identitySchema.parse(await response.json());
765
764
  } catch (err) {
766
765
  this.logger.warn("Could not sign in user", { email, response, err });
767
766
  throw new MoonbaseError("Bad response", "Could not sign in user", response.status);
768
767
  }
768
+ this.tokenStore.setUser(identity);
769
+ return userSchema.parse(identity);
769
770
  }
770
771
  async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn) {
771
772
  const path = `/api/customer/identity/sign-up?scheme=JWT&communicationOptIn=${communicationOptIn ? "true" : "false"}`;
@@ -792,14 +793,15 @@ var IdentityEndpoints = class {
792
793
  const parsed = pendingActivationSchema.parse(data);
793
794
  return { status: "activation-pending", email: parsed.email };
794
795
  }
796
+ let identity;
795
797
  try {
796
- const identity = identitySchema.parse(data);
797
- this.tokenStore.setUser(identity);
798
- return { status: "signed-in", user: userSchema.parse(identity) };
798
+ identity = identitySchema.parse(data);
799
799
  } catch (err) {
800
800
  this.logger.warn("Could not sign up user", { email, response, err });
801
801
  throw new MoonbaseError("Bad response", "Could not sign up user", response.status);
802
802
  }
803
+ this.tokenStore.setUser(identity);
804
+ return { status: "signed-in", user: userSchema.parse(identity) };
803
805
  }
804
806
  async signOut() {
805
807
  this.tokenStore.clear();
@@ -1079,6 +1081,15 @@ var MoonbaseApi = class {
1079
1081
  status: response.status,
1080
1082
  duration: finishedAt.getTime() - startedAt.getTime()
1081
1083
  });
1084
+ if (response.type === "opaqueredirect" || response.status === 0) {
1085
+ const err = new MoonbaseError(
1086
+ "Unexpected redirect",
1087
+ `The Moonbase API redirected ${path}, which the SDK is not configured to follow`,
1088
+ response.status
1089
+ );
1090
+ this.reportRequestProblem(path, request, response, err);
1091
+ throw err;
1092
+ }
1082
1093
  if (response.status >= 400) {
1083
1094
  try {
1084
1095
  await handleResponseProblem(response, this.logger);
@@ -1543,28 +1554,81 @@ var ConsoleLogger = class {
1543
1554
  };
1544
1555
 
1545
1556
  // src/utils/store.ts
1557
+ function isLocalStorageAvailable() {
1558
+ try {
1559
+ return typeof window !== "undefined" && !!window.localStorage;
1560
+ } catch (e) {
1561
+ return false;
1562
+ }
1563
+ }
1546
1564
  var LocalStorageStore = class {
1565
+ constructor(onDegraded) {
1566
+ this.onDegraded = onDegraded;
1567
+ // Values whose write to localStorage failed. Read back in preference to
1568
+ // localStorage so the rest of the session sees what it just wrote.
1569
+ this.fallback = /* @__PURE__ */ new Map();
1570
+ this.degraded = false;
1571
+ }
1572
+ get isDegraded() {
1573
+ return this.degraded;
1574
+ }
1547
1575
  get(key) {
1548
- const item = localStorage.getItem(key);
1549
- if (item) {
1550
- return JSON.parse(item);
1576
+ if (this.fallback.has(key))
1577
+ return this.fallback.get(key);
1578
+ try {
1579
+ const item = localStorage.getItem(key);
1580
+ if (item) {
1581
+ return JSON.parse(item);
1582
+ }
1583
+ return null;
1584
+ } catch (err) {
1585
+ this.reportDegraded(err);
1586
+ return null;
1551
1587
  }
1552
- return null;
1553
1588
  }
1554
1589
  set(key, item) {
1555
- localStorage.setItem(key, JSON.stringify(item));
1590
+ try {
1591
+ localStorage.setItem(key, JSON.stringify(item));
1592
+ this.fallback.delete(key);
1593
+ } catch (err) {
1594
+ this.reportDegraded(err);
1595
+ this.fallback.set(key, item);
1596
+ }
1556
1597
  }
1557
1598
  remove(key) {
1558
- localStorage.removeItem(key);
1599
+ try {
1600
+ localStorage.removeItem(key);
1601
+ this.fallback.delete(key);
1602
+ return;
1603
+ } catch (err) {
1604
+ this.reportDegraded(err);
1605
+ }
1606
+ this.fallback.set(key, null);
1607
+ try {
1608
+ localStorage.setItem(key, "");
1609
+ } catch (e) {
1610
+ }
1559
1611
  }
1560
1612
  listen(key, callback) {
1561
- if (typeof window !== "undefined") {
1562
- window.addEventListener("storage", (event) => {
1563
- if (event.key === key) {
1564
- callback(event.newValue ? JSON.parse(event.newValue) : null);
1565
- }
1566
- });
1567
- }
1613
+ if (typeof window === "undefined")
1614
+ return;
1615
+ window.addEventListener("storage", (event) => {
1616
+ if (event.key !== key)
1617
+ return;
1618
+ this.fallback.delete(key);
1619
+ try {
1620
+ callback(event.newValue ? JSON.parse(event.newValue) : null);
1621
+ } catch (err) {
1622
+ console.warn(`Moonbase could not parse the stored value for ${key}`, err);
1623
+ }
1624
+ });
1625
+ }
1626
+ reportDegraded(err) {
1627
+ var _a;
1628
+ if (this.degraded)
1629
+ return;
1630
+ this.degraded = true;
1631
+ (_a = this.onDegraded) == null ? void 0 : _a.call(this, err);
1568
1632
  }
1569
1633
  };
1570
1634
  var InMemoryStore = class {
@@ -1602,8 +1666,11 @@ var _TokenStore = class _TokenStore {
1602
1666
  this.tokens = null;
1603
1667
  this.refreshTimeoutId = null;
1604
1668
  this.refreshPromise = null;
1605
- var _a;
1606
- this.store = (_a = configuration.store) != null ? _a : typeof window !== "undefined" && localStorage ? new LocalStorageStore() : new InMemoryStore();
1669
+ var _a, _b;
1670
+ this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
1671
+ this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
1672
+ if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
1673
+ this.warnAboutStorage();
1607
1674
  const storedToken = this.store.get(_TokenStore.storageKey);
1608
1675
  if (storedToken) {
1609
1676
  this.tokens = {
@@ -1618,18 +1685,17 @@ var _TokenStore = class _TokenStore {
1618
1685
  return this.tokens;
1619
1686
  return null;
1620
1687
  }
1688
+ get isPersistent() {
1689
+ return this.store instanceof LocalStorageStore && !this.store.isDegraded;
1690
+ }
1621
1691
  async getAccessToken() {
1622
1692
  var _a, _b, _c;
1623
- if (this.isExpired) {
1624
- if (this.refreshPromise) {
1625
- const tokens2 = await this.refreshPromise;
1626
- return (_a = tokens2 == null ? void 0 : tokens2.accessToken) != null ? _a : null;
1627
- }
1628
- this.refreshPromise = this.refreshTokens();
1629
- const tokens = await this.refreshPromise;
1630
- return (_b = tokens == null ? void 0 : tokens.accessToken) != null ? _b : null;
1693
+ if (!this.isExpired) {
1694
+ return ((_a = this.tokens) == null ? void 0 : _a.accessToken) || null;
1631
1695
  }
1632
- return ((_c = this.tokens) == null ? void 0 : _c.accessToken) || null;
1696
+ const pending = (_b = this.refreshPromise) != null ? _b : this.startRefresh();
1697
+ const tokens = await pending;
1698
+ return (_c = tokens == null ? void 0 : tokens.accessToken) != null ? _c : null;
1633
1699
  }
1634
1700
  setUser(user) {
1635
1701
  const identity = user;
@@ -1654,7 +1720,7 @@ var _TokenStore = class _TokenStore {
1654
1720
  if (this.refreshTimeoutId != null)
1655
1721
  window.clearTimeout(this.refreshTimeoutId);
1656
1722
  this.refreshTimeoutId = window.setTimeout(() => {
1657
- this.refreshPromise = this.refreshTokens();
1723
+ this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
1658
1724
  }, 10 * 60 * 1e3);
1659
1725
  }
1660
1726
  return this.tokens;
@@ -1669,10 +1735,25 @@ var _TokenStore = class _TokenStore {
1669
1735
  get isExpired() {
1670
1736
  return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
1671
1737
  }
1738
+ /**
1739
+ * Start a refresh and hold it so concurrent callers join it rather than firing
1740
+ * their own. The slot is cleared as soon as it settles: a retained promise
1741
+ * would hand every later expiry the same stale access token, and a rejected one
1742
+ * that stayed put would re-throw on every subsequent request (including the
1743
+ * anonymous ones) for the rest of the session.
1744
+ */
1745
+ startRefresh() {
1746
+ const pending = this.refreshTokens().finally(() => {
1747
+ if (this.refreshPromise === pending)
1748
+ this.refreshPromise = null;
1749
+ });
1750
+ this.refreshPromise = pending;
1751
+ return pending;
1752
+ }
1672
1753
  async refreshTokens() {
1673
1754
  if (!this.tokens)
1674
1755
  throw new Error("No tokens found to refresh");
1675
- const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${this.tokens.refreshToken}`, {
1756
+ const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
1676
1757
  method: "POST",
1677
1758
  headers: {
1678
1759
  "Accept": "application/json",
@@ -1694,12 +1775,18 @@ var _TokenStore = class _TokenStore {
1694
1775
  }
1695
1776
  handleStorageUpdate(updatedTokens) {
1696
1777
  this.tokens = updatedTokens;
1697
- if (this.refreshTimeoutId != null)
1778
+ if (this.refreshTimeoutId != null && typeof window !== "undefined")
1698
1779
  window.clearTimeout(this.refreshTimeoutId);
1699
1780
  if (this.tokens) {
1700
1781
  this.tokens.expiresAt = new Date(this.tokens.expiresAt);
1701
1782
  }
1702
1783
  }
1784
+ warnAboutStorage(err) {
1785
+ this.logger.warn(
1786
+ "Moonbase could not persist credentials to localStorage: this browser is blocking or has exhausted site storage. Sign-in will work for this page load but will not survive a reload.",
1787
+ err
1788
+ );
1789
+ }
1703
1790
  };
1704
1791
  _TokenStore.storageKey = "moonbase_auth";
1705
1792
  var TokenStore = _TokenStore;
@@ -2432,6 +2519,7 @@ var MoonbaseClient = class {
2432
2519
  SubscriptionStatus,
2433
2520
  TokenStore,
2434
2521
  imageWidths,
2522
+ isLocalStorageAvailable,
2435
2523
  isPromotionDisplay,
2436
2524
  isTransformableImageUrl,
2437
2525
  maxImageDimension,
package/dist/index.d.cts CHANGED
@@ -26,12 +26,47 @@ interface IStore {
26
26
  set: <TItem>(key: string, item: TItem) => void;
27
27
  remove: (key: string) => void;
28
28
  listen: <TItem>(key: string, callback: (item: TItem | null) => void) => void;
29
+ /**
30
+ * True once any storage operation has failed and the store has fallen back to
31
+ * memory. Optional so custom stores don't have to implement it.
32
+ */
33
+ readonly isDegraded?: boolean;
29
34
  }
35
+ /**
36
+ * Whether `localStorage` can be reached at all.
37
+ *
38
+ * Deliberately *not* a write probe. Writability is not the right question here,
39
+ * because `LocalStorageStore` degrades per operation when a write fails, so a
40
+ * storage that reads fine but is at quota is still worth using: it is holding the
41
+ * buyer's existing session, and refusing it would throw away a perfectly readable
42
+ * sign-in and send them back to the login form. The only thing that makes
43
+ * `localStorage` unusable is not being able to touch it, which is what Firefox
44
+ * does (`SecurityError` from the property getter) when the buyer has blocked
45
+ * cookies and site data. That is why even the truthiness check sits inside a
46
+ * `try`.
47
+ */
48
+ declare function isLocalStorageAvailable(): boolean;
49
+ /**
50
+ * `localStorage`-backed store that degrades to memory instead of throwing.
51
+ *
52
+ * Storage can start failing at any point in a session, not just at construction:
53
+ * the origin hits quota, an extension clears site data, the profile's storage
54
+ * file goes corrupt. Since this is where the auth tokens live, an unhandled
55
+ * throw here surfaces as a failed sign-in for a buyer whose credentials were
56
+ * accepted. So every operation falls back to an in-memory copy. The session
57
+ * keeps working; it just stops surviving a reload.
58
+ */
30
59
  declare class LocalStorageStore implements IStore {
60
+ private readonly onDegraded?;
61
+ private readonly fallback;
62
+ private degraded;
63
+ constructor(onDegraded?: ((err: unknown) => void) | undefined);
64
+ get isDegraded(): boolean;
31
65
  get<TItem>(key: string): TItem | null;
32
66
  set<TItem>(key: string, item: TItem): void;
33
67
  remove(key: string): void;
34
68
  listen<TItem>(key: string, callback: (item: TItem | null) => void): void;
69
+ private reportDegraded;
35
70
  }
36
71
  declare class InMemoryStore implements IStore {
37
72
  private readonly store;
@@ -47,6 +82,7 @@ type IdentityWithExpiry = Identity & {
47
82
  };
48
83
  interface ITokenStore {
49
84
  get user(): User | null;
85
+ get isPersistent(): boolean;
50
86
  getAccessToken: () => Promise<string | null>;
51
87
  setUser: (user: Identity | User) => User | null;
52
88
  clear: () => void;
@@ -57,15 +93,26 @@ declare class TokenStore implements ITokenStore {
57
93
  private refreshTimeoutId;
58
94
  private refreshPromise;
59
95
  private readonly store;
96
+ private readonly logger;
60
97
  static readonly storageKey = "moonbase_auth";
61
98
  constructor(configuration: MoonbaseConfiguration);
62
99
  get user(): User | null;
100
+ get isPersistent(): boolean;
63
101
  getAccessToken(): Promise<string | null>;
64
102
  setUser(user: Identity | User): IdentityWithExpiry | null;
65
103
  clear(): null;
66
104
  private get isExpired();
105
+ /**
106
+ * Start a refresh and hold it so concurrent callers join it rather than firing
107
+ * their own. The slot is cleared as soon as it settles: a retained promise
108
+ * would hand every later expiry the same stale access token, and a rejected one
109
+ * that stayed put would re-throw on every subsequent request (including the
110
+ * anonymous ones) for the rest of the session.
111
+ */
112
+ private startRefresh;
67
113
  private refreshTokens;
68
114
  private handleStorageUpdate;
115
+ private warnAboutStorage;
69
116
  }
70
117
 
71
118
  type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
@@ -47032,4 +47079,4 @@ declare class MoonbaseClient {
47032
47079
  orders: OrderEndpoints;
47033
47080
  }
47034
47081
 
47035
- export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
47082
+ export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
package/dist/index.d.ts CHANGED
@@ -26,12 +26,47 @@ interface IStore {
26
26
  set: <TItem>(key: string, item: TItem) => void;
27
27
  remove: (key: string) => void;
28
28
  listen: <TItem>(key: string, callback: (item: TItem | null) => void) => void;
29
+ /**
30
+ * True once any storage operation has failed and the store has fallen back to
31
+ * memory. Optional so custom stores don't have to implement it.
32
+ */
33
+ readonly isDegraded?: boolean;
29
34
  }
35
+ /**
36
+ * Whether `localStorage` can be reached at all.
37
+ *
38
+ * Deliberately *not* a write probe. Writability is not the right question here,
39
+ * because `LocalStorageStore` degrades per operation when a write fails, so a
40
+ * storage that reads fine but is at quota is still worth using: it is holding the
41
+ * buyer's existing session, and refusing it would throw away a perfectly readable
42
+ * sign-in and send them back to the login form. The only thing that makes
43
+ * `localStorage` unusable is not being able to touch it, which is what Firefox
44
+ * does (`SecurityError` from the property getter) when the buyer has blocked
45
+ * cookies and site data. That is why even the truthiness check sits inside a
46
+ * `try`.
47
+ */
48
+ declare function isLocalStorageAvailable(): boolean;
49
+ /**
50
+ * `localStorage`-backed store that degrades to memory instead of throwing.
51
+ *
52
+ * Storage can start failing at any point in a session, not just at construction:
53
+ * the origin hits quota, an extension clears site data, the profile's storage
54
+ * file goes corrupt. Since this is where the auth tokens live, an unhandled
55
+ * throw here surfaces as a failed sign-in for a buyer whose credentials were
56
+ * accepted. So every operation falls back to an in-memory copy. The session
57
+ * keeps working; it just stops surviving a reload.
58
+ */
30
59
  declare class LocalStorageStore implements IStore {
60
+ private readonly onDegraded?;
61
+ private readonly fallback;
62
+ private degraded;
63
+ constructor(onDegraded?: ((err: unknown) => void) | undefined);
64
+ get isDegraded(): boolean;
31
65
  get<TItem>(key: string): TItem | null;
32
66
  set<TItem>(key: string, item: TItem): void;
33
67
  remove(key: string): void;
34
68
  listen<TItem>(key: string, callback: (item: TItem | null) => void): void;
69
+ private reportDegraded;
35
70
  }
36
71
  declare class InMemoryStore implements IStore {
37
72
  private readonly store;
@@ -47,6 +82,7 @@ type IdentityWithExpiry = Identity & {
47
82
  };
48
83
  interface ITokenStore {
49
84
  get user(): User | null;
85
+ get isPersistent(): boolean;
50
86
  getAccessToken: () => Promise<string | null>;
51
87
  setUser: (user: Identity | User) => User | null;
52
88
  clear: () => void;
@@ -57,15 +93,26 @@ declare class TokenStore implements ITokenStore {
57
93
  private refreshTimeoutId;
58
94
  private refreshPromise;
59
95
  private readonly store;
96
+ private readonly logger;
60
97
  static readonly storageKey = "moonbase_auth";
61
98
  constructor(configuration: MoonbaseConfiguration);
62
99
  get user(): User | null;
100
+ get isPersistent(): boolean;
63
101
  getAccessToken(): Promise<string | null>;
64
102
  setUser(user: Identity | User): IdentityWithExpiry | null;
65
103
  clear(): null;
66
104
  private get isExpired();
105
+ /**
106
+ * Start a refresh and hold it so concurrent callers join it rather than firing
107
+ * their own. The slot is cleared as soon as it settles: a retained promise
108
+ * would hand every later expiry the same stale access token, and a rejected one
109
+ * that stayed put would re-throw on every subsequent request (including the
110
+ * anonymous ones) for the rest of the session.
111
+ */
112
+ private startRefresh;
67
113
  private refreshTokens;
68
114
  private handleStorageUpdate;
115
+ private warnAboutStorage;
69
116
  }
70
117
 
71
118
  type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
@@ -47032,4 +47079,4 @@ declare class MoonbaseClient {
47032
47079
  orders: OrderEndpoints;
47033
47080
  }
47034
47081
 
47035
- export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
47082
+ export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
package/dist/index.js CHANGED
@@ -699,15 +699,15 @@ var IdentityEndpoints = class {
699
699
  throw new NotFoundError("User not found");
700
700
  if (response.status >= 400)
701
701
  await handleResponseProblem(response, this.logger);
702
+ let identity;
702
703
  try {
703
- const data = await response.json();
704
- const user = identitySchema.parse(data);
705
- this.tokenStore.setUser(user);
706
- return userSchema.parse(user);
704
+ identity = identitySchema.parse(await response.json());
707
705
  } catch (err) {
708
706
  this.logger.warn("Could not sign in user", { email, response, err });
709
707
  throw new MoonbaseError("Bad response", "Could not sign in user", response.status);
710
708
  }
709
+ this.tokenStore.setUser(identity);
710
+ return userSchema.parse(identity);
711
711
  }
712
712
  async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn) {
713
713
  const path = `/api/customer/identity/sign-up?scheme=JWT&communicationOptIn=${communicationOptIn ? "true" : "false"}`;
@@ -734,14 +734,15 @@ var IdentityEndpoints = class {
734
734
  const parsed = pendingActivationSchema.parse(data);
735
735
  return { status: "activation-pending", email: parsed.email };
736
736
  }
737
+ let identity;
737
738
  try {
738
- const identity = identitySchema.parse(data);
739
- this.tokenStore.setUser(identity);
740
- return { status: "signed-in", user: userSchema.parse(identity) };
739
+ identity = identitySchema.parse(data);
741
740
  } catch (err) {
742
741
  this.logger.warn("Could not sign up user", { email, response, err });
743
742
  throw new MoonbaseError("Bad response", "Could not sign up user", response.status);
744
743
  }
744
+ this.tokenStore.setUser(identity);
745
+ return { status: "signed-in", user: userSchema.parse(identity) };
745
746
  }
746
747
  async signOut() {
747
748
  this.tokenStore.clear();
@@ -1021,6 +1022,15 @@ var MoonbaseApi = class {
1021
1022
  status: response.status,
1022
1023
  duration: finishedAt.getTime() - startedAt.getTime()
1023
1024
  });
1025
+ if (response.type === "opaqueredirect" || response.status === 0) {
1026
+ const err = new MoonbaseError(
1027
+ "Unexpected redirect",
1028
+ `The Moonbase API redirected ${path}, which the SDK is not configured to follow`,
1029
+ response.status
1030
+ );
1031
+ this.reportRequestProblem(path, request, response, err);
1032
+ throw err;
1033
+ }
1024
1034
  if (response.status >= 400) {
1025
1035
  try {
1026
1036
  await handleResponseProblem(response, this.logger);
@@ -1485,28 +1495,81 @@ var ConsoleLogger = class {
1485
1495
  };
1486
1496
 
1487
1497
  // src/utils/store.ts
1498
+ function isLocalStorageAvailable() {
1499
+ try {
1500
+ return typeof window !== "undefined" && !!window.localStorage;
1501
+ } catch (e) {
1502
+ return false;
1503
+ }
1504
+ }
1488
1505
  var LocalStorageStore = class {
1506
+ constructor(onDegraded) {
1507
+ this.onDegraded = onDegraded;
1508
+ // Values whose write to localStorage failed. Read back in preference to
1509
+ // localStorage so the rest of the session sees what it just wrote.
1510
+ this.fallback = /* @__PURE__ */ new Map();
1511
+ this.degraded = false;
1512
+ }
1513
+ get isDegraded() {
1514
+ return this.degraded;
1515
+ }
1489
1516
  get(key) {
1490
- const item = localStorage.getItem(key);
1491
- if (item) {
1492
- return JSON.parse(item);
1517
+ if (this.fallback.has(key))
1518
+ return this.fallback.get(key);
1519
+ try {
1520
+ const item = localStorage.getItem(key);
1521
+ if (item) {
1522
+ return JSON.parse(item);
1523
+ }
1524
+ return null;
1525
+ } catch (err) {
1526
+ this.reportDegraded(err);
1527
+ return null;
1493
1528
  }
1494
- return null;
1495
1529
  }
1496
1530
  set(key, item) {
1497
- localStorage.setItem(key, JSON.stringify(item));
1531
+ try {
1532
+ localStorage.setItem(key, JSON.stringify(item));
1533
+ this.fallback.delete(key);
1534
+ } catch (err) {
1535
+ this.reportDegraded(err);
1536
+ this.fallback.set(key, item);
1537
+ }
1498
1538
  }
1499
1539
  remove(key) {
1500
- localStorage.removeItem(key);
1540
+ try {
1541
+ localStorage.removeItem(key);
1542
+ this.fallback.delete(key);
1543
+ return;
1544
+ } catch (err) {
1545
+ this.reportDegraded(err);
1546
+ }
1547
+ this.fallback.set(key, null);
1548
+ try {
1549
+ localStorage.setItem(key, "");
1550
+ } catch (e) {
1551
+ }
1501
1552
  }
1502
1553
  listen(key, callback) {
1503
- if (typeof window !== "undefined") {
1504
- window.addEventListener("storage", (event) => {
1505
- if (event.key === key) {
1506
- callback(event.newValue ? JSON.parse(event.newValue) : null);
1507
- }
1508
- });
1509
- }
1554
+ if (typeof window === "undefined")
1555
+ return;
1556
+ window.addEventListener("storage", (event) => {
1557
+ if (event.key !== key)
1558
+ return;
1559
+ this.fallback.delete(key);
1560
+ try {
1561
+ callback(event.newValue ? JSON.parse(event.newValue) : null);
1562
+ } catch (err) {
1563
+ console.warn(`Moonbase could not parse the stored value for ${key}`, err);
1564
+ }
1565
+ });
1566
+ }
1567
+ reportDegraded(err) {
1568
+ var _a;
1569
+ if (this.degraded)
1570
+ return;
1571
+ this.degraded = true;
1572
+ (_a = this.onDegraded) == null ? void 0 : _a.call(this, err);
1510
1573
  }
1511
1574
  };
1512
1575
  var InMemoryStore = class {
@@ -1544,8 +1607,11 @@ var _TokenStore = class _TokenStore {
1544
1607
  this.tokens = null;
1545
1608
  this.refreshTimeoutId = null;
1546
1609
  this.refreshPromise = null;
1547
- var _a;
1548
- this.store = (_a = configuration.store) != null ? _a : typeof window !== "undefined" && localStorage ? new LocalStorageStore() : new InMemoryStore();
1610
+ var _a, _b;
1611
+ this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
1612
+ this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
1613
+ if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
1614
+ this.warnAboutStorage();
1549
1615
  const storedToken = this.store.get(_TokenStore.storageKey);
1550
1616
  if (storedToken) {
1551
1617
  this.tokens = {
@@ -1560,18 +1626,17 @@ var _TokenStore = class _TokenStore {
1560
1626
  return this.tokens;
1561
1627
  return null;
1562
1628
  }
1629
+ get isPersistent() {
1630
+ return this.store instanceof LocalStorageStore && !this.store.isDegraded;
1631
+ }
1563
1632
  async getAccessToken() {
1564
1633
  var _a, _b, _c;
1565
- if (this.isExpired) {
1566
- if (this.refreshPromise) {
1567
- const tokens2 = await this.refreshPromise;
1568
- return (_a = tokens2 == null ? void 0 : tokens2.accessToken) != null ? _a : null;
1569
- }
1570
- this.refreshPromise = this.refreshTokens();
1571
- const tokens = await this.refreshPromise;
1572
- return (_b = tokens == null ? void 0 : tokens.accessToken) != null ? _b : null;
1634
+ if (!this.isExpired) {
1635
+ return ((_a = this.tokens) == null ? void 0 : _a.accessToken) || null;
1573
1636
  }
1574
- return ((_c = this.tokens) == null ? void 0 : _c.accessToken) || null;
1637
+ const pending = (_b = this.refreshPromise) != null ? _b : this.startRefresh();
1638
+ const tokens = await pending;
1639
+ return (_c = tokens == null ? void 0 : tokens.accessToken) != null ? _c : null;
1575
1640
  }
1576
1641
  setUser(user) {
1577
1642
  const identity = user;
@@ -1596,7 +1661,7 @@ var _TokenStore = class _TokenStore {
1596
1661
  if (this.refreshTimeoutId != null)
1597
1662
  window.clearTimeout(this.refreshTimeoutId);
1598
1663
  this.refreshTimeoutId = window.setTimeout(() => {
1599
- this.refreshPromise = this.refreshTokens();
1664
+ this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
1600
1665
  }, 10 * 60 * 1e3);
1601
1666
  }
1602
1667
  return this.tokens;
@@ -1611,10 +1676,25 @@ var _TokenStore = class _TokenStore {
1611
1676
  get isExpired() {
1612
1677
  return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
1613
1678
  }
1679
+ /**
1680
+ * Start a refresh and hold it so concurrent callers join it rather than firing
1681
+ * their own. The slot is cleared as soon as it settles: a retained promise
1682
+ * would hand every later expiry the same stale access token, and a rejected one
1683
+ * that stayed put would re-throw on every subsequent request (including the
1684
+ * anonymous ones) for the rest of the session.
1685
+ */
1686
+ startRefresh() {
1687
+ const pending = this.refreshTokens().finally(() => {
1688
+ if (this.refreshPromise === pending)
1689
+ this.refreshPromise = null;
1690
+ });
1691
+ this.refreshPromise = pending;
1692
+ return pending;
1693
+ }
1614
1694
  async refreshTokens() {
1615
1695
  if (!this.tokens)
1616
1696
  throw new Error("No tokens found to refresh");
1617
- const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${this.tokens.refreshToken}`, {
1697
+ const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
1618
1698
  method: "POST",
1619
1699
  headers: {
1620
1700
  "Accept": "application/json",
@@ -1636,12 +1716,18 @@ var _TokenStore = class _TokenStore {
1636
1716
  }
1637
1717
  handleStorageUpdate(updatedTokens) {
1638
1718
  this.tokens = updatedTokens;
1639
- if (this.refreshTimeoutId != null)
1719
+ if (this.refreshTimeoutId != null && typeof window !== "undefined")
1640
1720
  window.clearTimeout(this.refreshTimeoutId);
1641
1721
  if (this.tokens) {
1642
1722
  this.tokens.expiresAt = new Date(this.tokens.expiresAt);
1643
1723
  }
1644
1724
  }
1725
+ warnAboutStorage(err) {
1726
+ this.logger.warn(
1727
+ "Moonbase could not persist credentials to localStorage: this browser is blocking or has exhausted site storage. Sign-in will work for this page load but will not survive a reload.",
1728
+ err
1729
+ );
1730
+ }
1645
1731
  };
1646
1732
  _TokenStore.storageKey = "moonbase_auth";
1647
1733
  var TokenStore = _TokenStore;
@@ -2373,6 +2459,7 @@ export {
2373
2459
  SubscriptionStatus,
2374
2460
  TokenStore,
2375
2461
  imageWidths,
2462
+ isLocalStorageAvailable,
2376
2463
  isPromotionDisplay,
2377
2464
  isTransformableImageUrl,
2378
2465
  maxImageDimension,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@moonbase.sh/storefront-api",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "3.2.0",
5
5
  "description": "Package to let you build storefronts with Moonbase.sh as payment and delivery provider",
6
6
  "author": "Tobias Lønnerød Madsen <m@dsen.tv>",
7
7
  "license": "MIT",