@godaddy/gd-commerce-server 0.2.0 → 0.4.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
@@ -1,5 +1,5 @@
1
1
  import { Router } from "express";
2
- import { createHash } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
 
4
4
  //#region src/lib/commerce/checkout-config.ts
5
5
  const DEFAULT_CHECKOUT_CONFIGURATION = {
@@ -35,6 +35,68 @@ function parseCommerceCheckoutConfiguration(raw) {
35
35
  };
36
36
  }
37
37
 
38
+ //#endregion
39
+ //#region src/lib/commerce/errors.ts
40
+ var CommerceError = class extends Error {
41
+ httpStatus;
42
+ code;
43
+ publicMessage;
44
+ details;
45
+ constructor(httpStatus, code, message, options = {}) {
46
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
47
+ this.name = "CommerceError";
48
+ this.httpStatus = httpStatus;
49
+ this.code = code;
50
+ this.publicMessage = options.publicMessage;
51
+ this.details = options.details;
52
+ }
53
+ };
54
+ var InvalidRequestError = class extends CommerceError {
55
+ constructor(publicMessage, { message = publicMessage,...options } = {}) {
56
+ super(400, "invalid_request", message, {
57
+ ...options,
58
+ publicMessage
59
+ });
60
+ this.name = "InvalidRequestError";
61
+ }
62
+ };
63
+ var ScopeMismatchError = class extends CommerceError {
64
+ constructor() {
65
+ const publicMessage = "The connected store changed. Reload the page before continuing.";
66
+ super(409, "scope_mismatch", publicMessage, { publicMessage });
67
+ this.name = "ScopeMismatchError";
68
+ }
69
+ };
70
+ var NotFoundError = class extends CommerceError {
71
+ constructor(publicMessage, { message = publicMessage,...options } = {}) {
72
+ super(404, "not_found", message, {
73
+ ...options,
74
+ publicMessage
75
+ });
76
+ this.name = "NotFoundError";
77
+ }
78
+ };
79
+ var CommerceNotConfiguredError = class extends CommerceError {
80
+ constructor(message, { publicMessage = "Commerce configuration is unavailable. Complete the store connection before continuing.",...options } = {}) {
81
+ super(503, "not_configured", message, {
82
+ ...options,
83
+ publicMessage
84
+ });
85
+ this.name = "CommerceNotConfiguredError";
86
+ }
87
+ };
88
+ /**
89
+ * Commerce returned a failure, an unexpected response, or could not be reached. Always 502:
90
+ * even an upstream 401/403 concerns this server's client credentials, not the caller, and
91
+ * answering 401 would collide with the host's own authentication handling.
92
+ */
93
+ var UpstreamError = class extends CommerceError {
94
+ constructor(message, { unauthorized = false,...options } = {}) {
95
+ super(502, unauthorized ? "upstream_unauthorized" : "upstream_error", message, options);
96
+ this.name = "UpstreamError";
97
+ }
98
+ };
99
+
38
100
  //#endregion
39
101
  //#region src/lib/commerce/config.ts
40
102
  const DEFAULT_API_BASE_URL = "https://api.godaddy.com";
@@ -43,14 +105,14 @@ function normalizeApiBaseUrl(raw) {
43
105
  try {
44
106
  url = new URL(raw);
45
107
  } catch {
46
- throw new Error("Commerce config: apiBaseUrl must be a valid HTTPS origin.");
108
+ throw new CommerceNotConfiguredError("Commerce config: apiBaseUrl must be a valid HTTPS origin.");
47
109
  }
48
- if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error("Commerce config: apiBaseUrl must be an HTTPS origin without credentials, path, query, or fragment.");
110
+ if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new CommerceNotConfiguredError("Commerce config: apiBaseUrl must be an HTTPS origin without credentials, path, query, or fragment.");
49
111
  return url.origin;
50
112
  }
51
113
  function requireValue(environment, key) {
52
114
  const value = environment[key]?.trim();
53
- if (!value) throw new Error(`Commerce config: ${key} is missing. Configure it in the server environment.`);
115
+ if (!value) throw new CommerceNotConfiguredError(`Commerce config: ${key} is missing. Configure it in the server environment.`);
54
116
  return value;
55
117
  }
56
118
  /** Read server environment values on each call. Loading files or secret stores belongs to the host. */
@@ -73,20 +135,81 @@ function createRuntimeCommerceConfiguration(options = {}) {
73
135
  readCheckout: () => parseCommerceCheckoutConfiguration((options.environment ?? process.env).GODADDY_CHECKOUT_CONFIGURATION)
74
136
  };
75
137
  }
138
+ function fromHost(read) {
139
+ try {
140
+ return read();
141
+ } catch (cause) {
142
+ if (cause instanceof CommerceError) throw cause;
143
+ throw new CommerceNotConfiguredError("Commerce configuration could not be read", { cause });
144
+ }
145
+ }
146
+ const CONNECTION_STATES = new Set([
147
+ "unbound",
148
+ "connecting",
149
+ "ready"
150
+ ]);
151
+ /** The host-reported connection state, or `undefined` when the host does not report one. */
152
+ function readCommerceConnectionState(configuration) {
153
+ const state = fromHost(() => configuration.readConnectionState?.());
154
+ if (state !== void 0 && !CONNECTION_STATES.has(state)) throw new CommerceNotConfiguredError("Invalid Commerce connection state.");
155
+ return state;
156
+ }
157
+ /** Nonready connection states must never use leftover credentials. */
158
+ function assertCommerceConnectionReady(configuration) {
159
+ const state = readCommerceConnectionState(configuration);
160
+ if (state === "unbound" || state === "connecting") throw new CommerceNotConfiguredError("Commerce store connection is not ready.", { details: { connectionState: state } });
161
+ }
162
+ function readReadyCommerceConfig(configuration) {
163
+ assertCommerceConnectionReady(configuration);
164
+ return fromHost(() => configuration.read());
165
+ }
166
+ function readReadyCheckoutConfig(configuration) {
167
+ assertCommerceConnectionReady(configuration);
168
+ return fromHost(() => configuration.readCheckout());
169
+ }
76
170
  function commerceConfigurationForResponse(res) {
77
- const configuration = res.locals.commerceConfiguration;
78
- if (configuration && typeof configuration === "object" && "read" in configuration && typeof configuration.read === "function" && "readCheckout" in configuration && typeof configuration.readCheckout === "function") return configuration;
79
- return createRuntimeCommerceConfiguration();
171
+ const supplied = res.locals.commerceConfiguration;
172
+ return supplied && typeof supplied === "object" && "read" in supplied && typeof supplied.read === "function" && "readCheckout" in supplied && typeof supplied.readCheckout === "function" ? supplied : createRuntimeCommerceConfiguration();
80
173
  }
81
174
  function readCommerceConfigForResponse(res) {
82
- const configuration = commerceConfigurationForResponse(res);
83
- assertCommerceConnectionReady(configuration);
84
- return configuration.read();
175
+ return readReadyCommerceConfig(commerceConfigurationForResponse(res));
85
176
  }
86
- /** Nonready and invalid connection states must never use leftover credentials. */
87
- function assertCommerceConnectionReady(configuration) {
88
- const state = configuration.readConnectionState?.();
89
- if (state !== void 0 && state !== "ready") throw new Error("Commerce store connection is not ready. Complete the store connection before continuing.");
177
+
178
+ //#endregion
179
+ //#region src/lib/commerce/upstream-http.ts
180
+ async function fetchUpstream(input, init, { operation, details }, fetchImplementation = fetch) {
181
+ try {
182
+ return await fetchImplementation(input, init);
183
+ } catch (cause) {
184
+ throw new UpstreamError(`${operation} could not reach Commerce`, {
185
+ cause,
186
+ details
187
+ });
188
+ }
189
+ }
190
+ async function readUpstreamJson(response, { operation, details }) {
191
+ try {
192
+ return await response.json();
193
+ } catch (cause) {
194
+ throw new UpstreamError(`${operation} returned a non-JSON response`, {
195
+ cause,
196
+ details: {
197
+ ...details,
198
+ upstreamStatus: response.status
199
+ }
200
+ });
201
+ }
202
+ }
203
+ /** Builds the error for a non-2xx response. 401 and 403 always mean this server's credentials were rejected. */
204
+ async function upstreamStatusError(response, { operation, details }, { unauthorized = false } = {}) {
205
+ if (!response.bodyUsed) await response.body?.cancel();
206
+ return new UpstreamError(`${operation} failed: upstream returned ${response.status}`, {
207
+ unauthorized: unauthorized || response.status === 401 || response.status === 403,
208
+ details: {
209
+ ...details,
210
+ upstreamStatus: response.status
211
+ }
212
+ });
90
213
  }
91
214
 
92
215
  //#endregion
@@ -128,22 +251,52 @@ function checkoutGraphqlEndpoint({ apiBaseUrl }) {
128
251
  function authorizationHeaders({ accessToken }) {
129
252
  return { Authorization: `Bearer ${accessToken}` };
130
253
  }
254
+ const UNAUTHORIZED_OAUTH_ERRORS = new Set([
255
+ "invalid_client",
256
+ "invalid_grant",
257
+ "unauthorized_client",
258
+ "invalid_scope"
259
+ ]);
260
+ async function readOAuthError(response) {
261
+ try {
262
+ const body = await response.json();
263
+ return body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : void 0;
264
+ } catch {
265
+ return;
266
+ }
267
+ }
131
268
  async function getOAuthAccessToken({ clientId, clientSecret, apiBaseUrl, scope = "commerce.product:read", fetch: fetchImplementation }) {
132
- if (!clientId || !clientSecret) throw new Error("clientId and clientSecret are required");
133
- const requestFetch = fetchImplementation ?? fetch;
269
+ if (!clientId || !clientSecret) throw new CommerceNotConfiguredError("clientId and clientSecret are required");
134
270
  const body = new URLSearchParams();
135
271
  body.append("grant_type", "client_credentials");
136
272
  body.append("client_id", clientId);
137
273
  body.append("client_secret", clientSecret);
138
274
  body.append("scope", scope);
139
- const response = await requestFetch(new URL("/v2/oauth2/token", apiBaseUrl).toString(), {
275
+ const endpoint = new URL("/v2/oauth2/token", apiBaseUrl);
276
+ const request = {
277
+ operation: "Access token request",
278
+ details: {
279
+ endpoint: endpoint.href,
280
+ scope
281
+ }
282
+ };
283
+ const response = await fetchUpstream(endpoint, {
140
284
  method: "POST",
141
285
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
142
286
  body: body.toString(),
143
287
  cache: "no-store"
144
- });
145
- if (!response.ok) throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`);
146
- return await response.json();
288
+ }, request, fetchImplementation);
289
+ if (!response.ok) {
290
+ const oauthError = response.status === 400 ? await readOAuthError(response) : void 0;
291
+ throw await upstreamStatusError(response, {
292
+ ...request,
293
+ details: {
294
+ ...request.details,
295
+ oauthError
296
+ }
297
+ }, { unauthorized: oauthError !== void 0 && UNAUTHORIZED_OAUTH_ERRORS.has(oauthError) });
298
+ }
299
+ return readUpstreamJson(response, request);
147
300
  }
148
301
  function buildBuyNowCheckoutInput({ storeId, skuId, quantity = 1, returnUrl, successUrl, channelId }, overrides = {}) {
149
302
  return applyCheckoutDefaults({
@@ -181,10 +334,18 @@ function buildNonCatalogCheckoutInput({ storeId, lineItemData, quantity = 1, ret
181
334
 
182
335
  //#endregion
183
336
  //#region src/lib/commerce/gql.ts
184
- var GraphQLErrorWithCodes = class extends Error {
337
+ /** `status` is the upstream HTTP status; the route responds with `httpStatus` (502) unless mapped. */
338
+ var GraphQLErrorWithCodes = class extends UpstreamError {
185
339
  constructor(errors, status) {
186
340
  const errorMessage = errors.length === 1 ? `${errors[0]?.message || "Unknown error"}` : errors.map((error) => error.message).filter(Boolean).join("; ");
187
- super(errorMessage);
341
+ super(errorMessage, {
342
+ unauthorized: [status, ...errors.map((error) => error.status)].some((value) => value === 401 || value === 403),
343
+ details: {
344
+ upstreamStatus: status,
345
+ upstreamCodes: errors.map((error) => error.code).filter(Boolean),
346
+ upstreamStatuses: errors.map((error) => error.status).filter((value) => typeof value === "number")
347
+ }
348
+ });
188
349
  this.errors = errors;
189
350
  this.status = status;
190
351
  this.name = "GraphQLErrorWithCodes";
@@ -209,7 +370,7 @@ async function gqlRequest({ endpoint, query, variables, headers: headersInit, fe
209
370
  const requestHeaders = new Headers(headersInit);
210
371
  requestHeaders.set("Accept", "application/json");
211
372
  requestHeaders.set("Content-Type", "application/json");
212
- const response = await (fetchImplementation ?? fetch)(endpoint, {
373
+ const response = await fetchUpstream(endpoint, {
213
374
  method: "POST",
214
375
  headers: requestHeaders,
215
376
  body: JSON.stringify({
@@ -217,7 +378,10 @@ async function gqlRequest({ endpoint, query, variables, headers: headersInit, fe
217
378
  variables: variables ?? {}
218
379
  }),
219
380
  cache: "no-store"
220
- });
381
+ }, {
382
+ operation: "GraphQL request",
383
+ details: { endpoint }
384
+ }, fetchImplementation);
221
385
  let result;
222
386
  try {
223
387
  result = await response.json();
@@ -236,7 +400,7 @@ async function gqlRequest({ endpoint, query, variables, headers: headersInit, fe
236
400
  message: `GraphQL request failed: ${response.status} ${response.statusText}`,
237
401
  status: response.status
238
402
  }], response.status);
239
- if (result.data === void 0) throw new Error("GraphQL response did not include data");
403
+ if (result.data === void 0) throw new UpstreamError("GraphQL response did not include data", { details: { endpoint } });
240
404
  return result.data;
241
405
  }
242
406
 
@@ -331,15 +495,14 @@ function promotionCodesEnabled(configuration) {
331
495
  }
332
496
  async function createCheckoutSession(params, configuration = createRuntimeCommerceConfiguration()) {
333
497
  const { draftOrderId, skuId, quantity, lineItemData, returnUrl, successUrl } = params;
334
- if (!returnUrl || !successUrl) throw new Error("createCheckoutSession: returnUrl and successUrl are required");
498
+ if (!returnUrl || !successUrl) throw new InvalidRequestError("createCheckoutSession: returnUrl and successUrl are required");
335
499
  if ([
336
500
  draftOrderId,
337
501
  skuId,
338
502
  lineItemData
339
- ].filter(Boolean).length !== 1) throw new Error("createCheckoutSession: exactly one of draftOrderId, skuId, or lineItemData is required");
340
- assertCommerceConnectionReady(configuration);
341
- const { storeId, channelId, clientId, clientSecret, apiBaseUrl, sourceApp, owner, currencyCode: configCurrencyCode } = configuration.read();
342
- const checkoutConfiguration = configuration.readCheckout();
503
+ ].filter(Boolean).length !== 1) throw new InvalidRequestError("createCheckoutSession: exactly one of draftOrderId, skuId, or lineItemData is required");
504
+ const { storeId, channelId, clientId, clientSecret, apiBaseUrl, sourceApp, owner, currencyCode: configCurrencyCode } = readReadyCommerceConfig(configuration);
505
+ const checkoutConfiguration = readReadyCheckoutConfig(configuration);
343
506
  const enablePromotionCodes = promotionCodesEnabled(checkoutConfiguration);
344
507
  const catalogShippingEnabled = lineItemData === void 0 && checkoutConfiguration.enableShipping;
345
508
  const token = await getOAuthAccessToken({
@@ -402,16 +565,20 @@ async function createCheckoutSession(params, configuration = createRuntimeCommer
402
565
  headers: authorizationHeaders({ accessToken: token.access_token })
403
566
  });
404
567
  } catch (error) {
405
- throw new Error("Commerce checkout session could not be created", { cause: error });
568
+ throw new UpstreamError("Commerce checkout session could not be created", {
569
+ cause: error,
570
+ unauthorized: error instanceof UpstreamError && error.code === "upstream_unauthorized",
571
+ details: error instanceof UpstreamError ? error.details : void 0
572
+ });
406
573
  }
407
574
  const session = result.createCheckoutSession;
408
- if (!session?.url || !session.id) throw new Error("Checkout session was not created");
409
- if (session.storeId !== storeId || session.channelId !== channelId) throw new Error(`Checkout session binding mismatch: expected store ${storeId} and channel ${channelId}, received store ${session.storeId ?? "missing"} and channel ${session.channelId ?? "missing"}`);
575
+ if (!session?.url || !session.id) throw new UpstreamError("Checkout session was not created");
576
+ if (session.storeId !== storeId || session.channelId !== channelId) throw new UpstreamError(`Checkout session binding mismatch: expected store ${storeId} and channel ${channelId}, received store ${session.storeId ?? "missing"} and channel ${session.channelId ?? "missing"}`);
410
577
  const expectedShipping = lineItemData === void 0 && checkoutConfiguration.enableShipping;
411
- if (lineItemData === void 0 && enablePromotionCodes && session.enablePromotionCodes !== true) throw new Error("Checkout session did not enable configured promotion codes");
412
- if (checkoutConfiguration.enableTaxCollection && session.enableTaxCollection !== true) throw new Error("Checkout session did not enable configured tax collection");
413
- if (expectedShipping && (session.enableShipping !== true || session.enableShippingAddressCollection !== true)) throw new Error("Checkout session did not enable configured shipping and address collection");
414
- if (!session.paymentMethods?.card?.processor) throw new Error("Checkout session did not configure payment methods.");
578
+ if (lineItemData === void 0 && enablePromotionCodes && session.enablePromotionCodes !== true) throw new UpstreamError("Checkout session did not enable configured promotion codes");
579
+ if (checkoutConfiguration.enableTaxCollection && session.enableTaxCollection !== true) throw new UpstreamError("Checkout session did not enable configured tax collection");
580
+ if (expectedShipping && (session.enableShipping !== true || session.enableShippingAddressCollection !== true)) throw new UpstreamError("Checkout session did not enable configured shipping and address collection");
581
+ if (!session.paymentMethods?.card?.processor) throw new UpstreamError("Checkout session did not configure payment methods.");
415
582
  return {
416
583
  url: session.url,
417
584
  id: session.id,
@@ -426,32 +593,73 @@ async function createCheckoutSession(params, configuration = createRuntimeCommer
426
593
 
427
594
  //#endregion
428
595
  //#region src/lib/commerce/get-order-status.ts
596
+ /** `CommerceOrderStatus.status` when the Orders API reports no payment status. */
597
+ const ORDER_STATUS_UNKNOWN = "unknown";
598
+ var InvalidOrderIdError = class extends InvalidRequestError {
599
+ constructor() {
600
+ super("missing or invalid orderId query parameter", { message: "getOrderStatus: a valid orderId is required" });
601
+ this.name = "InvalidOrderIdError";
602
+ }
603
+ };
604
+ var OrderNotFoundError = class extends NotFoundError {
605
+ constructor() {
606
+ super("Order not found");
607
+ this.name = "OrderNotFoundError";
608
+ }
609
+ };
610
+ function isBindingId(value) {
611
+ return typeof value === "string" && value !== "";
612
+ }
613
+ async function readErrorCode(response) {
614
+ try {
615
+ const body = await response.json();
616
+ return body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
617
+ } catch {
618
+ return null;
619
+ }
620
+ }
429
621
  async function getOrderStatus(orderId, configuration = createRuntimeCommerceConfiguration()) {
430
- if (typeof orderId !== "string" || !orderId.trim() || orderId === "." || orderId === "..") throw new Error("getOrderStatus: a valid orderId is required");
431
- assertCommerceConnectionReady(configuration);
432
- const { storeId, channelId, clientId, clientSecret, apiBaseUrl, currencyCode } = configuration.read();
622
+ if (typeof orderId !== "string" || !orderId || orderId !== orderId.trim() || orderId === "." || orderId === "..") throw new InvalidOrderIdError();
623
+ const { storeId, channelId, clientId, clientSecret, apiBaseUrl, currencyCode } = readReadyCommerceConfig(configuration);
433
624
  const token = await getOAuthAccessToken({
434
625
  clientId,
435
626
  clientSecret,
436
627
  apiBaseUrl,
437
628
  scope: "commerce.order:read"
438
629
  });
439
- const response = await fetch(new URL(`/v1/commerce/stores/${encodeURIComponent(storeId)}/orders/${encodeURIComponent(orderId)}`, apiBaseUrl), {
630
+ const endpoint = new URL(`/v1/commerce/stores/${encodeURIComponent(storeId)}/orders/${encodeURIComponent(orderId)}`, apiBaseUrl);
631
+ const request = {
632
+ operation: "Order lookup",
633
+ details: { endpoint: endpoint.href }
634
+ };
635
+ const response = await fetchUpstream(endpoint, {
440
636
  method: "GET",
441
637
  headers: {
442
638
  ...authorizationHeaders({ accessToken: token.access_token }),
443
639
  Accept: "application/json"
444
640
  },
445
641
  cache: "no-store"
446
- });
447
- if (!response.ok) throw new Error(`Failed to load order: upstream returned ${response.status}`);
448
- const order = (await response.json())?.order;
449
- if (!order?.id || order.id !== orderId) throw new Error("Order lookup did not return the requested order");
450
- if (order.context?.storeId !== storeId || order.context?.channelId !== channelId) throw new Error("Order lookup returned a different store or channel");
642
+ }, request);
643
+ if (!response.ok) {
644
+ const code = response.status === 404 || response.status === 422 ? await readErrorCode(response) : void 0;
645
+ if (response.status === 404 && code === "NOT_FOUND") throw new OrderNotFoundError();
646
+ if (response.status === 422 && code === "VALIDATION_FAILED") throw new InvalidOrderIdError();
647
+ throw await upstreamStatusError(response, {
648
+ ...request,
649
+ details: {
650
+ ...request.details,
651
+ upstreamCode: code ?? void 0
652
+ }
653
+ });
654
+ }
655
+ const order = (await readUpstreamJson(response, request))?.order;
656
+ const context = order?.context;
657
+ if (!isBindingId(order?.id) || order.id !== orderId || !isBindingId(context?.storeId) || !isBindingId(context?.channelId)) throw new UpstreamError("Order lookup did not return the requested order", { details: { endpoint: endpoint.href } });
658
+ if (context.storeId !== storeId || context.channelId !== channelId) throw new OrderNotFoundError();
451
659
  const total = order.totals?.total;
452
660
  return {
453
661
  id: order.id,
454
- status: order.statuses?.paymentStatus ?? "unknown",
662
+ status: order.statuses?.paymentStatus ?? ORDER_STATUS_UNKNOWN,
455
663
  amount: total?.value ?? 0,
456
664
  currency: total?.currencyCode ?? currencyCode,
457
665
  createdAt: order.createdAt,
@@ -505,6 +713,75 @@ function createCheckoutReturnUrlValidator(policy) {
505
713
  };
506
714
  }
507
715
 
716
+ //#endregion
717
+ //#region src/lib/commerce/commerce-route.ts
718
+ const consoleCommerceLogger = { error: (message, context) => console.error(message, context) };
719
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
720
+ function observabilityLocals(res) {
721
+ return res.locals;
722
+ }
723
+ function resolveRequestId(req, resolver) {
724
+ let supplied;
725
+ try {
726
+ supplied = resolver?.(req);
727
+ } catch {
728
+ supplied = void 0;
729
+ }
730
+ return typeof supplied === "string" && REQUEST_ID_PATTERN.test(supplied) ? supplied : randomUUID();
731
+ }
732
+ function requestIdFor(req, res) {
733
+ const locals = observabilityLocals(res);
734
+ locals.commerceRequestId ??= resolveRequestId(req, locals.commerceRequestIdResolver);
735
+ return locals.commerceRequestId;
736
+ }
737
+ /**
738
+ * The single place Commerce failures become HTTP responses. Typed errors keep their status and
739
+ * public message; unmapped upstream failures are 502 and anything else is 500. 5xx detail goes
740
+ * to the configured logger, never into the body.
741
+ */
742
+ function sendCommerceError(req, res, label, error, { failureFields = {}, classifyUpstreamError } = {}) {
743
+ const resolved = error instanceof UpstreamError ? classifyUpstreamError?.(error) ?? error : error instanceof CommerceError ? error : void 0;
744
+ const httpStatus = resolved?.httpStatus ?? 500;
745
+ const code = resolved?.code ?? "internal_error";
746
+ const requestId = requestIdFor(req, res);
747
+ if (httpStatus >= 500) {
748
+ const context = {
749
+ requestId,
750
+ method: req.method,
751
+ path: req.path,
752
+ httpStatus,
753
+ code,
754
+ ...resolved?.details ? { details: resolved.details } : {},
755
+ error
756
+ };
757
+ try {
758
+ (observabilityLocals(res).commerceLogger ?? consoleCommerceLogger).error(`commerce-server: ${label}`, context);
759
+ } catch (loggerError) {
760
+ console.error(`commerce-server: ${label} (host logger failed)`, {
761
+ ...context,
762
+ loggerError
763
+ });
764
+ }
765
+ }
766
+ if (res.headersSent) return;
767
+ res.status(httpStatus).json({
768
+ ...failureFields,
769
+ error: resolved?.publicMessage ?? label,
770
+ code,
771
+ requestId
772
+ });
773
+ }
774
+ /** Wraps a route so thrown errors are answered by `sendCommerceError` with the route's label. */
775
+ function commerceRoute(label, handler, options = {}) {
776
+ return async (req, res) => {
777
+ try {
778
+ await handler(req, res);
779
+ } catch (error) {
780
+ sendCommerceError(req, res, label, error, options);
781
+ }
782
+ };
783
+ }
784
+
508
785
  //#endregion
509
786
  //#region src/lib/commerce/cart-scope.ts
510
787
  /** Public cache/storage scope, not an authorization credential. */
@@ -517,11 +794,10 @@ function getCommerceCartScope(config) {
517
794
  ])).digest("hex").slice(0, 32);
518
795
  }
519
796
  /** Existing custom clients may omit the header; managed components always send it. */
520
- function validateCommerceCartScope(req, res, config) {
797
+ function assertCommerceCartScope(req, config) {
521
798
  const suppliedScope = req.headers?.["x-commerce-scope"];
522
- if (suppliedScope === void 0 || suppliedScope === getCommerceCartScope(config)) return true;
523
- res.status(409).json({ error: "The connected store changed. Reload the page before continuing." });
524
- return false;
799
+ if (suppliedScope === void 0 || suppliedScope === getCommerceCartScope(config)) return;
800
+ throw new ScopeMismatchError();
525
801
  }
526
802
 
527
803
  //#endregion
@@ -731,61 +1007,7 @@ const getCartOrderQuery = `
731
1007
  `;
732
1008
 
733
1009
  //#endregion
734
- //#region src/server/api/commerce/cart/[id]/discounts/POST.ts
735
- const applyDiscountCodesMutation = `
736
- mutation ApplyDiscountCodes($input: ApplyDiscountCodesInput!) {
737
- applyDiscountCodes(input: $input) {
738
- id
739
- }
740
- }
741
- `;
742
- async function handler(req, res) {
743
- try {
744
- const cartId = req.params.id;
745
- if (typeof cartId !== "string" || !cartId) {
746
- res.status(400).json({ error: "Missing cart id" });
747
- return;
748
- }
749
- const body = req.body ?? {};
750
- const codes = Array.isArray(body.discountCodes) ? body.discountCodes.filter((code) => typeof code === "string" && code.length > 0) : [];
751
- if (codes.length === 0) {
752
- res.status(400).json({ error: "discountCodes must be a non-empty string array" });
753
- return;
754
- }
755
- const config = readCommerceConfigForResponse(res);
756
- if (!validateCommerceCartScope(req, res, config)) return;
757
- const { storeId, clientId, apiBaseUrl } = config;
758
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
759
- const headers = storefrontHeaders({
760
- storeId,
761
- clientId
762
- });
763
- await gqlRequest({
764
- endpoint,
765
- query: applyDiscountCodesMutation,
766
- variables: { input: {
767
- orderId: cartId,
768
- discountCodes: codes
769
- } },
770
- headers
771
- });
772
- const hydrated = await gqlRequest({
773
- endpoint,
774
- query: getCartOrderQuery,
775
- variables: { id: cartId },
776
- headers
777
- });
778
- res.json({ cart: hydrated.orderById ?? null });
779
- } catch (error) {
780
- res.status(500).json({
781
- error: "Failed to apply discount codes",
782
- message: error instanceof Error ? error.message : String(error)
783
- });
784
- }
785
- }
786
-
787
- //#endregion
788
- //#region src/server/api/commerce/cart/[id]/GET.ts
1010
+ //#region src/lib/commerce/upstream-errors.ts
789
1011
  function isCartNotFoundError(error) {
790
1012
  if (!(error instanceof GraphQLErrorWithCodes)) return false;
791
1013
  const isFailure = (status) => status !== void 0 && status >= 400 && status !== 404 && status !== 410;
@@ -798,17 +1020,68 @@ function isCartNotFoundError(error) {
798
1020
  return /\b(?:cart|(?:draft[ -])?order)\s+(?:(?:is|was|has)\s+)?(?:not found|expired)\b/i.test(message ?? "");
799
1021
  });
800
1022
  }
801
- async function handler$1(req, res) {
1023
+ const CART_UPSTREAM_ERROR_RULES = [{
1024
+ matches: isCartNotFoundError,
1025
+ toError: (error) => new NotFoundError("Cart not found", { cause: error })
1026
+ }];
1027
+ /** For routes acting on an existing saved cart (`/cart/:id`); creating a cart has none to miss. */
1028
+ function classifyCartUpstreamError(error) {
1029
+ return CART_UPSTREAM_ERROR_RULES.find((rule) => rule.matches(error))?.toError(error) ?? error;
1030
+ }
1031
+
1032
+ //#endregion
1033
+ //#region src/server/api/commerce/cart/[id]/discounts/POST.ts
1034
+ const applyDiscountCodesMutation = `
1035
+ mutation ApplyDiscountCodes($input: ApplyDiscountCodesInput!) {
1036
+ applyDiscountCodes(input: $input) {
1037
+ id
1038
+ }
1039
+ }
1040
+ `;
1041
+ async function applyDiscountCodes(req, res) {
1042
+ const cartId = req.params.id;
1043
+ if (typeof cartId !== "string" || !cartId) throw new InvalidRequestError("Missing cart id");
1044
+ const body = req.body ?? {};
1045
+ const codes = Array.isArray(body.discountCodes) ? body.discountCodes.filter((code) => typeof code === "string" && code.length > 0) : [];
1046
+ if (codes.length === 0) throw new InvalidRequestError("discountCodes must be a non-empty string array");
1047
+ const config = readCommerceConfigForResponse(res);
1048
+ assertCommerceCartScope(req, config);
1049
+ const { storeId, clientId, apiBaseUrl } = config;
1050
+ const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
1051
+ const headers = storefrontHeaders({
1052
+ storeId,
1053
+ clientId
1054
+ });
1055
+ await gqlRequest({
1056
+ endpoint,
1057
+ query: applyDiscountCodesMutation,
1058
+ variables: { input: {
1059
+ orderId: cartId,
1060
+ discountCodes: codes
1061
+ } },
1062
+ headers
1063
+ });
1064
+ const hydrated = await gqlRequest({
1065
+ endpoint,
1066
+ query: getCartOrderQuery,
1067
+ variables: { id: cartId },
1068
+ headers
1069
+ });
1070
+ res.json({ cart: hydrated.orderById ?? null });
1071
+ }
1072
+ var POST_default = commerceRoute("Failed to apply discount codes", applyDiscountCodes, { classifyUpstreamError: classifyCartUpstreamError });
1073
+
1074
+ //#endregion
1075
+ //#region src/server/api/commerce/cart/[id]/GET.ts
1076
+ async function readCart(req, res) {
1077
+ const cartId = req.params.id;
1078
+ if (typeof cartId !== "string" || !cartId) throw new InvalidRequestError("Missing cart id");
1079
+ const config = readCommerceConfigForResponse(res);
1080
+ assertCommerceCartScope(req, config);
1081
+ const { storeId, clientId, apiBaseUrl } = config;
1082
+ let data;
802
1083
  try {
803
- const cartId = req.params.id;
804
- if (typeof cartId !== "string" || !cartId) {
805
- res.status(400).json({ error: "Missing cart id" });
806
- return;
807
- }
808
- const config = readCommerceConfigForResponse(res);
809
- if (!validateCommerceCartScope(req, res, config)) return;
810
- const { storeId, clientId, apiBaseUrl } = config;
811
- const data = await gqlRequest({
1084
+ data = await gqlRequest({
812
1085
  endpoint: orderStorefrontEndpoint({ apiBaseUrl }),
813
1086
  query: getCartOrderQuery,
814
1087
  variables: { id: cartId },
@@ -817,18 +1090,16 @@ async function handler$1(req, res) {
817
1090
  clientId
818
1091
  })
819
1092
  });
820
- res.json({ cart: data.orderById ?? null });
821
1093
  } catch (error) {
822
1094
  if (isCartNotFoundError(error)) {
823
1095
  res.status(200).json({ cart: null });
824
1096
  return;
825
1097
  }
826
- res.status(500).json({
827
- error: "Failed to load cart",
828
- message: error instanceof Error ? error.message : String(error)
829
- });
1098
+ throw error;
830
1099
  }
1100
+ res.json({ cart: data.orderById ?? null });
831
1101
  }
1102
+ var GET_default = commerceRoute("Failed to load cart", readCart, { classifyUpstreamError: classifyCartUpstreamError });
832
1103
 
833
1104
  //#endregion
834
1105
  //#region src/server/api/commerce/cart/[id]/items/[itemId]/DELETE.ts
@@ -837,45 +1108,36 @@ const deleteLineItemByIdMutation = `
837
1108
  deleteLineItemById(id: $id, orderId: $orderId)
838
1109
  }
839
1110
  `;
840
- async function handler$2(req, res) {
841
- try {
842
- const cartId = req.params.id;
843
- const itemId = req.params.itemId;
844
- if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) {
845
- res.status(400).json({ error: "Missing cart id or item id" });
846
- return;
847
- }
848
- const config = readCommerceConfigForResponse(res);
849
- if (!validateCommerceCartScope(req, res, config)) return;
850
- const { storeId, clientId, apiBaseUrl } = config;
851
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
852
- const headers = storefrontHeaders({
853
- storeId,
854
- clientId
855
- });
856
- await gqlRequest({
857
- endpoint,
858
- query: deleteLineItemByIdMutation,
859
- variables: {
860
- id: itemId,
861
- orderId: cartId
862
- },
863
- headers
864
- });
865
- const hydrated = await gqlRequest({
866
- endpoint,
867
- query: getCartOrderQuery,
868
- variables: { id: cartId },
869
- headers
870
- });
871
- res.json({ cart: hydrated.orderById ?? null });
872
- } catch (error) {
873
- res.status(500).json({
874
- error: "Failed to delete line item",
875
- message: error instanceof Error ? error.message : String(error)
876
- });
877
- }
1111
+ async function deleteLineItem(req, res) {
1112
+ const cartId = req.params.id;
1113
+ const itemId = req.params.itemId;
1114
+ if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) throw new InvalidRequestError("Missing cart id or item id");
1115
+ const config = readCommerceConfigForResponse(res);
1116
+ assertCommerceCartScope(req, config);
1117
+ const { storeId, clientId, apiBaseUrl } = config;
1118
+ const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
1119
+ const headers = storefrontHeaders({
1120
+ storeId,
1121
+ clientId
1122
+ });
1123
+ await gqlRequest({
1124
+ endpoint,
1125
+ query: deleteLineItemByIdMutation,
1126
+ variables: {
1127
+ id: itemId,
1128
+ orderId: cartId
1129
+ },
1130
+ headers
1131
+ });
1132
+ const hydrated = await gqlRequest({
1133
+ endpoint,
1134
+ query: getCartOrderQuery,
1135
+ variables: { id: cartId },
1136
+ headers
1137
+ });
1138
+ res.json({ cart: hydrated.orderById ?? null });
878
1139
  }
1140
+ var DELETE_default = commerceRoute("Failed to delete line item", deleteLineItem, { classifyUpstreamError: classifyCartUpstreamError });
879
1141
 
880
1142
  //#endregion
881
1143
  //#region src/server/api/commerce/cart/[id]/items/[itemId]/PATCH.ts
@@ -886,52 +1148,43 @@ const updateLineItemByIdMutation = `
886
1148
  }
887
1149
  }
888
1150
  `;
889
- async function handler$3(req, res) {
890
- try {
891
- const cartId = req.params.id;
892
- const itemId = req.params.itemId;
893
- if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) {
894
- res.status(400).json({ error: "Missing cart id or item id" });
895
- return;
896
- }
897
- const body = req.body ?? {};
898
- const config = readCommerceConfigForResponse(res);
899
- if (!validateCommerceCartScope(req, res, config)) return;
900
- const { storeId, clientId, apiBaseUrl } = config;
901
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
902
- const headers = storefrontHeaders({
903
- storeId,
904
- clientId
905
- });
906
- await gqlRequest({
907
- endpoint,
908
- query: updateLineItemByIdMutation,
909
- variables: { input: {
910
- id: itemId,
911
- orderId: cartId,
912
- name: body.name,
913
- quantity: body.quantity,
914
- fulfillmentMode: body.fulfillmentMode,
915
- status: body.status,
916
- type: body.type,
917
- details: body.details
918
- } },
919
- headers
920
- });
921
- const hydrated = await gqlRequest({
922
- endpoint,
923
- query: getCartOrderQuery,
924
- variables: { id: cartId },
925
- headers
926
- });
927
- res.json({ cart: hydrated.orderById ?? null });
928
- } catch (error) {
929
- res.status(500).json({
930
- error: "Failed to update line item",
931
- message: error instanceof Error ? error.message : String(error)
932
- });
933
- }
1151
+ async function updateLineItem(req, res) {
1152
+ const cartId = req.params.id;
1153
+ const itemId = req.params.itemId;
1154
+ if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) throw new InvalidRequestError("Missing cart id or item id");
1155
+ const body = req.body ?? {};
1156
+ const config = readCommerceConfigForResponse(res);
1157
+ assertCommerceCartScope(req, config);
1158
+ const { storeId, clientId, apiBaseUrl } = config;
1159
+ const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
1160
+ const headers = storefrontHeaders({
1161
+ storeId,
1162
+ clientId
1163
+ });
1164
+ await gqlRequest({
1165
+ endpoint,
1166
+ query: updateLineItemByIdMutation,
1167
+ variables: { input: {
1168
+ id: itemId,
1169
+ orderId: cartId,
1170
+ name: body.name,
1171
+ quantity: body.quantity,
1172
+ fulfillmentMode: body.fulfillmentMode,
1173
+ status: body.status,
1174
+ type: body.type,
1175
+ details: body.details
1176
+ } },
1177
+ headers
1178
+ });
1179
+ const hydrated = await gqlRequest({
1180
+ endpoint,
1181
+ query: getCartOrderQuery,
1182
+ variables: { id: cartId },
1183
+ headers
1184
+ });
1185
+ res.json({ cart: hydrated.orderById ?? null });
934
1186
  }
1187
+ var PATCH_default = commerceRoute("Failed to update line item", updateLineItem, { classifyUpstreamError: classifyCartUpstreamError });
935
1188
 
936
1189
  //#endregion
937
1190
  //#region src/server/api/commerce/cart/[id]/items/POST.ts
@@ -942,50 +1195,38 @@ const addLineItemBySkuIdMutation$1 = `
942
1195
  }
943
1196
  }
944
1197
  `;
945
- async function handler$4(req, res) {
946
- try {
947
- const cartId = req.params.id;
948
- if (typeof cartId !== "string" || !cartId) {
949
- res.status(400).json({ error: "Missing cart id" });
950
- return;
951
- }
952
- const body = req.body ?? {};
953
- if (!body.skuId || !body.name || typeof body.quantity !== "number") {
954
- res.status(400).json({ error: "Missing required fields: skuId, name, quantity" });
955
- return;
956
- }
957
- const config = readCommerceConfigForResponse(res);
958
- if (!validateCommerceCartScope(req, res, config)) return;
959
- const { storeId, clientId, apiBaseUrl } = config;
960
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
961
- const headers = storefrontHeaders({
962
- storeId,
963
- clientId
964
- });
965
- await gqlRequest({
966
- endpoint,
967
- query: addLineItemBySkuIdMutation$1,
968
- variables: { input: buildAddLineItemBySkuIdInput(cartId, {
969
- skuId: body.skuId,
970
- name: body.name,
971
- quantity: body.quantity
972
- }) },
973
- headers
974
- });
975
- const hydrated = await gqlRequest({
976
- endpoint,
977
- query: getCartOrderQuery,
978
- variables: { id: cartId },
979
- headers
980
- });
981
- res.status(201).json({ cart: hydrated.orderById ?? null });
982
- } catch (error) {
983
- res.status(500).json({
984
- error: "Failed to add line item",
985
- message: error instanceof Error ? error.message : String(error)
986
- });
987
- }
1198
+ async function addLineItem(req, res) {
1199
+ const cartId = req.params.id;
1200
+ if (typeof cartId !== "string" || !cartId) throw new InvalidRequestError("Missing cart id");
1201
+ const body = req.body ?? {};
1202
+ if (!body.skuId || !body.name || typeof body.quantity !== "number") throw new InvalidRequestError("Missing required fields: skuId, name, quantity");
1203
+ const config = readCommerceConfigForResponse(res);
1204
+ assertCommerceCartScope(req, config);
1205
+ const { storeId, clientId, apiBaseUrl } = config;
1206
+ const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
1207
+ const headers = storefrontHeaders({
1208
+ storeId,
1209
+ clientId
1210
+ });
1211
+ await gqlRequest({
1212
+ endpoint,
1213
+ query: addLineItemBySkuIdMutation$1,
1214
+ variables: { input: buildAddLineItemBySkuIdInput(cartId, {
1215
+ skuId: body.skuId,
1216
+ name: body.name,
1217
+ quantity: body.quantity
1218
+ }) },
1219
+ headers
1220
+ });
1221
+ const hydrated = await gqlRequest({
1222
+ endpoint,
1223
+ query: getCartOrderQuery,
1224
+ variables: { id: cartId },
1225
+ headers
1226
+ });
1227
+ res.status(201).json({ cart: hydrated.orderById ?? null });
988
1228
  }
1229
+ var POST_default$1 = commerceRoute("Failed to add line item", addLineItem, { classifyUpstreamError: classifyCartUpstreamError });
989
1230
 
990
1231
  //#endregion
991
1232
  //#region src/server/api/commerce/cart/POST.ts
@@ -1003,156 +1244,100 @@ const addLineItemBySkuIdMutation = `
1003
1244
  }
1004
1245
  }
1005
1246
  `;
1006
- async function handler$5(req, res) {
1007
- try {
1008
- const body = req.body ?? {};
1009
- const initialItems = Array.isArray(body.lineItems) ? body.lineItems : [];
1010
- for (const item of initialItems) if (!item.skuId || !item.name || typeof item.quantity !== "number") {
1011
- res.status(400).json({ error: "Each lineItem must have skuId, a non-empty name, and a numeric quantity" });
1012
- return;
1013
- }
1014
- const config = readCommerceConfigForResponse(res);
1015
- if (!validateCommerceCartScope(req, res, config)) return;
1016
- const { storeId, channelId, clientId, apiBaseUrl, currencyCode } = config;
1017
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
1018
- const headers = storefrontHeaders({
1247
+ async function createCart(req, res) {
1248
+ const body = req.body ?? {};
1249
+ const initialItems = Array.isArray(body.lineItems) ? body.lineItems : [];
1250
+ for (const item of initialItems) if (!item.skuId || !item.name || typeof item.quantity !== "number") throw new InvalidRequestError("Each lineItem must have skuId, a non-empty name, and a numeric quantity");
1251
+ const config = readCommerceConfigForResponse(res);
1252
+ assertCommerceCartScope(req, config);
1253
+ const { storeId, channelId, clientId, apiBaseUrl, currencyCode } = config;
1254
+ const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
1255
+ const headers = storefrontHeaders({
1256
+ storeId,
1257
+ clientId
1258
+ });
1259
+ const newCartId = (await gqlRequest({
1260
+ endpoint,
1261
+ query: addCartOrderMutation,
1262
+ variables: { input: buildEmptyCartOrderInput({
1019
1263
  storeId,
1020
- clientId
1021
- });
1022
- const newCartId = (await gqlRequest({
1023
- endpoint,
1024
- query: addCartOrderMutation,
1025
- variables: { input: buildEmptyCartOrderInput({
1026
- storeId,
1027
- channelId,
1028
- owner: config.owner,
1029
- currencyCode: body.currencyCode ?? currencyCode
1030
- }) },
1031
- headers
1032
- })).addDraftOrder?.id;
1033
- if (!newCartId) {
1034
- res.status(500).json({ error: "Failed to create cart: missing id in mutation response" });
1035
- return;
1036
- }
1037
- for (const item of initialItems) await gqlRequest({
1038
- endpoint,
1039
- query: addLineItemBySkuIdMutation,
1040
- variables: { input: buildAddLineItemBySkuIdInput(newCartId, item) },
1041
- headers
1042
- });
1043
- const hydrated = await gqlRequest({
1044
- endpoint,
1045
- query: getCartOrderQuery,
1046
- variables: { id: newCartId },
1047
- headers
1048
- });
1049
- res.status(201).json({ cart: hydrated.orderById ?? null });
1050
- } catch (error) {
1051
- res.status(500).json({
1052
- error: "Failed to create cart",
1053
- message: error instanceof Error ? error.message : String(error)
1054
- });
1055
- }
1264
+ channelId,
1265
+ owner: config.owner,
1266
+ currencyCode: body.currencyCode ?? currencyCode
1267
+ }) },
1268
+ headers
1269
+ })).addDraftOrder?.id;
1270
+ if (!newCartId) throw new UpstreamError("addDraftOrder response did not include a cart id");
1271
+ for (const item of initialItems) await gqlRequest({
1272
+ endpoint,
1273
+ query: addLineItemBySkuIdMutation,
1274
+ variables: { input: buildAddLineItemBySkuIdInput(newCartId, item) },
1275
+ headers
1276
+ });
1277
+ const hydrated = await gqlRequest({
1278
+ endpoint,
1279
+ query: getCartOrderQuery,
1280
+ variables: { id: newCartId },
1281
+ headers
1282
+ });
1283
+ res.status(201).json({ cart: hydrated.orderById ?? null });
1056
1284
  }
1285
+ var POST_default$2 = commerceRoute("Failed to create cart", createCart);
1057
1286
 
1058
1287
  //#endregion
1059
1288
  //#region src/server/api/commerce/checkout/POST.ts
1060
- async function handler$6(req, res) {
1061
- try {
1062
- const configuration = commerceConfigurationForResponse(res);
1063
- const { draftOrderId, skuId, quantity, lineItemData, returnUrl, successUrl } = req.body ?? {};
1064
- if (lineItemData !== void 0) {
1065
- res.status(400).json({ error: "Non-catalog checkout must be created by the server." });
1066
- return;
1067
- }
1068
- if ([draftOrderId, skuId].filter(Boolean).length !== 1 || [draftOrderId, skuId].some((value) => value !== void 0 && (typeof value !== "string" || !value.trim()))) {
1069
- res.status(400).json({ error: "exactly one non-empty draftOrderId or skuId is required" });
1070
- return;
1071
- }
1072
- if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 1)) {
1073
- res.status(400).json({ error: "quantity must be a positive whole number" });
1074
- return;
1075
- }
1076
- if (req.headers?.["x-commerce-scope"] !== void 0) {
1077
- assertCommerceConnectionReady(configuration);
1078
- if (!validateCommerceCartScope(req, res, configuration.read())) return;
1079
- }
1080
- const validateReturnUrls = res.locals.commerceCheckoutReturnUrlValidator;
1081
- if (!validateReturnUrls) {
1082
- res.status(503).json({ error: "Checkout return destinations are not configured." });
1083
- return;
1084
- }
1085
- const destinations = validateReturnUrls(returnUrl, successUrl);
1086
- if (!destinations) {
1087
- res.status(400).json({ error: "Checkout return destinations are not allowed." });
1088
- return;
1089
- }
1090
- const session = await createCheckoutSession({
1091
- draftOrderId,
1092
- skuId,
1093
- quantity,
1094
- ...destinations
1095
- }, configuration);
1096
- res.status(200).json(session);
1097
- } catch (error) {
1098
- res.status(500).json({
1099
- error: "Failed to create checkout session",
1100
- message: error instanceof Error ? error.message : String(error)
1101
- });
1102
- }
1289
+ async function createCheckout(req, res) {
1290
+ const configuration = commerceConfigurationForResponse(res);
1291
+ const { draftOrderId, skuId, quantity, lineItemData, returnUrl, successUrl } = req.body ?? {};
1292
+ if (lineItemData !== void 0) throw new InvalidRequestError("Non-catalog checkout must be created by the server.");
1293
+ if ([draftOrderId, skuId].filter(Boolean).length !== 1 || [draftOrderId, skuId].some((value) => value !== void 0 && (typeof value !== "string" || !value.trim()))) throw new InvalidRequestError("exactly one non-empty draftOrderId or skuId is required");
1294
+ if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 1)) throw new InvalidRequestError("quantity must be a positive whole number");
1295
+ if (req.headers?.["x-commerce-scope"] !== void 0) assertCommerceCartScope(req, readReadyCommerceConfig(configuration));
1296
+ const validateReturnUrls = res.locals.commerceCheckoutReturnUrlValidator;
1297
+ if (!validateReturnUrls) throw new CommerceNotConfiguredError("Checkout return URLs are not configured on the router.", { publicMessage: "Checkout return destinations are not configured." });
1298
+ const destinations = validateReturnUrls(returnUrl, successUrl);
1299
+ if (!destinations) throw new InvalidRequestError("Checkout return destinations are not allowed.");
1300
+ const session = await createCheckoutSession({
1301
+ draftOrderId,
1302
+ skuId,
1303
+ quantity,
1304
+ ...destinations
1305
+ }, configuration);
1306
+ res.status(200).json(session);
1103
1307
  }
1308
+ var POST_default$3 = commerceRoute("Failed to create checkout session", createCheckout);
1104
1309
 
1105
1310
  //#endregion
1106
1311
  //#region src/server/api/commerce/config/GET.ts
1107
- async function handler$7(_req, res) {
1312
+ async function readPublicConfig(_req, res) {
1108
1313
  res.setHeader("Cache-Control", "no-store");
1109
- try {
1110
- const configuration = commerceConfigurationForResponse(res);
1111
- const state = configuration.readConnectionState?.();
1112
- if (state === "unbound" || state === "connecting") {
1113
- res.json({ state });
1114
- return;
1115
- }
1116
- if (state !== void 0 && state !== "ready") throw new Error("Invalid Commerce connection state.");
1117
- const config = configuration.read();
1118
- res.json({
1119
- ...state === "ready" ? { state } : {},
1120
- cartScope: getCommerceCartScope(config),
1121
- currencyCode: config.currencyCode
1122
- });
1123
- } catch (cause) {
1124
- res.status(503).json({
1125
- error: "Commerce configuration is unavailable. Complete the store connection before continuing.",
1126
- message: cause instanceof Error ? cause.message : "Invalid Commerce configuration."
1127
- });
1314
+ const configuration = commerceConfigurationForResponse(res);
1315
+ const state = readCommerceConnectionState(configuration);
1316
+ if (state === "unbound" || state === "connecting") {
1317
+ res.json({ state });
1318
+ return;
1128
1319
  }
1320
+ const config = readReadyCommerceConfig(configuration);
1321
+ res.json({
1322
+ ...state === "ready" ? { state } : {},
1323
+ cartScope: getCommerceCartScope(config),
1324
+ currencyCode: config.currencyCode
1325
+ });
1129
1326
  }
1327
+ var GET_default$1 = commerceRoute("Failed to load Commerce configuration", readPublicConfig);
1130
1328
 
1131
1329
  //#endregion
1132
1330
  //#region src/server/api/commerce/order-status/GET.ts
1133
- async function handler$8(req, res) {
1134
- try {
1135
- const { orderId } = req.query;
1136
- if (!orderId || typeof orderId !== "string") {
1137
- res.status(400).json({
1138
- success: false,
1139
- error: "missing or invalid orderId query parameter"
1140
- });
1141
- return;
1142
- }
1143
- const order = await getOrderStatus(orderId, commerceConfigurationForResponse(res));
1144
- res.status(200).json({
1145
- success: true,
1146
- order
1147
- });
1148
- } catch (error) {
1149
- res.status(500).json({
1150
- success: false,
1151
- error: "Failed to get order status",
1152
- message: error instanceof Error ? error.message : String(error)
1153
- });
1154
- }
1331
+ async function readOrderStatus(req, res) {
1332
+ const { orderId } = req.query;
1333
+ if (typeof orderId !== "string") throw new InvalidOrderIdError();
1334
+ const order = await getOrderStatus(orderId, commerceConfigurationForResponse(res));
1335
+ res.status(200).json({
1336
+ success: true,
1337
+ order
1338
+ });
1155
1339
  }
1340
+ var GET_default$2 = commerceRoute("Failed to get order status", readOrderStatus, { failureFields: { success: false } });
1156
1341
 
1157
1342
  //#endregion
1158
1343
  //#region src/lib/commerce/catalog-subgraph.ts
@@ -1250,6 +1435,9 @@ const skuGroupQuery = `
1250
1435
  mediaObjects(first: 25) {
1251
1436
  edges { node { url type } }
1252
1437
  }
1438
+ attributeValues(first: 50) {
1439
+ edges { node { name } }
1440
+ }
1253
1441
  inventoryCounts {
1254
1442
  edges {
1255
1443
  node {
@@ -1275,45 +1463,33 @@ function asNumber$1(value) {
1275
1463
  const n = Number(value);
1276
1464
  return Number.isFinite(n) ? n : void 0;
1277
1465
  }
1278
- async function handler$9(req, res) {
1279
- try {
1280
- const productId = req.params.id;
1281
- if (typeof productId !== "string" || !productId) {
1282
- res.status(400).json({ error: "Missing product id" });
1283
- return;
1284
- }
1285
- const config = readCommerceConfigForResponse(res);
1286
- if (!validateCommerceCartScope(req, res, config)) return;
1287
- const { storeId, clientId, apiBaseUrl } = config;
1288
- const variables = buildSkuGroupVariables({
1289
- productId,
1290
- selectedAttributeValues: asStringArray$1(req.query.attributeValues),
1291
- skuGroupFirst: asNumber$1(req.query.skuGroupFirst)
1292
- });
1293
- const data = await gqlRequest({
1294
- endpoint: catalogStorefrontEndpoint({
1295
- storeId,
1296
- apiBaseUrl
1297
- }),
1298
- query: skuGroupQuery,
1299
- variables,
1300
- headers: storefrontHeaders({
1301
- storeId,
1302
- clientId
1303
- })
1304
- });
1305
- if (!data.skuGroup || !data.activeSkuGroups?.edges?.some((edge) => edge?.node?.id === productId)) {
1306
- res.status(404).json({ error: "Product not found" });
1307
- return;
1308
- }
1309
- res.json({ skuGroup: data.skuGroup });
1310
- } catch (error) {
1311
- res.status(500).json({
1312
- error: "Failed to load product",
1313
- message: error instanceof Error ? error.message : String(error)
1314
- });
1315
- }
1466
+ async function readProduct(req, res) {
1467
+ const productId = req.params.id;
1468
+ if (typeof productId !== "string" || !productId) throw new InvalidRequestError("Missing product id");
1469
+ const config = readCommerceConfigForResponse(res);
1470
+ assertCommerceCartScope(req, config);
1471
+ const { storeId, clientId, apiBaseUrl } = config;
1472
+ const variables = buildSkuGroupVariables({
1473
+ productId,
1474
+ selectedAttributeValues: asStringArray$1(req.query.attributeValues),
1475
+ skuGroupFirst: asNumber$1(req.query.skuGroupFirst)
1476
+ });
1477
+ const data = await gqlRequest({
1478
+ endpoint: catalogStorefrontEndpoint({
1479
+ storeId,
1480
+ apiBaseUrl
1481
+ }),
1482
+ query: skuGroupQuery,
1483
+ variables,
1484
+ headers: storefrontHeaders({
1485
+ storeId,
1486
+ clientId
1487
+ })
1488
+ });
1489
+ if (!data.skuGroup || !data.activeSkuGroups?.edges?.some((edge) => edge?.node?.id === productId)) throw new NotFoundError("Product not found");
1490
+ res.json({ skuGroup: data.skuGroup });
1316
1491
  }
1492
+ var GET_default$3 = commerceRoute("Failed to load product", readProduct);
1317
1493
 
1318
1494
  //#endregion
1319
1495
  //#region src/server/api/commerce/products/GET.ts
@@ -1414,38 +1590,32 @@ function asNumber(value) {
1414
1590
  const n = Number(value);
1415
1591
  return Number.isFinite(n) ? n : void 0;
1416
1592
  }
1417
- async function handler$10(req, res) {
1418
- try {
1419
- const config = readCommerceConfigForResponse(res);
1420
- if (!validateCommerceCartScope(req, res, config)) return;
1421
- const { storeId, clientId, apiBaseUrl } = config;
1422
- const variables = buildSkuGroupsVariables({
1423
- first: asNumber(req.query.first) ?? 24,
1424
- after: typeof req.query.after === "string" ? req.query.after : void 0,
1425
- searchQuery: typeof req.query.searchQuery === "string" ? req.query.searchQuery : void 0,
1426
- productIds: asStringArray(req.query.productIds),
1427
- categoryIds: asStringArray(req.query.categoryIds)
1428
- });
1429
- const data = await gqlRequest({
1430
- endpoint: catalogStorefrontEndpoint({
1431
- storeId,
1432
- apiBaseUrl
1433
- }),
1434
- query: skuGroupsQuery,
1435
- variables,
1436
- headers: storefrontHeaders({
1437
- storeId,
1438
- clientId
1439
- })
1440
- });
1441
- res.json(data);
1442
- } catch (error) {
1443
- res.status(500).json({
1444
- error: "Failed to load products",
1445
- message: error instanceof Error ? error.message : String(error)
1446
- });
1447
- }
1593
+ async function listProducts(req, res) {
1594
+ const config = readCommerceConfigForResponse(res);
1595
+ assertCommerceCartScope(req, config);
1596
+ const { storeId, clientId, apiBaseUrl } = config;
1597
+ const variables = buildSkuGroupsVariables({
1598
+ first: asNumber(req.query.first) ?? 24,
1599
+ after: typeof req.query.after === "string" ? req.query.after : void 0,
1600
+ searchQuery: typeof req.query.searchQuery === "string" ? req.query.searchQuery : void 0,
1601
+ productIds: asStringArray(req.query.productIds),
1602
+ categoryIds: asStringArray(req.query.categoryIds)
1603
+ });
1604
+ const data = await gqlRequest({
1605
+ endpoint: catalogStorefrontEndpoint({
1606
+ storeId,
1607
+ apiBaseUrl
1608
+ }),
1609
+ query: skuGroupsQuery,
1610
+ variables,
1611
+ headers: storefrontHeaders({
1612
+ storeId,
1613
+ clientId
1614
+ })
1615
+ });
1616
+ res.json(data);
1448
1617
  }
1618
+ var GET_default$4 = commerceRoute("Failed to load products", listProducts);
1449
1619
 
1450
1620
  //#endregion
1451
1621
  //#region src/server/api/commerce/skus/[id]/GET.ts
@@ -1505,36 +1675,27 @@ const skuQuery = `
1505
1675
  }
1506
1676
  }
1507
1677
  `;
1508
- async function handler$11(req, res) {
1509
- try {
1510
- const skuId = req.params.id;
1511
- if (typeof skuId !== "string" || !skuId) {
1512
- res.status(400).json({ error: "Missing sku id" });
1513
- return;
1514
- }
1515
- const config = readCommerceConfigForResponse(res);
1516
- if (!validateCommerceCartScope(req, res, config)) return;
1517
- const { storeId, clientId, apiBaseUrl } = config;
1518
- const data = await gqlRequest({
1519
- endpoint: catalogStorefrontEndpoint({
1520
- storeId,
1521
- apiBaseUrl
1522
- }),
1523
- query: skuQuery,
1524
- variables: { id: skuId },
1525
- headers: storefrontHeaders({
1526
- storeId,
1527
- clientId
1528
- })
1529
- });
1530
- res.json(data);
1531
- } catch (error) {
1532
- res.status(500).json({
1533
- error: "Failed to load sku",
1534
- message: error instanceof Error ? error.message : String(error)
1535
- });
1536
- }
1678
+ async function readSku(req, res) {
1679
+ const skuId = req.params.id;
1680
+ if (typeof skuId !== "string" || !skuId) throw new InvalidRequestError("Missing sku id");
1681
+ const config = readCommerceConfigForResponse(res);
1682
+ assertCommerceCartScope(req, config);
1683
+ const { storeId, clientId, apiBaseUrl } = config;
1684
+ const data = await gqlRequest({
1685
+ endpoint: catalogStorefrontEndpoint({
1686
+ storeId,
1687
+ apiBaseUrl
1688
+ }),
1689
+ query: skuQuery,
1690
+ variables: { id: skuId },
1691
+ headers: storefrontHeaders({
1692
+ storeId,
1693
+ clientId
1694
+ })
1695
+ });
1696
+ res.json(data);
1537
1697
  }
1698
+ var GET_default$5 = commerceRoute("Failed to load sku", readSku);
1538
1699
 
1539
1700
  //#endregion
1540
1701
  //#region src/router.ts
@@ -1543,32 +1704,39 @@ function createCommerceRouter(options = {}) {
1543
1704
  const configuration = options.configuration ?? createRuntimeCommerceConfiguration();
1544
1705
  const catalogEnabled = options.features?.catalog ?? true;
1545
1706
  const paymentsEnabled = options.features?.payments ?? true;
1707
+ const logger = options.logger ?? consoleCommerceLogger;
1546
1708
  const validateCheckoutReturnUrls = options.checkoutReturnUrls ? createCheckoutReturnUrlValidator(options.checkoutReturnUrls) : void 0;
1547
1709
  router.use((_req, res, next) => {
1710
+ const observability = {
1711
+ commerceLogger: logger,
1712
+ commerceRequestIdResolver: options.getRequestId
1713
+ };
1714
+ Object.assign(res.locals, observability);
1548
1715
  res.locals.commerceConfiguration = configuration;
1549
1716
  res.locals.commerceCheckoutReturnUrlValidator = validateCheckoutReturnUrls;
1550
1717
  next();
1551
1718
  });
1552
1719
  if (catalogEnabled) {
1553
- router.get("/config", handler$7);
1554
- router.get("/products", handler$10);
1555
- router.get("/products/:id", handler$9);
1556
- router.get("/skus/:id", handler$11);
1557
- router.post("/cart", handler$5);
1558
- router.get("/cart/:id", handler$1);
1559
- router.post("/cart/:id/items", handler$4);
1560
- router.patch("/cart/:id/items/:itemId", handler$3);
1561
- router.delete("/cart/:id/items/:itemId", handler$2);
1562
- router.post("/cart/:id/discounts", handler);
1720
+ router.get("/config", GET_default$1);
1721
+ router.get("/products", GET_default$4);
1722
+ router.get("/products/:id", GET_default$3);
1723
+ router.get("/skus/:id", GET_default$5);
1724
+ router.post("/cart", POST_default$2);
1725
+ router.get("/cart/:id", GET_default);
1726
+ router.post("/cart/:id/items", POST_default$1);
1727
+ router.patch("/cart/:id/items/:itemId", PATCH_default);
1728
+ router.delete("/cart/:id/items/:itemId", DELETE_default);
1729
+ router.post("/cart/:id/discounts", POST_default);
1563
1730
  }
1564
1731
  if (paymentsEnabled) {
1565
- router.post("/checkout", handler$6);
1566
- router.get("/order-status", handler$8);
1732
+ router.post("/checkout", POST_default$3);
1733
+ router.get("/order-status", GET_default$2);
1567
1734
  }
1568
1735
  return router;
1569
1736
  }
1570
- function createCommerceCatalogRouter(configuration) {
1737
+ function createCommerceCatalogRouter(configuration, observability = {}) {
1571
1738
  return createCommerceRouter({
1739
+ ...observability,
1572
1740
  configuration,
1573
1741
  features: {
1574
1742
  catalog: true,
@@ -1576,8 +1744,9 @@ function createCommerceCatalogRouter(configuration) {
1576
1744
  }
1577
1745
  });
1578
1746
  }
1579
- function createGoDaddyPaymentsRouter(configuration, checkoutReturnUrls) {
1747
+ function createGoDaddyPaymentsRouter(configuration, checkoutReturnUrls, observability = {}) {
1580
1748
  return createCommerceRouter({
1749
+ ...observability,
1581
1750
  configuration,
1582
1751
  checkoutReturnUrls,
1583
1752
  features: {
@@ -1588,4 +1757,4 @@ function createGoDaddyPaymentsRouter(configuration, checkoutReturnUrls) {
1588
1757
  }
1589
1758
 
1590
1759
  //#endregion
1591
- export { createCheckoutSession, createCommerceCatalogRouter, createCommerceRouter, createGoDaddyPaymentsRouter, createRuntimeCommerceConfiguration, getOrderStatus, parseCommerceCheckoutConfiguration, readCommerceConfig };
1760
+ export { CommerceError, CommerceNotConfiguredError, InvalidOrderIdError, InvalidRequestError, NotFoundError, ORDER_STATUS_UNKNOWN, OrderNotFoundError, ScopeMismatchError, UpstreamError, createCheckoutSession, createCommerceCatalogRouter, createCommerceRouter, createGoDaddyPaymentsRouter, createRuntimeCommerceConfiguration, getOrderStatus, parseCommerceCheckoutConfiguration, readCommerceConfig };