@doswiftly/storefront-operations 11.3.1 → 11.5.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.3.1
31
- - **Queries**: 48
30
+ - **Schema version**: 11.5.0
31
+ - **Queries**: 49
32
32
  - **Mutations**: 40
33
33
  - **Fragments**: 100
34
34
  <!-- AUTOGEN:STATS:END -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 11.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 190fd9d: Version sync with `@doswiftly/storefront-sdk` (linked release pair). No operations file changes — `OrderByToken` query and `Order.accessToken` field were shipped in the previous version (11.4.0); this bump keeps the operations package version aligned with the SDK release that now selects `accessToken` in its built-in `Order` fragment for `cartComplete`.
8
+
9
+ If you already use `@doswiftly/storefront-operations` for codegen, no regeneration is required (`queries.graphql` and `fragments.graphql` are unchanged). The companion `@doswiftly/storefront-sdk` upgrade is where the actual behavior change lands (see that changelog entry).
10
+
11
+ - e6c80ce: Auth route handlers (`createSetTokenHandler`, `createClearTokenHandler`, `createWhoamiHandler`) now accept an optional `isTrustedOrigin` predicate. This unblocks the BFF auth flow when the storefront runs behind a reverse proxy (DoSwiftly hosting, Vercel, custom edge proxy) that rewrites or strips the `Host` header — since 11.3.0 the strict `Origin host = Host` comparison returned 403 for every login, logout, and page-load hydration in that topology, leaving the auth cookie unset and every auth-gated route redirecting to login.
12
+
13
+ ### Fix
14
+
15
+ Each handler accepts a new `isTrustedOrigin` callback:
16
+
17
+ ```ts
18
+ type OriginValidator = (ctx: {
19
+ origin: string;
20
+ originHost: string;
21
+ request: Request;
22
+ }) => boolean | Promise<boolean>;
23
+ ```
24
+
25
+ When the predicate returns truthy the strict `Origin host = Host header` comparison is bypassed; when it returns falsy (or is not configured) the existing strict check applies. A thrown predicate fails closed — the error is logged and the strict check applies as if the predicate returned `false`.
26
+
27
+ Two pre-built predicates are exported:
28
+ - `trustedForwardedHostValidator` — passes when `Origin host` equals `X-Forwarded-Host` (falling back to `X-Original-Host`). Use this when a reverse proxy you control sets one of those headers per request. The DoSwiftly hosting platform and Vercel both qualify.
29
+ - `originAllowlistValidator(['https://shop.example.com', 'other-shop.example'])` — passes when `Origin` matches an entry in a static list. Useful when one storefront is hosted on multiple hostnames (custom apex + platform subdomain) and you do not want to depend on forwarded-host headers.
30
+
31
+ ### Upgrade impact
32
+ - New storefronts scaffolded via `doswiftly init` ship with `isTrustedOrigin: trustedForwardedHostValidator` configured by default.
33
+ - Existing storefronts using the SDK behind a reverse proxy need a 3-line change to each route handler under `app/api/auth/`:
34
+ ```ts
35
+ import {
36
+ createSetTokenHandler,
37
+ trustedForwardedHostValidator,
38
+ } from "@doswiftly/storefront-sdk";
39
+ export const POST = createSetTokenHandler({
40
+ isTrustedOrigin: trustedForwardedHostValidator,
41
+ });
42
+ ```
43
+ - Storefronts deployed without a reverse proxy (single-tier hosting where the Next.js process receives traffic directly) need no changes — the default strict check still works because the Host header arrives intact.
44
+
45
+ ### Security
46
+
47
+ The predicate is invoked AFTER the Origin header is parsed (rejecting malformed origins) and BEFORE the strict Host comparison. Forwarded-host validation is safe because the trusted intermediary overwrites those headers on every inbound request (`headers.set(...)`, not `append`), so an attacker cannot forge them via a browser `fetch()` — browsers cannot set `X-Forwarded-*` from JavaScript (they are forbidden request headers per the fetch spec).
48
+
49
+ Defense-in-depth layers continue to apply: CORS preflight at the backend, `SameSite=Lax` on the auth cookie, `HttpOnly` + `Secure` cookie attributes, and the strict Origin URL parse (still rejects malformed origin and the `?host=trusted` query-string bypass).
50
+
51
+ ## 11.4.0
52
+
53
+ ### Minor Changes
54
+
55
+ - 9bd60e8: Added `OrderByToken` query and `accessToken` field on the `Order` type for guest order summary access.
56
+
57
+ **What's new for storefronts:**
58
+ - **`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).
59
+ - **`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).
60
+ - Rate-limited to 5 requests per minute per IP+shop. Throttled clients receive a GraphQL error with `extensions.code: THROTTLED`.
61
+ - Response carries `Cache-Control: no-store` so per-customer data is never served from CDN or browser cache between users.
62
+
63
+ **Storage recommendation:** persist the token in an **HTTP-only cookie** (preferred) or `sessionStorage` for the post-checkout summary page — **never `localStorage`** (XSS-readable).
64
+
65
+ **Backward compatibility:** strictly additive. Existing queries (`CustomerOrder`, cart mutations, customer auth) are unchanged — upgrade is opt-in.
66
+
3
67
  ## 11.3.1
4
68
 
5
69
  ### Patch 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.3.1**
4
- > 48 queries · 40 mutations · 100 fragments
3
+ > Schema version: **11.5.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.3.1",
2
+ "schemaVersion": "11.5.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.3.1",
3
+ "version": "11.5.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"""