@moonbase.sh/storefront-api 3.5.1 → 3.7.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
@@ -110,6 +110,68 @@ another tenant's) arrives as the same `NotFoundError`, so that a signed-in
110
110
  customer cannot use it to enumerate a merchant's group IDs. Joining twice
111
111
  succeeds and changes nothing.
112
112
 
113
+ ### Captchas on sign-up
114
+
115
+ A merchant can ask customers to solve a captcha before they sign up. The vendor
116
+ says whether this store does:
117
+
118
+ ```ts
119
+ const vendor = await client.vendor.get()
120
+ const captcha = vendor.features?.captcha // { url } when it does, null or undefined when not
121
+ ```
122
+
123
+ `captcha.url` is a page on the store's own `<account-id>.moonbase.sh` that draws
124
+ the challenge. Embed it in an iframe with `action=signup`, and it hands you the
125
+ solution with `postMessage`:
126
+
127
+ ```ts
128
+ const src = new URL(captcha.url)
129
+ src.searchParams.set('action', 'signup') // optional: theme=light|dark|auto, size=normal|compact
130
+
131
+ const frame = document.createElement('iframe')
132
+ frame.src = src.href
133
+ frame.title = 'Captcha'
134
+ frame.style.cssText = 'border: none; color-scheme: normal; width: 300px; height: 65px'
135
+ form.append(frame)
136
+
137
+ let captchaToken: string | undefined
138
+ window.addEventListener('message', (event) => {
139
+ if (event.source !== frame.contentWindow || event.origin !== src.origin || event.data?.source !== 'moonbase-captcha')
140
+ return
141
+ if (event.data.event === 'resize')
142
+ Object.assign(frame.style, { width: `${event.data.width}px`, height: `${event.data.height}px` })
143
+ else if (event.data.event === 'token')
144
+ captchaToken = event.data.token
145
+ else // 'expired', or 'error' with a `code` ('load-failed' when the challenge could not load)
146
+ captchaToken = undefined
147
+ })
148
+ ```
149
+
150
+ Then pass the solution along, and reset the frame after every attempt, since a
151
+ solution is good for one:
152
+
153
+ ```ts
154
+ try {
155
+ await client.identity.signUp('Jane', 'jane@example.com', 'password', null, true, true, true, {
156
+ captchaToken,
157
+ })
158
+ }
159
+ catch (err) {
160
+ if (err instanceof CaptchaRequiredError)
161
+ showError(err.message) // the API's own copy, safe to show
162
+ throw err
163
+ }
164
+ finally {
165
+ captchaToken = undefined
166
+ frame.contentWindow?.postMessage({ source: 'moonbase-captcha', command: 'reset' }, src.origin)
167
+ }
168
+ ```
169
+
170
+ Without a solution, or with one that is expired or already used, the sign-up is
171
+ refused with `CaptchaRequiredError` (a `MoonbaseError`). A site with a
172
+ Content-Security-Policy has to allow the store's `https://<account-id>.moonbase.sh`
173
+ in `frame-src`.
174
+
113
175
  ## Inventory flow
114
176
 
115
177
  Most inventory endpoints require an authenticated user:
package/dist/index.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  ActivationRequestStatus: () => ActivationRequestStatus,
26
26
  ActivationStatus: () => ActivationStatus,
27
27
  Architecture: () => Architecture,
28
+ CaptchaRequiredError: () => CaptchaRequiredError,
28
29
  CartUtils: () => CartUtils,
29
30
  ConnectableAccountProvider: () => ConnectableAccountProvider,
30
31
  ConsoleLogger: () => ConsoleLogger,
@@ -50,6 +51,7 @@ __export(index_exports, {
50
51
  SubscriptionCancellationReason: () => SubscriptionCancellationReason,
51
52
  SubscriptionStatus: () => SubscriptionStatus,
52
53
  TokenStore: () => TokenStore,
54
+ accessTokenLifetime: () => accessTokenLifetime,
53
55
  imageWidths: () => imageWidths,
54
56
  isLocalStorageAvailable: () => isLocalStorageAvailable,
55
57
  isPromotionDisplay: () => isPromotionDisplay,
@@ -459,7 +461,10 @@ var activationRequestSchema = import_zod4.z.object({
459
461
  requiresAccount: import_zod4.z.boolean(),
460
462
  requiresNewsletterOptIn: import_zod4.z.boolean(),
461
463
  numberOfDaysPerProduct: import_zod4.z.number(),
462
- numberOfDaysRemaining: import_zod4.z.number()
464
+ numberOfDaysRemaining: import_zod4.z.number(),
465
+ // The trial never expires, so the day counts are not worth showing. Absent from a backend that
466
+ // predates endless trials.
467
+ endless: import_zod4.z.boolean().default(false)
463
468
  }).optional(),
464
469
  licenseEligibility: import_zod4.z.object({
465
470
  eligible: import_zod4.z.boolean()
@@ -657,6 +662,12 @@ var MoonbaseError = class extends Error {
657
662
  }
658
663
  }
659
664
  };
665
+ var CaptchaRequiredError = class extends MoonbaseError {
666
+ constructor(detail, title) {
667
+ super(title != null ? title : "Captcha required", detail != null ? detail : "Please confirm you're not a robot.", 400);
668
+ this.name = "CaptchaRequiredError";
669
+ }
670
+ };
660
671
 
661
672
  // src/utils/problemDetails.ts
662
673
  var import_zod7 = require("zod");
@@ -699,6 +710,8 @@ async function handleResponseProblem(response, logger) {
699
710
  logger.debug("The response indicates a problem", problemDetails);
700
711
  if (problemDetails.errorType === "StoreClosed")
701
712
  throw new StoreClosedError(problemDetails.detail);
713
+ if (problemDetails.errorType === "CaptchaRequired")
714
+ throw new CaptchaRequiredError(problemDetails.detail, problemDetails.title);
702
715
  throw new MoonbaseError(
703
716
  problemDetails.title,
704
717
  problemDetails.detail,
@@ -835,7 +848,8 @@ var IdentityEndpoints = class {
835
848
  "Accept": "application/json",
836
849
  "Content-Type": "application/json",
837
850
  "x-mb-cors": "1",
838
- "x-mb-client": this.api.clientHeader
851
+ "x-mb-client": this.api.clientHeader,
852
+ ...(options == null ? void 0 : options.captchaToken) ? { "X-MB-CAPTCHA": options.captchaToken } : {}
839
853
  },
840
854
  body: JSON.stringify({
841
855
  name,
@@ -864,8 +878,13 @@ var IdentityEndpoints = class {
864
878
  this.tokenStore.setUser(identity);
865
879
  return { status: "signed-in", user: userSchema.parse(identity) };
866
880
  }
881
+ /**
882
+ * Signs the buyer out here at once, and ends the session on the server so its refresh
883
+ * token stops working too. Resolves once the server has answered, and never rejects:
884
+ * the buyer is signed out in this browser whatever happens to that request.
885
+ */
867
886
  async signOut() {
868
- this.tokenStore.clear();
887
+ await this.tokenStore.signOut();
869
888
  }
870
889
  async update(name, email, emailConfirmationToken, communicationPreferences) {
871
890
  const response = await this.api.authenticatedFetch("/api/customer/identity", userSchema, {
@@ -1098,7 +1117,7 @@ var ProductEndpoints = class {
1098
1117
  var import_zod14 = require("zod");
1099
1118
 
1100
1119
  // package.json
1101
- var version = "3.5.1";
1120
+ var version = "3.7.0";
1102
1121
 
1103
1122
  // src/utils/client.ts
1104
1123
  function buildClientHeader(sdk) {
@@ -1777,24 +1796,36 @@ var InMemoryStore = class {
1777
1796
  };
1778
1797
 
1779
1798
  // src/utils/tokenStore.ts
1799
+ var defaultAccessTokenLifetime = 15 * 60 * 1e3;
1800
+ var renewBeforeExpiry = 5 * 60 * 1e3;
1801
+ var expiryMargin = 30 * 1e3;
1802
+ var refreshLockName = "moonbase_auth_refresh";
1803
+ function accessTokenLifetime(accessToken) {
1804
+ try {
1805
+ const payload = accessToken.split(".")[1].replaceAll("-", "+").replaceAll("_", "/");
1806
+ const claims = JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, "=")));
1807
+ const lifetime = (Number(claims.exp) - Number(claims.iat)) * 1e3;
1808
+ if (Number.isFinite(lifetime) && lifetime > 0 && lifetime <= 24 * 60 * 60 * 1e3)
1809
+ return lifetime;
1810
+ } catch (e) {
1811
+ }
1812
+ return defaultAccessTokenLifetime;
1813
+ }
1780
1814
  var _TokenStore = class _TokenStore {
1781
1815
  constructor(configuration) {
1782
1816
  this.configuration = configuration;
1783
1817
  this.tokens = null;
1784
1818
  this.refreshTimeoutId = null;
1785
1819
  this.refreshPromise = null;
1820
+ this.waitingForVisibility = false;
1821
+ this.sessionEndedListeners = [];
1786
1822
  var _a, _b;
1787
1823
  this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
1788
1824
  this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
1789
1825
  if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
1790
1826
  this.warnAboutStorage();
1791
- const storedToken = this.store.get(_TokenStore.storageKey);
1792
- if (storedToken) {
1793
- this.tokens = {
1794
- ...storedToken,
1795
- expiresAt: new Date(storedToken.expiresAt)
1796
- };
1797
- }
1827
+ this.tokens = this.readStoredTokens();
1828
+ this.scheduleRefresh();
1798
1829
  this.store.listen(_TokenStore.storageKey, (updatedTokens) => this.handleStorageUpdate(updatedTokens));
1799
1830
  }
1800
1831
  get user() {
@@ -1828,29 +1859,91 @@ var _TokenStore = class _TokenStore {
1828
1859
  } else {
1829
1860
  this.tokens = {
1830
1861
  ...identity,
1831
- // Hardcoded 15 minutes now, might want to check the JWT tho
1832
- expiresAt: new Date((/* @__PURE__ */ new Date()).getTime() + 15 * 60 * 1e3)
1862
+ expiresAt: new Date(Date.now() + accessTokenLifetime(identity.accessToken))
1833
1863
  };
1834
1864
  }
1835
1865
  this.store.set(_TokenStore.storageKey, this.tokens);
1836
- if (typeof window !== "undefined") {
1837
- if (this.refreshTimeoutId != null)
1838
- window.clearTimeout(this.refreshTimeoutId);
1839
- this.refreshTimeoutId = window.setTimeout(() => {
1840
- this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
1841
- }, 10 * 60 * 1e3);
1842
- }
1866
+ this.scheduleRefresh();
1843
1867
  return this.tokens;
1844
1868
  }
1845
1869
  clear() {
1846
1870
  this.tokens = null;
1847
1871
  this.store.remove(_TokenStore.storageKey);
1848
- if (this.refreshTimeoutId != null && typeof window !== "undefined")
1849
- window.clearTimeout(this.refreshTimeoutId);
1872
+ this.scheduleRefresh();
1850
1873
  return null;
1851
1874
  }
1875
+ async signOut() {
1876
+ const tokens = this.tokens;
1877
+ this.clear();
1878
+ if (!tokens)
1879
+ return;
1880
+ try {
1881
+ await fetch(`${this.configuration.endpoint}/api/customer/identity/revoke?token=${encodeURIComponent(tokens.refreshToken)}`, {
1882
+ method: "POST",
1883
+ keepalive: true,
1884
+ headers: {
1885
+ "Accept": "application/json",
1886
+ "Content-Type": "text/plain",
1887
+ "x-mb-cors": "1",
1888
+ "x-mb-client": buildClientHeader(this.configuration.sdk)
1889
+ },
1890
+ body: tokens.accessToken
1891
+ });
1892
+ } catch (err) {
1893
+ this.logger.warn("Could not end the session on the server", err);
1894
+ }
1895
+ }
1896
+ onSessionEnded(listener) {
1897
+ this.sessionEndedListeners.push(listener);
1898
+ }
1852
1899
  get isExpired() {
1853
- return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
1900
+ return this.tokens != null && this.tokens.expiresAt.getTime() - expiryMargin < Date.now();
1901
+ }
1902
+ readStoredTokens() {
1903
+ const stored = this.store.get(_TokenStore.storageKey);
1904
+ return stored ? { ...stored, expiresAt: new Date(stored.expiresAt) } : null;
1905
+ }
1906
+ /**
1907
+ * (Re)arms the timer that renews the access token shortly before it runs out, or disarms
1908
+ * it when there is nothing to renew. Every tab arms one, and `refreshTokens` makes sure
1909
+ * only one of them actually renews.
1910
+ */
1911
+ scheduleRefresh() {
1912
+ if (typeof window === "undefined")
1913
+ return;
1914
+ if (this.refreshTimeoutId != null)
1915
+ window.clearTimeout(this.refreshTimeoutId);
1916
+ this.refreshTimeoutId = null;
1917
+ if (!this.tokens)
1918
+ return;
1919
+ const delay = Math.max(0, this.renewalDueAt(this.tokens) - Date.now());
1920
+ this.refreshTimeoutId = window.setTimeout(() => this.renewInBackground(), delay);
1921
+ }
1922
+ renewalDueAt(tokens) {
1923
+ const lead = Math.min(renewBeforeExpiry, accessTokenLifetime(tokens.accessToken) / 3);
1924
+ return tokens.expiresAt.getTime() - lead;
1925
+ }
1926
+ renewInBackground() {
1927
+ var _a;
1928
+ this.refreshTimeoutId = null;
1929
+ if (!this.tokens)
1930
+ return;
1931
+ if (this.renewalDueAt(this.tokens) > Date.now()) {
1932
+ this.scheduleRefresh();
1933
+ return;
1934
+ }
1935
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1936
+ if (!this.waitingForVisibility) {
1937
+ this.waitingForVisibility = true;
1938
+ document.addEventListener("visibilitychange", () => {
1939
+ this.waitingForVisibility = false;
1940
+ this.renewInBackground();
1941
+ }, { once: true });
1942
+ }
1943
+ return;
1944
+ }
1945
+ const pending = (_a = this.refreshPromise) != null ? _a : this.startRefresh();
1946
+ pending.catch((err) => this.logger.warn("Could not refresh access token", err));
1854
1947
  }
1855
1948
  /**
1856
1949
  * Start a refresh and hold it so concurrent callers join it rather than firing
@@ -1867,7 +1960,45 @@ var _TokenStore = class _TokenStore {
1867
1960
  this.refreshPromise = pending;
1868
1961
  return pending;
1869
1962
  }
1963
+ /**
1964
+ * A refresh token can be used once: the server answers a second use by revoking the
1965
+ * session, taking it for a stolen token. So refreshes are serialised across every tab of
1966
+ * the origin, and whoever gets the lock second takes up what the first one stored rather
1967
+ * than spending the same refresh token again. The stored copy has to be read, not waited
1968
+ * for: storage events never reach the document that wrote them, so a second client on
1969
+ * the same page would not hear about it.
1970
+ */
1870
1971
  async refreshTokens() {
1972
+ var _a;
1973
+ const spending = (_a = this.tokens) == null ? void 0 : _a.refreshToken;
1974
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
1975
+ if (locks == null ? void 0 : locks.request) {
1976
+ let held = false;
1977
+ try {
1978
+ return await locks.request(refreshLockName, () => {
1979
+ held = true;
1980
+ return this.refreshHoldingLock(spending);
1981
+ });
1982
+ } catch (err) {
1983
+ if (held)
1984
+ throw err;
1985
+ }
1986
+ }
1987
+ return await this.refreshHoldingLock(spending);
1988
+ }
1989
+ /**
1990
+ * @param spending The refresh token this refresh set out to use. If the session has moved
1991
+ * on from it while we waited for the lock, someone else has renewed already.
1992
+ */
1993
+ async refreshHoldingLock(spending) {
1994
+ var _a;
1995
+ const stored = this.readStoredTokens();
1996
+ if (stored && stored.refreshToken !== ((_a = this.tokens) == null ? void 0 : _a.refreshToken))
1997
+ this.tokens = stored;
1998
+ if (this.tokens && this.tokens.refreshToken !== spending && !this.isExpired) {
1999
+ this.scheduleRefresh();
2000
+ return this.tokens;
2001
+ }
1871
2002
  if (!this.tokens)
1872
2003
  throw new Error("No tokens found to refresh");
1873
2004
  const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
@@ -1883,6 +2014,13 @@ var _TokenStore = class _TokenStore {
1883
2014
  if (response.status !== 200) {
1884
2015
  if (response.status === 403 || response.status === 404) {
1885
2016
  this.clear();
2017
+ for (const listener of this.sessionEndedListeners) {
2018
+ try {
2019
+ listener();
2020
+ } catch (err) {
2021
+ this.logger.warn("A listener for the end of the session failed", err);
2022
+ }
2023
+ }
1886
2024
  return null;
1887
2025
  }
1888
2026
  throw new MoonbaseError("Unexpected result", `Could not refresh access token, status code ${response.status}`, response.status);
@@ -1893,11 +2031,10 @@ var _TokenStore = class _TokenStore {
1893
2031
  }
1894
2032
  handleStorageUpdate(updatedTokens) {
1895
2033
  this.tokens = updatedTokens;
1896
- if (this.refreshTimeoutId != null && typeof window !== "undefined")
1897
- window.clearTimeout(this.refreshTimeoutId);
1898
2034
  if (this.tokens) {
1899
2035
  this.tokens.expiresAt = new Date(this.tokens.expiresAt);
1900
2036
  }
2037
+ this.scheduleRefresh();
1901
2038
  }
1902
2039
  warnAboutStorage(err) {
1903
2040
  this.logger.warn(
@@ -1912,13 +2049,26 @@ var TokenStore = _TokenStore;
1912
2049
  // src/vendor/schemas.ts
1913
2050
  var schemas_exports9 = {};
1914
2051
  __export(schemas_exports9, {
2052
+ vendorCaptchaSchema: () => vendorCaptchaSchema,
2053
+ vendorFeaturesSchema: () => vendorFeaturesSchema,
1915
2054
  vendorSchema: () => vendorSchema
1916
2055
  });
1917
2056
  var import_zod15 = require("zod");
2057
+ var vendorCaptchaSchema = import_zod15.z.object({
2058
+ url: import_zod15.z.string()
2059
+ });
2060
+ var vendorFeaturesSchema = import_zod15.z.object({
2061
+ customerPortal: import_zod15.z.boolean().optional(),
2062
+ phoneNumber: import_zod15.z.boolean().optional(),
2063
+ marketingConsent: import_zod15.z.nativeEnum(MarketingConsentType).optional().catch(void 0),
2064
+ // Absent on backends from before captchas, which never ask for one.
2065
+ captcha: vendorCaptchaSchema.nullish()
2066
+ });
1918
2067
  var vendorSchema = import_zod15.z.object({
1919
2068
  id: import_zod15.z.string(),
1920
2069
  name: import_zod15.z.string(),
1921
- logoUrl: import_zod15.z.string().nullable()
2070
+ logoUrl: import_zod15.z.string().nullable(),
2071
+ features: vendorFeaturesSchema.optional()
1922
2072
  });
1923
2073
 
1924
2074
  // src/vendor/endpoints.ts
@@ -2614,6 +2764,7 @@ var MoonbaseClient = class {
2614
2764
  ActivationRequestStatus,
2615
2765
  ActivationStatus,
2616
2766
  Architecture,
2767
+ CaptchaRequiredError,
2617
2768
  CartUtils,
2618
2769
  ConnectableAccountProvider,
2619
2770
  ConsoleLogger,
@@ -2639,6 +2790,7 @@ var MoonbaseClient = class {
2639
2790
  SubscriptionCancellationReason,
2640
2791
  SubscriptionStatus,
2641
2792
  TokenStore,
2793
+ accessTokenLifetime,
2642
2794
  imageWidths,
2643
2795
  isLocalStorageAvailable,
2644
2796
  isPromotionDisplay,
package/dist/index.d.cts CHANGED
@@ -80,18 +80,28 @@ declare class InMemoryStore implements IStore {
80
80
  type IdentityWithExpiry = Identity & {
81
81
  expiresAt: Date;
82
82
  };
83
+ /**
84
+ * How long the server issued an access token for, from its `iat` and `exp`. The lifetime rather than
85
+ * `exp` itself, measured against this browser's clock from the moment the token arrives: a clock that is
86
+ * minutes off would otherwise make every token look expired, or none.
87
+ */
88
+ declare function accessTokenLifetime(accessToken: string): number;
83
89
  interface ITokenStore {
84
90
  get user(): User | null;
85
91
  get isPersistent(): boolean;
86
92
  getAccessToken: () => Promise<string | null>;
87
93
  setUser: (user: Identity | User) => User | null;
88
94
  clear: () => void;
95
+ signOut: () => Promise<void>;
96
+ onSessionEnded: (listener: () => void) => void;
89
97
  }
90
98
  declare class TokenStore implements ITokenStore {
91
99
  private configuration;
92
100
  private tokens;
93
101
  private refreshTimeoutId;
94
102
  private refreshPromise;
103
+ private waitingForVisibility;
104
+ private readonly sessionEndedListeners;
95
105
  private readonly store;
96
106
  private readonly logger;
97
107
  static readonly storageKey = "moonbase_auth";
@@ -101,7 +111,18 @@ declare class TokenStore implements ITokenStore {
101
111
  getAccessToken(): Promise<string | null>;
102
112
  setUser(user: Identity | User): IdentityWithExpiry | null;
103
113
  clear(): null;
114
+ signOut(): Promise<void>;
115
+ onSessionEnded(listener: () => void): void;
104
116
  private get isExpired();
117
+ private readStoredTokens;
118
+ /**
119
+ * (Re)arms the timer that renews the access token shortly before it runs out, or disarms
120
+ * it when there is nothing to renew. Every tab arms one, and `refreshTokens` makes sure
121
+ * only one of them actually renews.
122
+ */
123
+ private scheduleRefresh;
124
+ private renewalDueAt;
125
+ private renewInBackground;
105
126
  /**
106
127
  * Start a refresh and hold it so concurrent callers join it rather than firing
107
128
  * their own. The slot is cleared as soon as it settles: a retained promise
@@ -110,7 +131,20 @@ declare class TokenStore implements ITokenStore {
110
131
  * anonymous ones) for the rest of the session.
111
132
  */
112
133
  private startRefresh;
134
+ /**
135
+ * A refresh token can be used once: the server answers a second use by revoking the
136
+ * session, taking it for a stolen token. So refreshes are serialised across every tab of
137
+ * the origin, and whoever gets the lock second takes up what the first one stored rather
138
+ * than spending the same refresh token again. The stored copy has to be read, not waited
139
+ * for: storage events never reach the document that wrote them, so a second client on
140
+ * the same page would not hear about it.
141
+ */
113
142
  private refreshTokens;
143
+ /**
144
+ * @param spending The refresh token this refresh set out to use. If the session has moved
145
+ * on from it while we waited for the lock, someone else has renewed already.
146
+ */
147
+ private refreshHoldingLock;
114
148
  private handleStorageUpdate;
115
149
  private warnAboutStorage;
116
150
  }
@@ -797,6 +831,7 @@ declare const activationRequestSchema: z.ZodObject<{
797
831
  requiresNewsletterOptIn: z.ZodBoolean;
798
832
  numberOfDaysPerProduct: z.ZodNumber;
799
833
  numberOfDaysRemaining: z.ZodNumber;
834
+ endless: z.ZodDefault<z.ZodBoolean>;
800
835
  }, "strip", z.ZodTypeAny, {
801
836
  eligible: boolean;
802
837
  existing: boolean;
@@ -804,6 +839,7 @@ declare const activationRequestSchema: z.ZodObject<{
804
839
  requiresNewsletterOptIn: boolean;
805
840
  numberOfDaysPerProduct: number;
806
841
  numberOfDaysRemaining: number;
842
+ endless: boolean;
807
843
  }, {
808
844
  eligible: boolean;
809
845
  existing: boolean;
@@ -811,6 +847,7 @@ declare const activationRequestSchema: z.ZodObject<{
811
847
  requiresNewsletterOptIn: boolean;
812
848
  numberOfDaysPerProduct: number;
813
849
  numberOfDaysRemaining: number;
850
+ endless?: boolean | undefined;
814
851
  }>>;
815
852
  licenseEligibility: z.ZodOptional<z.ZodObject<{
816
853
  eligible: z.ZodBoolean;
@@ -937,6 +974,7 @@ declare const activationRequestSchema: z.ZodObject<{
937
974
  requiresNewsletterOptIn: boolean;
938
975
  numberOfDaysPerProduct: number;
939
976
  numberOfDaysRemaining: number;
977
+ endless: boolean;
940
978
  } | undefined;
941
979
  licenseEligibility?: {
942
980
  eligible: boolean;
@@ -1059,6 +1097,7 @@ declare const activationRequestSchema: z.ZodObject<{
1059
1097
  requiresNewsletterOptIn: boolean;
1060
1098
  numberOfDaysPerProduct: number;
1061
1099
  numberOfDaysRemaining: number;
1100
+ endless?: boolean | undefined;
1062
1101
  } | undefined;
1063
1102
  licenseEligibility?: {
1064
1103
  eligible: boolean;
@@ -1677,6 +1716,15 @@ interface SignUpOptions {
1677
1716
  * ID left in a form can never fail a registration.
1678
1717
  */
1679
1718
  groupIds?: string[];
1719
+ /**
1720
+ * The customer's solution to the store's captcha, for stores that ask for one
1721
+ * (`vendor.features.captcha`). Sent in the `X-MB-CAPTCHA` header.
1722
+ *
1723
+ * Without it (or with one that does not check out) such a store refuses the
1724
+ * sign-up with a `CaptchaRequiredError`. Solutions are single-use and expire
1725
+ * after about two minutes, so reset the captcha after every attempt.
1726
+ */
1727
+ captchaToken?: string;
1680
1728
  }
1681
1729
 
1682
1730
  declare class IdentityEndpoints {
@@ -1687,6 +1735,11 @@ declare class IdentityEndpoints {
1687
1735
  get(): Promise<User>;
1688
1736
  signIn(email: string, password: string): Promise<User>;
1689
1737
  signUp(name: string, email: string, password: string, address: Address | null | undefined, acceptedPrivacyPolicy: boolean, acceptedTermsAndConditions: boolean, communicationOptIn?: boolean, options?: SignUpOptions): Promise<SignUpResult>;
1738
+ /**
1739
+ * Signs the buyer out here at once, and ends the session on the server so its refresh
1740
+ * token stops working too. Resolves once the server has answered, and never rejects:
1741
+ * the buyer is signed out in this browser whatever happens to that request.
1742
+ */
1690
1743
  signOut(): Promise<void>;
1691
1744
  update(name: string | null, email: string, emailConfirmationToken?: string, communicationPreferences?: CommunicationPreferences): Promise<{
1692
1745
  needsEmailConfirmationToken: boolean;
@@ -24989,26 +25042,110 @@ declare class StorefrontEndpoints {
24989
25042
  }>;
24990
25043
  }
24991
25044
 
25045
+ /**
25046
+ * The captcha a store asks customers to solve before signing up and checking out. `url` is a
25047
+ * page on the store's own `<account-id>.moonbase.sh` that draws the challenge: embed it in an
25048
+ * iframe, and it hands the solution to its parent with `postMessage`.
25049
+ */
25050
+ declare const vendorCaptchaSchema: z.ZodObject<{
25051
+ url: z.ZodString;
25052
+ }, "strip", z.ZodTypeAny, {
25053
+ url: string;
25054
+ }, {
25055
+ url: string;
25056
+ }>;
25057
+ declare const vendorFeaturesSchema: z.ZodObject<{
25058
+ customerPortal: z.ZodOptional<z.ZodBoolean>;
25059
+ phoneNumber: z.ZodOptional<z.ZodBoolean>;
25060
+ marketingConsent: z.ZodCatch<z.ZodOptional<z.ZodNativeEnum<typeof MarketingConsentType>>>;
25061
+ captcha: z.ZodOptional<z.ZodNullable<z.ZodObject<{
25062
+ url: z.ZodString;
25063
+ }, "strip", z.ZodTypeAny, {
25064
+ url: string;
25065
+ }, {
25066
+ url: string;
25067
+ }>>>;
25068
+ }, "strip", z.ZodTypeAny, {
25069
+ customerPortal?: boolean | undefined;
25070
+ phoneNumber?: boolean | undefined;
25071
+ marketingConsent?: MarketingConsentType | undefined;
25072
+ captcha?: {
25073
+ url: string;
25074
+ } | null | undefined;
25075
+ }, {
25076
+ customerPortal?: boolean | undefined;
25077
+ phoneNumber?: boolean | undefined;
25078
+ marketingConsent?: unknown;
25079
+ captcha?: {
25080
+ url: string;
25081
+ } | null | undefined;
25082
+ }>;
24992
25083
  declare const vendorSchema: z.ZodObject<{
24993
25084
  id: z.ZodString;
24994
25085
  name: z.ZodString;
24995
25086
  logoUrl: z.ZodNullable<z.ZodString>;
25087
+ features: z.ZodOptional<z.ZodObject<{
25088
+ customerPortal: z.ZodOptional<z.ZodBoolean>;
25089
+ phoneNumber: z.ZodOptional<z.ZodBoolean>;
25090
+ marketingConsent: z.ZodCatch<z.ZodOptional<z.ZodNativeEnum<typeof MarketingConsentType>>>;
25091
+ captcha: z.ZodOptional<z.ZodNullable<z.ZodObject<{
25092
+ url: z.ZodString;
25093
+ }, "strip", z.ZodTypeAny, {
25094
+ url: string;
25095
+ }, {
25096
+ url: string;
25097
+ }>>>;
25098
+ }, "strip", z.ZodTypeAny, {
25099
+ customerPortal?: boolean | undefined;
25100
+ phoneNumber?: boolean | undefined;
25101
+ marketingConsent?: MarketingConsentType | undefined;
25102
+ captcha?: {
25103
+ url: string;
25104
+ } | null | undefined;
25105
+ }, {
25106
+ customerPortal?: boolean | undefined;
25107
+ phoneNumber?: boolean | undefined;
25108
+ marketingConsent?: unknown;
25109
+ captcha?: {
25110
+ url: string;
25111
+ } | null | undefined;
25112
+ }>>;
24996
25113
  }, "strip", z.ZodTypeAny, {
24997
25114
  id: string;
24998
25115
  name: string;
24999
25116
  logoUrl: string | null;
25117
+ features?: {
25118
+ customerPortal?: boolean | undefined;
25119
+ phoneNumber?: boolean | undefined;
25120
+ marketingConsent?: MarketingConsentType | undefined;
25121
+ captcha?: {
25122
+ url: string;
25123
+ } | null | undefined;
25124
+ } | undefined;
25000
25125
  }, {
25001
25126
  id: string;
25002
25127
  name: string;
25003
25128
  logoUrl: string | null;
25129
+ features?: {
25130
+ customerPortal?: boolean | undefined;
25131
+ phoneNumber?: boolean | undefined;
25132
+ marketingConsent?: unknown;
25133
+ captcha?: {
25134
+ url: string;
25135
+ } | null | undefined;
25136
+ } | undefined;
25004
25137
  }>;
25005
25138
 
25139
+ declare const schemas$4_vendorCaptchaSchema: typeof vendorCaptchaSchema;
25140
+ declare const schemas$4_vendorFeaturesSchema: typeof vendorFeaturesSchema;
25006
25141
  declare const schemas$4_vendorSchema: typeof vendorSchema;
25007
25142
  declare namespace schemas$4 {
25008
- export { schemas$4_vendorSchema as vendorSchema };
25143
+ export { schemas$4_vendorCaptchaSchema as vendorCaptchaSchema, schemas$4_vendorFeaturesSchema as vendorFeaturesSchema, schemas$4_vendorSchema as vendorSchema };
25009
25144
  }
25010
25145
 
25011
25146
  type Vendor = z.infer<typeof vendorSchema>;
25147
+ type VendorFeatures = z.infer<typeof vendorFeaturesSchema>;
25148
+ type VendorCaptcha = z.infer<typeof vendorCaptchaSchema>;
25012
25149
 
25013
25150
  declare class VendorEndpoints {
25014
25151
  private api;
@@ -47026,6 +47163,17 @@ declare class MoonbaseError extends Error {
47026
47163
  readonly errors?: Record<string, string> | undefined;
47027
47164
  constructor(title: string, detail: string | undefined, status?: number | undefined, errors?: Record<string, string> | undefined);
47028
47165
  }
47166
+ /**
47167
+ * The store asks customers to solve a captcha (see `Vendor.features.captcha`), and the request
47168
+ * carried none, or one that did not check out: unsolved, expired, already used, or solved on a
47169
+ * site that is not one of the store's. `message` is the API's own copy for the customer, safe to
47170
+ * show as is. Solutions are single-use, so reset the captcha and let them try again.
47171
+ *
47172
+ * A `MoonbaseError` like any other refusal, so code that already handles those keeps working.
47173
+ */
47174
+ declare class CaptchaRequiredError extends MoonbaseError {
47175
+ constructor(detail?: string, title?: string);
47176
+ }
47029
47177
 
47030
47178
  /**
47031
47179
  * Cleans up merchant-authored group IDs before they go on the wire.
@@ -47206,4 +47354,4 @@ declare class MoonbaseClient {
47206
47354
  orders: OrderEndpoints;
47207
47355
  }
47208
47356
 
47209
- 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 SignUpOptions, type SignUpResult, type SingleOrMultiple, StoreClosedError, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, type SubscriptionCancellationFeedback, SubscriptionCancellationReason, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, normalizeGroupIds, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
47357
+ export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, CaptchaRequiredError, 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 SignUpOptions, type SignUpResult, type SingleOrMultiple, StoreClosedError, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, type SubscriptionCancellationFeedback, SubscriptionCancellationReason, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type VendorCaptcha, type VendorFeatures, type Voucher, accessTokenLifetime, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, normalizeGroupIds, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
package/dist/index.d.ts CHANGED
@@ -80,18 +80,28 @@ declare class InMemoryStore implements IStore {
80
80
  type IdentityWithExpiry = Identity & {
81
81
  expiresAt: Date;
82
82
  };
83
+ /**
84
+ * How long the server issued an access token for, from its `iat` and `exp`. The lifetime rather than
85
+ * `exp` itself, measured against this browser's clock from the moment the token arrives: a clock that is
86
+ * minutes off would otherwise make every token look expired, or none.
87
+ */
88
+ declare function accessTokenLifetime(accessToken: string): number;
83
89
  interface ITokenStore {
84
90
  get user(): User | null;
85
91
  get isPersistent(): boolean;
86
92
  getAccessToken: () => Promise<string | null>;
87
93
  setUser: (user: Identity | User) => User | null;
88
94
  clear: () => void;
95
+ signOut: () => Promise<void>;
96
+ onSessionEnded: (listener: () => void) => void;
89
97
  }
90
98
  declare class TokenStore implements ITokenStore {
91
99
  private configuration;
92
100
  private tokens;
93
101
  private refreshTimeoutId;
94
102
  private refreshPromise;
103
+ private waitingForVisibility;
104
+ private readonly sessionEndedListeners;
95
105
  private readonly store;
96
106
  private readonly logger;
97
107
  static readonly storageKey = "moonbase_auth";
@@ -101,7 +111,18 @@ declare class TokenStore implements ITokenStore {
101
111
  getAccessToken(): Promise<string | null>;
102
112
  setUser(user: Identity | User): IdentityWithExpiry | null;
103
113
  clear(): null;
114
+ signOut(): Promise<void>;
115
+ onSessionEnded(listener: () => void): void;
104
116
  private get isExpired();
117
+ private readStoredTokens;
118
+ /**
119
+ * (Re)arms the timer that renews the access token shortly before it runs out, or disarms
120
+ * it when there is nothing to renew. Every tab arms one, and `refreshTokens` makes sure
121
+ * only one of them actually renews.
122
+ */
123
+ private scheduleRefresh;
124
+ private renewalDueAt;
125
+ private renewInBackground;
105
126
  /**
106
127
  * Start a refresh and hold it so concurrent callers join it rather than firing
107
128
  * their own. The slot is cleared as soon as it settles: a retained promise
@@ -110,7 +131,20 @@ declare class TokenStore implements ITokenStore {
110
131
  * anonymous ones) for the rest of the session.
111
132
  */
112
133
  private startRefresh;
134
+ /**
135
+ * A refresh token can be used once: the server answers a second use by revoking the
136
+ * session, taking it for a stolen token. So refreshes are serialised across every tab of
137
+ * the origin, and whoever gets the lock second takes up what the first one stored rather
138
+ * than spending the same refresh token again. The stored copy has to be read, not waited
139
+ * for: storage events never reach the document that wrote them, so a second client on
140
+ * the same page would not hear about it.
141
+ */
113
142
  private refreshTokens;
143
+ /**
144
+ * @param spending The refresh token this refresh set out to use. If the session has moved
145
+ * on from it while we waited for the lock, someone else has renewed already.
146
+ */
147
+ private refreshHoldingLock;
114
148
  private handleStorageUpdate;
115
149
  private warnAboutStorage;
116
150
  }
@@ -797,6 +831,7 @@ declare const activationRequestSchema: z.ZodObject<{
797
831
  requiresNewsletterOptIn: z.ZodBoolean;
798
832
  numberOfDaysPerProduct: z.ZodNumber;
799
833
  numberOfDaysRemaining: z.ZodNumber;
834
+ endless: z.ZodDefault<z.ZodBoolean>;
800
835
  }, "strip", z.ZodTypeAny, {
801
836
  eligible: boolean;
802
837
  existing: boolean;
@@ -804,6 +839,7 @@ declare const activationRequestSchema: z.ZodObject<{
804
839
  requiresNewsletterOptIn: boolean;
805
840
  numberOfDaysPerProduct: number;
806
841
  numberOfDaysRemaining: number;
842
+ endless: boolean;
807
843
  }, {
808
844
  eligible: boolean;
809
845
  existing: boolean;
@@ -811,6 +847,7 @@ declare const activationRequestSchema: z.ZodObject<{
811
847
  requiresNewsletterOptIn: boolean;
812
848
  numberOfDaysPerProduct: number;
813
849
  numberOfDaysRemaining: number;
850
+ endless?: boolean | undefined;
814
851
  }>>;
815
852
  licenseEligibility: z.ZodOptional<z.ZodObject<{
816
853
  eligible: z.ZodBoolean;
@@ -937,6 +974,7 @@ declare const activationRequestSchema: z.ZodObject<{
937
974
  requiresNewsletterOptIn: boolean;
938
975
  numberOfDaysPerProduct: number;
939
976
  numberOfDaysRemaining: number;
977
+ endless: boolean;
940
978
  } | undefined;
941
979
  licenseEligibility?: {
942
980
  eligible: boolean;
@@ -1059,6 +1097,7 @@ declare const activationRequestSchema: z.ZodObject<{
1059
1097
  requiresNewsletterOptIn: boolean;
1060
1098
  numberOfDaysPerProduct: number;
1061
1099
  numberOfDaysRemaining: number;
1100
+ endless?: boolean | undefined;
1062
1101
  } | undefined;
1063
1102
  licenseEligibility?: {
1064
1103
  eligible: boolean;
@@ -1677,6 +1716,15 @@ interface SignUpOptions {
1677
1716
  * ID left in a form can never fail a registration.
1678
1717
  */
1679
1718
  groupIds?: string[];
1719
+ /**
1720
+ * The customer's solution to the store's captcha, for stores that ask for one
1721
+ * (`vendor.features.captcha`). Sent in the `X-MB-CAPTCHA` header.
1722
+ *
1723
+ * Without it (or with one that does not check out) such a store refuses the
1724
+ * sign-up with a `CaptchaRequiredError`. Solutions are single-use and expire
1725
+ * after about two minutes, so reset the captcha after every attempt.
1726
+ */
1727
+ captchaToken?: string;
1680
1728
  }
1681
1729
 
1682
1730
  declare class IdentityEndpoints {
@@ -1687,6 +1735,11 @@ declare class IdentityEndpoints {
1687
1735
  get(): Promise<User>;
1688
1736
  signIn(email: string, password: string): Promise<User>;
1689
1737
  signUp(name: string, email: string, password: string, address: Address | null | undefined, acceptedPrivacyPolicy: boolean, acceptedTermsAndConditions: boolean, communicationOptIn?: boolean, options?: SignUpOptions): Promise<SignUpResult>;
1738
+ /**
1739
+ * Signs the buyer out here at once, and ends the session on the server so its refresh
1740
+ * token stops working too. Resolves once the server has answered, and never rejects:
1741
+ * the buyer is signed out in this browser whatever happens to that request.
1742
+ */
1690
1743
  signOut(): Promise<void>;
1691
1744
  update(name: string | null, email: string, emailConfirmationToken?: string, communicationPreferences?: CommunicationPreferences): Promise<{
1692
1745
  needsEmailConfirmationToken: boolean;
@@ -24989,26 +25042,110 @@ declare class StorefrontEndpoints {
24989
25042
  }>;
24990
25043
  }
24991
25044
 
25045
+ /**
25046
+ * The captcha a store asks customers to solve before signing up and checking out. `url` is a
25047
+ * page on the store's own `<account-id>.moonbase.sh` that draws the challenge: embed it in an
25048
+ * iframe, and it hands the solution to its parent with `postMessage`.
25049
+ */
25050
+ declare const vendorCaptchaSchema: z.ZodObject<{
25051
+ url: z.ZodString;
25052
+ }, "strip", z.ZodTypeAny, {
25053
+ url: string;
25054
+ }, {
25055
+ url: string;
25056
+ }>;
25057
+ declare const vendorFeaturesSchema: z.ZodObject<{
25058
+ customerPortal: z.ZodOptional<z.ZodBoolean>;
25059
+ phoneNumber: z.ZodOptional<z.ZodBoolean>;
25060
+ marketingConsent: z.ZodCatch<z.ZodOptional<z.ZodNativeEnum<typeof MarketingConsentType>>>;
25061
+ captcha: z.ZodOptional<z.ZodNullable<z.ZodObject<{
25062
+ url: z.ZodString;
25063
+ }, "strip", z.ZodTypeAny, {
25064
+ url: string;
25065
+ }, {
25066
+ url: string;
25067
+ }>>>;
25068
+ }, "strip", z.ZodTypeAny, {
25069
+ customerPortal?: boolean | undefined;
25070
+ phoneNumber?: boolean | undefined;
25071
+ marketingConsent?: MarketingConsentType | undefined;
25072
+ captcha?: {
25073
+ url: string;
25074
+ } | null | undefined;
25075
+ }, {
25076
+ customerPortal?: boolean | undefined;
25077
+ phoneNumber?: boolean | undefined;
25078
+ marketingConsent?: unknown;
25079
+ captcha?: {
25080
+ url: string;
25081
+ } | null | undefined;
25082
+ }>;
24992
25083
  declare const vendorSchema: z.ZodObject<{
24993
25084
  id: z.ZodString;
24994
25085
  name: z.ZodString;
24995
25086
  logoUrl: z.ZodNullable<z.ZodString>;
25087
+ features: z.ZodOptional<z.ZodObject<{
25088
+ customerPortal: z.ZodOptional<z.ZodBoolean>;
25089
+ phoneNumber: z.ZodOptional<z.ZodBoolean>;
25090
+ marketingConsent: z.ZodCatch<z.ZodOptional<z.ZodNativeEnum<typeof MarketingConsentType>>>;
25091
+ captcha: z.ZodOptional<z.ZodNullable<z.ZodObject<{
25092
+ url: z.ZodString;
25093
+ }, "strip", z.ZodTypeAny, {
25094
+ url: string;
25095
+ }, {
25096
+ url: string;
25097
+ }>>>;
25098
+ }, "strip", z.ZodTypeAny, {
25099
+ customerPortal?: boolean | undefined;
25100
+ phoneNumber?: boolean | undefined;
25101
+ marketingConsent?: MarketingConsentType | undefined;
25102
+ captcha?: {
25103
+ url: string;
25104
+ } | null | undefined;
25105
+ }, {
25106
+ customerPortal?: boolean | undefined;
25107
+ phoneNumber?: boolean | undefined;
25108
+ marketingConsent?: unknown;
25109
+ captcha?: {
25110
+ url: string;
25111
+ } | null | undefined;
25112
+ }>>;
24996
25113
  }, "strip", z.ZodTypeAny, {
24997
25114
  id: string;
24998
25115
  name: string;
24999
25116
  logoUrl: string | null;
25117
+ features?: {
25118
+ customerPortal?: boolean | undefined;
25119
+ phoneNumber?: boolean | undefined;
25120
+ marketingConsent?: MarketingConsentType | undefined;
25121
+ captcha?: {
25122
+ url: string;
25123
+ } | null | undefined;
25124
+ } | undefined;
25000
25125
  }, {
25001
25126
  id: string;
25002
25127
  name: string;
25003
25128
  logoUrl: string | null;
25129
+ features?: {
25130
+ customerPortal?: boolean | undefined;
25131
+ phoneNumber?: boolean | undefined;
25132
+ marketingConsent?: unknown;
25133
+ captcha?: {
25134
+ url: string;
25135
+ } | null | undefined;
25136
+ } | undefined;
25004
25137
  }>;
25005
25138
 
25139
+ declare const schemas$4_vendorCaptchaSchema: typeof vendorCaptchaSchema;
25140
+ declare const schemas$4_vendorFeaturesSchema: typeof vendorFeaturesSchema;
25006
25141
  declare const schemas$4_vendorSchema: typeof vendorSchema;
25007
25142
  declare namespace schemas$4 {
25008
- export { schemas$4_vendorSchema as vendorSchema };
25143
+ export { schemas$4_vendorCaptchaSchema as vendorCaptchaSchema, schemas$4_vendorFeaturesSchema as vendorFeaturesSchema, schemas$4_vendorSchema as vendorSchema };
25009
25144
  }
25010
25145
 
25011
25146
  type Vendor = z.infer<typeof vendorSchema>;
25147
+ type VendorFeatures = z.infer<typeof vendorFeaturesSchema>;
25148
+ type VendorCaptcha = z.infer<typeof vendorCaptchaSchema>;
25012
25149
 
25013
25150
  declare class VendorEndpoints {
25014
25151
  private api;
@@ -47026,6 +47163,17 @@ declare class MoonbaseError extends Error {
47026
47163
  readonly errors?: Record<string, string> | undefined;
47027
47164
  constructor(title: string, detail: string | undefined, status?: number | undefined, errors?: Record<string, string> | undefined);
47028
47165
  }
47166
+ /**
47167
+ * The store asks customers to solve a captcha (see `Vendor.features.captcha`), and the request
47168
+ * carried none, or one that did not check out: unsolved, expired, already used, or solved on a
47169
+ * site that is not one of the store's. `message` is the API's own copy for the customer, safe to
47170
+ * show as is. Solutions are single-use, so reset the captcha and let them try again.
47171
+ *
47172
+ * A `MoonbaseError` like any other refusal, so code that already handles those keeps working.
47173
+ */
47174
+ declare class CaptchaRequiredError extends MoonbaseError {
47175
+ constructor(detail?: string, title?: string);
47176
+ }
47029
47177
 
47030
47178
  /**
47031
47179
  * Cleans up merchant-authored group IDs before they go on the wire.
@@ -47206,4 +47354,4 @@ declare class MoonbaseClient {
47206
47354
  orders: OrderEndpoints;
47207
47355
  }
47208
47356
 
47209
- 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 SignUpOptions, type SignUpResult, type SingleOrMultiple, StoreClosedError, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, type SubscriptionCancellationFeedback, SubscriptionCancellationReason, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, normalizeGroupIds, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
47357
+ export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, CaptchaRequiredError, 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 SignUpOptions, type SignUpResult, type SingleOrMultiple, StoreClosedError, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, type SubscriptionCancellationFeedback, SubscriptionCancellationReason, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type VendorCaptcha, type VendorFeatures, type Voucher, accessTokenLifetime, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, normalizeGroupIds, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
package/dist/index.js CHANGED
@@ -397,7 +397,10 @@ var activationRequestSchema = z4.object({
397
397
  requiresAccount: z4.boolean(),
398
398
  requiresNewsletterOptIn: z4.boolean(),
399
399
  numberOfDaysPerProduct: z4.number(),
400
- numberOfDaysRemaining: z4.number()
400
+ numberOfDaysRemaining: z4.number(),
401
+ // The trial never expires, so the day counts are not worth showing. Absent from a backend that
402
+ // predates endless trials.
403
+ endless: z4.boolean().default(false)
401
404
  }).optional(),
402
405
  licenseEligibility: z4.object({
403
406
  eligible: z4.boolean()
@@ -595,6 +598,12 @@ var MoonbaseError = class extends Error {
595
598
  }
596
599
  }
597
600
  };
601
+ var CaptchaRequiredError = class extends MoonbaseError {
602
+ constructor(detail, title) {
603
+ super(title != null ? title : "Captcha required", detail != null ? detail : "Please confirm you're not a robot.", 400);
604
+ this.name = "CaptchaRequiredError";
605
+ }
606
+ };
598
607
 
599
608
  // src/utils/problemDetails.ts
600
609
  import { z as z7 } from "zod";
@@ -637,6 +646,8 @@ async function handleResponseProblem(response, logger) {
637
646
  logger.debug("The response indicates a problem", problemDetails);
638
647
  if (problemDetails.errorType === "StoreClosed")
639
648
  throw new StoreClosedError(problemDetails.detail);
649
+ if (problemDetails.errorType === "CaptchaRequired")
650
+ throw new CaptchaRequiredError(problemDetails.detail, problemDetails.title);
640
651
  throw new MoonbaseError(
641
652
  problemDetails.title,
642
653
  problemDetails.detail,
@@ -773,7 +784,8 @@ var IdentityEndpoints = class {
773
784
  "Accept": "application/json",
774
785
  "Content-Type": "application/json",
775
786
  "x-mb-cors": "1",
776
- "x-mb-client": this.api.clientHeader
787
+ "x-mb-client": this.api.clientHeader,
788
+ ...(options == null ? void 0 : options.captchaToken) ? { "X-MB-CAPTCHA": options.captchaToken } : {}
777
789
  },
778
790
  body: JSON.stringify({
779
791
  name,
@@ -802,8 +814,13 @@ var IdentityEndpoints = class {
802
814
  this.tokenStore.setUser(identity);
803
815
  return { status: "signed-in", user: userSchema.parse(identity) };
804
816
  }
817
+ /**
818
+ * Signs the buyer out here at once, and ends the session on the server so its refresh
819
+ * token stops working too. Resolves once the server has answered, and never rejects:
820
+ * the buyer is signed out in this browser whatever happens to that request.
821
+ */
805
822
  async signOut() {
806
- this.tokenStore.clear();
823
+ await this.tokenStore.signOut();
807
824
  }
808
825
  async update(name, email, emailConfirmationToken, communicationPreferences) {
809
826
  const response = await this.api.authenticatedFetch("/api/customer/identity", userSchema, {
@@ -1036,7 +1053,7 @@ var ProductEndpoints = class {
1036
1053
  import { z as z14 } from "zod";
1037
1054
 
1038
1055
  // package.json
1039
- var version = "3.5.1";
1056
+ var version = "3.7.0";
1040
1057
 
1041
1058
  // src/utils/client.ts
1042
1059
  function buildClientHeader(sdk) {
@@ -1715,24 +1732,36 @@ var InMemoryStore = class {
1715
1732
  };
1716
1733
 
1717
1734
  // src/utils/tokenStore.ts
1735
+ var defaultAccessTokenLifetime = 15 * 60 * 1e3;
1736
+ var renewBeforeExpiry = 5 * 60 * 1e3;
1737
+ var expiryMargin = 30 * 1e3;
1738
+ var refreshLockName = "moonbase_auth_refresh";
1739
+ function accessTokenLifetime(accessToken) {
1740
+ try {
1741
+ const payload = accessToken.split(".")[1].replaceAll("-", "+").replaceAll("_", "/");
1742
+ const claims = JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, "=")));
1743
+ const lifetime = (Number(claims.exp) - Number(claims.iat)) * 1e3;
1744
+ if (Number.isFinite(lifetime) && lifetime > 0 && lifetime <= 24 * 60 * 60 * 1e3)
1745
+ return lifetime;
1746
+ } catch (e) {
1747
+ }
1748
+ return defaultAccessTokenLifetime;
1749
+ }
1718
1750
  var _TokenStore = class _TokenStore {
1719
1751
  constructor(configuration) {
1720
1752
  this.configuration = configuration;
1721
1753
  this.tokens = null;
1722
1754
  this.refreshTimeoutId = null;
1723
1755
  this.refreshPromise = null;
1756
+ this.waitingForVisibility = false;
1757
+ this.sessionEndedListeners = [];
1724
1758
  var _a, _b;
1725
1759
  this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
1726
1760
  this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
1727
1761
  if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
1728
1762
  this.warnAboutStorage();
1729
- const storedToken = this.store.get(_TokenStore.storageKey);
1730
- if (storedToken) {
1731
- this.tokens = {
1732
- ...storedToken,
1733
- expiresAt: new Date(storedToken.expiresAt)
1734
- };
1735
- }
1763
+ this.tokens = this.readStoredTokens();
1764
+ this.scheduleRefresh();
1736
1765
  this.store.listen(_TokenStore.storageKey, (updatedTokens) => this.handleStorageUpdate(updatedTokens));
1737
1766
  }
1738
1767
  get user() {
@@ -1766,29 +1795,91 @@ var _TokenStore = class _TokenStore {
1766
1795
  } else {
1767
1796
  this.tokens = {
1768
1797
  ...identity,
1769
- // Hardcoded 15 minutes now, might want to check the JWT tho
1770
- expiresAt: new Date((/* @__PURE__ */ new Date()).getTime() + 15 * 60 * 1e3)
1798
+ expiresAt: new Date(Date.now() + accessTokenLifetime(identity.accessToken))
1771
1799
  };
1772
1800
  }
1773
1801
  this.store.set(_TokenStore.storageKey, this.tokens);
1774
- if (typeof window !== "undefined") {
1775
- if (this.refreshTimeoutId != null)
1776
- window.clearTimeout(this.refreshTimeoutId);
1777
- this.refreshTimeoutId = window.setTimeout(() => {
1778
- this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
1779
- }, 10 * 60 * 1e3);
1780
- }
1802
+ this.scheduleRefresh();
1781
1803
  return this.tokens;
1782
1804
  }
1783
1805
  clear() {
1784
1806
  this.tokens = null;
1785
1807
  this.store.remove(_TokenStore.storageKey);
1786
- if (this.refreshTimeoutId != null && typeof window !== "undefined")
1787
- window.clearTimeout(this.refreshTimeoutId);
1808
+ this.scheduleRefresh();
1788
1809
  return null;
1789
1810
  }
1811
+ async signOut() {
1812
+ const tokens = this.tokens;
1813
+ this.clear();
1814
+ if (!tokens)
1815
+ return;
1816
+ try {
1817
+ await fetch(`${this.configuration.endpoint}/api/customer/identity/revoke?token=${encodeURIComponent(tokens.refreshToken)}`, {
1818
+ method: "POST",
1819
+ keepalive: true,
1820
+ headers: {
1821
+ "Accept": "application/json",
1822
+ "Content-Type": "text/plain",
1823
+ "x-mb-cors": "1",
1824
+ "x-mb-client": buildClientHeader(this.configuration.sdk)
1825
+ },
1826
+ body: tokens.accessToken
1827
+ });
1828
+ } catch (err) {
1829
+ this.logger.warn("Could not end the session on the server", err);
1830
+ }
1831
+ }
1832
+ onSessionEnded(listener) {
1833
+ this.sessionEndedListeners.push(listener);
1834
+ }
1790
1835
  get isExpired() {
1791
- return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
1836
+ return this.tokens != null && this.tokens.expiresAt.getTime() - expiryMargin < Date.now();
1837
+ }
1838
+ readStoredTokens() {
1839
+ const stored = this.store.get(_TokenStore.storageKey);
1840
+ return stored ? { ...stored, expiresAt: new Date(stored.expiresAt) } : null;
1841
+ }
1842
+ /**
1843
+ * (Re)arms the timer that renews the access token shortly before it runs out, or disarms
1844
+ * it when there is nothing to renew. Every tab arms one, and `refreshTokens` makes sure
1845
+ * only one of them actually renews.
1846
+ */
1847
+ scheduleRefresh() {
1848
+ if (typeof window === "undefined")
1849
+ return;
1850
+ if (this.refreshTimeoutId != null)
1851
+ window.clearTimeout(this.refreshTimeoutId);
1852
+ this.refreshTimeoutId = null;
1853
+ if (!this.tokens)
1854
+ return;
1855
+ const delay = Math.max(0, this.renewalDueAt(this.tokens) - Date.now());
1856
+ this.refreshTimeoutId = window.setTimeout(() => this.renewInBackground(), delay);
1857
+ }
1858
+ renewalDueAt(tokens) {
1859
+ const lead = Math.min(renewBeforeExpiry, accessTokenLifetime(tokens.accessToken) / 3);
1860
+ return tokens.expiresAt.getTime() - lead;
1861
+ }
1862
+ renewInBackground() {
1863
+ var _a;
1864
+ this.refreshTimeoutId = null;
1865
+ if (!this.tokens)
1866
+ return;
1867
+ if (this.renewalDueAt(this.tokens) > Date.now()) {
1868
+ this.scheduleRefresh();
1869
+ return;
1870
+ }
1871
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1872
+ if (!this.waitingForVisibility) {
1873
+ this.waitingForVisibility = true;
1874
+ document.addEventListener("visibilitychange", () => {
1875
+ this.waitingForVisibility = false;
1876
+ this.renewInBackground();
1877
+ }, { once: true });
1878
+ }
1879
+ return;
1880
+ }
1881
+ const pending = (_a = this.refreshPromise) != null ? _a : this.startRefresh();
1882
+ pending.catch((err) => this.logger.warn("Could not refresh access token", err));
1792
1883
  }
1793
1884
  /**
1794
1885
  * Start a refresh and hold it so concurrent callers join it rather than firing
@@ -1805,7 +1896,45 @@ var _TokenStore = class _TokenStore {
1805
1896
  this.refreshPromise = pending;
1806
1897
  return pending;
1807
1898
  }
1899
+ /**
1900
+ * A refresh token can be used once: the server answers a second use by revoking the
1901
+ * session, taking it for a stolen token. So refreshes are serialised across every tab of
1902
+ * the origin, and whoever gets the lock second takes up what the first one stored rather
1903
+ * than spending the same refresh token again. The stored copy has to be read, not waited
1904
+ * for: storage events never reach the document that wrote them, so a second client on
1905
+ * the same page would not hear about it.
1906
+ */
1808
1907
  async refreshTokens() {
1908
+ var _a;
1909
+ const spending = (_a = this.tokens) == null ? void 0 : _a.refreshToken;
1910
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
1911
+ if (locks == null ? void 0 : locks.request) {
1912
+ let held = false;
1913
+ try {
1914
+ return await locks.request(refreshLockName, () => {
1915
+ held = true;
1916
+ return this.refreshHoldingLock(spending);
1917
+ });
1918
+ } catch (err) {
1919
+ if (held)
1920
+ throw err;
1921
+ }
1922
+ }
1923
+ return await this.refreshHoldingLock(spending);
1924
+ }
1925
+ /**
1926
+ * @param spending The refresh token this refresh set out to use. If the session has moved
1927
+ * on from it while we waited for the lock, someone else has renewed already.
1928
+ */
1929
+ async refreshHoldingLock(spending) {
1930
+ var _a;
1931
+ const stored = this.readStoredTokens();
1932
+ if (stored && stored.refreshToken !== ((_a = this.tokens) == null ? void 0 : _a.refreshToken))
1933
+ this.tokens = stored;
1934
+ if (this.tokens && this.tokens.refreshToken !== spending && !this.isExpired) {
1935
+ this.scheduleRefresh();
1936
+ return this.tokens;
1937
+ }
1809
1938
  if (!this.tokens)
1810
1939
  throw new Error("No tokens found to refresh");
1811
1940
  const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
@@ -1821,6 +1950,13 @@ var _TokenStore = class _TokenStore {
1821
1950
  if (response.status !== 200) {
1822
1951
  if (response.status === 403 || response.status === 404) {
1823
1952
  this.clear();
1953
+ for (const listener of this.sessionEndedListeners) {
1954
+ try {
1955
+ listener();
1956
+ } catch (err) {
1957
+ this.logger.warn("A listener for the end of the session failed", err);
1958
+ }
1959
+ }
1824
1960
  return null;
1825
1961
  }
1826
1962
  throw new MoonbaseError("Unexpected result", `Could not refresh access token, status code ${response.status}`, response.status);
@@ -1831,11 +1967,10 @@ var _TokenStore = class _TokenStore {
1831
1967
  }
1832
1968
  handleStorageUpdate(updatedTokens) {
1833
1969
  this.tokens = updatedTokens;
1834
- if (this.refreshTimeoutId != null && typeof window !== "undefined")
1835
- window.clearTimeout(this.refreshTimeoutId);
1836
1970
  if (this.tokens) {
1837
1971
  this.tokens.expiresAt = new Date(this.tokens.expiresAt);
1838
1972
  }
1973
+ this.scheduleRefresh();
1839
1974
  }
1840
1975
  warnAboutStorage(err) {
1841
1976
  this.logger.warn(
@@ -1850,13 +1985,26 @@ var TokenStore = _TokenStore;
1850
1985
  // src/vendor/schemas.ts
1851
1986
  var schemas_exports9 = {};
1852
1987
  __export(schemas_exports9, {
1988
+ vendorCaptchaSchema: () => vendorCaptchaSchema,
1989
+ vendorFeaturesSchema: () => vendorFeaturesSchema,
1853
1990
  vendorSchema: () => vendorSchema
1854
1991
  });
1855
1992
  import { z as z15 } from "zod";
1993
+ var vendorCaptchaSchema = z15.object({
1994
+ url: z15.string()
1995
+ });
1996
+ var vendorFeaturesSchema = z15.object({
1997
+ customerPortal: z15.boolean().optional(),
1998
+ phoneNumber: z15.boolean().optional(),
1999
+ marketingConsent: z15.nativeEnum(MarketingConsentType).optional().catch(void 0),
2000
+ // Absent on backends from before captchas, which never ask for one.
2001
+ captcha: vendorCaptchaSchema.nullish()
2002
+ });
1856
2003
  var vendorSchema = z15.object({
1857
2004
  id: z15.string(),
1858
2005
  name: z15.string(),
1859
- logoUrl: z15.string().nullable()
2006
+ logoUrl: z15.string().nullable(),
2007
+ features: vendorFeaturesSchema.optional()
1860
2008
  });
1861
2009
 
1862
2010
  // src/vendor/endpoints.ts
@@ -2551,6 +2699,7 @@ export {
2551
2699
  ActivationRequestStatus,
2552
2700
  ActivationStatus,
2553
2701
  Architecture,
2702
+ CaptchaRequiredError,
2554
2703
  CartUtils,
2555
2704
  ConnectableAccountProvider,
2556
2705
  ConsoleLogger,
@@ -2576,6 +2725,7 @@ export {
2576
2725
  SubscriptionCancellationReason,
2577
2726
  SubscriptionStatus,
2578
2727
  TokenStore,
2728
+ accessTokenLifetime,
2579
2729
  imageWidths,
2580
2730
  isLocalStorageAvailable,
2581
2731
  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.5.1",
4
+ "version": "3.7.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",