@doswiftly/storefront-sdk 22.1.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,31 @@
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
+
3
29
  ## 22.1.0
4
30
 
5
31
  ### Minor Changes
package/README.md CHANGED
@@ -1078,34 +1078,32 @@ middleware: [
1078
1078
  ### Forwarding the buyer IP for rate limiting
1079
1079
 
1080
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. The server client
1082
- forwards the buyer's real IP so rate limiting applies per buyer again.
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
1083
 
1084
- **Automatic on the server nothing to wire.** Call `getStorefrontClient` as usual:
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):
1085
1089
 
1086
1090
  ```typescript
1091
+ import { headers } from 'next/headers';
1092
+
1093
+ // In a DYNAMIC route/segment only — headers() forces dynamic rendering:
1087
1094
  const client = getStorefrontClient({
1088
1095
  apiUrl: process.env.DOSWIFTLY_API_URL!,
1089
1096
  shopSlug: process.env.DOSWIFTLY_SHOP_SLUG!,
1097
+ getBuyerIp: async () => (await headers()).get('cf-connecting-ip'),
1090
1098
  });
1091
1099
  ```
1092
1100
 
1093
- It applies only inside a server request with forwarding configured by your
1094
- deployment, and is otherwise an inert pass-through existing setups are unaffected.
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`).
1095
1105
  **Server-only**: nothing IP-related reaches the browser.
1096
1106
 
1097
- **Non-Next server runtimes** — supply the buyer IP yourself (e.g. from
1098
- `cf-connecting-ip`), plus the signing secret if your runtime has no `process.env`:
1099
-
1100
- ```typescript
1101
- const client = getStorefrontClient({
1102
- apiUrl,
1103
- shopSlug,
1104
- getBuyerIp: () => incomingRequest.headers.get('cf-connecting-ip'),
1105
- // getForwardedIpSecret: () => ...
1106
- });
1107
- ```
1108
-
1109
1107
  The lower-level `forwardedIpMiddleware({ getBuyerIp, getSecret })` is also exported
1110
1108
  for fully custom clients.
1111
1109
 
@@ -41,12 +41,22 @@ export interface ServerClientOptions extends Omit<StorefrontClientConfig, 'middl
41
41
  */
42
42
  middleware?: Middleware[];
43
43
  /**
44
- * OPTIONAL override for the buyer-IP source. Forwarded-IP signing is
45
- * auto-configured: by default the SDK reads the request's `cf-connecting-ip`
46
- * via `next/headers` (a server-rendered storefront would otherwise collapse
47
- * every buyer onto its own server IP for rate limiting). Provide this only for
48
- * non-Next server runtimes where `next/headers` is unavailable. May be async.
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.
49
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') });
50
60
  */
51
61
  getBuyerIp?: () => string | null | undefined | Promise<string | null | undefined>;
52
62
  /**
@@ -1 +1 @@
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;;;;;;;OAOG;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;AAqBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,gBAAgB,CA4BlF"}
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"}
@@ -24,25 +24,6 @@ import { retryMiddleware } from '../../core/middleware/retry';
24
24
  import { timeoutMiddleware } from '../../core/middleware/timeout';
25
25
  import { errorMiddleware } from '../../core/middleware/errors';
26
26
  import { forwardedIpMiddleware } from '../../core/middleware/forwarded-ip';
27
- /**
28
- * Default buyer-IP source: the request's `cf-connecting-ip` header, read via
29
- * `next/headers` (the same dynamic-import pattern used to read request cookies — a
30
- * graceful no-op outside a Next request scope or in runtimes without
31
- * `next/headers`). Override via `getBuyerIp` for other server frameworks.
32
- */
33
- async function readCfConnectingIp() {
34
- try {
35
- const { headers } = await import('next/headers');
36
- const store = await headers();
37
- // Prefer a forwarded client-IP header when present: if the request was
38
- // proxied, the direct `cf-connecting-ip` is the proxy's address while this
39
- // header carries the original client IP. Fall back to the direct connection IP.
40
- return store.get('x-doswiftly-client-ip') ?? store.get('cf-connecting-ip');
41
- }
42
- catch {
43
- return null;
44
- }
45
- }
46
27
  /**
47
28
  * Create a StorefrontClient for server-side use.
48
29
  *
@@ -52,23 +33,29 @@ async function readCfConnectingIp() {
52
33
  */
53
34
  export function getStorefrontClient(options) {
54
35
  const { middleware: customMiddleware = [], getBuyerIp, getForwardedIpSecret, ...config } = options;
55
- // Forward the real buyer IP for per-buyer rate limiting fully self-configured,
56
- // nothing for the storefront to wire. Buyer IP defaults to the request's
57
- // `cf-connecting-ip` (via next/headers); the signing secret defaults to
58
- // `process.env.DOSWIFTLY_FORWARDED_IP_SECRET` (set in the DoSwiftly deployment
59
- // environment). The slug comes from the `X-Shop-Slug` header the client already
60
- // sends, so the
61
- // signed value matches what the backend verifies. The middleware signs ONLY when
62
- // BOTH a buyer IP and a secret resolve at request time — so with no secret
63
- // configured (or outside a Next request) it is an inert pass-through. Both
64
- // getters can be overridden for non-Next runtimes.
65
- const resolveBuyerIp = getBuyerIp ?? readCfConnectingIp;
66
- const resolveSecret = getForwardedIpSecret ??
67
- (() => (typeof process !== 'undefined' ? process.env?.DOSWIFTLY_FORWARDED_IP_SECRET : undefined));
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
+ : [];
68
55
  return createStorefrontClient({
69
56
  ...config,
70
57
  middleware: [
71
- forwardedIpMiddleware({ getBuyerIp: resolveBuyerIp, getSecret: resolveSecret }),
58
+ ...forwardedIp,
72
59
  ...customMiddleware,
73
60
  retryMiddleware({ maxRetries: 2 }),
74
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.1.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,