@doswiftly/storefront-sdk 22.0.0 → 22.2.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,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 22.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 27934d1: Server `getStorefrontClient`: forwarded-IP signing is now **opt-in** via `getBuyerIp`.
8
+
9
+ Previously the server client auto-read the buyer IP through `next/headers` on every
10
+ request. That is a dynamic API and is illegal in statically-generated / ISR routes —
11
+ it crashed such pages with "Page changed from static to dynamic at runtime".
12
+
13
+ Forwarded-IP is now wired only when you pass `getBuyerIp`, which you should do **only
14
+ on routes that are already dynamic** (per-request rendered). Without it the client is
15
+ a fully static-safe, inert pass-through.
16
+
17
+ Migration — to keep forwarding the buyer IP, pass `getBuyerIp` on your dynamic routes:
18
+
19
+ ```typescript
20
+ import { headers } from "next/headers";
21
+
22
+ getStorefrontClient({
23
+ apiUrl,
24
+ shopSlug,
25
+ getBuyerIp: async () => (await headers()).get("cf-connecting-ip"),
26
+ });
27
+ ```
28
+
29
+ ## 22.1.0
30
+
31
+ ### Minor Changes
32
+
33
+ - 32ee745: Forward the real buyer IP from server-rendered storefronts (per-buyer rate limiting)
34
+
35
+ A server-rendered (BFF) storefront fetches from its own server, so the API sees one
36
+ source IP for every buyer and per-IP rate limits collapse onto that single address.
37
+ The server client now forwards the real buyer IP to the API so rate limiting applies
38
+ per buyer again.
39
+
40
+ **Automatic on the server — no wiring, fully backward-compatible.** Call
41
+ `getStorefrontClient({ apiUrl, shopSlug })` as before; forwarding configures itself
42
+ and stays inert when it cannot apply, so existing setups are unaffected. Server-only.
43
+
44
+ Non-Next server runtimes can supply the buyer IP explicitly via `getBuyerIp` (and
45
+ `getForwardedIpSecret`). The lower-level `forwardedIpMiddleware` is also exported.
46
+
47
+ (`@doswiftly/storefront-operations` is a version-sync bump — no code change.)
48
+
3
49
  ## 22.0.0
4
50
 
5
51
  ### Major Changes
package/README.md CHANGED
@@ -772,6 +772,11 @@ Middleware that reads mutable state takes a **lazy getter**
772
772
  (`authMiddleware(() => store.getState().accessToken)`) so rotated values are
773
773
  picked up without rebuilding the client.
774
774
 
775
+ A server-only `forwardedIpMiddleware` is also available for server-rendered (BFF)
776
+ storefronts — it forwards the real buyer IP to the backend so per-IP rate limits
777
+ do not collapse onto the storefront server's address. See
778
+ [Server-side](#server-side-reactserver).
779
+
775
780
  ## Core API
776
781
 
777
782
  ### createStorefrontClient
@@ -1070,6 +1075,38 @@ middleware: [
1070
1075
  ],
1071
1076
  ```
1072
1077
 
1078
+ ### Forwarding the buyer IP for rate limiting
1079
+
1080
+ When a storefront fetches on the server, the API sees the storefront server's IP for
1081
+ every buyer, so per-IP rate limits collapse onto a single address. Forwarding the
1082
+ buyer's real IP restores per-buyer limits.
1083
+
1084
+ **Opt-in — supply `getBuyerIp`.** Reading the buyer IP requires a request-scoped
1085
+ dynamic API (e.g. `next/headers` `headers()`), which is **illegal in
1086
+ statically-generated / ISR routes** and would crash them ("static to dynamic at
1087
+ runtime"). So enable it **only on routes that are already dynamic** (per-request
1088
+ rendered):
1089
+
1090
+ ```typescript
1091
+ import { headers } from 'next/headers';
1092
+
1093
+ // In a DYNAMIC route/segment only — headers() forces dynamic rendering:
1094
+ const client = getStorefrontClient({
1095
+ apiUrl: process.env.DOSWIFTLY_API_URL!,
1096
+ shopSlug: process.env.DOSWIFTLY_SHOP_SLUG!,
1097
+ getBuyerIp: async () => (await headers()).get('cf-connecting-ip'),
1098
+ });
1099
+ ```
1100
+
1101
+ Without `getBuyerIp` the client never reads the IP and never signs — a fully
1102
+ static-safe, inert pass-through (static / ISR routes are unaffected). Signing also
1103
+ requires the secret `process.env.DOSWIFTLY_FORWARDED_IP_SECRET` (set by your
1104
+ deployment; override via `getForwardedIpSecret` for runtimes without `process.env`).
1105
+ **Server-only**: nothing IP-related reaches the browser.
1106
+
1107
+ The lower-level `forwardedIpMiddleware({ getBuyerIp, getSecret })` is also exported
1108
+ for fully custom clients.
1109
+
1073
1110
  ## Caching
1074
1111
 
1075
1112
  ```typescript
@@ -41,6 +41,7 @@ export { authMiddleware } from './middleware/auth';
41
41
  export { cartSecretMiddleware, serverCartSecretMiddleware, CART_SECRET_HEADER, } from './middleware/cart-secret';
42
42
  export { currencyMiddleware } from './middleware/currency';
43
43
  export { languageMiddleware } from './middleware/language';
44
+ export { forwardedIpMiddleware, forwardedIpSignedMessage, FORWARDED_IP_HEADER, FORWARDED_IP_TS_HEADER, FORWARDED_IP_SIG_HEADER, type ForwardedIpMiddlewareOptions, } from './middleware/forwarded-ip';
44
45
  export { botProtectionMiddleware, BOT_PROTECTION_HEADER, type BotProtectionTokenProvider, type BotProtectionConfig, type BotProtectionProviderConfig, type BotProtectionMiddlewareOptions, type FailStrategy, } from './middleware/bot-protection';
45
46
  export { retryMiddleware, type RetryOptions } from './middleware/retry';
46
47
  export { timeoutMiddleware, type TimeoutOptions } from './middleware/timeout';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGhE,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,UAAU,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAIxB,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAGxG,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,4BAA4B,GAClC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,2BAA2B,EAChC,KAAK,8BAA8B,EACnC,KAAK,YAAY,GAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG9F,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAGjF,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,KAAK,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGpF,OAAO,EACL,SAAS,EACT,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,0BAA0B,EAC1B,KAAK,cAAc,GACpB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,4BAA4B,EAC5B,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,wBAAwB,EACxB,eAAe,EACf,qBAAqB,EACrB,yBAAyB,EACzB,gBAAgB,EAChB,kBAAkB,EAClB,+BAA+B,EAC/B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAEV,IAAI,EACJ,QAAQ,EACR,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,QAAQ,EACR,kBAAkB,EAClB,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,cAAc,EACd,KAAK,EACL,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,EACL,cAAc,EAGd,aAAa,EACb,uBAAuB,EACvB,uBAAuB,EACvB,+BAA+B,EAC/B,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,WAAW,EAEX,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,EAC3B,gBAAgB,EAChB,2BAA2B,EAC3B,0BAA0B,EAC1B,6BAA6B,EAC7B,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,gCAAgC,EAChC,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAGhB,gBAAgB,EAEhB,wBAAwB,EACxB,YAAY,EACZ,uBAAuB,GACxB,MAAM,cAAc,CAAC;AAQtB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACrB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,eAAe,EACf,UAAU,EACV,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,4BAA4B,EAC5B,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,eAAe,EACf,qBAAqB,EACrB,4BAA4B,EAC5B,8BAA8B,GAC/B,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACnG,YAAY,EACV,QAAQ,EACR,mBAAmB,EACnB,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,2BAA2B,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EACL,mBAAmB,EACnB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,UAAU,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,mBAAmB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,KAAK,yBAAyB,GAC/B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG/G,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG/G,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,YAAY,EAAE,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGzE,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,6BAA6B,GAC9B,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAG/E,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGlF,OAAO,EAAE,KAAK,SAAS,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
@@ -43,6 +43,7 @@ export { authMiddleware } from './middleware/auth';
43
43
  export { cartSecretMiddleware, serverCartSecretMiddleware, CART_SECRET_HEADER, } from './middleware/cart-secret';
44
44
  export { currencyMiddleware } from './middleware/currency';
45
45
  export { languageMiddleware } from './middleware/language';
46
+ export { forwardedIpMiddleware, forwardedIpSignedMessage, FORWARDED_IP_HEADER, FORWARDED_IP_TS_HEADER, FORWARDED_IP_SIG_HEADER, } from './middleware/forwarded-ip';
46
47
  export { botProtectionMiddleware, BOT_PROTECTION_HEADER, } from './middleware/bot-protection';
47
48
  export { retryMiddleware } from './middleware/retry';
48
49
  export { timeoutMiddleware } from './middleware/timeout';
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Forwarded-IP middleware — lets a SERVER-SIDE (BFF) storefront forward the real
3
+ * buyer IP to the backend, so per-buyer rate limiting works even though the
4
+ * backend's connection comes from the storefront server, not the browser.
5
+ *
6
+ * Why this exists: when a storefront renders/fetches on the server (server
7
+ * components, route handlers, server actions), the backend sees ONE source IP —
8
+ * the storefront server's — for every buyer. Per-IP limits then collapse onto
9
+ * that single address. This middleware carries the buyer's real IP (read from the
10
+ * incoming request, e.g. the `cf-connecting-ip` header) in a signed header the
11
+ * backend can trust.
12
+ *
13
+ * Trust model: the header is signed with HMAC-SHA256 over `${ip}.${ts}.${shopSlug}`
14
+ * using a shared platform secret. The backend recomputes the signature and only
15
+ * trusts the IP when it matches and the timestamp is fresh. Binding the shop slug
16
+ * stops a signature from one shop being replayed against another; the timestamp
17
+ * bounds replay. A spoofed header without a valid signature is ignored by the
18
+ * backend (it falls back to the connection IP).
19
+ *
20
+ * SERVER-ONLY: the secret must never reach the browser. Use this middleware only
21
+ * in server-side clients (`getStorefrontClient`), never in the browser pipeline.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { forwardedIpMiddleware } from '@doswiftly/storefront-sdk';
26
+ *
27
+ * // `buyerIp` comes from your server framework's incoming request — typically the
28
+ * // `cf-connecting-ip` request header. `serverSecret` is a server-only env value.
29
+ * getStorefrontClient({
30
+ * apiUrl,
31
+ * shopSlug,
32
+ * middleware: [
33
+ * forwardedIpMiddleware({
34
+ * getBuyerIp: () => buyerIp,
35
+ * getSecret: () => serverSecret,
36
+ * }),
37
+ * ],
38
+ * });
39
+ * ```
40
+ */
41
+ import type { Middleware } from '../client/types';
42
+ /** Header carrying the buyer's real IP, forwarded by a server-side storefront. */
43
+ export declare const FORWARDED_IP_HEADER = "x-doswiftly-buyer-ip";
44
+ /** Header carrying the signature timestamp (unix milliseconds) — bounds replay. */
45
+ export declare const FORWARDED_IP_TS_HEADER = "x-doswiftly-fwd-ts";
46
+ /** Header carrying the hex HMAC-SHA256 signature over `${ip}.${ts}.${shopSlug}`. */
47
+ export declare const FORWARDED_IP_SIG_HEADER = "x-doswiftly-fwd-sig";
48
+ /**
49
+ * Canonical signed message. The backend MUST build this identically before
50
+ * verifying the signature. Order and separators are load-bearing.
51
+ */
52
+ export declare function forwardedIpSignedMessage(ip: string, timestamp: string, shopSlug: string): string;
53
+ /** A getter that may be sync or async (e.g. `async () => (await headers()).get('cf-connecting-ip')`). */
54
+ type MaybeAsyncGetter = () => string | null | undefined | Promise<string | null | undefined>;
55
+ export interface ForwardedIpMiddlewareOptions {
56
+ /** Real buyer IP from the incoming request (e.g. the `cf-connecting-ip` header). */
57
+ getBuyerIp: MaybeAsyncGetter;
58
+ /** Shared platform secret used to sign. Server-side only — never expose to the browser. */
59
+ getSecret: MaybeAsyncGetter;
60
+ /**
61
+ * OPTIONAL override for the shop slug bound into the signature. Defaults to the
62
+ * `X-Shop-Slug` header the client already sends — so you normally don't pass it,
63
+ * and the signed slug is guaranteed to match what the backend verifies. Provide
64
+ * only for non-standard transports that don't set that header.
65
+ */
66
+ getShopSlug?: MaybeAsyncGetter;
67
+ /** Clock source (unix milliseconds). Injectable for tests; defaults to `Date.now`. */
68
+ now?: () => number;
69
+ }
70
+ /**
71
+ * Server-side forwarded-IP middleware. On every request it reads the buyer IP,
72
+ * shop slug and secret (lazy getters — a rotated secret is picked up without
73
+ * rebuilding the pipeline), signs `${ip}.${ts}.${shopSlug}` with HMAC-SHA256 and
74
+ * adds the three forwarded-IP headers. When any input is missing it adds nothing
75
+ * — the backend then keys the connection IP (the server-side default).
76
+ *
77
+ * The HMAC key is imported once per distinct secret value and reused across
78
+ * requests (re-imported only on rotation).
79
+ */
80
+ export declare function forwardedIpMiddleware(options: ForwardedIpMiddlewareOptions): Middleware;
81
+ export {};
82
+ //# sourceMappingURL=forwarded-ip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forwarded-ip.d.ts","sourceRoot":"","sources":["../../../src/core/middleware/forwarded-ip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,kFAAkF;AAClF,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAC1D,mFAAmF;AACnF,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAC3D,oFAAoF;AACpF,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAQ7D;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhG;AAED,yGAAyG;AACzG,KAAK,gBAAgB,GAAG,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAE7F,MAAM,WAAW,4BAA4B;IAC3C,oFAAoF;IACpF,UAAU,EAAE,gBAAgB,CAAC;IAC7B,2FAA2F;IAC3F,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAeD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,UAAU,CAiCvF"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Forwarded-IP middleware — lets a SERVER-SIDE (BFF) storefront forward the real
3
+ * buyer IP to the backend, so per-buyer rate limiting works even though the
4
+ * backend's connection comes from the storefront server, not the browser.
5
+ *
6
+ * Why this exists: when a storefront renders/fetches on the server (server
7
+ * components, route handlers, server actions), the backend sees ONE source IP —
8
+ * the storefront server's — for every buyer. Per-IP limits then collapse onto
9
+ * that single address. This middleware carries the buyer's real IP (read from the
10
+ * incoming request, e.g. the `cf-connecting-ip` header) in a signed header the
11
+ * backend can trust.
12
+ *
13
+ * Trust model: the header is signed with HMAC-SHA256 over `${ip}.${ts}.${shopSlug}`
14
+ * using a shared platform secret. The backend recomputes the signature and only
15
+ * trusts the IP when it matches and the timestamp is fresh. Binding the shop slug
16
+ * stops a signature from one shop being replayed against another; the timestamp
17
+ * bounds replay. A spoofed header without a valid signature is ignored by the
18
+ * backend (it falls back to the connection IP).
19
+ *
20
+ * SERVER-ONLY: the secret must never reach the browser. Use this middleware only
21
+ * in server-side clients (`getStorefrontClient`), never in the browser pipeline.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { forwardedIpMiddleware } from '@doswiftly/storefront-sdk';
26
+ *
27
+ * // `buyerIp` comes from your server framework's incoming request — typically the
28
+ * // `cf-connecting-ip` request header. `serverSecret` is a server-only env value.
29
+ * getStorefrontClient({
30
+ * apiUrl,
31
+ * shopSlug,
32
+ * middleware: [
33
+ * forwardedIpMiddleware({
34
+ * getBuyerIp: () => buyerIp,
35
+ * getSecret: () => serverSecret,
36
+ * }),
37
+ * ],
38
+ * });
39
+ * ```
40
+ */
41
+ /** Header carrying the buyer's real IP, forwarded by a server-side storefront. */
42
+ export const FORWARDED_IP_HEADER = 'x-doswiftly-buyer-ip';
43
+ /** Header carrying the signature timestamp (unix milliseconds) — bounds replay. */
44
+ export const FORWARDED_IP_TS_HEADER = 'x-doswiftly-fwd-ts';
45
+ /** Header carrying the hex HMAC-SHA256 signature over `${ip}.${ts}.${shopSlug}`. */
46
+ export const FORWARDED_IP_SIG_HEADER = 'x-doswiftly-fwd-sig';
47
+ /**
48
+ * Shop routing header the client already attaches to every request (see
49
+ * `create-client.ts`). The signature binds to THIS slug by default, so the signed
50
+ * value always matches what the backend verifies from the same header.
51
+ */
52
+ const SHOP_SLUG_HEADER = 'X-Shop-Slug';
53
+ /**
54
+ * Canonical signed message. The backend MUST build this identically before
55
+ * verifying the signature. Order and separators are load-bearing.
56
+ */
57
+ export function forwardedIpSignedMessage(ip, timestamp, shopSlug) {
58
+ return `${ip}.${timestamp}.${shopSlug}`;
59
+ }
60
+ async function importHmacKey(secret) {
61
+ return crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, [
62
+ 'sign',
63
+ ]);
64
+ }
65
+ async function signHex(key, message) {
66
+ const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message));
67
+ return Array.from(new Uint8Array(signature))
68
+ .map((byte) => byte.toString(16).padStart(2, '0'))
69
+ .join('');
70
+ }
71
+ /**
72
+ * Server-side forwarded-IP middleware. On every request it reads the buyer IP,
73
+ * shop slug and secret (lazy getters — a rotated secret is picked up without
74
+ * rebuilding the pipeline), signs `${ip}.${ts}.${shopSlug}` with HMAC-SHA256 and
75
+ * adds the three forwarded-IP headers. When any input is missing it adds nothing
76
+ * — the backend then keys the connection IP (the server-side default).
77
+ *
78
+ * The HMAC key is imported once per distinct secret value and reused across
79
+ * requests (re-imported only on rotation).
80
+ */
81
+ export function forwardedIpMiddleware(options) {
82
+ const { getBuyerIp, getShopSlug, getSecret, now = () => Date.now() } = options;
83
+ let cachedSecret = null;
84
+ let cachedKey = null;
85
+ return async (request, next) => {
86
+ // Getters may be async (e.g. reading the IP from `headers()` per request);
87
+ // `await` resolves sync values unchanged, so sync getters keep working.
88
+ const ip = await getBuyerIp();
89
+ // Slug defaults to the routing header the client already sends — so it always
90
+ // matches what the backend verifies. The optional getter only overrides it.
91
+ const shopSlug = (await getShopSlug?.()) ?? request.headers[SHOP_SLUG_HEADER];
92
+ const secret = await getSecret();
93
+ // Forward only when everything needed for a trusted, shop-bound signature is
94
+ // present. Missing any piece -> send nothing (backend falls back to conn IP).
95
+ if (ip && shopSlug && secret) {
96
+ const timestamp = String(now());
97
+ if (cachedSecret !== secret || cachedKey === null) {
98
+ cachedSecret = secret;
99
+ cachedKey = importHmacKey(secret);
100
+ }
101
+ const key = await cachedKey;
102
+ const signature = await signHex(key, forwardedIpSignedMessage(ip, timestamp, shopSlug));
103
+ request.headers[FORWARDED_IP_HEADER] = ip;
104
+ request.headers[FORWARDED_IP_TS_HEADER] = timestamp;
105
+ request.headers[FORWARDED_IP_SIG_HEADER] = signature;
106
+ }
107
+ return next(request);
108
+ };
109
+ }
@@ -40,11 +40,39 @@ export interface ServerClientOptions extends Omit<StorefrontClientConfig, 'middl
40
40
  * ```
41
41
  */
42
42
  middleware?: Middleware[];
43
+ /**
44
+ * Buyer-IP source that ENABLES forwarded-IP signing (opt-in). The forwarded-IP
45
+ * middleware is wired ONLY when this is provided — without it the client never
46
+ * reads the buyer IP and never signs (a fully static-safe, inert pass-through).
47
+ *
48
+ * Reading the buyer IP needs a request-scoped dynamic API (e.g. `next/headers`
49
+ * `headers()`), which is ILLEGAL in statically-generated / ISR routes — calling it
50
+ * there crashes the page ("static to dynamic at runtime"). So provide this ONLY on
51
+ * routes that are already dynamic (per-request rendered). A server-rendered
52
+ * storefront otherwise collapses every buyer onto its own server IP for rate
53
+ * limiting; forwarding the real IP restores per-buyer limits. May be async.
54
+ * Server-side only.
55
+ *
56
+ * @example
57
+ * // ONLY on a dynamic route — `headers()` forces dynamic rendering:
58
+ * import { headers } from 'next/headers';
59
+ * getStorefrontClient({ apiUrl, shopSlug, getBuyerIp: async () => (await headers()).get('cf-connecting-ip') });
60
+ */
61
+ getBuyerIp?: () => string | null | undefined | Promise<string | null | undefined>;
62
+ /**
63
+ * OPTIONAL override for the forwarded-IP signing secret. By default it is read
64
+ * from `process.env.DOSWIFTLY_FORWARDED_IP_SECRET`, set in your DoSwiftly
65
+ * deployment environment. Provide this getter only to override the env source —
66
+ * e.g. a runtime that does not expose the secret on `process.env`. Lazy getter —
67
+ * a rotated secret is picked up without rebuilding the client. NEVER expose this
68
+ * to the browser. Sync or async.
69
+ */
70
+ getForwardedIpSecret?: () => string | null | undefined | Promise<string | null | undefined>;
43
71
  }
44
72
  /**
45
73
  * Create a StorefrontClient for server-side use.
46
74
  *
47
- * Includes default middleware: retry → timeout → errors.
75
+ * Includes default middleware: forwarded-IP → retry → timeout → errors.
48
76
  * Does NOT include auth/currency middleware (server has no Zustand stores).
49
77
  * Pass headers via config.defaultHeaders or getHeaders option.
50
78
  */
@@ -1 +1 @@
1
- {"version":3,"file":"get-storefront-client.d.ts","sourceRoot":"","sources":["../../../src/react/server/get-storefront-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEpG,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,sBAAsB,EAAE,YAAY,CAAC;IACrF;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,gBAAgB,CAYlF"}
1
+ {"version":3,"file":"get-storefront-client.d.ts","sourceRoot":"","sources":["../../../src/react/server/get-storefront-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEpG,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,sBAAsB,EAAE,YAAY,CAAC;IACrF;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;IAE1B;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAElF;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC7F;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,gBAAgB,CAkClF"}
@@ -23,18 +23,39 @@ import { createStorefrontClient } from '../../core/client/create-client';
23
23
  import { retryMiddleware } from '../../core/middleware/retry';
24
24
  import { timeoutMiddleware } from '../../core/middleware/timeout';
25
25
  import { errorMiddleware } from '../../core/middleware/errors';
26
+ import { forwardedIpMiddleware } from '../../core/middleware/forwarded-ip';
26
27
  /**
27
28
  * Create a StorefrontClient for server-side use.
28
29
  *
29
- * Includes default middleware: retry → timeout → errors.
30
+ * Includes default middleware: forwarded-IP → retry → timeout → errors.
30
31
  * Does NOT include auth/currency middleware (server has no Zustand stores).
31
32
  * Pass headers via config.defaultHeaders or getHeaders option.
32
33
  */
33
34
  export function getStorefrontClient(options) {
34
- const { middleware: customMiddleware = [], ...config } = options;
35
+ const { middleware: customMiddleware = [], getBuyerIp, getForwardedIpSecret, ...config } = options;
36
+ // Forwarded-IP signing is OPT-IN: the middleware is wired ONLY when the caller
37
+ // supplies `getBuyerIp`. Reading the buyer IP needs a request-scoped dynamic API
38
+ // (`next/headers` `headers()`), which is illegal in statically-generated / ISR
39
+ // routes — auto-reading it there crashes the page ("static to dynamic at runtime").
40
+ // So the caller provides the IP source and uses it ONLY on dynamic routes; without
41
+ // `getBuyerIp` this client is a fully static-safe pass-through. The slug comes from
42
+ // the `X-Shop-Slug` header the client already sends, so the signed value matches
43
+ // what the backend verifies. The secret defaults to
44
+ // `process.env.DOSWIFTLY_FORWARDED_IP_SECRET` (override via `getForwardedIpSecret`);
45
+ // signing happens only when BOTH a buyer IP and a secret resolve at request time.
46
+ const forwardedIp = getBuyerIp
47
+ ? [
48
+ forwardedIpMiddleware({
49
+ getBuyerIp,
50
+ getSecret: getForwardedIpSecret ??
51
+ (() => (typeof process !== 'undefined' ? process.env?.DOSWIFTLY_FORWARDED_IP_SECRET : undefined)),
52
+ }),
53
+ ]
54
+ : [];
35
55
  return createStorefrontClient({
36
56
  ...config,
37
57
  middleware: [
58
+ ...forwardedIp,
38
59
  ...customMiddleware,
39
60
  retryMiddleware({ maxRetries: 2 }),
40
61
  timeoutMiddleware({ timeout: 10000 }), // Server-side: 10s timeout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-sdk",
3
- "version": "22.0.0",
3
+ "version": "22.2.0",
4
4
  "description": "Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,