@farthershore/backend 0.12.0 → 0.14.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/README.md CHANGED
@@ -12,7 +12,7 @@ graceful lifecycle (health + shutdown). Everything else — your product, backen
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.12.0`.** Pre-1.0: minor releases may include breaking changes, so
15
+ > **Status: `0.14.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
 
@@ -142,6 +142,40 @@ Delivery is at-least-once; the event idempotency key keeps ingestion safe.
142
142
  Background usage is tallied and billed after the cycle, not enforced in
143
143
  real time.
144
144
 
145
+ ## Post-stream usage reporting
146
+
147
+ Use `fs.reportUsage()` when a gateway request streams its response and the
148
+ billable total is known only after the stream completes. Declare that route
149
+ with `postStreamBilling: true`, then report from the request-scoped verified
150
+ context so the SDK retains the attested subscription subject:
151
+
152
+ ```ts
153
+ const context = await fs.verifyRequest({ method, path, query, headers, body });
154
+
155
+ await streamResponse(context);
156
+ await context.reportUsage?.({
157
+ meters: { output_tokens: 1280 },
158
+ measureContext: { model: "apsu-1" },
159
+ });
160
+ ```
161
+
162
+ The callback is HMAC-attested and requires a subscription subject. Core binds it
163
+ to the immutable gateway request row and writes one billable `UsageEvent` with
164
+ the gateway-known request units merged with reported actual units, the served
165
+ plan, served route, and served request time. The gateway evidence row is
166
+ unbilled and omits only response-derived dimensions. This is a billing-only channel: it
167
+ does not settle or mutate Durable Object enforcement windows, so units that are
168
+ unknown at admission cannot be hard-enforced. Knowable request dimensions still
169
+ follow the normal admission path and are billed once on the merged callback.
170
+
171
+ Gateway evidence is published asynchronously. If the callback arrives first,
172
+ Core parks the verified payload and the SDK retries only
173
+ `post_stream_request_not_found` with bounded backoff, reusing the exact signed
174
+ payload and nonce. A maintenance pass binds any remaining parked callback once
175
+ evidence lands and durably alerts if it expires. The SDK method is best-effort and resolves
176
+ `{ ok: false, reason }` instead of rejecting, so handle or log a failed report
177
+ according to your service's delivery policy.
178
+
145
179
  ## Lifecycle
146
180
 
147
181
  - `fs.health()` returns the current local health report (token present, bootstrap
@@ -149,6 +183,58 @@ real time.
149
183
  - `fs.shutdown()` flushes any buffered metering and sends a `stopping` heartbeat.
150
184
  Call it on `SIGTERM` / `SIGINT` for graceful shutdown.
151
185
 
186
+ ## Local development & testing — the mode ladder
187
+
188
+ You do not need the platform to test a backend that runs behind it. Pick the
189
+ lowest tier that answers your question — each is a superset of the one below.
190
+
191
+ **Tier 0 — off (needs NOTHING from the SDK).** Unit-test your business logic
192
+ directly. The gateway sits in front of you in production; your pure handlers
193
+ don't import Farther Shore to be tested. There is no SDK step at this tier — see
194
+ `templates/1-unit.test.ts`.
195
+
196
+ **Tier 1 — passthrough.** Run your real app over HTTP with `fs.middleware()`
197
+ mounted but verification OFF, matching the pre-keystone deploy order: the
198
+ middleware passes requests through without attaching a context, so your routes
199
+ run normally. Activate with `FS_DEV_MODE=passthrough`. See
200
+ `templates/2-passthrough-http.test.ts`.
201
+
202
+ **Tier 2 — simulated.** A real runtime wired to an in-process gateway with
203
+ fail-closed verification ON and SIGNED personas driving requests. Assert the
204
+ fail-closed boundary (a persona without a permission gets 403) and that usage is
205
+ metered. Activate with `FS_DEV_MODE=simulated`, or construct explicitly:
206
+
207
+ ```ts
208
+ import { createDevRuntime, definePersona } from "@farthershore/backend/testing";
209
+
210
+ const rt = createDevRuntime({
211
+ mode: "simulated",
212
+ personas: {
213
+ creator: definePersona({
214
+ name: "creator",
215
+ permissions: ["widgets:create"],
216
+ }),
217
+ },
218
+ });
219
+
220
+ // Sign a request as a persona and drive your app (supertest, fetch, or inject):
221
+ const headers = await rt.asPersona("creator").headers({ path: "/v1/widgets" });
222
+ // rt.usage.byMeter() → assert reported usage
223
+ // rt.trace.forRequest(id) → why a request verified / was denied
224
+ ```
225
+
226
+ `@farthershore/backend/testing` gives you signed personas (owner / admin /
227
+ member / anonymous, plus your own), an in-process gateway fixture, and
228
+ assertable `usage` + per-request `trace` side channels. It is dev/test tooling
229
+ only — **it throws when `NODE_ENV=production`**, and `FS_DEV_MODE` is never a
230
+ required-at-boot variable. When `FS_DEV_MODE` is set, `initFromEnv()`
231
+ self-constructs the simulator (ephemeral keys, a loud banner, JSONL usage/trace
232
+ logs under `.farthershore/`, and a mode-600 `.farthershore/dev-keys.json` so a
233
+ separate test-runner process can sign against a running service via
234
+ `personaClientFromKeysFile`).
235
+
236
+ See `templates/3-simulated-authz.test.ts` for the full fail-closed + usage flow.
237
+
152
238
  ## Key exports
153
239
 
154
240
  | Export | Purpose |
@@ -157,13 +243,32 @@ real time.
157
243
  | `fs.middleware()` | Express fail-closed verify → `req.fartherShore`. |
158
244
  | `fs.verifyRequest({...})` | Framework-neutral request verification. |
159
245
  | `withUsage()` / `createUsage()` | Response-bound usage reporting (no network call). |
246
+ | `computeMeteringHeaders()` | Metering headers as a plain map — never throws. |
160
247
  | `fs.meter(meter, qty, opts)` | Async/background usage event. |
248
+ | `fs.reportUsage(input)` | Attested post-stream usage callback. |
161
249
  | `fs.health()` / `fs.shutdown()` | Health report and graceful shutdown. |
162
250
  | `FartherShoreError`, `MeteringError` | Typed errors. |
251
+ | `@farthershore/backend/testing` | Dev-mode + persona test harness (dev/test only). |
163
252
 
164
253
  A subpath export, `@farthershore/backend/express`, exposes the Express adapter
165
254
  types directly if you prefer to wire the middleware yourself.
166
255
 
256
+ ## Metering channels
257
+
258
+ Three usage channels exist and are **not** interchangeable:
259
+
260
+ - **Response-bound** (`withUsage` / `createUsage` / `computeMeteringHeaders`) is
261
+ the attested, request-bound settlement channel: the gateway verifies the HMAC
262
+ and settles the reported units against the request's lease in the same
263
+ lifecycle. Wire recipe (any language): [`docs/response-metering-wire.md`](docs/response-metering-wire.md).
264
+ - **Post-stream** (`fs.reportUsage` or the request-scoped
265
+ `context.reportUsage`) is attested and request-bound for streaming routes
266
+ declared with `postStreamBilling: true`. It writes the sole billable row for
267
+ gateway-known plus reported actual units and never mutates real-time
268
+ enforcement windows.
269
+ - **Background** (`fs.meter`) is a billing-only, unattested, post-cycle tally for
270
+ usage not tied to a gateway response. It never settles a lease.
271
+
167
272
  ## Learn more
168
273
 
169
274
  - Platform documentation: https://docs.farthershore.com
@@ -173,14 +173,29 @@ var RUNTIME_METERING_CONTRACT = {
173
173
  backend_id: "string",
174
174
  route_id: "string?",
175
175
  request_id: "string?",
176
+ requestId: "string?",
177
+ subscriptionId: "string",
178
+ nonce: "string?",
176
179
  meter: "string",
177
180
  qty: "number",
178
181
  timestamp: "string"
179
182
  },
183
+ postStreamEvent: {
184
+ requestId: "string",
185
+ subscriptionId: "string?",
186
+ nonce: "string",
187
+ meters: "Record<string, number>",
188
+ creditUnitsConsumed: "Record<string, number>?",
189
+ measureContext: "Record<string, unknown>?",
190
+ signature: "string"
191
+ },
180
192
  idempotencyKey: "event_id",
181
193
  delivery: "at-least-once",
182
194
  billingOnly: true,
183
195
  realtimeEnforced: false,
196
+ postStreamBillingOnly: true,
197
+ postStreamRealtimeEnforced: false,
198
+ postStreamTrustModel: "HMAC-attested and bound to one served postStreamBilling gateway request. Core writes one billable UsageEvent using the served plan and time. The callback never mutates Durable Object enforcement windows.",
184
199
  trustModel: "upstream-reported values are NOT cryptographically attested; a buggy or compromised upstream can self-report arbitrary values for its OWN product only. Core enforces allowedMeters/allowedRoutes from the authoritative token record at ingest, applies a per-event sanity max (perEventMax), and raises an implausible-volume alert."
185
200
  };
186
201
  var RUNTIME_RESPONSE_METERING_CONTRACT = {