@moonbase.sh/storefront-api 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -56,7 +56,13 @@ function utmToObject(utm) {
56
56
  utm_campaign: utm == null ? void 0 : utm.campaign,
57
57
  utm_term: utm == null ? void 0 : utm.term,
58
58
  utm_content: utm == null ? void 0 : utm.content,
59
- utm_referrer: utm == null ? void 0 : utm.referrer
59
+ utm_referrer: utm == null ? void 0 : utm.referrer,
60
+ gclid: utm == null ? void 0 : utm.gclid,
61
+ gbraid: utm == null ? void 0 : utm.gbraid,
62
+ wbraid: utm == null ? void 0 : utm.wbraid,
63
+ fbclid: utm == null ? void 0 : utm.fbclid,
64
+ msclkid: utm == null ? void 0 : utm.msclkid,
65
+ ttclid: utm == null ? void 0 : utm.ttclid
60
66
  }).filter(([_, value]) => value !== void 0).reduce((obj, [key, value]) => ({
61
67
  ...obj,
62
68
  [key]: value
@@ -148,6 +154,7 @@ var schemas_exports = {};
148
154
  __export(schemas_exports, {
149
155
  downloadSchema: () => downloadSchema,
150
156
  manifestSchema: () => manifestSchema,
157
+ productReleaseSchema: () => productReleaseSchema,
151
158
  productSummarySchema: () => productSummarySchema
152
159
  });
153
160
  import { z as z2 } from "zod";
@@ -210,10 +217,20 @@ var productSummarySchema = z2.object({
210
217
  downloadsNeedsOwnership: z2.boolean(),
211
218
  downloadsNeedsGroupMembership: z2.boolean(),
212
219
  // Whether this caller clears every requirement above, including the group membership the client
213
- // can't see for itself.
220
+ // can't see for itself. False with none of them set means the merchant closed the release to everyone.
214
221
  downloadsAllowed: z2.boolean(),
215
222
  downloads: z2.array(downloadSchema).optional()
216
223
  });
224
+ var productReleaseSchema = z2.object({
225
+ version: z2.string(),
226
+ description: z2.string().nullish(),
227
+ publishedAt: z2.coerce.date(),
228
+ // Whether this caller may download the release, resolved against its own access control: a
229
+ // merchant can keep past releases to owners while the current one is open to anyone.
230
+ downloadsAllowed: z2.boolean(),
231
+ downloads: z2.array(downloadSchema),
232
+ properties: z2.record(rawPropertyValueSchema).nullish()
233
+ });
217
234
 
218
235
  // src/storefront/schemas.ts
219
236
  var storefrontProductSchema = z3.object({
@@ -397,7 +414,10 @@ var activationRequestSchema = z4.object({
397
414
  requiresAccount: z4.boolean(),
398
415
  requiresNewsletterOptIn: z4.boolean(),
399
416
  numberOfDaysPerProduct: z4.number(),
400
- numberOfDaysRemaining: z4.number()
417
+ numberOfDaysRemaining: z4.number(),
418
+ // The trial never expires, so the day counts are not worth showing. Absent from a backend that
419
+ // predates endless trials.
420
+ endless: z4.boolean().default(false)
401
421
  }).optional(),
402
422
  licenseEligibility: z4.object({
403
423
  eligible: z4.boolean()
@@ -774,8 +794,12 @@ var IdentityEndpoints = class {
774
794
  return userSchema.parse(identity);
775
795
  }
776
796
  async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn, options) {
777
- const path = `/api/customer/identity/sign-up?scheme=JWT&communicationOptIn=${communicationOptIn ? "true" : "false"}`;
778
- const response = await fetch(`${this.api.baseUrl}${path}`, {
797
+ const query = new URLSearchParams({
798
+ scheme: "JWT",
799
+ communicationOptIn: communicationOptIn ? "true" : "false",
800
+ ...utmToObject(options == null ? void 0 : options.utm)
801
+ });
802
+ const response = await fetch(`${this.api.baseUrl}/api/customer/identity/sign-up?${query.toString()}`, {
779
803
  method: "POST",
780
804
  headers: {
781
805
  "Accept": "application/json",
@@ -811,8 +835,13 @@ var IdentityEndpoints = class {
811
835
  this.tokenStore.setUser(identity);
812
836
  return { status: "signed-in", user: userSchema.parse(identity) };
813
837
  }
838
+ /**
839
+ * Signs the buyer out here at once, and ends the session on the server so its refresh
840
+ * token stops working too. Resolves once the server has answered, and never rejects:
841
+ * the buyer is signed out in this browser whatever happens to that request.
842
+ */
814
843
  async signOut() {
815
- this.tokenStore.clear();
844
+ await this.tokenStore.signOut();
816
845
  }
817
846
  async update(name, email, emailConfirmationToken, communicationPreferences) {
818
847
  const response = await this.api.authenticatedFetch("/api/customer/identity", userSchema, {
@@ -1019,6 +1048,15 @@ var ProductEndpoints = class {
1019
1048
  const response = await this.api.fetch(`/api/customer/inventory/products/${productId}${version2 ? `?version=${version2}${this.configuration.includeManifests ? "&includeManifests=true" : ""}` : this.configuration.includeManifests ? "?includeManifests=true" : ""}`, productSummarySchema);
1020
1049
  return response.data;
1021
1050
  }
1051
+ /**
1052
+ * The product's published releases, highest version first, with their release notes and files,
1053
+ * and whether the caller may download each. Open to anyone who can see the product, signed in or
1054
+ * not. A page can hold fewer releases than its size, so keep following `next` while `hasMore` is set.
1055
+ */
1056
+ async getReleases(productId, nextUrl) {
1057
+ const response = await this.api.fetch(nextUrl || `/api/customer/inventory/products/${productId}/releases${this.configuration.includeManifests ? "?includeManifests=true" : ""}`, paged(productReleaseSchema));
1058
+ return response.data;
1059
+ }
1022
1060
  async getOwned(nextUrl) {
1023
1061
  const response = await this.api.authenticatedFetch(nextUrl || `/api/customer/inventory/products${this.configuration.includeManifests ? "?includeManifests=true" : ""}`, paged(productSummarySchema));
1024
1062
  return response.data;
@@ -1045,7 +1083,7 @@ var ProductEndpoints = class {
1045
1083
  import { z as z14 } from "zod";
1046
1084
 
1047
1085
  // package.json
1048
- var version = "3.6.0";
1086
+ var version = "3.8.0";
1049
1087
 
1050
1088
  // src/utils/client.ts
1051
1089
  function buildClientHeader(sdk) {
@@ -1724,24 +1762,36 @@ var InMemoryStore = class {
1724
1762
  };
1725
1763
 
1726
1764
  // src/utils/tokenStore.ts
1765
+ var defaultAccessTokenLifetime = 15 * 60 * 1e3;
1766
+ var renewBeforeExpiry = 5 * 60 * 1e3;
1767
+ var expiryMargin = 30 * 1e3;
1768
+ var refreshLockName = "moonbase_auth_refresh";
1769
+ function accessTokenLifetime(accessToken) {
1770
+ try {
1771
+ const payload = accessToken.split(".")[1].replaceAll("-", "+").replaceAll("_", "/");
1772
+ const claims = JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, "=")));
1773
+ const lifetime = (Number(claims.exp) - Number(claims.iat)) * 1e3;
1774
+ if (Number.isFinite(lifetime) && lifetime > 0 && lifetime <= 24 * 60 * 60 * 1e3)
1775
+ return lifetime;
1776
+ } catch (e) {
1777
+ }
1778
+ return defaultAccessTokenLifetime;
1779
+ }
1727
1780
  var _TokenStore = class _TokenStore {
1728
1781
  constructor(configuration) {
1729
1782
  this.configuration = configuration;
1730
1783
  this.tokens = null;
1731
1784
  this.refreshTimeoutId = null;
1732
1785
  this.refreshPromise = null;
1786
+ this.waitingForVisibility = false;
1787
+ this.sessionEndedListeners = [];
1733
1788
  var _a, _b;
1734
1789
  this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
1735
1790
  this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
1736
1791
  if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
1737
1792
  this.warnAboutStorage();
1738
- const storedToken = this.store.get(_TokenStore.storageKey);
1739
- if (storedToken) {
1740
- this.tokens = {
1741
- ...storedToken,
1742
- expiresAt: new Date(storedToken.expiresAt)
1743
- };
1744
- }
1793
+ this.tokens = this.readStoredTokens();
1794
+ this.scheduleRefresh();
1745
1795
  this.store.listen(_TokenStore.storageKey, (updatedTokens) => this.handleStorageUpdate(updatedTokens));
1746
1796
  }
1747
1797
  get user() {
@@ -1775,29 +1825,91 @@ var _TokenStore = class _TokenStore {
1775
1825
  } else {
1776
1826
  this.tokens = {
1777
1827
  ...identity,
1778
- // Hardcoded 15 minutes now, might want to check the JWT tho
1779
- expiresAt: new Date((/* @__PURE__ */ new Date()).getTime() + 15 * 60 * 1e3)
1828
+ expiresAt: new Date(Date.now() + accessTokenLifetime(identity.accessToken))
1780
1829
  };
1781
1830
  }
1782
1831
  this.store.set(_TokenStore.storageKey, this.tokens);
1783
- if (typeof window !== "undefined") {
1784
- if (this.refreshTimeoutId != null)
1785
- window.clearTimeout(this.refreshTimeoutId);
1786
- this.refreshTimeoutId = window.setTimeout(() => {
1787
- this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
1788
- }, 10 * 60 * 1e3);
1789
- }
1832
+ this.scheduleRefresh();
1790
1833
  return this.tokens;
1791
1834
  }
1792
1835
  clear() {
1793
1836
  this.tokens = null;
1794
1837
  this.store.remove(_TokenStore.storageKey);
1795
- if (this.refreshTimeoutId != null && typeof window !== "undefined")
1796
- window.clearTimeout(this.refreshTimeoutId);
1838
+ this.scheduleRefresh();
1797
1839
  return null;
1798
1840
  }
1841
+ async signOut() {
1842
+ const tokens = this.tokens;
1843
+ this.clear();
1844
+ if (!tokens)
1845
+ return;
1846
+ try {
1847
+ await fetch(`${this.configuration.endpoint}/api/customer/identity/revoke?token=${encodeURIComponent(tokens.refreshToken)}`, {
1848
+ method: "POST",
1849
+ keepalive: true,
1850
+ headers: {
1851
+ "Accept": "application/json",
1852
+ "Content-Type": "text/plain",
1853
+ "x-mb-cors": "1",
1854
+ "x-mb-client": buildClientHeader(this.configuration.sdk)
1855
+ },
1856
+ body: tokens.accessToken
1857
+ });
1858
+ } catch (err) {
1859
+ this.logger.warn("Could not end the session on the server", err);
1860
+ }
1861
+ }
1862
+ onSessionEnded(listener) {
1863
+ this.sessionEndedListeners.push(listener);
1864
+ }
1799
1865
  get isExpired() {
1800
- return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
1866
+ return this.tokens != null && this.tokens.expiresAt.getTime() - expiryMargin < Date.now();
1867
+ }
1868
+ readStoredTokens() {
1869
+ const stored = this.store.get(_TokenStore.storageKey);
1870
+ return stored ? { ...stored, expiresAt: new Date(stored.expiresAt) } : null;
1871
+ }
1872
+ /**
1873
+ * (Re)arms the timer that renews the access token shortly before it runs out, or disarms
1874
+ * it when there is nothing to renew. Every tab arms one, and `refreshTokens` makes sure
1875
+ * only one of them actually renews.
1876
+ */
1877
+ scheduleRefresh() {
1878
+ if (typeof window === "undefined")
1879
+ return;
1880
+ if (this.refreshTimeoutId != null)
1881
+ window.clearTimeout(this.refreshTimeoutId);
1882
+ this.refreshTimeoutId = null;
1883
+ if (!this.tokens)
1884
+ return;
1885
+ const delay = Math.max(0, this.renewalDueAt(this.tokens) - Date.now());
1886
+ this.refreshTimeoutId = window.setTimeout(() => this.renewInBackground(), delay);
1887
+ }
1888
+ renewalDueAt(tokens) {
1889
+ const lead = Math.min(renewBeforeExpiry, accessTokenLifetime(tokens.accessToken) / 3);
1890
+ return tokens.expiresAt.getTime() - lead;
1891
+ }
1892
+ renewInBackground() {
1893
+ var _a;
1894
+ this.refreshTimeoutId = null;
1895
+ if (!this.tokens)
1896
+ return;
1897
+ if (this.renewalDueAt(this.tokens) > Date.now()) {
1898
+ this.scheduleRefresh();
1899
+ return;
1900
+ }
1901
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1902
+ if (!this.waitingForVisibility) {
1903
+ this.waitingForVisibility = true;
1904
+ document.addEventListener("visibilitychange", () => {
1905
+ this.waitingForVisibility = false;
1906
+ this.renewInBackground();
1907
+ }, { once: true });
1908
+ }
1909
+ return;
1910
+ }
1911
+ const pending = (_a = this.refreshPromise) != null ? _a : this.startRefresh();
1912
+ pending.catch((err) => this.logger.warn("Could not refresh access token", err));
1801
1913
  }
1802
1914
  /**
1803
1915
  * Start a refresh and hold it so concurrent callers join it rather than firing
@@ -1814,7 +1926,45 @@ var _TokenStore = class _TokenStore {
1814
1926
  this.refreshPromise = pending;
1815
1927
  return pending;
1816
1928
  }
1929
+ /**
1930
+ * A refresh token can be used once: the server answers a second use by revoking the
1931
+ * session, taking it for a stolen token. So refreshes are serialised across every tab of
1932
+ * the origin, and whoever gets the lock second takes up what the first one stored rather
1933
+ * than spending the same refresh token again. The stored copy has to be read, not waited
1934
+ * for: storage events never reach the document that wrote them, so a second client on
1935
+ * the same page would not hear about it.
1936
+ */
1817
1937
  async refreshTokens() {
1938
+ var _a;
1939
+ const spending = (_a = this.tokens) == null ? void 0 : _a.refreshToken;
1940
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
1941
+ if (locks == null ? void 0 : locks.request) {
1942
+ let held = false;
1943
+ try {
1944
+ return await locks.request(refreshLockName, () => {
1945
+ held = true;
1946
+ return this.refreshHoldingLock(spending);
1947
+ });
1948
+ } catch (err) {
1949
+ if (held)
1950
+ throw err;
1951
+ }
1952
+ }
1953
+ return await this.refreshHoldingLock(spending);
1954
+ }
1955
+ /**
1956
+ * @param spending The refresh token this refresh set out to use. If the session has moved
1957
+ * on from it while we waited for the lock, someone else has renewed already.
1958
+ */
1959
+ async refreshHoldingLock(spending) {
1960
+ var _a;
1961
+ const stored = this.readStoredTokens();
1962
+ if (stored && stored.refreshToken !== ((_a = this.tokens) == null ? void 0 : _a.refreshToken))
1963
+ this.tokens = stored;
1964
+ if (this.tokens && this.tokens.refreshToken !== spending && !this.isExpired) {
1965
+ this.scheduleRefresh();
1966
+ return this.tokens;
1967
+ }
1818
1968
  if (!this.tokens)
1819
1969
  throw new Error("No tokens found to refresh");
1820
1970
  const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
@@ -1830,6 +1980,13 @@ var _TokenStore = class _TokenStore {
1830
1980
  if (response.status !== 200) {
1831
1981
  if (response.status === 403 || response.status === 404) {
1832
1982
  this.clear();
1983
+ for (const listener of this.sessionEndedListeners) {
1984
+ try {
1985
+ listener();
1986
+ } catch (err) {
1987
+ this.logger.warn("A listener for the end of the session failed", err);
1988
+ }
1989
+ }
1833
1990
  return null;
1834
1991
  }
1835
1992
  throw new MoonbaseError("Unexpected result", `Could not refresh access token, status code ${response.status}`, response.status);
@@ -1840,11 +1997,10 @@ var _TokenStore = class _TokenStore {
1840
1997
  }
1841
1998
  handleStorageUpdate(updatedTokens) {
1842
1999
  this.tokens = updatedTokens;
1843
- if (this.refreshTimeoutId != null && typeof window !== "undefined")
1844
- window.clearTimeout(this.refreshTimeoutId);
1845
2000
  if (this.tokens) {
1846
2001
  this.tokens.expiresAt = new Date(this.tokens.expiresAt);
1847
2002
  }
2003
+ this.scheduleRefresh();
1848
2004
  }
1849
2005
  warnAboutStorage(err) {
1850
2006
  this.logger.warn(
@@ -2541,7 +2697,8 @@ function parseOffSiteReferrer(rawReferrer, currentOrigin) {
2541
2697
  }
2542
2698
  function resolveUtm(cached, fresh) {
2543
2699
  const freshHasUtm = !!(fresh.source || fresh.medium || fresh.campaign || fresh.term || fresh.content);
2544
- const freshIsExternalArrival = freshHasUtm || !!fresh.referrer;
2700
+ const freshHasClickId = !!(fresh.gclid || fresh.gbraid || fresh.wbraid || fresh.fbclid || fresh.msclkid || fresh.ttclid);
2701
+ const freshIsExternalArrival = freshHasUtm || freshHasClickId || !!fresh.referrer;
2545
2702
  if (freshIsExternalArrival)
2546
2703
  return fresh;
2547
2704
  return cached != null ? cached : fresh;
@@ -2599,6 +2756,7 @@ export {
2599
2756
  SubscriptionCancellationReason,
2600
2757
  SubscriptionStatus,
2601
2758
  TokenStore,
2759
+ accessTokenLifetime,
2602
2760
  imageWidths,
2603
2761
  isLocalStorageAvailable,
2604
2762
  isPromotionDisplay,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@moonbase.sh/storefront-api",
3
3
  "type": "module",
4
- "version": "3.6.0",
4
+ "version": "3.8.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",