@doswiftly/storefront-sdk 22.8.1 → 22.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 22.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3961ca4: Attach the cart access secret (`x-cart-secret`) only to cart operations.
8
+
9
+ Previously the header was sent on every request whenever a cart cookie was
10
+ present, which kept public catalog reads (products, search, recommendations,
11
+ payment methods) off the shared response cache for any visitor who had a cart.
12
+ The secret is now attached only to cart-scoped operations, so those public
13
+ reads become cacheable again — faster responses, with no change needed in your
14
+ code.
15
+
16
+ A new `isCartScopedOperation` helper is exported, and `cartSecretMiddleware` /
17
+ `serverCartSecretMiddleware` accept an optional classifier as their second
18
+ argument. Cart operations are recognised by a `Cart` operation-name prefix. If
19
+ you issue custom cart queries through the SDK, name them with a `Cart` prefix,
20
+ or pass your own classifier:
21
+ `cartSecretMiddleware(getSecret, (operationName) => ...)`.
22
+
23
+ ## 22.9.0
24
+
25
+ ### Minor Changes
26
+
27
+ - 36c28ff: Add `customerNote` to the storefront `Order` type — the note a buyer left at checkout (delivery instructions, gift message, etc.). It is selected by the order fragment, so it is available on every order query, and is `null` when the buyer left no note.
28
+
29
+ ```tsx
30
+ // Order confirmation / detail page
31
+ const note = order.customerNote;
32
+ if (note) {
33
+ return <p className="order-note">{note}</p>;
34
+ }
35
+ ```
36
+
37
+ - a399d36: Image loader now produces clean image URLs (no internal storage prefix) on custom domains.
38
+
39
+ **Why**: when your storefront is served from its own image domain, the built `<Image>` URLs no longer carry the `/s/{shopId}` storage prefix (`https://img.yourshop.com/_next/static/media/hero.webp?width=256` instead of `.../s/{shopId}/_next/...`). The stored object is unchanged — the platform restores the prefix toward storage — so this is purely a nicer public URL. Branding-friendly, and one fewer internal detail in the markup.
40
+
41
+ **This is automatic — no code change.** The common `createImageLoader()` setup reads the flag from the platform-injected env var, so clean URLs simply turn on when your storefront is served from a custom domain configured for them:
42
+
43
+ ```ts
44
+ // lib/image-loader.ts — unchanged; clean URLs turn on automatically.
45
+ import { createImageLoader } from "@doswiftly/storefront-sdk/next";
46
+ export default createImageLoader();
47
+ ```
48
+
49
+ **Additive (backward-compatible)**:
50
+ 1. `ImageLoaderConfig` gains an optional `cleanUrl?: boolean` (defaults to `false`).
51
+ 2. `createImageLoader()` reads it automatically from `NEXT_PUBLIC_ASSET_CLEAN_URL`.
52
+
53
+ When `cleanUrl` is off (the default), URLs keep the `/s/{shopId}` prefix exactly as before — existing storefronts are unaffected. The boolean is also accepted by the low-level `buildImageLoaderUrl` for non-`<Image>` usage (e.g. CSS backgrounds) or tests:
54
+
55
+ ```ts
56
+ import { buildImageLoaderUrl } from "@doswiftly/storefront-sdk";
57
+ buildImageLoaderUrl(
58
+ { shopId, version, cleanUrl: true, cdnBase: "https://img.yourshop.com" },
59
+ { src: "/hero.webp", width: 256 },
60
+ );
61
+ // → https://img.yourshop.com/public/hero.webp?width=256&v=<version>
62
+ ```
63
+
64
+ **Migration checklist for existing storefronts**:
65
+ - [ ] None required — the default behavior is unchanged. Re-deploy to pick up clean URLs once your custom domain is configured.
66
+
3
67
  ## 22.8.1
4
68
 
5
69
  ### Patch Changes
package/README.md CHANGED
@@ -197,7 +197,7 @@ const client = createStorefrontClient({
197
197
  apiUrl: 'https://api.doswiftly.pl',
198
198
  shopSlug: 'my-shop',
199
199
  middleware: [
200
- cartSecretMiddleware(() => cartSecret), // lazy getter picks up rotation
200
+ cartSecretMiddleware(() => cartSecret), // lazy getter; secret rides cart ops only
201
201
  retryMiddleware({ maxRetries: 2 }),
202
202
  timeoutMiddleware({ timeout: 5000 }),
203
203
  errorMiddleware(), // ALWAYS LAST
@@ -425,17 +425,24 @@ Cart access is authorized by **possession of a secret**, not by the customer
425
425
  session. The `cart-id` cookie stores a composite value `"<cartId>.<secret>"`
426
426
  (30 days, SSR/edge-visible, not httpOnly — the cart carries no payment data).
427
427
  `CartClient.create()` and `recoveryRedeem()` reveal the secret **once**; the SDK
428
- persists it into the cookie for you. Every request then carries the secret in
429
- the `x-cart-secret` header via middleware:
428
+ persists it into the cookie for you. **Cart operations** then carry the secret in
429
+ the `x-cart-secret` header via middleware — public reads (product listings,
430
+ search, recommendations, payment methods) do **not**, so they stay eligible for
431
+ the shared cache even when the visitor has a cart:
430
432
 
431
433
  - **Browser**: `StorefrontProvider` wires `cartSecretMiddleware` automatically —
432
- the secret is read lazily from the cookie on every request, so a rotated
433
- secret is picked up without rebuilding the client.
434
+ the secret is read lazily from the cookie and attached only to cart
435
+ operations, so a rotated secret is picked up without rebuilding the client.
434
436
  - **Server (SSR/edge)**: prepend `serverCartSecretMiddleware(await readCartCredentials())`
435
437
  to your server client — see [Server-side](#server-side-reactserver).
436
438
  - **Custom runtimes**: `cartSecretMiddleware(() => secret)` + the
437
439
  `parseCartCookieValue` / `formatCartCookieValue` helpers.
438
440
 
441
+ Cart operations are recognised by a `Cart` operation-name prefix
442
+ (`isCartScopedOperation`). If you issue custom cart queries, name them with a
443
+ `Cart` prefix, or pass your own classifier as the second argument:
444
+ `cartSecretMiddleware(getSecret, (operationName) => ...)`.
445
+
439
446
  A cookie without the secret half (or a stale capability) makes the cart
440
447
  unreachable — mutations reject with `CART_NOT_FOUND` and the standard recovery
441
448
  flow recreates a fresh cart.
@@ -777,13 +784,16 @@ The loader is global and handles each `<Image src>` by category:
777
784
  | Image imported in code (`import hero from './hero.webp'`) | Routed through the CDN — the framework bundles it under `/_next/static/media/`, the loader resizes it per `srcset` (no code change needed) |
778
785
  | Other build asset (`/_next/*` JS/CSS, `/_nuxt/`, `/_astro/`, `/_app/`), absolute external URL, `data:` URI, SVG | Returned unchanged |
779
786
 
780
- The loader reads `NEXT_PUBLIC_SHOP_ID`, `NEXT_PUBLIC_DEPLOYMENT_COMMIT`, and
781
- `NEXT_PUBLIC_IMGPROXY_BASE` — all injected by the platform at build/deploy time. When
782
- they are absent (e.g. local `doswiftly dev`, where `public/` images are served by the
783
- dev server) the loader leaves local `public/` images untouched; product images, being
784
- absolute CDN URLs, are unaffected. `<Image quality>` is a no-op — quality is applied
785
- server-side (the format is still auto-negotiated AVIF/WebP). Pass explicit overrides only for non-standard setups:
786
- `createImageLoader({ shopId, version, cdnBase })`.
787
+ The loader reads `NEXT_PUBLIC_SHOP_ID`, `NEXT_PUBLIC_DEPLOYMENT_COMMIT`,
788
+ `NEXT_PUBLIC_IMGPROXY_BASE`, and `NEXT_PUBLIC_ASSET_CLEAN_URL` — all injected by the platform
789
+ at build/deploy time. When they are absent (e.g. local `doswiftly dev`, where `public/` images
790
+ are served by the dev server) the loader leaves local `public/` images untouched; product
791
+ images, being absolute CDN URLs, are unaffected. `NEXT_PUBLIC_ASSET_CLEAN_URL` is set to `true`
792
+ only when your storefront is served from a custom domain configured for clean URLs — then the
793
+ built URLs omit the internal storage prefix (`{host}/_next/static/media/...` instead of
794
+ `{host}/s/{shopId}/_next/...`); the stored object is unchanged. `<Image quality>` is a no-op —
795
+ quality is applied server-side (the format is still auto-negotiated AVIF/WebP). Pass explicit
796
+ overrides only for non-standard setups: `createImageLoader({ shopId, version, cdnBase, cleanUrl })`.
787
797
 
788
798
  For non-`<Image>` usage (CSS backgrounds, raw `<img>`), build CDN URLs yourself
789
799
  with the pure helper `buildImageLoaderUrl` from `@doswiftly/storefront-sdk`.
@@ -803,7 +813,7 @@ replay; a 401 on a mutation fires the `session-expired` signal instead.
803
813
  ```typescript
804
814
  import {
805
815
  authMiddleware, // Authorization: Bearer <token> (lazy getter)
806
- cartSecretMiddleware, // x-cart-secret header (lazy getter)
816
+ cartSecretMiddleware, // x-cart-secret header (cart operations only)
807
817
  currencyMiddleware, // X-Preferred-Currency header
808
818
  languageMiddleware, // X-Language header (skipped when null — intentional)
809
819
  botProtectionMiddleware, // challenge token for protected mutations only
@@ -2439,6 +2439,8 @@ export type Order = Node & {
2439
2439
  cancelledAt?: Maybe<Scalars['DateTime']['output']>;
2440
2440
  /** When the order was confirmed (e.g. payment authorised / approved). Null until confirmation. */
2441
2441
  confirmedAt?: Maybe<Scalars['DateTime']['output']>;
2442
+ /** The note the buyer left at checkout (delivery instructions, gift message, etc.). Null when no note was provided. */
2443
+ customerNote?: Maybe<Scalars['String']['output']>;
2442
2444
  /** Per-code discount allocations on the order (parity with `Cart.discountAllocations`). One entry per code that reduced the price; empty when no discount applied. The sum of `amount` equals the order-level discount. */
2443
2445
  discountAllocations: Array<OrderDiscountAllocation>;
2444
2446
  /** When the order expired (e.g. pending payment timed out). Null when not expired. */
@@ -4182,7 +4184,7 @@ export type CartCompleteMutationVariables = Exact<{
4182
4184
  }>;
4183
4185
  export type CartCompleteMutation = {
4184
4186
  cartComplete: {
4185
- order?: Maybe<(Pick<Order, 'id' | 'orderNumber' | 'accessToken' | 'status' | 'paymentStatus' | 'fulfillmentStatus' | 'processedAt' | 'confirmedAt' | 'cancelledAt' | 'expiredAt' | 'itemCount' | 'canCreatePayment' | 'paymentMethodType'> & {
4187
+ order?: Maybe<(Pick<Order, 'id' | 'orderNumber' | 'accessToken' | 'status' | 'paymentStatus' | 'fulfillmentStatus' | 'processedAt' | 'confirmedAt' | 'cancelledAt' | 'expiredAt' | 'itemCount' | 'customerNote' | 'canCreatePayment' | 'paymentMethodType'> & {
4186
4188
  totals: {
4187
4189
  total: Pick<Money, 'amount' | 'currencyCode'>;
4188
4190
  subtotal: Pick<Money, 'amount' | 'currencyCode'>;
@@ -5307,7 +5309,7 @@ export type OrderByTokenQueryVariables = Exact<{
5307
5309
  email?: InputMaybe<Scalars['String']['input']>;
5308
5310
  }>;
5309
5311
  export type OrderByTokenQuery = {
5310
- orderByToken?: Maybe<(Pick<Order, 'id' | 'orderNumber' | 'accessToken' | 'status' | 'paymentStatus' | 'fulfillmentStatus' | 'processedAt' | 'confirmedAt' | 'cancelledAt' | 'expiredAt' | 'itemCount' | 'canCreatePayment' | 'paymentMethodType'> & {
5312
+ orderByToken?: Maybe<(Pick<Order, 'id' | 'orderNumber' | 'accessToken' | 'status' | 'paymentStatus' | 'fulfillmentStatus' | 'processedAt' | 'confirmedAt' | 'cancelledAt' | 'expiredAt' | 'itemCount' | 'customerNote' | 'canCreatePayment' | 'paymentMethodType'> & {
5311
5313
  totals: {
5312
5314
  total: Pick<Money, 'amount' | 'currencyCode'>;
5313
5315
  subtotal: Pick<Money, 'amount' | 'currencyCode'>;
@@ -5568,7 +5570,7 @@ export type FreeShippingProgressFragment = (Pick<FreeShippingProgress, 'qualifie
5568
5570
  threshold?: Maybe<Pick<Money, 'amount' | 'currencyCode'>>;
5569
5571
  remaining?: Maybe<Pick<Money, 'amount' | 'currencyCode'>>;
5570
5572
  });
5571
- export type OrderFragment = (Pick<Order, 'id' | 'orderNumber' | 'accessToken' | 'status' | 'paymentStatus' | 'fulfillmentStatus' | 'processedAt' | 'confirmedAt' | 'cancelledAt' | 'expiredAt' | 'itemCount' | 'canCreatePayment' | 'paymentMethodType'> & {
5573
+ export type OrderFragment = (Pick<Order, 'id' | 'orderNumber' | 'accessToken' | 'status' | 'paymentStatus' | 'fulfillmentStatus' | 'processedAt' | 'confirmedAt' | 'cancelledAt' | 'expiredAt' | 'itemCount' | 'customerNote' | 'canCreatePayment' | 'paymentMethodType'> & {
5572
5574
  totals: {
5573
5575
  total: Pick<Money, 'amount' | 'currencyCode'>;
5574
5576
  subtotal: Pick<Money, 'amount' | 'currencyCode'>;