@moonbase.sh/storefront-api 3.7.0 → 3.9.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
@@ -24,7 +24,7 @@ Configuration notes:
24
24
 
25
25
  - `endpoint`: your Moonbase storefront URL.
26
26
  - `includeManifests`: include downloadable file manifests when available.
27
- - `persistUtm`: store detected UTM values in localStorage instead of sessionStorage.
27
+ - `persistUtm`: store detected UTM values and ad click IDs in localStorage instead of sessionStorage.
28
28
 
29
29
  ## Common usage
30
30
 
@@ -172,6 +172,38 @@ refused with `CaptchaRequiredError` (a `MoonbaseError`). A site with a
172
172
  Content-Security-Policy has to allow the store's `https://<account-id>.moonbase.sh`
173
173
  in `frame-src`.
174
174
 
175
+ ### Where a customer came from
176
+
177
+ Pass the UTM tags, ad click IDs and referrer the visitor arrived with, the same
178
+ shape orders take, so the merchant can see how the customer was acquired:
179
+
180
+ ```ts
181
+ import { parseOffSiteReferrer } from '@moonbase.sh/storefront-api'
182
+
183
+ const params = new URLSearchParams(window.location.search)
184
+
185
+ await client.identity.signUp('Jane', 'jane@example.com', 'password', null, true, true, true, {
186
+ utm: {
187
+ source: params.get('utm_source') || undefined,
188
+ campaign: params.get('utm_campaign') || undefined,
189
+ // Click IDs ad platforms add to the link, often with no UTM tags beside them.
190
+ gclid: params.get('gclid') || undefined,
191
+ fbclid: params.get('fbclid') || undefined,
192
+ msclkid: params.get('msclkid') || undefined,
193
+ ttclid: params.get('ttclid') || undefined,
194
+ // The referring site's host only, and nothing when the visitor came from this site.
195
+ referrer: parseOffSiteReferrer(document.referrer, window.location.origin),
196
+ },
197
+ })
198
+ ```
199
+
200
+ Fields left out are not sent. The click IDs (`gclid`, `gbraid` and `wbraid` from
201
+ Google Ads, `fbclid` from Meta, `msclkid` from Microsoft Advertising and `ttclid`
202
+ from TikTok) go to the API under those names, and the rest as `utm_*`.
203
+ `useAuth().signUp` in `@moonbase.sh/vue`, and so the storefront widget, fills this
204
+ in from the tags and click IDs it has already picked up, unless you pass your own
205
+ `utm`.
206
+
175
207
  ## Inventory flow
176
208
 
177
209
  Most inventory endpoints require an authenticated user:
@@ -191,6 +223,14 @@ while (page.hasMore && page.next) {
191
223
  }
192
224
  ```
193
225
 
226
+ A product's release history needs no sign-in. `getReleases` lists its published releases, highest
227
+ version first, each with its version, release notes, publish date and files, and whether the caller
228
+ may download it (`downloadsAllowed`). It pages the same way:
229
+
230
+ ```ts
231
+ const releases = await client.inventory.products.getReleases('my-product')
232
+ ```
233
+
194
234
  ## Endpoint groups
195
235
 
196
236
  `client.storefront`, `client.identity`, `client.orders`, `client.vouchers`, `client.inventory`, `client.activationRequests`, `client.vendor`, `client.communications`, `client.groups`.
package/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  NotFoundError: () => NotFoundError,
47
47
  OfferUtils: () => OfferUtils,
48
48
  OrderStatus: () => OrderStatus,
49
+ PasswordSetupRequiredError: () => PasswordSetupRequiredError,
49
50
  Platform: () => Platform,
50
51
  StoreClosedError: () => StoreClosedError,
51
52
  SubscriptionCancellationReason: () => SubscriptionCancellationReason,
@@ -120,7 +121,13 @@ function utmToObject(utm) {
120
121
  utm_campaign: utm == null ? void 0 : utm.campaign,
121
122
  utm_term: utm == null ? void 0 : utm.term,
122
123
  utm_content: utm == null ? void 0 : utm.content,
123
- utm_referrer: utm == null ? void 0 : utm.referrer
124
+ utm_referrer: utm == null ? void 0 : utm.referrer,
125
+ gclid: utm == null ? void 0 : utm.gclid,
126
+ gbraid: utm == null ? void 0 : utm.gbraid,
127
+ wbraid: utm == null ? void 0 : utm.wbraid,
128
+ fbclid: utm == null ? void 0 : utm.fbclid,
129
+ msclkid: utm == null ? void 0 : utm.msclkid,
130
+ ttclid: utm == null ? void 0 : utm.ttclid
124
131
  }).filter(([_, value]) => value !== void 0).reduce((obj, [key, value]) => ({
125
132
  ...obj,
126
133
  [key]: value
@@ -212,6 +219,7 @@ var schemas_exports = {};
212
219
  __export(schemas_exports, {
213
220
  downloadSchema: () => downloadSchema,
214
221
  manifestSchema: () => manifestSchema,
222
+ productReleaseSchema: () => productReleaseSchema,
215
223
  productSummarySchema: () => productSummarySchema
216
224
  });
217
225
  var import_zod2 = require("zod");
@@ -274,10 +282,20 @@ var productSummarySchema = import_zod2.z.object({
274
282
  downloadsNeedsOwnership: import_zod2.z.boolean(),
275
283
  downloadsNeedsGroupMembership: import_zod2.z.boolean(),
276
284
  // Whether this caller clears every requirement above, including the group membership the client
277
- // can't see for itself.
285
+ // can't see for itself. False with none of them set means the merchant closed the release to everyone.
278
286
  downloadsAllowed: import_zod2.z.boolean(),
279
287
  downloads: import_zod2.z.array(downloadSchema).optional()
280
288
  });
289
+ var productReleaseSchema = import_zod2.z.object({
290
+ version: import_zod2.z.string(),
291
+ description: import_zod2.z.string().nullish(),
292
+ publishedAt: import_zod2.z.coerce.date(),
293
+ // Whether this caller may download the release, resolved against its own access control: a
294
+ // merchant can keep past releases to owners while the current one is open to anyone.
295
+ downloadsAllowed: import_zod2.z.boolean(),
296
+ downloads: import_zod2.z.array(downloadSchema),
297
+ properties: import_zod2.z.record(rawPropertyValueSchema).nullish()
298
+ });
281
299
 
282
300
  // src/storefront/schemas.ts
283
301
  var storefrontProductSchema = import_zod3.z.object({
@@ -668,6 +686,12 @@ var CaptchaRequiredError = class extends MoonbaseError {
668
686
  this.name = "CaptchaRequiredError";
669
687
  }
670
688
  };
689
+ var PasswordSetupRequiredError = class extends MoonbaseError {
690
+ constructor(detail, title, status) {
691
+ super(title != null ? title : "Password setup required", detail, status);
692
+ this.name = "PasswordSetupRequiredError";
693
+ }
694
+ };
671
695
 
672
696
  // src/utils/problemDetails.ts
673
697
  var import_zod7 = require("zod");
@@ -691,6 +715,7 @@ function camelCase(str) {
691
715
  }).join("");
692
716
  }
693
717
  async function handleResponseProblem(response, logger) {
718
+ var _a;
694
719
  if (response.status === 404)
695
720
  throw new NotFoundError();
696
721
  if (response.status === 401) {
@@ -712,6 +737,8 @@ async function handleResponseProblem(response, logger) {
712
737
  throw new StoreClosedError(problemDetails.detail);
713
738
  if (problemDetails.errorType === "CaptchaRequired")
714
739
  throw new CaptchaRequiredError(problemDetails.detail, problemDetails.title);
740
+ if (problemDetails.errorType === "PasswordSetupRequired")
741
+ throw new PasswordSetupRequiredError(problemDetails.detail, problemDetails.title, (_a = problemDetails.status) != null ? _a : response.status);
715
742
  throw new MoonbaseError(
716
743
  problemDetails.title,
717
744
  problemDetails.detail,
@@ -730,6 +757,7 @@ __export(schemas_exports5, {
730
757
  identitySchema: () => identitySchema,
731
758
  ilokConnectedAccountSchema: () => ilokConnectedAccountSchema,
732
759
  pendingActivationSchema: () => pendingActivationSchema,
760
+ pendingPasswordProblemSchema: () => pendingPasswordProblemSchema,
733
761
  userAccountConfirmedSchema: () => userAccountConfirmedSchema,
734
762
  userAccountConfirmedStatusSchema: () => userAccountConfirmedStatusSchema,
735
763
  userSchema: () => userSchema
@@ -803,13 +831,18 @@ var pendingActivationSchema = import_zod8.z.object({
803
831
  status: import_zod8.z.literal("activation_pending"),
804
832
  email: import_zod8.z.string()
805
833
  });
834
+ var pendingPasswordProblemSchema = import_zod8.z.object({
835
+ errorType: import_zod8.z.literal("PasswordSetupRequired"),
836
+ pendingPasswordToken: import_zod8.z.string()
837
+ });
806
838
 
807
839
  // src/identity/endpoints.ts
808
840
  var IdentityEndpoints = class {
809
- constructor(api, tokenStore, logger) {
841
+ constructor(api, tokenStore, logger, pendingPasswords) {
810
842
  this.api = api;
811
843
  this.tokenStore = tokenStore;
812
844
  this.logger = logger;
845
+ this.pendingPasswords = pendingPasswords;
813
846
  }
814
847
  async get() {
815
848
  const response = await this.api.authenticatedFetch("/api/customer/meta/user", userSchema);
@@ -841,8 +874,12 @@ var IdentityEndpoints = class {
841
874
  return userSchema.parse(identity);
842
875
  }
843
876
  async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn, options) {
844
- const path = `/api/customer/identity/sign-up?scheme=JWT&communicationOptIn=${communicationOptIn ? "true" : "false"}`;
845
- const response = await fetch(`${this.api.baseUrl}${path}`, {
877
+ const query = new URLSearchParams({
878
+ scheme: "JWT",
879
+ communicationOptIn: communicationOptIn ? "true" : "false",
880
+ ...utmToObject(options == null ? void 0 : options.utm)
881
+ });
882
+ const response = await fetch(`${this.api.baseUrl}/api/customer/identity/sign-up?${query.toString()}`, {
846
883
  method: "POST",
847
884
  headers: {
848
885
  "Accept": "application/json",
@@ -861,8 +898,11 @@ var IdentityEndpoints = class {
861
898
  groupIds: normalizeGroupIds(options == null ? void 0 : options.groupIds)
862
899
  })
863
900
  });
864
- if (response.status >= 400)
901
+ if (response.status >= 400) {
902
+ if (response.status === 409)
903
+ await this.keepPendingPassword(email, response.clone());
865
904
  await handleResponseProblem(response, this.logger);
905
+ }
866
906
  const data = await response.json();
867
907
  if (response.status === 202) {
868
908
  const parsed = pendingActivationSchema.parse(data);
@@ -878,6 +918,19 @@ var IdentityEndpoints = class {
878
918
  this.tokenStore.setUser(identity);
879
919
  return { status: "signed-in", user: userSchema.parse(identity) };
880
920
  }
921
+ /**
922
+ * The address already has an account with no password, so the server emailed it a link and
923
+ * handed back the password just typed, sealed, for that link to set once it is opened here.
924
+ * Never thrown: the caller still gets the `PasswordSetupRequiredError` from the response.
925
+ */
926
+ async keepPendingPassword(email, response) {
927
+ try {
928
+ const problem = pendingPasswordProblemSchema.safeParse(await response.json());
929
+ if (problem.success)
930
+ this.pendingPasswords.set(email, problem.data.pendingPasswordToken);
931
+ } catch (e) {
932
+ }
933
+ }
881
934
  /**
882
935
  * Signs the buyer out here at once, and ends the session on the server so its refresh
883
936
  * token stops working too. Resolves once the server has answered, and never rejects:
@@ -929,7 +982,13 @@ var IdentityEndpoints = class {
929
982
  await handleResponseProblem(response, this.logger);
930
983
  }
931
984
  async confirmAccount(email, code) {
932
- const response = await this.api.fetch(`/api/customer/identity/confirm-account?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code.replaceAll(" ", "+"))}&scheme=JWT`, userAccountConfirmedSchema, { method: "POST" });
985
+ const pendingPasswordToken = this.pendingPasswords.get(email);
986
+ const response = await this.api.fetch(`/api/customer/identity/confirm-account?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code.replaceAll(" ", "+"))}&scheme=JWT`, userAccountConfirmedSchema, {
987
+ method: "POST",
988
+ body: pendingPasswordToken ? { pendingPasswordToken } : void 0
989
+ });
990
+ if (pendingPasswordToken)
991
+ this.pendingPasswords.remove(email);
933
992
  const result = response.data;
934
993
  if (result.status === "SignedIn") {
935
994
  if (!result.accessToken || !result.refreshToken) {
@@ -1091,6 +1150,15 @@ var ProductEndpoints = class {
1091
1150
  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);
1092
1151
  return response.data;
1093
1152
  }
1153
+ /**
1154
+ * The product's published releases, highest version first, with their release notes and files,
1155
+ * and whether the caller may download each. Open to anyone who can see the product, signed in or
1156
+ * not. A page can hold fewer releases than its size, so keep following `next` while `hasMore` is set.
1157
+ */
1158
+ async getReleases(productId, nextUrl) {
1159
+ const response = await this.api.fetch(nextUrl || `/api/customer/inventory/products/${productId}/releases${this.configuration.includeManifests ? "?includeManifests=true" : ""}`, paged(productReleaseSchema));
1160
+ return response.data;
1161
+ }
1094
1162
  async getOwned(nextUrl) {
1095
1163
  const response = await this.api.authenticatedFetch(nextUrl || `/api/customer/inventory/products${this.configuration.includeManifests ? "?includeManifests=true" : ""}`, paged(productSummarySchema));
1096
1164
  return response.data;
@@ -1117,7 +1185,7 @@ var ProductEndpoints = class {
1117
1185
  var import_zod14 = require("zod");
1118
1186
 
1119
1187
  // package.json
1120
- var version = "3.7.0";
1188
+ var version = "3.9.0";
1121
1189
 
1122
1190
  // src/utils/client.ts
1123
1191
  function buildClientHeader(sdk) {
@@ -1125,6 +1193,11 @@ function buildClientHeader(sdk) {
1125
1193
  }
1126
1194
 
1127
1195
  // src/utils/api.ts
1196
+ function loggableBody(body) {
1197
+ if (!body || typeof body !== "object" || Array.isArray(body))
1198
+ return body;
1199
+ return Object.fromEntries(Object.entries(body).map(([key, value]) => [key, /password|token/i.test(key) && value != null ? "[redacted]" : value]));
1200
+ }
1128
1201
  function objectToQuery(obj) {
1129
1202
  return Object.entries(obj != null ? obj : {}).filter(([_, value]) => value !== void 0).map(([key, value]) => `${key}=${encodeURIComponent(value instanceof Date ? value.toISOString() : value)}`).join("&");
1130
1203
  }
@@ -1146,7 +1219,7 @@ var MoonbaseApi = class {
1146
1219
  const contentType = (_a = options == null ? void 0 : options.contentType) != null ? _a : "application/json";
1147
1220
  this.logger.debug("Making request to Moonbase API...", {
1148
1221
  path,
1149
- body: options == null ? void 0 : options.body
1222
+ body: loggableBody(options == null ? void 0 : options.body)
1150
1223
  });
1151
1224
  const startedAt = /* @__PURE__ */ new Date();
1152
1225
  const request = {
@@ -1795,6 +1868,50 @@ var InMemoryStore = class {
1795
1868
  }
1796
1869
  };
1797
1870
 
1871
+ // src/utils/pendingPasswords.ts
1872
+ var lifetime = 24 * 60 * 60 * 1e3;
1873
+ function normalizeEmail(email) {
1874
+ return email.trim().toLowerCase();
1875
+ }
1876
+ var _PendingPasswordStore = class _PendingPasswordStore {
1877
+ constructor(store) {
1878
+ this.store = store != null ? store : isLocalStorageAvailable() ? new LocalStorageStore() : new InMemoryStore();
1879
+ }
1880
+ get(email) {
1881
+ var _a, _b;
1882
+ return (_b = (_a = this.read()[normalizeEmail(email)]) == null ? void 0 : _a.token) != null ? _b : null;
1883
+ }
1884
+ set(email, token) {
1885
+ this.write({ ...this.read(), [normalizeEmail(email)]: { token, storedAt: Date.now() } });
1886
+ }
1887
+ remove(email) {
1888
+ const entries = this.read();
1889
+ delete entries[normalizeEmail(email)];
1890
+ this.write(entries);
1891
+ }
1892
+ /** The entries still worth keeping. Drops the rest from storage as it goes. */
1893
+ read() {
1894
+ const stored = this.store.get(_PendingPasswordStore.storageKey);
1895
+ if (!stored || typeof stored !== "object")
1896
+ return {};
1897
+ const now = Date.now();
1898
+ const fresh = Object.fromEntries(Object.entries(stored).filter(
1899
+ ([, entry]) => typeof (entry == null ? void 0 : entry.token) === "string" && typeof entry.storedAt === "number" && now - entry.storedAt < lifetime
1900
+ ));
1901
+ if (Object.keys(fresh).length !== Object.keys(stored).length)
1902
+ this.write(fresh);
1903
+ return fresh;
1904
+ }
1905
+ write(entries) {
1906
+ if (Object.keys(entries).length > 0)
1907
+ this.store.set(_PendingPasswordStore.storageKey, entries);
1908
+ else
1909
+ this.store.remove(_PendingPasswordStore.storageKey);
1910
+ }
1911
+ };
1912
+ _PendingPasswordStore.storageKey = "moonbase_pending_passwords";
1913
+ var PendingPasswordStore = _PendingPasswordStore;
1914
+
1798
1915
  // src/utils/tokenStore.ts
1799
1916
  var defaultAccessTokenLifetime = 15 * 60 * 1e3;
1800
1917
  var renewBeforeExpiry = 5 * 60 * 1e3;
@@ -1804,9 +1921,9 @@ function accessTokenLifetime(accessToken) {
1804
1921
  try {
1805
1922
  const payload = accessToken.split(".")[1].replaceAll("-", "+").replaceAll("_", "/");
1806
1923
  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;
1924
+ const lifetime2 = (Number(claims.exp) - Number(claims.iat)) * 1e3;
1925
+ if (Number.isFinite(lifetime2) && lifetime2 > 0 && lifetime2 <= 24 * 60 * 60 * 1e3)
1926
+ return lifetime2;
1810
1927
  } catch (e) {
1811
1928
  }
1812
1929
  return defaultAccessTokenLifetime;
@@ -2731,7 +2848,8 @@ function parseOffSiteReferrer(rawReferrer, currentOrigin) {
2731
2848
  }
2732
2849
  function resolveUtm(cached, fresh) {
2733
2850
  const freshHasUtm = !!(fresh.source || fresh.medium || fresh.campaign || fresh.term || fresh.content);
2734
- const freshIsExternalArrival = freshHasUtm || !!fresh.referrer;
2851
+ const freshHasClickId = !!(fresh.gclid || fresh.gbraid || fresh.wbraid || fresh.fbclid || fresh.msclkid || fresh.ttclid);
2852
+ const freshIsExternalArrival = freshHasUtm || freshHasClickId || !!fresh.referrer;
2735
2853
  if (freshIsExternalArrival)
2736
2854
  return fresh;
2737
2855
  return cached != null ? cached : fresh;
@@ -2747,7 +2865,7 @@ var MoonbaseClient = class {
2747
2865
  this.tokenStore = new TokenStore(configuration);
2748
2866
  this.api = new MoonbaseApi(this.configuration.endpoint, this.tokenStore, this.logger, this.configuration.sdk);
2749
2867
  this.storefront = new StorefrontEndpoints(this.api, this.configuration);
2750
- this.identity = new IdentityEndpoints(this.api, this.tokenStore, this.logger);
2868
+ this.identity = new IdentityEndpoints(this.api, this.tokenStore, this.logger, new PendingPasswordStore(configuration.store));
2751
2869
  this.communications = new CommunicationsEndpoints(this.api);
2752
2870
  this.groups = new GroupEndpoints(this.api);
2753
2871
  this.vouchers = new VoucherEndpoints(this.api);
@@ -2785,6 +2903,7 @@ var MoonbaseClient = class {
2785
2903
  NotFoundError,
2786
2904
  OfferUtils,
2787
2905
  OrderStatus,
2906
+ PasswordSetupRequiredError,
2788
2907
  Platform,
2789
2908
  StoreClosedError,
2790
2909
  SubscriptionCancellationReason,