@godaddy/gd-commerce-server 0.1.1 → 0.3.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,13 +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
- return commerceConfigurationForResponse(res).read();
175
+ return readReadyCommerceConfig(commerceConfigurationForResponse(res));
176
+ }
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
+ });
83
213
  }
84
214
 
85
215
  //#endregion
@@ -121,22 +251,52 @@ function checkoutGraphqlEndpoint({ apiBaseUrl }) {
121
251
  function authorizationHeaders({ accessToken }) {
122
252
  return { Authorization: `Bearer ${accessToken}` };
123
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
+ }
124
268
  async function getOAuthAccessToken({ clientId, clientSecret, apiBaseUrl, scope = "commerce.product:read", fetch: fetchImplementation }) {
125
- if (!clientId || !clientSecret) throw new Error("clientId and clientSecret are required");
126
- const requestFetch = fetchImplementation ?? fetch;
269
+ if (!clientId || !clientSecret) throw new CommerceNotConfiguredError("clientId and clientSecret are required");
127
270
  const body = new URLSearchParams();
128
271
  body.append("grant_type", "client_credentials");
129
272
  body.append("client_id", clientId);
130
273
  body.append("client_secret", clientSecret);
131
274
  body.append("scope", scope);
132
- 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, {
133
284
  method: "POST",
134
285
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
135
286
  body: body.toString(),
136
287
  cache: "no-store"
137
- });
138
- if (!response.ok) throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`);
139
- 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);
140
300
  }
141
301
  function buildBuyNowCheckoutInput({ storeId, skuId, quantity = 1, returnUrl, successUrl, channelId }, overrides = {}) {
142
302
  return applyCheckoutDefaults({
@@ -174,10 +334,18 @@ function buildNonCatalogCheckoutInput({ storeId, lineItemData, quantity = 1, ret
174
334
 
175
335
  //#endregion
176
336
  //#region src/lib/commerce/gql.ts
177
- 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 {
178
339
  constructor(errors, status) {
179
340
  const errorMessage = errors.length === 1 ? `${errors[0]?.message || "Unknown error"}` : errors.map((error) => error.message).filter(Boolean).join("; ");
180
- 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
+ });
181
349
  this.errors = errors;
182
350
  this.status = status;
183
351
  this.name = "GraphQLErrorWithCodes";
@@ -202,7 +370,7 @@ async function gqlRequest({ endpoint, query, variables, headers: headersInit, fe
202
370
  const requestHeaders = new Headers(headersInit);
203
371
  requestHeaders.set("Accept", "application/json");
204
372
  requestHeaders.set("Content-Type", "application/json");
205
- const response = await (fetchImplementation ?? fetch)(endpoint, {
373
+ const response = await fetchUpstream(endpoint, {
206
374
  method: "POST",
207
375
  headers: requestHeaders,
208
376
  body: JSON.stringify({
@@ -210,7 +378,10 @@ async function gqlRequest({ endpoint, query, variables, headers: headersInit, fe
210
378
  variables: variables ?? {}
211
379
  }),
212
380
  cache: "no-store"
213
- });
381
+ }, {
382
+ operation: "GraphQL request",
383
+ details: { endpoint }
384
+ }, fetchImplementation);
214
385
  let result;
215
386
  try {
216
387
  result = await response.json();
@@ -229,7 +400,7 @@ async function gqlRequest({ endpoint, query, variables, headers: headersInit, fe
229
400
  message: `GraphQL request failed: ${response.status} ${response.statusText}`,
230
401
  status: response.status
231
402
  }], response.status);
232
- 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 } });
233
404
  return result.data;
234
405
  }
235
406
 
@@ -324,14 +495,14 @@ function promotionCodesEnabled(configuration) {
324
495
  }
325
496
  async function createCheckoutSession(params, configuration = createRuntimeCommerceConfiguration()) {
326
497
  const { draftOrderId, skuId, quantity, lineItemData, returnUrl, successUrl } = params;
327
- 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");
328
499
  if ([
329
500
  draftOrderId,
330
501
  skuId,
331
502
  lineItemData
332
- ].filter(Boolean).length !== 1) throw new Error("createCheckoutSession: exactly one of draftOrderId, skuId, or lineItemData is required");
333
- const { storeId, channelId, clientId, clientSecret, apiBaseUrl, sourceApp, owner, currencyCode: configCurrencyCode } = configuration.read();
334
- 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);
335
506
  const enablePromotionCodes = promotionCodesEnabled(checkoutConfiguration);
336
507
  const catalogShippingEnabled = lineItemData === void 0 && checkoutConfiguration.enableShipping;
337
508
  const token = await getOAuthAccessToken({
@@ -394,16 +565,20 @@ async function createCheckoutSession(params, configuration = createRuntimeCommer
394
565
  headers: authorizationHeaders({ accessToken: token.access_token })
395
566
  });
396
567
  } catch (error) {
397
- 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
+ });
398
573
  }
399
574
  const session = result.createCheckoutSession;
400
- if (!session?.url || !session.id) throw new Error("Checkout session was not created");
401
- 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"}`);
402
577
  const expectedShipping = lineItemData === void 0 && checkoutConfiguration.enableShipping;
403
- if (lineItemData === void 0 && enablePromotionCodes && session.enablePromotionCodes !== true) throw new Error("Checkout session did not enable configured promotion codes");
404
- if (checkoutConfiguration.enableTaxCollection && session.enableTaxCollection !== true) throw new Error("Checkout session did not enable configured tax collection");
405
- if (expectedShipping && (session.enableShipping !== true || session.enableShippingAddressCollection !== true)) throw new Error("Checkout session did not enable configured shipping and address collection");
406
- 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.");
407
582
  return {
408
583
  url: session.url,
409
584
  id: session.id,
@@ -418,31 +593,73 @@ async function createCheckoutSession(params, configuration = createRuntimeCommer
418
593
 
419
594
  //#endregion
420
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
+ }
421
621
  async function getOrderStatus(orderId, configuration = createRuntimeCommerceConfiguration()) {
422
- if (typeof orderId !== "string" || !orderId.trim() || orderId === "." || orderId === "..") throw new Error("getOrderStatus: a valid orderId is required");
423
- 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);
424
624
  const token = await getOAuthAccessToken({
425
625
  clientId,
426
626
  clientSecret,
427
627
  apiBaseUrl,
428
628
  scope: "commerce.order:read"
429
629
  });
430
- 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, {
431
636
  method: "GET",
432
637
  headers: {
433
638
  ...authorizationHeaders({ accessToken: token.access_token }),
434
639
  Accept: "application/json"
435
640
  },
436
641
  cache: "no-store"
437
- });
438
- if (!response.ok) throw new Error(`Failed to load order: upstream returned ${response.status}`);
439
- const order = (await response.json())?.order;
440
- if (!order?.id || order.id !== orderId) throw new Error("Order lookup did not return the requested order");
441
- 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();
442
659
  const total = order.totals?.total;
443
660
  return {
444
661
  id: order.id,
445
- status: order.statuses?.paymentStatus ?? "unknown",
662
+ status: order.statuses?.paymentStatus ?? ORDER_STATUS_UNKNOWN,
446
663
  amount: total?.value ?? 0,
447
664
  currency: total?.currencyCode ?? currencyCode,
448
665
  createdAt: order.createdAt,
@@ -496,6 +713,75 @@ function createCheckoutReturnUrlValidator(policy) {
496
713
  };
497
714
  }
498
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
+
499
785
  //#endregion
500
786
  //#region src/lib/commerce/cart-scope.ts
501
787
  /** Public cache/storage scope, not an authorization credential. */
@@ -508,11 +794,10 @@ function getCommerceCartScope(config) {
508
794
  ])).digest("hex").slice(0, 32);
509
795
  }
510
796
  /** Existing custom clients may omit the header; managed components always send it. */
511
- function validateCommerceCartScope(req, res, config) {
797
+ function assertCommerceCartScope(req, config) {
512
798
  const suppliedScope = req.headers?.["x-commerce-scope"];
513
- if (suppliedScope === void 0 || suppliedScope === getCommerceCartScope(config)) return true;
514
- res.status(409).json({ error: "The connected store changed. Reload the page before continuing." });
515
- return false;
799
+ if (suppliedScope === void 0 || suppliedScope === getCommerceCartScope(config)) return;
800
+ throw new ScopeMismatchError();
516
801
  }
517
802
 
518
803
  //#endregion
@@ -722,61 +1007,7 @@ const getCartOrderQuery = `
722
1007
  `;
723
1008
 
724
1009
  //#endregion
725
- //#region src/server/api/commerce/cart/[id]/discounts/POST.ts
726
- const applyDiscountCodesMutation = `
727
- mutation ApplyDiscountCodes($input: ApplyDiscountCodesInput!) {
728
- applyDiscountCodes(input: $input) {
729
- id
730
- }
731
- }
732
- `;
733
- async function handler(req, res) {
734
- try {
735
- const cartId = req.params.id;
736
- if (typeof cartId !== "string" || !cartId) {
737
- res.status(400).json({ error: "Missing cart id" });
738
- return;
739
- }
740
- const body = req.body ?? {};
741
- const codes = Array.isArray(body.discountCodes) ? body.discountCodes.filter((code) => typeof code === "string" && code.length > 0) : [];
742
- if (codes.length === 0) {
743
- res.status(400).json({ error: "discountCodes must be a non-empty string array" });
744
- return;
745
- }
746
- const config = readCommerceConfigForResponse(res);
747
- if (!validateCommerceCartScope(req, res, config)) return;
748
- const { storeId, clientId, apiBaseUrl } = config;
749
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
750
- const headers = storefrontHeaders({
751
- storeId,
752
- clientId
753
- });
754
- await gqlRequest({
755
- endpoint,
756
- query: applyDiscountCodesMutation,
757
- variables: { input: {
758
- orderId: cartId,
759
- discountCodes: codes
760
- } },
761
- headers
762
- });
763
- const hydrated = await gqlRequest({
764
- endpoint,
765
- query: getCartOrderQuery,
766
- variables: { id: cartId },
767
- headers
768
- });
769
- res.json({ cart: hydrated.orderById ?? null });
770
- } catch (error) {
771
- res.status(500).json({
772
- error: "Failed to apply discount codes",
773
- message: error instanceof Error ? error.message : String(error)
774
- });
775
- }
776
- }
777
-
778
- //#endregion
779
- //#region src/server/api/commerce/cart/[id]/GET.ts
1010
+ //#region src/lib/commerce/upstream-errors.ts
780
1011
  function isCartNotFoundError(error) {
781
1012
  if (!(error instanceof GraphQLErrorWithCodes)) return false;
782
1013
  const isFailure = (status) => status !== void 0 && status >= 400 && status !== 404 && status !== 410;
@@ -789,17 +1020,68 @@ function isCartNotFoundError(error) {
789
1020
  return /\b(?:cart|(?:draft[ -])?order)\s+(?:(?:is|was|has)\s+)?(?:not found|expired)\b/i.test(message ?? "");
790
1021
  });
791
1022
  }
792
- 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;
793
1083
  try {
794
- const cartId = req.params.id;
795
- if (typeof cartId !== "string" || !cartId) {
796
- res.status(400).json({ error: "Missing cart id" });
797
- return;
798
- }
799
- const config = readCommerceConfigForResponse(res);
800
- if (!validateCommerceCartScope(req, res, config)) return;
801
- const { storeId, clientId, apiBaseUrl } = config;
802
- const data = await gqlRequest({
1084
+ data = await gqlRequest({
803
1085
  endpoint: orderStorefrontEndpoint({ apiBaseUrl }),
804
1086
  query: getCartOrderQuery,
805
1087
  variables: { id: cartId },
@@ -808,18 +1090,16 @@ async function handler$1(req, res) {
808
1090
  clientId
809
1091
  })
810
1092
  });
811
- res.json({ cart: data.orderById ?? null });
812
1093
  } catch (error) {
813
1094
  if (isCartNotFoundError(error)) {
814
1095
  res.status(200).json({ cart: null });
815
1096
  return;
816
1097
  }
817
- res.status(500).json({
818
- error: "Failed to load cart",
819
- message: error instanceof Error ? error.message : String(error)
820
- });
1098
+ throw error;
821
1099
  }
1100
+ res.json({ cart: data.orderById ?? null });
822
1101
  }
1102
+ var GET_default = commerceRoute("Failed to load cart", readCart, { classifyUpstreamError: classifyCartUpstreamError });
823
1103
 
824
1104
  //#endregion
825
1105
  //#region src/server/api/commerce/cart/[id]/items/[itemId]/DELETE.ts
@@ -828,45 +1108,36 @@ const deleteLineItemByIdMutation = `
828
1108
  deleteLineItemById(id: $id, orderId: $orderId)
829
1109
  }
830
1110
  `;
831
- async function handler$2(req, res) {
832
- try {
833
- const cartId = req.params.id;
834
- const itemId = req.params.itemId;
835
- if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) {
836
- res.status(400).json({ error: "Missing cart id or item id" });
837
- return;
838
- }
839
- const config = readCommerceConfigForResponse(res);
840
- if (!validateCommerceCartScope(req, res, config)) return;
841
- const { storeId, clientId, apiBaseUrl } = config;
842
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
843
- const headers = storefrontHeaders({
844
- storeId,
845
- clientId
846
- });
847
- await gqlRequest({
848
- endpoint,
849
- query: deleteLineItemByIdMutation,
850
- variables: {
851
- id: itemId,
852
- orderId: cartId
853
- },
854
- headers
855
- });
856
- const hydrated = await gqlRequest({
857
- endpoint,
858
- query: getCartOrderQuery,
859
- variables: { id: cartId },
860
- headers
861
- });
862
- res.json({ cart: hydrated.orderById ?? null });
863
- } catch (error) {
864
- res.status(500).json({
865
- error: "Failed to delete line item",
866
- message: error instanceof Error ? error.message : String(error)
867
- });
868
- }
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 });
869
1139
  }
1140
+ var DELETE_default = commerceRoute("Failed to delete line item", deleteLineItem, { classifyUpstreamError: classifyCartUpstreamError });
870
1141
 
871
1142
  //#endregion
872
1143
  //#region src/server/api/commerce/cart/[id]/items/[itemId]/PATCH.ts
@@ -877,52 +1148,43 @@ const updateLineItemByIdMutation = `
877
1148
  }
878
1149
  }
879
1150
  `;
880
- async function handler$3(req, res) {
881
- try {
882
- const cartId = req.params.id;
883
- const itemId = req.params.itemId;
884
- if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) {
885
- res.status(400).json({ error: "Missing cart id or item id" });
886
- return;
887
- }
888
- const body = req.body ?? {};
889
- const config = readCommerceConfigForResponse(res);
890
- if (!validateCommerceCartScope(req, res, config)) return;
891
- const { storeId, clientId, apiBaseUrl } = config;
892
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
893
- const headers = storefrontHeaders({
894
- storeId,
895
- clientId
896
- });
897
- await gqlRequest({
898
- endpoint,
899
- query: updateLineItemByIdMutation,
900
- variables: { input: {
901
- id: itemId,
902
- orderId: cartId,
903
- name: body.name,
904
- quantity: body.quantity,
905
- fulfillmentMode: body.fulfillmentMode,
906
- status: body.status,
907
- type: body.type,
908
- details: body.details
909
- } },
910
- headers
911
- });
912
- const hydrated = await gqlRequest({
913
- endpoint,
914
- query: getCartOrderQuery,
915
- variables: { id: cartId },
916
- headers
917
- });
918
- res.json({ cart: hydrated.orderById ?? null });
919
- } catch (error) {
920
- res.status(500).json({
921
- error: "Failed to update line item",
922
- message: error instanceof Error ? error.message : String(error)
923
- });
924
- }
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 });
925
1186
  }
1187
+ var PATCH_default = commerceRoute("Failed to update line item", updateLineItem, { classifyUpstreamError: classifyCartUpstreamError });
926
1188
 
927
1189
  //#endregion
928
1190
  //#region src/server/api/commerce/cart/[id]/items/POST.ts
@@ -933,50 +1195,38 @@ const addLineItemBySkuIdMutation$1 = `
933
1195
  }
934
1196
  }
935
1197
  `;
936
- async function handler$4(req, res) {
937
- try {
938
- const cartId = req.params.id;
939
- if (typeof cartId !== "string" || !cartId) {
940
- res.status(400).json({ error: "Missing cart id" });
941
- return;
942
- }
943
- const body = req.body ?? {};
944
- if (!body.skuId || !body.name || typeof body.quantity !== "number") {
945
- res.status(400).json({ error: "Missing required fields: skuId, name, quantity" });
946
- return;
947
- }
948
- const config = readCommerceConfigForResponse(res);
949
- if (!validateCommerceCartScope(req, res, config)) return;
950
- const { storeId, clientId, apiBaseUrl } = config;
951
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
952
- const headers = storefrontHeaders({
953
- storeId,
954
- clientId
955
- });
956
- await gqlRequest({
957
- endpoint,
958
- query: addLineItemBySkuIdMutation$1,
959
- variables: { input: buildAddLineItemBySkuIdInput(cartId, {
960
- skuId: body.skuId,
961
- name: body.name,
962
- quantity: body.quantity
963
- }) },
964
- headers
965
- });
966
- const hydrated = await gqlRequest({
967
- endpoint,
968
- query: getCartOrderQuery,
969
- variables: { id: cartId },
970
- headers
971
- });
972
- res.status(201).json({ cart: hydrated.orderById ?? null });
973
- } catch (error) {
974
- res.status(500).json({
975
- error: "Failed to add line item",
976
- message: error instanceof Error ? error.message : String(error)
977
- });
978
- }
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 });
979
1228
  }
1229
+ var POST_default$1 = commerceRoute("Failed to add line item", addLineItem, { classifyUpstreamError: classifyCartUpstreamError });
980
1230
 
981
1231
  //#endregion
982
1232
  //#region src/server/api/commerce/cart/POST.ts
@@ -994,147 +1244,100 @@ const addLineItemBySkuIdMutation = `
994
1244
  }
995
1245
  }
996
1246
  `;
997
- async function handler$5(req, res) {
998
- try {
999
- const body = req.body ?? {};
1000
- const initialItems = Array.isArray(body.lineItems) ? body.lineItems : [];
1001
- for (const item of initialItems) if (!item.skuId || !item.name || typeof item.quantity !== "number") {
1002
- res.status(400).json({ error: "Each lineItem must have skuId, a non-empty name, and a numeric quantity" });
1003
- return;
1004
- }
1005
- const config = readCommerceConfigForResponse(res);
1006
- if (!validateCommerceCartScope(req, res, config)) return;
1007
- const { storeId, channelId, clientId, apiBaseUrl, currencyCode } = config;
1008
- const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
1009
- 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({
1010
1263
  storeId,
1011
- clientId
1012
- });
1013
- const newCartId = (await gqlRequest({
1014
- endpoint,
1015
- query: addCartOrderMutation,
1016
- variables: { input: buildEmptyCartOrderInput({
1017
- storeId,
1018
- channelId,
1019
- owner: config.owner,
1020
- currencyCode: body.currencyCode ?? currencyCode
1021
- }) },
1022
- headers
1023
- })).addDraftOrder?.id;
1024
- if (!newCartId) {
1025
- res.status(500).json({ error: "Failed to create cart: missing id in mutation response" });
1026
- return;
1027
- }
1028
- for (const item of initialItems) await gqlRequest({
1029
- endpoint,
1030
- query: addLineItemBySkuIdMutation,
1031
- variables: { input: buildAddLineItemBySkuIdInput(newCartId, item) },
1032
- headers
1033
- });
1034
- const hydrated = await gqlRequest({
1035
- endpoint,
1036
- query: getCartOrderQuery,
1037
- variables: { id: newCartId },
1038
- headers
1039
- });
1040
- res.status(201).json({ cart: hydrated.orderById ?? null });
1041
- } catch (error) {
1042
- res.status(500).json({
1043
- error: "Failed to create cart",
1044
- message: error instanceof Error ? error.message : String(error)
1045
- });
1046
- }
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 });
1047
1284
  }
1285
+ var POST_default$2 = commerceRoute("Failed to create cart", createCart);
1048
1286
 
1049
1287
  //#endregion
1050
1288
  //#region src/server/api/commerce/checkout/POST.ts
1051
- async function handler$6(req, res) {
1052
- try {
1053
- const configuration = commerceConfigurationForResponse(res);
1054
- const { draftOrderId, skuId, quantity, lineItemData, returnUrl, successUrl } = req.body ?? {};
1055
- if (lineItemData !== void 0) {
1056
- res.status(400).json({ error: "Non-catalog checkout must be created by the server." });
1057
- return;
1058
- }
1059
- if ([draftOrderId, skuId].filter(Boolean).length !== 1 || [draftOrderId, skuId].some((value) => value !== void 0 && (typeof value !== "string" || !value.trim()))) {
1060
- res.status(400).json({ error: "exactly one non-empty draftOrderId or skuId is required" });
1061
- return;
1062
- }
1063
- if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 1)) {
1064
- res.status(400).json({ error: "quantity must be a positive whole number" });
1065
- return;
1066
- }
1067
- if (req.headers?.["x-commerce-scope"] !== void 0) {
1068
- if (!validateCommerceCartScope(req, res, configuration.read())) return;
1069
- }
1070
- const validateReturnUrls = res.locals.commerceCheckoutReturnUrlValidator;
1071
- if (!validateReturnUrls) {
1072
- res.status(503).json({ error: "Checkout return destinations are not configured." });
1073
- return;
1074
- }
1075
- const destinations = validateReturnUrls(returnUrl, successUrl);
1076
- if (!destinations) {
1077
- res.status(400).json({ error: "Checkout return destinations are not allowed." });
1078
- return;
1079
- }
1080
- const session = await createCheckoutSession({
1081
- draftOrderId,
1082
- skuId,
1083
- quantity,
1084
- ...destinations
1085
- }, configuration);
1086
- res.status(200).json(session);
1087
- } catch (error) {
1088
- res.status(500).json({
1089
- error: "Failed to create checkout session",
1090
- message: error instanceof Error ? error.message : String(error)
1091
- });
1092
- }
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);
1093
1307
  }
1308
+ var POST_default$3 = commerceRoute("Failed to create checkout session", createCheckout);
1094
1309
 
1095
1310
  //#endregion
1096
1311
  //#region src/server/api/commerce/config/GET.ts
1097
- async function handler$7(_req, res) {
1312
+ async function readPublicConfig(_req, res) {
1098
1313
  res.setHeader("Cache-Control", "no-store");
1099
- try {
1100
- const config = commerceConfigurationForResponse(res).read();
1101
- res.json({
1102
- cartScope: getCommerceCartScope(config),
1103
- currencyCode: config.currencyCode
1104
- });
1105
- } catch (cause) {
1106
- res.status(503).json({
1107
- error: "Commerce configuration is unavailable. Complete the store connection before continuing.",
1108
- message: cause instanceof Error ? cause.message : "Invalid Commerce configuration."
1109
- });
1314
+ const configuration = commerceConfigurationForResponse(res);
1315
+ const state = readCommerceConnectionState(configuration);
1316
+ if (state === "unbound" || state === "connecting") {
1317
+ res.json({ state });
1318
+ return;
1110
1319
  }
1320
+ const config = readReadyCommerceConfig(configuration);
1321
+ res.json({
1322
+ ...state === "ready" ? { state } : {},
1323
+ cartScope: getCommerceCartScope(config),
1324
+ currencyCode: config.currencyCode
1325
+ });
1111
1326
  }
1327
+ var GET_default$1 = commerceRoute("Failed to load Commerce configuration", readPublicConfig);
1112
1328
 
1113
1329
  //#endregion
1114
1330
  //#region src/server/api/commerce/order-status/GET.ts
1115
- async function handler$8(req, res) {
1116
- try {
1117
- const { orderId } = req.query;
1118
- if (!orderId || typeof orderId !== "string") {
1119
- res.status(400).json({
1120
- success: false,
1121
- error: "missing or invalid orderId query parameter"
1122
- });
1123
- return;
1124
- }
1125
- const order = await getOrderStatus(orderId, commerceConfigurationForResponse(res));
1126
- res.status(200).json({
1127
- success: true,
1128
- order
1129
- });
1130
- } catch (error) {
1131
- res.status(500).json({
1132
- success: false,
1133
- error: "Failed to get order status",
1134
- message: error instanceof Error ? error.message : String(error)
1135
- });
1136
- }
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
+ });
1137
1339
  }
1340
+ var GET_default$2 = commerceRoute("Failed to get order status", readOrderStatus, { failureFields: { success: false } });
1138
1341
 
1139
1342
  //#endregion
1140
1343
  //#region src/lib/commerce/catalog-subgraph.ts
@@ -1257,45 +1460,33 @@ function asNumber$1(value) {
1257
1460
  const n = Number(value);
1258
1461
  return Number.isFinite(n) ? n : void 0;
1259
1462
  }
1260
- async function handler$9(req, res) {
1261
- try {
1262
- const productId = req.params.id;
1263
- if (typeof productId !== "string" || !productId) {
1264
- res.status(400).json({ error: "Missing product id" });
1265
- return;
1266
- }
1267
- const config = readCommerceConfigForResponse(res);
1268
- if (!validateCommerceCartScope(req, res, config)) return;
1269
- const { storeId, clientId, apiBaseUrl } = config;
1270
- const variables = buildSkuGroupVariables({
1271
- productId,
1272
- selectedAttributeValues: asStringArray$1(req.query.attributeValues),
1273
- skuGroupFirst: asNumber$1(req.query.skuGroupFirst)
1274
- });
1275
- const data = await gqlRequest({
1276
- endpoint: catalogStorefrontEndpoint({
1277
- storeId,
1278
- apiBaseUrl
1279
- }),
1280
- query: skuGroupQuery,
1281
- variables,
1282
- headers: storefrontHeaders({
1283
- storeId,
1284
- clientId
1285
- })
1286
- });
1287
- if (!data.skuGroup || !data.activeSkuGroups?.edges?.some((edge) => edge?.node?.id === productId)) {
1288
- res.status(404).json({ error: "Product not found" });
1289
- return;
1290
- }
1291
- res.json({ skuGroup: data.skuGroup });
1292
- } catch (error) {
1293
- res.status(500).json({
1294
- error: "Failed to load product",
1295
- message: error instanceof Error ? error.message : String(error)
1296
- });
1297
- }
1463
+ async function readProduct(req, res) {
1464
+ const productId = req.params.id;
1465
+ if (typeof productId !== "string" || !productId) throw new InvalidRequestError("Missing product id");
1466
+ const config = readCommerceConfigForResponse(res);
1467
+ assertCommerceCartScope(req, config);
1468
+ const { storeId, clientId, apiBaseUrl } = config;
1469
+ const variables = buildSkuGroupVariables({
1470
+ productId,
1471
+ selectedAttributeValues: asStringArray$1(req.query.attributeValues),
1472
+ skuGroupFirst: asNumber$1(req.query.skuGroupFirst)
1473
+ });
1474
+ const data = await gqlRequest({
1475
+ endpoint: catalogStorefrontEndpoint({
1476
+ storeId,
1477
+ apiBaseUrl
1478
+ }),
1479
+ query: skuGroupQuery,
1480
+ variables,
1481
+ headers: storefrontHeaders({
1482
+ storeId,
1483
+ clientId
1484
+ })
1485
+ });
1486
+ if (!data.skuGroup || !data.activeSkuGroups?.edges?.some((edge) => edge?.node?.id === productId)) throw new NotFoundError("Product not found");
1487
+ res.json({ skuGroup: data.skuGroup });
1298
1488
  }
1489
+ var GET_default$3 = commerceRoute("Failed to load product", readProduct);
1299
1490
 
1300
1491
  //#endregion
1301
1492
  //#region src/server/api/commerce/products/GET.ts
@@ -1396,38 +1587,32 @@ function asNumber(value) {
1396
1587
  const n = Number(value);
1397
1588
  return Number.isFinite(n) ? n : void 0;
1398
1589
  }
1399
- async function handler$10(req, res) {
1400
- try {
1401
- const config = readCommerceConfigForResponse(res);
1402
- if (!validateCommerceCartScope(req, res, config)) return;
1403
- const { storeId, clientId, apiBaseUrl } = config;
1404
- const variables = buildSkuGroupsVariables({
1405
- first: asNumber(req.query.first) ?? 24,
1406
- after: typeof req.query.after === "string" ? req.query.after : void 0,
1407
- searchQuery: typeof req.query.searchQuery === "string" ? req.query.searchQuery : void 0,
1408
- productIds: asStringArray(req.query.productIds),
1409
- categoryIds: asStringArray(req.query.categoryIds)
1410
- });
1411
- const data = await gqlRequest({
1412
- endpoint: catalogStorefrontEndpoint({
1413
- storeId,
1414
- apiBaseUrl
1415
- }),
1416
- query: skuGroupsQuery,
1417
- variables,
1418
- headers: storefrontHeaders({
1419
- storeId,
1420
- clientId
1421
- })
1422
- });
1423
- res.json(data);
1424
- } catch (error) {
1425
- res.status(500).json({
1426
- error: "Failed to load products",
1427
- message: error instanceof Error ? error.message : String(error)
1428
- });
1429
- }
1590
+ async function listProducts(req, res) {
1591
+ const config = readCommerceConfigForResponse(res);
1592
+ assertCommerceCartScope(req, config);
1593
+ const { storeId, clientId, apiBaseUrl } = config;
1594
+ const variables = buildSkuGroupsVariables({
1595
+ first: asNumber(req.query.first) ?? 24,
1596
+ after: typeof req.query.after === "string" ? req.query.after : void 0,
1597
+ searchQuery: typeof req.query.searchQuery === "string" ? req.query.searchQuery : void 0,
1598
+ productIds: asStringArray(req.query.productIds),
1599
+ categoryIds: asStringArray(req.query.categoryIds)
1600
+ });
1601
+ const data = await gqlRequest({
1602
+ endpoint: catalogStorefrontEndpoint({
1603
+ storeId,
1604
+ apiBaseUrl
1605
+ }),
1606
+ query: skuGroupsQuery,
1607
+ variables,
1608
+ headers: storefrontHeaders({
1609
+ storeId,
1610
+ clientId
1611
+ })
1612
+ });
1613
+ res.json(data);
1430
1614
  }
1615
+ var GET_default$4 = commerceRoute("Failed to load products", listProducts);
1431
1616
 
1432
1617
  //#endregion
1433
1618
  //#region src/server/api/commerce/skus/[id]/GET.ts
@@ -1487,36 +1672,27 @@ const skuQuery = `
1487
1672
  }
1488
1673
  }
1489
1674
  `;
1490
- async function handler$11(req, res) {
1491
- try {
1492
- const skuId = req.params.id;
1493
- if (typeof skuId !== "string" || !skuId) {
1494
- res.status(400).json({ error: "Missing sku id" });
1495
- return;
1496
- }
1497
- const config = readCommerceConfigForResponse(res);
1498
- if (!validateCommerceCartScope(req, res, config)) return;
1499
- const { storeId, clientId, apiBaseUrl } = config;
1500
- const data = await gqlRequest({
1501
- endpoint: catalogStorefrontEndpoint({
1502
- storeId,
1503
- apiBaseUrl
1504
- }),
1505
- query: skuQuery,
1506
- variables: { id: skuId },
1507
- headers: storefrontHeaders({
1508
- storeId,
1509
- clientId
1510
- })
1511
- });
1512
- res.json(data);
1513
- } catch (error) {
1514
- res.status(500).json({
1515
- error: "Failed to load sku",
1516
- message: error instanceof Error ? error.message : String(error)
1517
- });
1518
- }
1675
+ async function readSku(req, res) {
1676
+ const skuId = req.params.id;
1677
+ if (typeof skuId !== "string" || !skuId) throw new InvalidRequestError("Missing sku id");
1678
+ const config = readCommerceConfigForResponse(res);
1679
+ assertCommerceCartScope(req, config);
1680
+ const { storeId, clientId, apiBaseUrl } = config;
1681
+ const data = await gqlRequest({
1682
+ endpoint: catalogStorefrontEndpoint({
1683
+ storeId,
1684
+ apiBaseUrl
1685
+ }),
1686
+ query: skuQuery,
1687
+ variables: { id: skuId },
1688
+ headers: storefrontHeaders({
1689
+ storeId,
1690
+ clientId
1691
+ })
1692
+ });
1693
+ res.json(data);
1519
1694
  }
1695
+ var GET_default$5 = commerceRoute("Failed to load sku", readSku);
1520
1696
 
1521
1697
  //#endregion
1522
1698
  //#region src/router.ts
@@ -1525,32 +1701,39 @@ function createCommerceRouter(options = {}) {
1525
1701
  const configuration = options.configuration ?? createRuntimeCommerceConfiguration();
1526
1702
  const catalogEnabled = options.features?.catalog ?? true;
1527
1703
  const paymentsEnabled = options.features?.payments ?? true;
1704
+ const logger = options.logger ?? consoleCommerceLogger;
1528
1705
  const validateCheckoutReturnUrls = options.checkoutReturnUrls ? createCheckoutReturnUrlValidator(options.checkoutReturnUrls) : void 0;
1529
1706
  router.use((_req, res, next) => {
1707
+ const observability = {
1708
+ commerceLogger: logger,
1709
+ commerceRequestIdResolver: options.getRequestId
1710
+ };
1711
+ Object.assign(res.locals, observability);
1530
1712
  res.locals.commerceConfiguration = configuration;
1531
1713
  res.locals.commerceCheckoutReturnUrlValidator = validateCheckoutReturnUrls;
1532
1714
  next();
1533
1715
  });
1534
1716
  if (catalogEnabled) {
1535
- router.get("/config", handler$7);
1536
- router.get("/products", handler$10);
1537
- router.get("/products/:id", handler$9);
1538
- router.get("/skus/:id", handler$11);
1539
- router.post("/cart", handler$5);
1540
- router.get("/cart/:id", handler$1);
1541
- router.post("/cart/:id/items", handler$4);
1542
- router.patch("/cart/:id/items/:itemId", handler$3);
1543
- router.delete("/cart/:id/items/:itemId", handler$2);
1544
- router.post("/cart/:id/discounts", handler);
1717
+ router.get("/config", GET_default$1);
1718
+ router.get("/products", GET_default$4);
1719
+ router.get("/products/:id", GET_default$3);
1720
+ router.get("/skus/:id", GET_default$5);
1721
+ router.post("/cart", POST_default$2);
1722
+ router.get("/cart/:id", GET_default);
1723
+ router.post("/cart/:id/items", POST_default$1);
1724
+ router.patch("/cart/:id/items/:itemId", PATCH_default);
1725
+ router.delete("/cart/:id/items/:itemId", DELETE_default);
1726
+ router.post("/cart/:id/discounts", POST_default);
1545
1727
  }
1546
1728
  if (paymentsEnabled) {
1547
- router.post("/checkout", handler$6);
1548
- router.get("/order-status", handler$8);
1729
+ router.post("/checkout", POST_default$3);
1730
+ router.get("/order-status", GET_default$2);
1549
1731
  }
1550
1732
  return router;
1551
1733
  }
1552
- function createCommerceCatalogRouter(configuration) {
1734
+ function createCommerceCatalogRouter(configuration, observability = {}) {
1553
1735
  return createCommerceRouter({
1736
+ ...observability,
1554
1737
  configuration,
1555
1738
  features: {
1556
1739
  catalog: true,
@@ -1558,8 +1741,9 @@ function createCommerceCatalogRouter(configuration) {
1558
1741
  }
1559
1742
  });
1560
1743
  }
1561
- function createGoDaddyPaymentsRouter(configuration, checkoutReturnUrls) {
1744
+ function createGoDaddyPaymentsRouter(configuration, checkoutReturnUrls, observability = {}) {
1562
1745
  return createCommerceRouter({
1746
+ ...observability,
1563
1747
  configuration,
1564
1748
  checkoutReturnUrls,
1565
1749
  features: {
@@ -1570,4 +1754,4 @@ function createGoDaddyPaymentsRouter(configuration, checkoutReturnUrls) {
1570
1754
  }
1571
1755
 
1572
1756
  //#endregion
1573
- export { createCheckoutSession, createCommerceCatalogRouter, createCommerceRouter, createGoDaddyPaymentsRouter, createRuntimeCommerceConfiguration, getOrderStatus, parseCommerceCheckoutConfiguration, readCommerceConfig };
1757
+ export { CommerceError, CommerceNotConfiguredError, InvalidOrderIdError, InvalidRequestError, NotFoundError, ORDER_STATUS_UNKNOWN, OrderNotFoundError, ScopeMismatchError, UpstreamError, createCheckoutSession, createCommerceCatalogRouter, createCommerceRouter, createGoDaddyPaymentsRouter, createRuntimeCommerceConfiguration, getOrderStatus, parseCommerceCheckoutConfiguration, readCommerceConfig };