local-storefront 0.3.0-rc.2 → 0.3.0-rc.3

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/CHANGELOG.md CHANGED
@@ -3,6 +3,48 @@
3
3
  What changed in each version of `lsf`, for someone who uses it. The section of a version is
4
4
  what its [GitHub release](https://github.com/0xtlt/local-storefront/releases) says.
5
5
 
6
+ ## 0.3.0-rc.3 - 2026-11-12
7
+
8
+ Third release candidate of 0.3.0. The local checkout applies the code discounts a backend makes
9
+ through the Admin API, and the session token of checkout and customer account UI extensions is
10
+ the one Shopify gives, signed with the client secret. A backend that read lsf's old token needs
11
+ a change: see Breaking changes.
12
+
13
+ ### Breaking changes
14
+
15
+ What to change when you upgrade from 0.3.0-rc.2.
16
+
17
+ - **The session token of a UI extension has Shopify's claims.** `dest` is the shop's bare
18
+ domain (`local-supply-co.myshopify.com`), no longer lsf's origin; there is no `iss` and no
19
+ `sid`; it lives five minutes; `sub` is there only when a customer is logged in. It is signed
20
+ with HS256 and the client secret, so a backend now verifies it as it verifies Shopify's, and
21
+ one that skipped the verification for lsf can stop doing so.
22
+
23
+ ### Added
24
+
25
+ #### The checkout of an app
26
+
27
+ - **`DiscountCodeBasic` and `DiscountCodeFreeShipping` codes apply at the checkout.** A code made
28
+ with `discountCodeBasicCreate` or `discountCodeFreeShippingCreate` takes an amount or a
29
+ percentage off the order, off listed products, variants or collections, or off each item, or
30
+ makes the rates up to `maximumShippingPrice` free in the countries of `destination`. It is taken
31
+ while the discount is active by its dates, under its `usageLimit`, for a buyer of its `context`
32
+ (all, listed customers, members of listed segments) who has not used it when it
33
+ `appliesOncePerCustomer`, and for a cart that meets its `minimumRequirement`. One such code
34
+ applies per checkout, with the Function discounts. The order and its `orders/create` and
35
+ `orders/paid` carry it in `discount_codes` and `discount_applications`, and its
36
+ `asyncUsageCount` goes up. `combinesWith`, buy X get Y codes and a `context` of markets are not
37
+ read.
38
+
39
+ ### Fixed
40
+
41
+ #### The checkout of an app
42
+
43
+ - **`shopify.sessionToken.get()` gives a token a backend can verify.** The page asks lsf for it
44
+ (`POST /__lsf/extensions/<handle>/session-token`), and lsf signs it with the client secret of
45
+ the Admin API (`--client-secret`, `SHOPIFY_API_SECRET`, the app's `.env`), which stays on the
46
+ server. `get()` rejects with the reason when lsf gives no token.
47
+
6
48
  ## 0.3.0-rc.2 - 2026-11-12
7
49
 
8
50
  Second release candidate of 0.3.0. It fixes what was reported on 0.2.0 and 0.3.0-rc.1, each
@@ -1079,8 +1079,11 @@ Function by `functionHandle` (or the deprecated `functionId`), which must be a d
1079
1079
  of the app; their `metafields` are the Function's configuration, checked as `metafieldsSet`
1080
1080
  checks them (a definition of the `DISCOUNT` owner type applies). The store sync writes an active
1081
1081
  app discount back to the store data's `checkout.discounts` (its Function, title, first code,
1082
- classes and metafields), so the local checkout runs it. Shopify's own types (basic, buy X get Y,
1083
- free shipping, automatic and code) are kept as records: the local checkout runs Functions only.
1082
+ classes and metafields), so the local checkout runs it. Of Shopify's own types, the local
1083
+ checkout applies the codes of `DiscountCodeBasic` and `DiscountCodeFreeShipping` itself, and
1084
+ counts the orders placed with them in `asyncUsageCount`
1085
+ ([what the checkout sees](admin.md#what-the-storefront-and-the-checkout-see)); buy X get Y and
1086
+ the automatic ones are kept as records.
1084
1087
  Their `summary` is the admin's: what the discount takes off, then its minimum, in the shop's
1085
1088
  money format (`$5.00 off entire order • Minimum purchase of $50.00`, `10% off 2 products`, `Buy
1086
1089
  2 items, get 1 item free`, `Free shipping on all products`), and `shortSummary` its first part.
package/docs/admin.md CHANGED
@@ -655,7 +655,7 @@ and delivery customizations, validations, cart transforms, fulfillment constrain
655
655
  app discounts (`DiscountAutomaticApp`, `DiscountCodeApp`), whether the app created them or the
656
656
  store data declared them (`checkout.functions.<handle>.enabled`, `checkout.discounts`). The
657
657
  answer counts them (`removedFunctionOwners`). The discounts of Shopify's own types (a
658
- `DiscountCodeBasic` the app made) stay. While the app is uninstalled, the shop's checkout runs
658
+ `DiscountCodeBasic` the app made) stay, and their codes still apply at the checkout. While the app is uninstalled, the shop's checkout runs
659
659
  none of its Functions, whatever a scenario declares or names in its `extensions`: the
660
660
  checkout's Functions dialog lists each with "the app is not installed on the shop". A reinstall
661
661
  gets none of those objects back, as on Shopify, where a reinstalled app "had to register them
@@ -1624,6 +1624,24 @@ What the backend changes through the Admin API is what the storefront and the
1624
1624
  extensions on the order status page (`appMetafields`, `owner_type = "ORDER"`);
1625
1625
  - an app discount it creates (`discountAutomaticAppCreate`, `discountCodeAppCreate`) runs its
1626
1626
  Function at the checkout, with the metafields it was given, while it is active;
1627
+ - a code discount of Shopify's own types it creates (`discountCodeBasicCreate`,
1628
+ `discountCodeFreeShippingCreate`) applies at the checkout once the buyer enters one of its
1629
+ codes, without a Function: an amount or a percentage off the order, off the products,
1630
+ variants and collections it lists (`appliesOnEachItem` included), or free shipping. The
1631
+ checkout takes the code while the discount is `ACTIVE` by its dates, its `usageLimit` is not
1632
+ reached, the buyer is one of its `context` (all buyers, the customers it lists, the members of
1633
+ the segments it lists: a guest is the customer whose email they gave), has not used it when it
1634
+ `appliesOncePerCustomer`, and the cart meets its `minimumRequirement` (the subtotal or the
1635
+ quantity of the items it applies to, before discounts); free shipping makes free the rates up
1636
+ to its `maximumShippingPrice`, to the countries of its `destination`. Otherwise the page says
1637
+ "Enter a valid discount code", and the state's `discountCodes` says why (`reason`). The order
1638
+ carries the code (`discountCodes`, a `DiscountCodeApplication`, the allocations), so do the
1639
+ `discount_codes` and `discount_applications` of `orders/create` and `orders/paid`, and the
1640
+ discount's `asyncUsageCount` (and its code's) goes up by one at once. One code of these types
1641
+ applies per checkout (lsf does not read `combinesWith`), and it stacks with the discounts of
1642
+ the Functions; of the shipping discounts, the best one applies. Not read: a `context` of
1643
+ markets (every buyer), `appliesOnOneTimePurchase` and `appliesOnSubscription`. Buy X get Y
1644
+ codes and Shopify's automatic discounts stay records;
1627
1645
  - the shipping rates of the general delivery profile it changes are the checkout's delivery
1628
1646
  options (see [delivery](admin-reference.md#inventory-transfers-fulfillment-and-delivery));
1629
1647
  - a theme file it writes to the main theme (`themeFilesUpsert`, `themeFilesCopy`,
@@ -1637,7 +1655,7 @@ What the backend changes through the Admin API is what the storefront and the
1637
1655
  | Syncs | Does not sync |
1638
1656
  |---|---|
1639
1657
  | Products: title, handle, description, vendor, type, tags, status and publication, options, variants (price, SKU, barcode, inventory policy, weight), stock per location, media from `files/`, collections and selling plan groups they are in | Orders and what belongs to them (`orderCreate`, draft orders, fulfillments, refunds, returns): the account pages show the orders of the data and of the checkout |
1640
- | Collections: title, handle, description, image, products in order | Discounts that are not app discounts (`DiscountCodeBasic`, …): the local checkout runs Functions only; of an app discount, `combinesWith`, usage limits and every code but the first |
1658
+ | Collections: title, handle, description, image, products in order | Discounts of Shopify's own types: the local checkout reads the code ones (`DiscountCodeBasic`, `DiscountCodeFreeShipping`) from the Admin API itself (above), and leaves the others (`…Bxgy`, the automatic ones) out; of an app discount, `combinesWith`, usage limits and every code but the first |
1641
1659
  | Customers: email, names, phone, tags, marketing consent, account state, addresses, their company | Gift cards the Admin API creates (changes to the data's cards sync) |
1642
1660
  | Companies and their locations, pages, blogs and articles, menus, locations, metaobjects and their translations, selling plan groups | SEO, product categories, bundle components and combined listings (a bundle is a product of its own there), collection rules and sources (a changed automated collection becomes a manual one), product feeds, point of sale |
1643
1661
  | Metafields of all of these, of the shop and of markets; markets, their countries and their currency; the published languages | Metafield and metaobject definitions, files, translations of other resources or for a market, publications other than the Online Store; catalogs, price lists, quantity rules, web presences, channels |
package/docs/checkout.md CHANGED
@@ -1117,6 +1117,22 @@ discounts on the subtotal they left, then shipping. Within a class:
1117
1117
  off −€0.00" while its settings are not set. A candidate of more than 0 that finds nothing left
1118
1118
  to take off (a line another discount made free) is not applied.
1119
1119
 
1120
+ **Codes of Shopify's own discount types.** With the [Admin API](admin.md), a code of a
1121
+ `DiscountCodeBasic` or a `DiscountCodeFreeShipping` the backend created applies without a
1122
+ Function ([what the checkout sees](admin.md#what-the-storefront-and-the-checkout-see)): an
1123
+ amount off the order is an order discount, one off listed products, variants or collections (or
1124
+ off each item) a product discount, free shipping a shipping discount, and each takes its place
1125
+ among the discounts of the Functions by the rules above. The code is checked when the
1126
+ checkout is computed: the discount's dates and usage limit, its buyers, one use per customer,
1127
+ its minimum (on the items it applies to, before discounts), and for free shipping the country
1128
+ of the address (a code is taken before there is one). A refused code shows "Enter a valid
1129
+ discount code", and its entry of `discountCodes` in the state has a `reason`
1130
+ (`"its minimum purchase is 50.00 USD, and the items it applies to come to 32.00 USD"`). One code
1131
+ of these types applies per checkout, the first one entered that qualifies: a second is refused,
1132
+ as on Shopify when a discount combines with no other (lsf does not read `combinesWith`). Its
1133
+ `DiscountApplication` is `ACROSS` (`EACH` off each item), `ALL` for the order and `ENTITLED`
1134
+ for listed items; free shipping is 100% off the shipping line, `EACH`.
1135
+
1120
1136
  An amount is shared between the lines in proportion to what they cost, to the cent, and the
1121
1137
  allocations of the lines (`discountAllocations`, in the page and for the Functions that run
1122
1138
  later) add up to the discount.
@@ -1298,6 +1314,39 @@ of checkout UI extensions has no such type): an `ORDER` declaration reads nothin
1298
1314
  a warning. Shopify lists `CART` as an owner type of checkout UI extensions only and `ORDER` of
1299
1315
  customer account UI extensions only; lsf takes both on either, as one extension can target both.
1300
1316
 
1317
+ ### Session tokens
1318
+
1319
+ `shopify.sessionToken.get()`, in a checkout or a customer account extension, gives the token
1320
+ Shopify gives, which the backend verifies as it verifies Shopify's (`authenticate.public.checkout`
1321
+ and `authenticate.public.customerAccount` of Shopify's libraries, `decodeSessionToken`): an
1322
+ HS256 JWT signed with the client secret of the [Admin API](admin.md#start)
1323
+ (`--client-secret`, `SHOPIFY_API_SECRET` of the environment or of the app's `.env`, else
1324
+ `lsf-local-client-secret`; the same without the admin part, without `--client-secret`). Its
1325
+ claims are those of shopify.dev's Session Token API, for
1326
+ [checkout](https://shopify.dev/docs/api/checkout-ui-extensions/latest/target-apis/platform-apis/session-token-api)
1327
+ and [customer account](https://shopify.dev/docs/api/customer-account-ui-extensions/latest/target-apis/platform-apis/session-token-api)
1328
+ extensions:
1329
+
1330
+ ```json
1331
+ {
1332
+ "dest": "local-supply-co.myshopify.com",
1333
+ "aud": "<client id>",
1334
+ "exp": 1794511949,
1335
+ "nbf": 1794511649,
1336
+ "iat": 1794511649,
1337
+ "jti": "7f63880b-b45d-49d4-beb1-af926c85653c",
1338
+ "sub": "gid://shopify/Customer/6523834743460"
1339
+ }
1340
+ ```
1341
+
1342
+ `dest` is the shop's domain, bare, as in shopify.dev's example (the session token of the admin
1343
+ has `https://`): the `permanent_domain` of the store data, the default shop of the Admin API.
1344
+ `aud` is the client id, the token lives five minutes, `jti` is a new UUID each time, and `sub`
1345
+ is there only when a customer is logged in (whether the app may read customer data is not
1346
+ checked). There is no `iss` and no `sid`. The page asks lsf for the token
1347
+ (`POST /__lsf/extensions/<handle>/session-token`, [control API](control-api.md#apps-checkout-and-admin-api))
1348
+ and hands it to the extension: the secret never reaches the browser.
1349
+
1301
1350
  ## Run a Function without a server
1302
1351
 
1303
1352
  ```bash
@@ -1505,7 +1554,7 @@ customization runs on them.
1505
1554
  | `shippingAddress` | `address`: `firstName`, `lastName`, `company`, `address1`, `address2`, `city`, `countryCode`, `provinceCode`, `zip`, `phone` | Sets the address. A country of the store becomes the country of the session. A `countryCode` that is not a country (ISO 3166-1 alpha-2, or `XK`, `AC`, `TA` as Shopify takes them; not `ZZ`) is refused and changes nothing. The store data has no shipping zones: any real country is taken. |
1506
1555
  | `deliveryOption` | `group` (`group-1`), `handle` | Chooses a shipping rate. |
1507
1556
  | `paymentMethod` | `handle` | Chooses a payment method. No handle clears it. |
1508
- | `discountCode` | `action` (`add` or `remove`), `code` | Enters or takes back a code. |
1557
+ | `discountCode` | `action` (`add` or `remove`), `code` | Enters or takes back a code. Each entered code is in the `discountCodes` of the state, `applicable` or not; a code of a discount of Shopify's own types that was refused has a `reason` ([how discounts stack](#how-discounts-stack)). |
1509
1558
  | `cartLines` | `change`: `addCartLine`, `removeCartLine` or `updateCartLine` of the UI extension API | Changes the cart of the session, the one the theme shows. |
1510
1559
  | `attribute` | `change`: `updateAttribute` or `removeAttribute`, with `key` and `value` | Changes the attributes of the cart. |
1511
1560
  | `note` | `change`: `updateNote` (`note`) or `removeNote` | Changes the note of the cart. |
@@ -1564,7 +1613,10 @@ The browser tests of a theme in CI: [testing](testing.md#in-ci).
1564
1613
  - Discounts have no `conditions` and no combinations: all of them stack, as described in
1565
1614
  [how discounts stack](#how-discounts-stack). Shopify lets the merchant choose which discounts
1566
1615
  combine, and applies the best of those that cannot; here all the product and order discounts
1567
- combine, and of the shipping discounts the best one applies.
1616
+ combine, and of the shipping discounts the best one applies. Of Shopify's own discount types,
1617
+ only the codes of `DiscountCodeBasic` and `DiscountCodeFreeShipping` apply, one per checkout,
1618
+ and only with the Admin API (which makes them): not buy X get Y, not the automatic ones, not a
1619
+ `context` of markets nor `appliesOnSubscription`.
1568
1620
  - Tax is one rate on the discounted subtotal, with no tax lines.
1569
1621
  - No Storefront API: `query` in an extension rejects. No Customer Account API either: a customer
1570
1622
  account extension reads the account through its `shopify` global only.
@@ -1592,6 +1644,9 @@ store before a release:
1592
1644
 
1593
1645
  - **Discount combinations and conditions**: lsf combines every product and order discount
1594
1646
  ([how discounts stack](#how-discounts-stack)).
1647
+ - **Codes of Shopify's own discount types**: one applies per checkout whatever its
1648
+ `combinesWith`, its minimum is checked on the items it applies to before any discount, and a
1649
+ refused code says lsf's reason, where Shopify's checkout has messages of its own.
1595
1650
  - **Currencies and price lists**: a buyer pays in the currency of the country of the shipping
1596
1651
  address (else of the one selected on the storefront), at its market's `rate` or fixed `prices`
1597
1652
  ([localization](data-format.md#localization)); the Functions read that rate as
@@ -52,6 +52,7 @@ backend's tests use, then the rest. How a test uses them:
52
52
  | `GET /checkout?scenario=<name>` | Opens a checkout for a scenario, in a new session whose cookie it sets. With `--app`. | A redirect |
53
53
  | `GET /__lsf/checkout/assets/<file>` | The files of the checkout page, which are in the binary. With `--app`. | The file |
54
54
  | `GET /__lsf/extensions/<handle>/<file>` | A file of the build (`dist/`) of a checkout UI extension. With `--app`. | The file |
55
+ | `POST /__lsf/extensions/<handle>/session-token` | What `shopify.sessionToken.get()` gives a checkout or customer account UI extension of the app: a token for the shop (`dest`, its bare domain), the app (`aud`) and the customer logged in in the caller's session (`sub`, absent logged out), valid five minutes, signed with HS256 and the client secret ([session tokens](checkout.md#session-tokens)). The checkout page and the customer account ask it for their extensions; a test can call the backend with it. No CORS: another site cannot read it. `404` for an extension the app does not serve. With `--app`. | `{ "token", "authorization", "shop", "claims" }` |
55
56
  | `GET /__lsf/admin` | The [Admin API](admin.md): its shops, their tokens, record counts and webhook subscriptions, the app, the throttle, the mutations, each shop's outbox. | JSON, [below](#admin-api) |
56
57
  | `GET /__lsf/graphiql` | GraphiQL on the [Admin API](admin.md#graphiql-in-the-browser): the shops, their tokens and the served versions filled in. | HTML |
57
58
  | `DELETE /__lsf/admin` | Puts every shop of the Admin API back to the store data (its clock and its outbox too), forgets the subscriptions the backend made and the tokens OAuth issued, and installs the app again (the printed token works). It revokes every token of the app, those of token exchange too, and sends no `app/uninstalled`: the backend's stored tokens are answered `401` ([reset between runs](admin.md#reset-between-runs)). | `{ "ok": true, "shops": [...] }` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-storefront",
3
- "version": "0.3.0-rc.2",
3
+ "version": "0.3.0-rc.3",
4
4
  "description": "Run Shopify themes and apps locally, without Shopify: the storefront, a local checkout for Functions and checkout UI extensions, and a local GraphQL Admin API with webhooks, from JSON store data. Made for tests in CI.",
5
5
  "keywords": [
6
6
  "shopify",
@@ -38,11 +38,11 @@
38
38
  "node": ">=18"
39
39
  },
40
40
  "optionalDependencies": {
41
- "local-storefront-darwin-arm64": "0.3.0-rc.2",
42
- "local-storefront-darwin-x64": "0.3.0-rc.2",
43
- "local-storefront-linux-arm64": "0.3.0-rc.2",
44
- "local-storefront-linux-x64": "0.3.0-rc.2",
45
- "local-storefront-win32-arm64": "0.3.0-rc.2",
46
- "local-storefront-win32-x64": "0.3.0-rc.2"
41
+ "local-storefront-darwin-arm64": "0.3.0-rc.3",
42
+ "local-storefront-darwin-x64": "0.3.0-rc.3",
43
+ "local-storefront-linux-arm64": "0.3.0-rc.3",
44
+ "local-storefront-linux-x64": "0.3.0-rc.3",
45
+ "local-storefront-win32-arm64": "0.3.0-rc.3",
46
+ "local-storefront-win32-x64": "0.3.0-rc.3"
47
47
  }
48
48
  }