hookwright 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +45 -1
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ var init_package = __esm({
24
24
  "package.json"() {
25
25
  package_default = {
26
26
  name: "hookwright",
27
- version: "1.7.0",
27
+ version: "1.8.0",
28
28
  description: "Build real, signed e-commerce webhooks from a live Shopify catalogue \u2014 interactive terminal UI, no backend",
29
29
  keywords: [
30
30
  "webhook",
@@ -163,6 +163,9 @@ var DEFAULT_CONFIG = {
163
163
  shopify: {
164
164
  domain: "",
165
165
  accessToken: "",
166
+ // Storefront API token. Creating a checkout needs one: the admin checkout API
167
+ // was removed, so a fresh checkout can only be minted through the storefront.
168
+ storefrontAccessToken: "",
166
169
  apiVersion: "2024-10",
167
170
  // cached from the last successful connection, for display only
168
171
  shop: null
@@ -2493,6 +2496,33 @@ var ShopifyClient = class {
2493
2496
  const { json } = await this.request("/checkouts.json", { query: { limit } });
2494
2497
  return json?.checkouts ?? [];
2495
2498
  }
2499
+ /**
2500
+ * Creates a real checkout through the storefront API and returns its recovery
2501
+ * link. The admin checkout API was removed, so this is the only way to mint a
2502
+ * new one; it needs a storefront token, which is separate from the admin one.
2503
+ */
2504
+ async createCheckout({ storefrontAccessToken, lineItems, email }) {
2505
+ if (!storefrontAccessToken) throw new ShopifyError("Storefront access token missing", { hint: "run `hookwright configure` and add it, or the checkout event reuses an existing checkout" });
2506
+ const mutation = `mutation checkoutCreate($input: CheckoutCreateInput!) {
2507
+ checkoutCreate(input: $input) {
2508
+ checkout { id webUrl totalPrice { amount currencyCode } }
2509
+ checkoutUserErrors { field message }
2510
+ }
2511
+ }`;
2512
+ const input = { lineItems: lineItems.map(({ variantId, quantity }) => ({ variantId: `gid://shopify/ProductVariant/${variantId}`, quantity })) };
2513
+ if (email) input.email = email;
2514
+ const res = await fetch(`https://${this.domain}/api/2024-01/graphql.json`, {
2515
+ method: "POST",
2516
+ headers: { "Content-Type": "application/json", "X-Shopify-Storefront-Access-Token": storefrontAccessToken },
2517
+ body: JSON.stringify({ query: mutation, variables: { input } })
2518
+ });
2519
+ const body = await res.json();
2520
+ const errors = body.errors ?? body.data?.checkoutCreate?.checkoutUserErrors;
2521
+ if (errors?.length) throw new ShopifyError(`Shopify rejected the checkout: ${errors.map((e) => e.message).join("; ")}`, { hint: "the storefront token needs unauthenticated_write_checkouts" });
2522
+ const checkout = body.data?.checkoutCreate?.checkout;
2523
+ if (!checkout) throw new ShopifyError("Shopify returned no checkout", { hint: "check the storefront token and api version" });
2524
+ return checkout;
2525
+ }
2496
2526
  /** Orders, newest first. */
2497
2527
  async orders({ limit = 10, status = "any" } = {}) {
2498
2528
  const { json } = await this.request("/orders.json", { query: { limit, status } });
@@ -2991,6 +3021,19 @@ function clientFor(config) {
2991
3021
  });
2992
3022
  }
2993
3023
  async function liveCheckout({ config, items = [], client = clientFor(config) }) {
3024
+ const storefrontAccessToken = config.shopify?.storefrontAccessToken;
3025
+ const lineItems = items.map(({ variant, product, quantity }) => ({ variantId: Number(variant?.id ?? product?.id), quantity: Number(quantity) || 1 })).filter((l) => Number.isFinite(l.variantId) && l.variantId > 0);
3026
+ if (storefrontAccessToken && lineItems.length) {
3027
+ const checkout2 = await client.createCheckout({ storefrontAccessToken, lineItems, email: config.customer?.email || void 0 });
3028
+ return {
3029
+ kind: "checkout",
3030
+ created: true,
3031
+ url: checkout2.webUrl,
3032
+ cartValue: money2(checkout2.totalPrice?.amount ?? 0).toFixed(2),
3033
+ cartToken: /\/checkouts\/([^/?]+)/.exec(checkout2.webUrl ?? "")?.[1],
3034
+ lineItems: []
3035
+ };
3036
+ }
2994
3037
  const checkouts = await client.abandonedCheckouts({ limit: 20 });
2995
3038
  if (checkouts.length === 0) return null;
2996
3039
  const wanted = new Set(items.map(({ variant }) => Number(variant?.id)).filter(Boolean));
@@ -2998,6 +3041,7 @@ async function liveCheckout({ config, items = [], client = clientFor(config) })
2998
3041
  const checkout = matching ?? checkouts[0];
2999
3042
  return {
3000
3043
  kind: "checkout",
3044
+ created: false,
3001
3045
  url: checkout.abandoned_checkout_url,
3002
3046
  cartValue: money2(checkout.total_price ?? 0).toFixed(2),
3003
3047
  cartToken: checkout.cart_token,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hookwright",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Build real, signed e-commerce webhooks from a live Shopify catalogue — interactive terminal UI, no backend",
5
5
  "keywords": [
6
6
  "webhook",