@farthershore/backend 0.19.0 → 0.20.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
@@ -4,6 +4,42 @@ All notable changes to the runtime backend SDK are documented here. This SDK
4
4
  versions independently from the frontend and business SDKs. Pre-1.0: minor
5
5
  versions may include breaking changes.
6
6
 
7
+ ## [0.20.0] - 2026-08-07
8
+
9
+ ### Changed
10
+
11
+ - **An injected shared nonce store now fails closed on outage.** If you supply
12
+ `nonceStore`, a throw from it rejects the request instead of falling through
13
+ to "not a replay" — otherwise knocking that store over would switch
14
+ one-time-use enforcement off entirely. No effect on the default in-memory
15
+ cache, which cannot fail this way.
16
+
17
+ `fs.replayProtection()` reports whether enforcement is `"shared"`
18
+ (cross-replica) or `"single-instance"` (this process), for boot logging.
19
+
20
+ Replay protection remains **zero-config**: the signature's ~305s time window
21
+ is the always-on defense and needs nothing from you. A shared store is purely
22
+ an opt-in upgrade for builders who want one-time-use enforced across replicas
23
+ rather than within each one. (FAR-760)
24
+
25
+ ### Security
26
+
27
+ - **A stale JWKS key set is no longer trusted forever.** The client held the
28
+ last successful fetch indefinitely, so during a prolonged Core/JWKS outage a
29
+ revoked signing key kept verifying. Stale-while-revalidate now has a ceiling:
30
+ fresh (<5m) → soft-stale (<15m, still served if refresh fails) → hard-stale
31
+ (fail closed). Adds an `onObservation` hook reporting
32
+ `fresh` / `soft_stale` / `hard_stale` / `cold`. (FAR-759)
33
+
34
+ ### Fixed
35
+
36
+ - **Every outbound SDK call now has a deadline.** Bootstrap, JWKS refresh,
37
+ metering flush, post-stream usage, the health heartbeat and the drift report
38
+ all awaited without an `AbortSignal`; a half-open connection could wedge
39
+ request verification, boot, readiness, or usage reporting indefinitely.
40
+ Caller cancellation composes with the SDK deadline, timeouts are classifiable,
41
+ and parsed response bodies are byte-bounded. (FAR-785)
42
+
7
43
  ## [0.19.0]
8
44
 
9
45
  ### Added
package/README.md CHANGED
@@ -12,7 +12,7 @@ graceful lifecycle (health + shutdown). Everything else — your business, backe
12
12
  and environment ids, the verification keys, and the metering endpoint — is
13
13
  fetched automatically from the token at startup.
14
14
 
15
- > **Status: `0.19.0`.** Pre-1.0: minor releases may include breaking changes, so
15
+ > **Status: `0.20.0`.** Pre-1.0: minor releases may include breaking changes, so
16
16
  > pin this package to an exact version (or a patch-only range) and upgrade
17
17
  > deliberately.
18
18
 
@@ -99,6 +99,34 @@ wrong-route / body-hash-mismatch / replayed-nonce / unknown-key /
99
99
  keys-unavailable) throws a typed `FartherShoreError` that maps to **HTTP 401**
100
100
  (413 for oversized bodies). There is no fail-open path.
101
101
 
102
+ ### Replay protection (nothing to configure)
103
+
104
+ A signed request is one-time-use, and two things enforce that:
105
+
106
+ - **A time window, always on.** The gateway signs a timestamp into the request;
107
+ anything older than ~305s (replay window + clock skew) is rejected outright.
108
+ This holds for every deployment shape and needs nothing from you.
109
+ - **A seen-id list.** Inside that window, the SDK remembers each
110
+ `X-Fs-Request-Id` it has accepted and rejects a second sighting. The default
111
+ list is in-memory and per-process.
112
+
113
+ That default is deliberate: running more than one replica means each has its own
114
+ list, so a captured request could be replayed once per replica **within the
115
+ ~305s window** — and closing that would mean asking you to provision and operate
116
+ a distributed cache. We would rather keep your setup to one environment variable
117
+ and let the time window bound the exposure.
118
+
119
+ If you do want one-time-use enforced across replicas, inject a shared store:
120
+
121
+ ```ts
122
+ initFromEnv({ nonceStore: myStore }); // checkAndRemember(id) => boolean | Promise<boolean>
123
+ ```
124
+
125
+ Make it TTL-bound to the signature validity window. If that store goes down,
126
+ requests fail **closed** — it never degrades to "not a replay".
127
+ `fs.replayProtection()` reports which mode is active (`"shared"` |
128
+ `"single-instance"`) if you want it in your boot logs.
129
+
102
130
  ## Response-bound usage reporting
103
131
 
104
132
  Use `withUsage()` (or the builder-style `createUsage()`) when you know the usage