@doswiftly/storefront-operations 11.2.0 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -27,8 +27,8 @@ consumer's `codegen.ts` references this package's `.graphql` files as
27
27
  live in the consumer's repo.
28
28
 
29
29
  <!-- AUTOGEN:STATS:BEGIN — auto-regenerated, do not edit by hand -->
30
- - **Schema version**: 11.2.0
31
- - **Queries**: 48
30
+ - **Schema version**: 11.4.0
31
+ - **Queries**: 49
32
32
  - **Mutations**: 40
33
33
  - **Fragments**: 100
34
34
  <!-- AUTOGEN:STATS:END -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,119 @@
1
1
  # Changelog
2
2
 
3
+ ## 11.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9bd60e8: Added `OrderByToken` query and `accessToken` field on the `Order` type for guest order summary access.
8
+
9
+ **What's new for storefronts:**
10
+ - **`Order.accessToken: String!`** — opaque per-order token returned in `cartComplete.order.accessToken`. Pass it to `OrderByToken` to render the order summary for guests who have not signed in. The token is permanent until the order is deleted and stable across reads (safe to share via signed URLs in confirmation emails).
11
+ - **`OrderByToken($token: String!, $email: String)` query** — fetches a single order using the access token. The optional `email` argument is a defense-in-depth guard: when provided, it must match the order's buyer email case-insensitively; on mismatch the query returns `null` exactly like an invalid token (response shape is identical so attackers cannot distinguish the failure mode).
12
+ - Rate-limited to 5 requests per minute per IP+shop. Throttled clients receive a GraphQL error with `extensions.code: THROTTLED`.
13
+ - Response carries `Cache-Control: no-store` so per-customer data is never served from CDN or browser cache between users.
14
+
15
+ **Storage recommendation:** persist the token in an **HTTP-only cookie** (preferred) or `sessionStorage` for the post-checkout summary page — **never `localStorage`** (XSS-readable).
16
+
17
+ **Backward compatibility:** strictly additive. Existing queries (`CustomerOrder`, cart mutations, customer auth) are unchanged — upgrade is opt-in.
18
+
19
+ ## 11.3.1
20
+
21
+ ### Patch Changes
22
+
23
+ - 9ca724e: Removes the `./schema` subpath export from `@doswiftly/storefront-sdk` — it was a duplicate of what `@doswiftly/storefront-operations` already publishes.
24
+
25
+ **Why:**
26
+
27
+ `@doswiftly/storefront-operations` is a linked package — it is always installed alongside `@doswiftly/storefront-sdk`. It exposes the GraphQL SDL as a raw `schema.graphql` file via the `./schema.graphql` subpath export, which is the format every codegen tool and IDE GraphQL plugin natively consumes. Bundling the same SDL again as an ESM string literal in `@doswiftly/storefront-sdk/schema` added ~128 KB to the npm tarball without giving consumers a meaningful capability that wasn't already there.
28
+
29
+ **Migration:**
30
+
31
+ Switch from:
32
+
33
+ ```ts
34
+ import { schema } from "@doswiftly/storefront-sdk/schema";
35
+ ```
36
+
37
+ to a direct path reference in your codegen config (or `graphql-config` for your IDE):
38
+
39
+ ```ts
40
+ // codegen.ts
41
+ const config = {
42
+ schema: "node_modules/@doswiftly/storefront-operations/schema.graphql",
43
+ // …
44
+ };
45
+ ```
46
+
47
+ The schema content is byte-for-byte identical — your codegen output does not change.
48
+
49
+ **Internal:**
50
+ - Removed `scripts/build-schema-export.cjs` and the matching `pnpm build` chain step.
51
+ - `pnpm build` is now plain `tsc` again.
52
+ - Removed the `dist/schema.js` + `dist/schema.d.ts` build artifacts and the `schema-export.test.ts` unit test.
53
+ - `pnpm run doctor` no longer checks for the schema bundle.
54
+
55
+ `@doswiftly/storefront-operations` is bumped in lockstep (no code change — required because the packages are linked and ship together).
56
+
57
+ ## 11.3.0
58
+
59
+ ### Minor Changes
60
+
61
+ - 0a2dfd8: Phase 1 of the senior-level audit ships security hardening, dead-code cleanup, and a coverage gate.
62
+
63
+ **Security:**
64
+ - `createSetTokenHandler`, `createClearTokenHandler`, `createWhoamiHandler` now validate the `Origin` header against the `Host` header by parsed URL host (`new URL(origin).host === host`) instead of a substring `includes()` check. The previous implementation was bypassable by empty Host (`includes('')` is always true) and by Origin URLs that embedded the trusted host in their query string. Requests without both `Origin` and `Host` headers now respond with `403`.
65
+
66
+ **Removed:**
67
+ - `ServerClientOptions.getHeaders` (accepted by `getStorefrontClient()` but never wired through). Inject request-scoped headers via the `middleware` option — see updated JSDoc.
68
+
69
+ **Internal:**
70
+ - Inconsistent `setIsRenewingToken` setter in `useAuth` is renamed to `setIsRefreshingToken` to match the exposed `isRefreshingToken` flag.
71
+ - Removed an unused `callbackName` field in `TurnstileManager` and a pre-cookie-era `localStorage.removeItem('cart-storage')` call in `createCartStore`.
72
+
73
+ **Infrastructure:**
74
+ - `package.json` now declares `sideEffects: false` so consumer bundlers can tree-shake unused exports aggressively.
75
+ - Adds `test:coverage` script (`vitest run --coverage`) with a baseline threshold (60% statements / 50% branches / 55% functions / 60% lines). The floor will rise as later phases cover the React providers, auth/currency/language stores, and remaining hooks.
76
+ - Adds 102 new unit tests across `format`, `image`, `cookies`, `auth/handlers`, and additional `errorMiddleware`/`timeoutMiddleware`/`languageMiddleware` cases — including two regression tests for the origin-validation bypass vectors.
77
+
78
+ `@doswiftly/storefront-operations` is bumped in lockstep (no code change — required because the packages are linked and ship together).
79
+
80
+ - 09c3744: Phase 2 of the SDK audit ships the developer-experience features external storefront builders have been writing by hand: pre-built React components, tagged-union mutation state, a bundled GraphQL schema export, and a focused-hook auth API.
81
+
82
+ **Pre-built React components** (from `@doswiftly/storefront-sdk/react`):
83
+
84
+ Six headless, accessibility-aware components — zero styling, framework-agnostic, no `next/image` dependency so they work in any React 18/19 environment.
85
+ - `<Money amount currency>` — locale-formatted price from minor units (wraps `formatPrice`).
86
+ - `<Image data sizes priority>` — `<img>` with thumbhash blur placeholder, lazy by default, `fetchpriority` hint.
87
+ - `<CartCount count label hideWhenEmpty>` — aria-live cart item count.
88
+ - `<AddToCartButton variantId quantity onSuccess onError>` — orchestrates `useCartManager.addItem` with loading state + sr-only error alert.
89
+ - `<PriceDisplay price compareAtPrice currency>` — current price + optional strikethrough sale price.
90
+ - `<CartTotals subtotal discount shipping tax total currency labels>` — `<dl>` breakdown that elides empty rows.
91
+
92
+ **Tagged-union status in `useCartManager`**:
93
+ - New `status` field on the hook return — `{ type: 'idle' | 'loading' | 'error' | 'success', operation? }` — enables exhaustive switching without juggling booleans.
94
+ - `isLoading` and `error` remain available as derived selectors (backward-compatible).
95
+ - `clearCart()` resets status to `idle`.
96
+
97
+ **Bundled GraphQL schema** (`@doswiftly/storefront-sdk/schema`):
98
+ - New export path ships the full GraphQL SDL (123 KB) as a string constant, regenerated from `@doswiftly/storefront-operations/schema.graphql` on each build.
99
+ - Wire GraphQL codegen / IDE plugins without a live backend — no extra `npm install`.
100
+
101
+ **`useAuth` split into focused hooks**:
102
+ - `useLogin({ onSetToken })` — login flow only, `{ login, isLoggingIn, error }`.
103
+ - `useLogout({ onClearToken })` — logout, always clears local store even on backend failure.
104
+ - `useRefreshToken({ onSetToken })` — token rotation, keeps existing customer.
105
+ - `useAuth(options)` is preserved as a thin facade composing the three (backward-compatible, no breaking change).
106
+
107
+ Prefer the focused hooks in new code — smaller bundles, isolated state, smaller dependency arrays.
108
+
109
+ **Docs**:
110
+ - README adds a 7-step Next.js 16 App Router setup, BFF route handler examples, components catalog, bundled schema usage.
111
+ - New `docs/storefront-developer/sdk/nextjs-setup.md` and `auth.md` with end-to-end recipes.
112
+
113
+ **Tests**: 476 → 494 unit tests (+18). New files: `components.test.tsx` (37 cases), `schema-export.test.ts` (5 cases), `use-auth.test.tsx` (13 cases). Build clean, 0-deps invariant preserved.
114
+
115
+ `@doswiftly/storefront-operations` is bumped in lockstep (no code change — required because the packages are linked and ship together).
116
+
3
117
  ## 11.2.0
4
118
 
5
119
  ### Minor Changes
package/README.md CHANGED
@@ -201,6 +201,7 @@ full executable body of each operation.
201
201
  | `Customer` | Full customer profile — basic info plus the first 10 addresses and first 10 orders. Heaviest customer query; for narrow use cases prefer `CustomerProfile` (no orders / addresses) or `CustomerOrder` (single order). Returns null if unauthenticated. |
202
202
  | `CustomerProfile` | Lightweight customer profile (no orders, no addresses list). Use for settings / profile pages that only need basic customer info — much cheaper than `Customer`. Returns null if unauthenticated. |
203
203
  | `CustomerOrder` | Single order by `orderId`. Returns only orders that belong to the authenticated customer (cross-customer access returns null, not an error). Much cheaper than fetching the full `Customer` payload to access one order. Use on the order detail page. |
204
+ | `OrderByToken` | Fetch a single order using its opaque access token (`Order.accessToken`) — designed for guest order summary pages where the buyer has not signed in. The token is returned in `cartComplete.order.accessToken` immediately after checkout completes; persist it in an HTTP-only cookie (preferred) or `sessionStorage` for the post-checkout page (NEVER `localStorage`). Optional `email` parameter adds defense-in-depth: when provided, it is matched case-insensitively against the order's buyer email; on mismatch the query returns `null` exactly like an invalid token (the response shape is identical, so an attacker cannot distinguish "token valid, wrong email" from "token invalid"). Rate-limited to 5 requests per minute per IP+shop combination to deter token enumeration; clients exceeding the limit receive a GraphQL error with `extensions.code: THROTTLED`. The response is marked `Cache-Control: no-store` so per-customer order data is never served from CDN or browser cache between users. Safe to retry; the token is permanent until the order is deleted. |
204
205
 
205
206
  #### Discount Code Validation
206
207
 
package/fragments.graphql CHANGED
@@ -301,6 +301,7 @@ fragment Customer on Customer {
301
301
  fragment Order on Order {
302
302
  id
303
303
  orderNumber
304
+ accessToken
304
305
  totals {
305
306
  total {
306
307
  ...Money
package/llms-full.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # DoSwiftly Storefront Operations — Full Reference
2
2
 
3
- > Schema version: **11.2.0**
4
- > 48 queries · 40 mutations · 100 fragments
3
+ > Schema version: **11.4.0**
4
+ > 49 queries · 40 mutations · 100 fragments
5
5
 
6
6
  Auto-generated from `.graphql` source files. Do not edit by hand — this file is
7
7
  regenerated on every release to match the published schema.
@@ -430,6 +430,27 @@ query CustomerOrder($orderId: ID!) {
430
430
  }
431
431
  ```
432
432
 
433
+ ### Query: `OrderByToken`
434
+
435
+ **Section**: Customer (requires auth)
436
+
437
+ **Description**: Fetch a single order using its opaque access token (`Order.accessToken`) — designed for guest order summary pages where the buyer has not signed in. The token is returned in `cartComplete.order.accessToken` immediately after checkout completes; persist it in an HTTP-only cookie (preferred) or `sessionStorage` for the post-checkout page (NEVER `localStorage`). Optional `email` parameter adds defense-in-depth: when provided, it is matched case-insensitively against the order's buyer email; on mismatch the query returns `null` exactly like an invalid token (the response shape is identical, so an attacker cannot distinguish "token valid, wrong email" from "token invalid"). Rate-limited to 5 requests per minute per IP+shop combination to deter token enumeration; clients exceeding the limit receive a GraphQL error with `extensions.code: THROTTLED`. The response is marked `Cache-Control: no-store` so per-customer order data is never served from CDN or browser cache between users. Safe to retry; the token is permanent until the order is deleted.
438
+
439
+ **Variables**:
440
+ - `$token`: `String!`
441
+ - `$email`: `String`
442
+
443
+ **Fragments used**: `Order`
444
+
445
+ **GraphQL**:
446
+ ```graphql
447
+ query OrderByToken($token: String!, $email: String) {
448
+ orderByToken(token: $token, email: $email) {
449
+ ...Order
450
+ }
451
+ }
452
+ ```
453
+
433
454
  ### Query: `CartValidateDiscountCode`
434
455
 
435
456
  **Section**: Discount Code Validation
@@ -2782,6 +2803,7 @@ fragment Customer on Customer {
2782
2803
  fragment Order on Order {
2783
2804
  id
2784
2805
  orderNumber
2806
+ accessToken
2785
2807
  totals {
2786
2808
  total {
2787
2809
  ...Money
package/operations.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": "11.2.0",
2
+ "schemaVersion": "11.4.0",
3
3
  "queries": [
4
4
  {
5
5
  "name": "Shop",
@@ -324,6 +324,28 @@
324
324
  ],
325
325
  "body": "query CustomerOrder($orderId: ID!) {\n customerOrder(orderId: $orderId) {\n ...Order\n }\n}"
326
326
  },
327
+ {
328
+ "name": "OrderByToken",
329
+ "kind": "query",
330
+ "section": "Customer (requires auth)",
331
+ "description": "Fetch a single order using its opaque access token (`Order.accessToken`) — designed for guest order summary pages where the buyer has not signed in. The token is returned in `cartComplete.order.accessToken` immediately after checkout completes; persist it in an HTTP-only cookie (preferred) or `sessionStorage` for the post-checkout page (NEVER `localStorage`). Optional `email` parameter adds defense-in-depth: when provided, it is matched case-insensitively against the order's buyer email; on mismatch the query returns `null` exactly like an invalid token (the response shape is identical, so an attacker cannot distinguish \"token valid, wrong email\" from \"token invalid\"). Rate-limited to 5 requests per minute per IP+shop combination to deter token enumeration; clients exceeding the limit receive a GraphQL error with `extensions.code: THROTTLED`. The response is marked `Cache-Control: no-store` so per-customer order data is never served from CDN or browser cache between users. Safe to retry; the token is permanent until the order is deleted.",
332
+ "variables": [
333
+ {
334
+ "name": "token",
335
+ "type": "String!",
336
+ "defaultValue": null
337
+ },
338
+ {
339
+ "name": "email",
340
+ "type": "String",
341
+ "defaultValue": null
342
+ }
343
+ ],
344
+ "fragmentRefs": [
345
+ "Order"
346
+ ],
347
+ "body": "query OrderByToken($token: String!, $email: String) {\n orderByToken(token: $token, email: $email) {\n ...Order\n }\n}"
348
+ },
327
349
  {
328
350
  "name": "CartValidateDiscountCode",
329
351
  "kind": "query",
@@ -1995,7 +2017,7 @@
1995
2017
  "MailingAddress",
1996
2018
  "Money"
1997
2019
  ],
1998
- "body": "fragment Order on Order {\n id\n orderNumber\n totals {\n total {\n ...Money\n }\n subtotal {\n ...Money\n }\n totalTax {\n ...Money\n }\n totalShipping {\n ...Money\n }\n }\n status\n paymentStatus\n fulfillmentStatus\n processedAt\n confirmedAt\n cancelledAt\n expiredAt\n shippingAddress {\n ...MailingAddress\n }\n itemCount\n canCreatePayment\n paymentMethodType\n}",
2020
+ "body": "fragment Order on Order {\n id\n orderNumber\n accessToken\n totals {\n total {\n ...Money\n }\n subtotal {\n ...Money\n }\n totalTax {\n ...Money\n }\n totalShipping {\n ...Money\n }\n }\n status\n paymentStatus\n fulfillmentStatus\n processedAt\n confirmedAt\n cancelledAt\n expiredAt\n shippingAddress {\n ...MailingAddress\n }\n itemCount\n canCreatePayment\n paymentMethodType\n}",
1999
2021
  "onType": "Order"
2000
2022
  },
2001
2023
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-operations",
3
- "version": "11.2.0",
3
+ "version": "11.4.0",
4
4
  "description": "GraphQL operations for DoSwiftly Storefront - SSOT from backend",
5
5
  "homepage": "https://doswiftly.pl",
6
6
  "publishConfig": {
package/queries.graphql CHANGED
@@ -231,6 +231,13 @@ query CustomerOrder($orderId: ID!) {
231
231
  }
232
232
  }
233
233
 
234
+ # Fetch a single order using its opaque access token (`Order.accessToken`) — designed for guest order summary pages where the buyer has not signed in. The token is returned in `cartComplete.order.accessToken` immediately after checkout completes; persist it in an HTTP-only cookie (preferred) or `sessionStorage` for the post-checkout page (NEVER `localStorage`). Optional `email` parameter adds defense-in-depth: when provided, it is matched case-insensitively against the order's buyer email; on mismatch the query returns `null` exactly like an invalid token (the response shape is identical, so an attacker cannot distinguish "token valid, wrong email" from "token invalid"). Rate-limited to 5 requests per minute per IP+shop combination to deter token enumeration; clients exceeding the limit receive a GraphQL error with `extensions.code: THROTTLED`. The response is marked `Cache-Control: no-store` so per-customer order data is never served from CDN or browser cache between users. Safe to retry; the token is permanent until the order is deleted.
235
+ query OrderByToken($token: String!, $email: String) {
236
+ orderByToken(token: $token, email: $email) {
237
+ ...Order
238
+ }
239
+ }
240
+
234
241
  # ============================================
235
242
  # Discount Code Validation
236
243
  # ============================================
package/schema.graphql CHANGED
@@ -3571,6 +3571,11 @@ enum NodeType {
3571
3571
 
3572
3572
  """Customer order summary"""
3573
3573
  type Order implements Node {
3574
+ """
3575
+ Opaque access token (UUID v4) umożliwiający dostęp do podsumowania zamówienia bez sesji (guest order access via orderByToken query). Trwały per order — share dla forwarded receipts. Storefront powinien przechowywać HTTP-only cookie / sessionStorage (NIGDY localStorage). Defense-in-depth: orderByToken akceptuje opcjonalny email guard żeby ograniczyć blast radius kompromisu.
3576
+ """
3577
+ accessToken: String!
3578
+
3574
3579
  """Czy storefront może zainicjować płatność dla tego order"""
3575
3580
  canCreatePayment: Boolean!
3576
3581
 
@@ -3592,6 +3597,11 @@ type Order implements Node {
3592
3597
  """Line items count"""
3593
3598
  itemCount: Int!
3594
3599
 
3600
+ """
3601
+ Pozycje zamówienia (top-level connection) — eliminuje N+1 vs Order.shipments.items
3602
+ """
3603
+ lineItems(after: String, first: Int = 10): OrderLineItemConnection!
3604
+
3595
3605
  """Lista meta properties (Relay Connection)"""
3596
3606
  metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
3597
3607
 
@@ -3625,9 +3635,12 @@ type Order implements Node {
3625
3635
 
3626
3636
  """Paginated order connection"""
3627
3637
  type OrderConnection {
3628
- """Order edges"""
3638
+ """Order edges with cursors"""
3629
3639
  edges: [OrderEdge!]!
3630
3640
 
3641
+ """Order nodes (shortcut for edges.map(e => e.node))"""
3642
+ nodes: [Order!]!
3643
+
3631
3644
  """Pagination info"""
3632
3645
  pageInfo: PageInfo!
3633
3646
 
@@ -3656,6 +3669,69 @@ enum OrderFulfillmentStatus {
3656
3669
  UNFULFILLED
3657
3670
  }
3658
3671
 
3672
+ """Pozycja zamówienia (line item) z variant snapshot i live enrichment"""
3673
+ type OrderLineItem {
3674
+ """Unique identifier"""
3675
+ id: ID!
3676
+
3677
+ """
3678
+ Całkowita cena linii z momentu zakupu (unitPrice × quantity, minor units)
3679
+ """
3680
+ originalTotalPrice: Money!
3681
+
3682
+ """
3683
+ Cena jednostkowa z momentu zakupu (minor units, include BUNDLED surcharges)
3684
+ """
3685
+ originalUnitPrice: Money!
3686
+
3687
+ """Ilość zamówiona (snapshot z checkout)"""
3688
+ quantity: Int!
3689
+
3690
+ """Ilość już zrealizowana (shipped — per-item fulfillment tracking)"""
3691
+ quantityFulfilled: Int!
3692
+
3693
+ """
3694
+ Ilość pozostała do zwrotu/refund (quantity - returnedQuantity, clamp ≥ 0). Industry-standard semantyka dla refund tracking.
3695
+ """
3696
+ refundableQuantity: Int!
3697
+
3698
+ """
3699
+ Nazwa produktu z momentu zakupu (snapshot). NIE live `Product.name` — preserved nawet po edycji produktu (KSeF/invoice compliance).
3700
+ """
3701
+ title: String!
3702
+
3703
+ """
3704
+ Live wariant produktu (fresh image/price). Null jeśli wariant został usunięty po zakupie — consumer powinien użyć `title` jako fallback.
3705
+ """
3706
+ variant: ProductVariant
3707
+ }
3708
+
3709
+ """Paginated order line items (Relay Connection)"""
3710
+ type OrderLineItemConnection {
3711
+ """Order line item edges with cursors"""
3712
+ edges: [OrderLineItemEdge!]!
3713
+
3714
+ """Order line item nodes (shortcut for edges.map(e => e.node))"""
3715
+ nodes: [OrderLineItem!]!
3716
+
3717
+ """Pagination info"""
3718
+ pageInfo: PageInfo!
3719
+
3720
+ """
3721
+ Total count of line items in the order (convention parity z OrderConnection/MailingAddressConnection)
3722
+ """
3723
+ totalCount: Int!
3724
+ }
3725
+
3726
+ """Order line item edge"""
3727
+ type OrderLineItemEdge {
3728
+ """Cursor for pagination"""
3729
+ cursor: String!
3730
+
3731
+ """Order line item node"""
3732
+ node: OrderLineItem!
3733
+ }
3734
+
3659
3735
  """Payment status of an order"""
3660
3736
  enum OrderPaymentStatus {
3661
3737
  AUTHORIZED
@@ -4701,6 +4777,19 @@ type Query {
4701
4777
  """
4702
4778
  nodes(ids: [ID!]!, type: NodeType!): [Node]!
4703
4779
 
4780
+ """
4781
+ Fetch a single order by its opaque access token (guest order access without a session). Optional `email` argument enables defense-in-depth: if provided, must match the order buyer email (case-insensitive); on mismatch the query returns null exactly like an invalid token. Rate-limited to 5 requests per minute per IP+shop. Response carries Cache-Control: no-store.
4782
+ """
4783
+ orderByToken(
4784
+ """
4785
+ Optional email guard (case-insensitive). When provided, must match the order buyer email; on mismatch returns null (same shape as invalid token). The storefront decides per threat model.
4786
+ """
4787
+ email: String
4788
+
4789
+ """Opaque access token from Order.accessToken"""
4790
+ token: String!
4791
+ ): Order
4792
+
4704
4793
  """Get a page by handle or ID"""
4705
4794
  page(
4706
4795
  """Page handle"""