@farthershore/backend 0.1.0 → 0.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/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # @farthershore/backend
2
2
 
3
- The secure BYO-backend runtime SDK. Install one package, set one token
4
- (`FS_RUNTIME_TOKEN`), and Farther Shore protects (signs every gateway→backend
5
- request; the SDK verifies and rejects anything not from Farther Shore) and meters
6
- your backend.
3
+ Runtime metering and gateway-verification SDK for builder upstreams. Install one
4
+ package, set one token (`FS_RUNTIME_TOKEN`), and Farther Shore handles signed
5
+ gateway-to-upstream request verification plus response-bound usage reporting.
7
6
 
8
7
  ## Install
9
8
 
@@ -11,20 +10,43 @@ your backend.
11
10
  npm install @farthershore/backend
12
11
  ```
13
12
 
14
- ## Quick start (Express)
13
+ ## Quick start (Fetch-compatible handlers)
15
14
 
16
15
  ```ts
17
- import { fartherShore } from "@farthershore/backend";
16
+ import { fartherShore, withUsage } from "@farthershore/backend";
18
17
 
19
18
  const fs = fartherShore.initFromEnv(); // derives everything from FS_RUNTIME_TOKEN
20
19
 
21
- app.use(fs.middleware()); // fail-closed verify req.fartherShore
20
+ export async function POST(request: Request) {
21
+ const url = new URL(request.url);
22
+ const body = new Uint8Array(await request.clone().arrayBuffer());
23
+
24
+ await fs.verifyRequest({
25
+ method: request.method,
26
+ path: url.pathname,
27
+ query: url.search,
28
+ headers: request.headers,
29
+ body,
30
+ });
31
+
32
+ const result = await runWorkflow(await request.json());
33
+ return withUsage(request, Response.json(result), {
34
+ tokens_used: result.tokensUsed,
35
+ });
36
+ }
37
+ ```
38
+
39
+ ## Express verification
40
+
41
+ ```ts
42
+ import { fartherShore } from "@farthershore/backend";
43
+
44
+ const fs = fartherShore.initFromEnv();
45
+
46
+ app.use(fs.middleware()); // fail-closed verify -> req.fartherShore
22
47
 
23
48
  app.post("/v1/runs", async (req, res) => {
24
49
  const result = await runWorkflow(req.body);
25
- await fs.meter("tokens", result.tokensUsed, {
26
- requestId: req.fartherShore.requestId,
27
- });
28
50
  res.json(result);
29
51
  });
30
52
 
@@ -35,7 +57,7 @@ process.on("SIGTERM", () => void fs.shutdown());
35
57
  ## What `initFromEnv()` derives
36
58
 
37
59
  The builder configures exactly one thing: `FS_RUNTIME_TOKEN`. Everything else —
38
- product/backend/environment ids, the JWKS url, the metering endpoint and
60
+ product/upstream/environment ids, the JWKS url, the metering endpoint and
39
61
  credential, verification config, transport — is fetched from
40
62
  `POST /v1/runtime/bootstrap` and cached in memory (refreshed lazily).
41
63
 
@@ -43,7 +65,7 @@ credential, verification config, transport — is fetched from
43
65
 
44
66
  `fs.middleware()` (Express) and the framework-neutral
45
67
  `fs.verifyRequest({ method, path, query, headers, body })` recompute the
46
- [canonical signing string](../../docs/superpowers/specs/backend-runtime-spec.md)
68
+ [canonical signing string](../../docs/superpowers/specs/metering-runtime-spec.md)
47
69
  from the actual request and verify the gateway's Ed25519 signature against a
48
70
  JWKS-resolved public key. The plaintext `X-FS-*` headers are **untrusted** —
49
71
  identity comes only from a signature whose claims match the real request.
@@ -53,18 +75,50 @@ wrong-route / body-hash-mismatch / replayed-nonce / unknown-kid /
53
75
  jwks-unavailable) returns a typed `FartherShoreError` → **HTTP 401** (413 for
54
76
  oversized bodies). There is no fail-open branch.
55
77
 
56
- ## Metering (billing-only)
78
+ ## Response-bound metering
79
+
80
+ Use `withUsage()` or `createUsage()` when usage is known while returning a
81
+ gateway-handled response. These helpers make no network call. They sign dynamic
82
+ usage into internal response headers, and the gateway verifies, settles, and
83
+ strips those headers before the subscriber receives the response.
84
+
85
+ ```ts
86
+ import { withUsage } from "@farthershore/backend";
87
+
88
+ export async function POST(request: Request) {
89
+ const result = await runWorkflow(await request.json());
90
+ return withUsage(
91
+ request,
92
+ Response.json(result),
93
+ { tokens_used: result.tokensUsed },
94
+ {
95
+ measureContext: { model: result.model },
96
+ creditUnitsConsumed: { credits: result.creditsUsed },
97
+ },
98
+ );
99
+ }
100
+ ```
101
+
102
+ `measureContext` is free-form pricing/analytics context persisted with the usage
103
+ event. `creditUnitsConsumed` is a numeric map for credit-wallet style products;
104
+ keys and values are validated locally before signing. The gateway accepts actual
105
+ request-bound usage only from the `x-fs-metering` response-header contract signed
106
+ with `FS_RUNTIME_TOKEN`; retired `x-fs-usage` actual-report headers are stripped
107
+ and ignored.
108
+
109
+ ## Async/background metering
57
110
 
58
- `fs.meter(meter, qty, { requestId, routeId })` enqueues an idempotent event and
111
+ Use `fs.meter(meter, qty, { requestId, routeId })` only for async/background
112
+ usage that is not tied to a gateway response. It enqueues an idempotent event and
59
113
  POSTs it to `/v1/metering/events` with a reusable bearer credential. Delivery is
60
114
  at-least-once; the `event_id` idempotency key keeps core's ingest safe. Values
61
115
  are tallied/billed post-cycle, not real-time enforced.
62
116
 
63
- ## Migrating from `@farthershore/metering`
117
+ The meter key is not hardcoded by the SDK. It must match a dynamic meter declared
118
+ in the product contract, such as `tokens_used` in the Product SDK examples.
64
119
 
65
- `@farthershore/metering` is now a deprecated re-export shim of this package and
66
- will be removed after the metering cutover. Replace the import; for new code,
67
- prefer `fs.meter("tokens", n)` over the response-header signer.
120
+ Product route defaults such as request counts remain platform-managed and
121
+ require no upstream code.
68
122
 
69
123
  The signing primitives and contract constants are re-exported from
70
124
  `@farthershore/contracts/runtime`, the language-neutral source of truth every
@@ -180,7 +180,44 @@ var RUNTIME_METERING_CONTRACT = {
180
180
  delivery: "at-least-once",
181
181
  billingOnly: true,
182
182
  realtimeEnforced: false,
183
- trustModel: "backend-reported values are NOT cryptographically attested; a buggy or compromised backend 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."
183
+ 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."
184
+ };
185
+ var RUNTIME_RESPONSE_METERING_CONTRACT = {
186
+ headers: {
187
+ payload: "x-fs-metering",
188
+ signature: "x-fs-metering-sig",
189
+ token: "x-fs-metering-token"
190
+ },
191
+ token: {
192
+ environmentVariable: "FS_RUNTIME_TOKEN",
193
+ presentation: "x-fs-metering-token",
194
+ storage: "sha256-hash-only"
195
+ },
196
+ signature: {
197
+ algorithm: "HMAC-SHA256",
198
+ encoding: "base64url",
199
+ input: "payload-json",
200
+ secret: "presented-runtime-token"
201
+ },
202
+ payload: {
203
+ method: "string",
204
+ path: "string",
205
+ rawDimsUnits: "Record<string, number>",
206
+ measureContext: "Record<string, unknown>?",
207
+ creditUnitsConsumed: "Record<string, number>?"
208
+ },
209
+ errors: {
210
+ missingToken: "missing_token",
211
+ invalidMeterKey: "invalid_meter_key",
212
+ invalidMeterValue: "invalid_meter_value"
213
+ },
214
+ httpAdapter: {
215
+ input: "Request",
216
+ output: "Response",
217
+ networkCalls: false,
218
+ preserves: ["body", "headers", "status", "statusText"],
219
+ gatewayStripsInternalHeaders: true
220
+ }
184
221
  };
185
222
  var RUNTIME_HEALTH_CONTRACT = {
186
223
  endpoint: "/v1/runtime/health",
@@ -232,6 +269,7 @@ export {
232
269
  RUNTIME_HEALTH_CONTRACT,
233
270
  RUNTIME_METERING_CONTRACT,
234
271
  RUNTIME_REPLAY_CONTRACT,
272
+ RUNTIME_RESPONSE_METERING_CONTRACT,
235
273
  RUNTIME_SIGNING_CONTRACT,
236
274
  RUNTIME_TOKEN_CONTRACT,
237
275
  RUNTIME_TOKEN_ENV,
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ var STREAMING_EXEMPT_BODY_HASH = "STREAM";
30
30
  var MAX_BODY_BYTES = 10 * 1024 * 1024;
31
31
 
32
32
  // ../contracts/dist/runtime.js
33
+ var FS_RUNTIME_TOKEN_ENV2 = "FS_RUNTIME_TOKEN";
33
34
  var RUNTIME_TOKEN_PREFIXES2 = {
34
35
  live: "fsrt_live_",
35
36
  test: "fsrt_test_"
@@ -41,6 +42,16 @@ function runtimeTokenKind(token) {
41
42
  return "test";
42
43
  return null;
43
44
  }
45
+ var RESPONSE_METERING_HEADER_NAMES = {
46
+ payload: "x-fs-metering",
47
+ signature: "x-fs-metering-sig",
48
+ token: "x-fs-metering-token"
49
+ };
50
+ var RESPONSE_METERING_TOKEN_CONTRACT = {
51
+ environmentVariable: FS_RUNTIME_TOKEN_ENV2,
52
+ presentation: RESPONSE_METERING_HEADER_NAMES.token,
53
+ storage: "sha256-hash-only"
54
+ };
44
55
  var MAX_BODY_BYTES2 = 10 * 1024 * 1024;
45
56
  async function hashBody(body) {
46
57
  const bytes = new Uint8Array(body);
@@ -469,11 +480,19 @@ var MeteringClient = class {
469
480
  `meter '${meter}' is not in the token's allowedMeters`
470
481
  );
471
482
  }
472
- if (options.routeId && this.config.allowedRoutes.length > 0 && !this.config.allowedRoutes.includes(options.routeId)) {
473
- throw new FartherShoreError(
474
- "invalid_token",
475
- `route '${options.routeId}' is not in the token's allowedRoutes`
476
- );
483
+ if (this.config.allowedRoutes.length > 0) {
484
+ if (!options.routeId) {
485
+ throw new FartherShoreError(
486
+ "invalid_token",
487
+ "routeId is required because this runtime token is route-scoped"
488
+ );
489
+ }
490
+ if (!this.config.allowedRoutes.includes(options.routeId)) {
491
+ throw new FartherShoreError(
492
+ "invalid_token",
493
+ `route '${options.routeId}' is not in the token's allowedRoutes`
494
+ );
495
+ }
477
496
  }
478
497
  if (this.config.perEventMax > 0 && qty > this.config.perEventMax) {
479
498
  throw new FartherShoreError(
@@ -521,9 +540,6 @@ var MeteringClient = class {
521
540
  body: JSON.stringify(event)
522
541
  });
523
542
  if (response.ok) return true;
524
- if (response.status >= 400 && response.status < 500 && response.status !== 429) {
525
- return true;
526
- }
527
543
  } catch {
528
544
  }
529
545
  }
@@ -1029,8 +1045,8 @@ function headerGetter(headers) {
1029
1045
  }
1030
1046
 
1031
1047
  // src/core/runtime.ts
1032
- var DEFAULT_CORE_URL = "https://api.farthershore.com";
1033
- var SDK_VERSION = "0.1.0";
1048
+ var DEFAULT_CORE_URL = "https://core.farthershore.com";
1049
+ var SDK_VERSION = "0.2.0".length > 0 ? "0.2.0" : "0.0.0-dev";
1034
1050
  var FartherShore = class {
1035
1051
  bootstrapClient;
1036
1052
  fetchImpl;
@@ -1244,6 +1260,43 @@ var RUNTIME_ERROR_CODES = {
1244
1260
  missingToken: "missing_token",
1245
1261
  invalidToken: "invalid_token"
1246
1262
  };
1263
+ var RUNTIME_RESPONSE_METERING_CONTRACT = {
1264
+ headers: {
1265
+ payload: "x-fs-metering",
1266
+ signature: "x-fs-metering-sig",
1267
+ token: "x-fs-metering-token"
1268
+ },
1269
+ token: {
1270
+ environmentVariable: "FS_RUNTIME_TOKEN",
1271
+ presentation: "x-fs-metering-token",
1272
+ storage: "sha256-hash-only"
1273
+ },
1274
+ signature: {
1275
+ algorithm: "HMAC-SHA256",
1276
+ encoding: "base64url",
1277
+ input: "payload-json",
1278
+ secret: "presented-runtime-token"
1279
+ },
1280
+ payload: {
1281
+ method: "string",
1282
+ path: "string",
1283
+ rawDimsUnits: "Record<string, number>",
1284
+ measureContext: "Record<string, unknown>?",
1285
+ creditUnitsConsumed: "Record<string, number>?"
1286
+ },
1287
+ errors: {
1288
+ missingToken: "missing_token",
1289
+ invalidMeterKey: "invalid_meter_key",
1290
+ invalidMeterValue: "invalid_meter_value"
1291
+ },
1292
+ httpAdapter: {
1293
+ input: "Request",
1294
+ output: "Response",
1295
+ networkCalls: false,
1296
+ preserves: ["body", "headers", "status", "statusText"],
1297
+ gatewayStripsInternalHeaders: true
1298
+ }
1299
+ };
1247
1300
 
1248
1301
  // src/adapters/express.ts
1249
1302
  var STREAMING_CONTENT_TYPES = new Set(
@@ -1311,24 +1364,13 @@ function headerValue(headers, name) {
1311
1364
  return value;
1312
1365
  }
1313
1366
 
1314
- // src/generated/metering-contract.ts
1315
- var METERING_HEADERS = {
1316
- payload: "x-fs-metering",
1317
- signature: "x-fs-metering-sig",
1318
- token: "x-fs-metering-token"
1319
- };
1320
- var METERING_TOKEN_ENV = "FARTHERSHORE_METERING_TOKEN";
1321
- var METERING_ERROR_CODES = {
1322
- missingToken: "missing_token",
1323
- invalidMeterKey: "invalid_meter_key",
1324
- invalidMeterValue: "invalid_meter_value"
1325
- };
1326
-
1327
- // src/legacy/metering.ts
1328
- var METERING_PAYLOAD_HEADER = METERING_HEADERS.payload;
1329
- var METERING_SIGNATURE_HEADER = METERING_HEADERS.signature;
1330
- var METERING_TOKEN_HEADER = METERING_HEADERS.token;
1331
- var DEFAULT_TOKEN_ENV = METERING_TOKEN_ENV;
1367
+ // src/response-metering.ts
1368
+ var RESPONSE_METERING_HEADERS = RUNTIME_RESPONSE_METERING_CONTRACT.headers;
1369
+ var RESPONSE_METERING_ERROR_CODES = RUNTIME_RESPONSE_METERING_CONTRACT.errors;
1370
+ var METERING_PAYLOAD_HEADER = RESPONSE_METERING_HEADERS.payload;
1371
+ var METERING_SIGNATURE_HEADER = RESPONSE_METERING_HEADERS.signature;
1372
+ var METERING_TOKEN_HEADER = RESPONSE_METERING_HEADERS.token;
1373
+ var DEFAULT_TOKEN_ENV = RUNTIME_RESPONSE_METERING_CONTRACT.token.environmentVariable;
1332
1374
  var MeteringError = class extends Error {
1333
1375
  code;
1334
1376
  constructor(code, message) {
@@ -1344,8 +1386,8 @@ function createUsage(request, options = {}) {
1344
1386
  usage[assertMeterKey(meter)] = assertMeterValue(meter, value);
1345
1387
  return reporter;
1346
1388
  },
1347
- async wrap(response) {
1348
- return signResponse(request, response, usage, options);
1389
+ async wrap(response, wrapOptions = {}) {
1390
+ return signResponse(request, response, usage, options, wrapOptions);
1349
1391
  }
1350
1392
  };
1351
1393
  return reporter;
@@ -1357,9 +1399,9 @@ async function withUsage(request, response, usage, options = {}) {
1357
1399
  }
1358
1400
  return reporter.wrap(response);
1359
1401
  }
1360
- async function signResponse(request, response, usage, options) {
1402
+ async function signResponse(request, response, usage, options, wrapOptions) {
1361
1403
  const token = resolveToken(options);
1362
- const payload = buildPayload(request, usage);
1404
+ const payload = buildPayload(request, usage, options, wrapOptions);
1363
1405
  const signature = await signPayload(payload, token);
1364
1406
  const headers = new Headers(response.headers);
1365
1407
  headers.set(METERING_PAYLOAD_HEADER, payload);
@@ -1371,12 +1413,20 @@ async function signResponse(request, response, usage, options) {
1371
1413
  headers
1372
1414
  });
1373
1415
  }
1374
- function buildPayload(request, usage) {
1416
+ function buildPayload(request, usage, options, wrapOptions) {
1375
1417
  const url = new URL(request.url);
1418
+ const measureContext = wrapOptions.measureContext ?? options.measureContext;
1419
+ const creditUnitsConsumed = wrapOptions.creditUnitsConsumed ?? options.creditUnitsConsumed;
1376
1420
  const payload = {
1377
1421
  method: request.method.toUpperCase(),
1378
1422
  path: url.pathname,
1379
- rawDimsUnits: sortUsage(usage)
1423
+ rawDimsUnits: sortUsage(usage),
1424
+ ...measureContext ? { measureContext } : {},
1425
+ ...creditUnitsConsumed ? {
1426
+ creditUnitsConsumed: sortUsage(
1427
+ validateUsageMap(creditUnitsConsumed, "creditUnitsConsumed")
1428
+ )
1429
+ } : {}
1380
1430
  };
1381
1431
  return JSON.stringify(payload);
1382
1432
  }
@@ -1385,10 +1435,18 @@ function sortUsage(usage) {
1385
1435
  Object.entries(usage).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
1386
1436
  );
1387
1437
  }
1438
+ function validateUsageMap(usage, label) {
1439
+ return Object.fromEntries(
1440
+ Object.entries(usage).map(([meter, value]) => [
1441
+ assertMeterKey(meter),
1442
+ assertMeterValue(`${label}.${meter}`, value)
1443
+ ])
1444
+ );
1445
+ }
1388
1446
  function assertMeterKey(meter) {
1389
1447
  if (!/^[a-z0-9_]{1,64}$/.test(meter)) {
1390
1448
  throw new MeteringError(
1391
- METERING_ERROR_CODES.invalidMeterKey,
1449
+ RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
1392
1450
  `meter key "${meter}" must be lowercase alphanumeric with underscores`
1393
1451
  );
1394
1452
  }
@@ -1397,7 +1455,7 @@ function assertMeterKey(meter) {
1397
1455
  function assertMeterValue(meter, value) {
1398
1456
  if (!Number.isFinite(value) || value < 0) {
1399
1457
  throw new MeteringError(
1400
- METERING_ERROR_CODES.invalidMeterValue,
1458
+ RESPONSE_METERING_ERROR_CODES.invalidMeterValue,
1401
1459
  `meter "${meter}" value must be a non-negative finite number`
1402
1460
  );
1403
1461
  }
@@ -1407,7 +1465,7 @@ function resolveToken(options) {
1407
1465
  const token = options.token ?? options.env?.[DEFAULT_TOKEN_ENV] ?? processEnv(DEFAULT_TOKEN_ENV);
1408
1466
  if (!token) {
1409
1467
  throw new MeteringError(
1410
- METERING_ERROR_CODES.missingToken,
1468
+ RESPONSE_METERING_ERROR_CODES.missingToken,
1411
1469
  `${DEFAULT_TOKEN_ENV} is required to sign Farther Shore metering reports`
1412
1470
  );
1413
1471
  }
@@ -2,7 +2,7 @@ import { type RuntimeBootstrapRequest, type RuntimeBootstrapResponse } from "../
2
2
  /** Where to reach core's bootstrap endpoint. Derived from the token's coreUrl. */
3
3
  export type BootstrapClientOptions = {
4
4
  runtimeToken: string;
5
- /** Core base URL, e.g. https://api.farthershore.com. */
5
+ /** Core base URL, e.g. https://core.farthershore.com. */
6
6
  coreUrl: string;
7
7
  /** Optional bootstrap request metadata. */
8
8
  request?: RuntimeBootstrapRequest;
@@ -20,7 +20,7 @@ export type FartherShoreInitOptions = {
20
20
  runtimeToken?: string;
21
21
  /**
22
22
  * Core base URL. Defaults to FS_CORE_URL / FARTHERSHORE_CORE_URL or
23
- * https://api.farthershore.com.
23
+ * https://core.farthershore.com.
24
24
  */
25
25
  coreUrl?: string;
26
26
  /** Env map (tests). Defaults to process.env. */
@@ -39,6 +39,7 @@ export type FartherShoreInitOptions = {
39
39
  /** SDK metadata forwarded to bootstrap. */
40
40
  instanceId?: string;
41
41
  };
42
+ export declare const SDK_VERSION: string;
42
43
  /**
43
44
  * The runtime instance. Lazily bootstraps; holds the JWKS client, nonce cache,
44
45
  * metering buffer, and shutdown hooks.
@@ -156,7 +156,44 @@ export declare const RUNTIME_METERING_CONTRACT: {
156
156
  readonly delivery: "at-least-once";
157
157
  readonly billingOnly: true;
158
158
  readonly realtimeEnforced: false;
159
- readonly trustModel: "backend-reported values are NOT cryptographically attested; a buggy or compromised backend 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.";
159
+ readonly 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.";
160
+ };
161
+ export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
162
+ readonly headers: {
163
+ readonly payload: "x-fs-metering";
164
+ readonly signature: "x-fs-metering-sig";
165
+ readonly token: "x-fs-metering-token";
166
+ };
167
+ readonly token: {
168
+ readonly environmentVariable: "FS_RUNTIME_TOKEN";
169
+ readonly presentation: "x-fs-metering-token";
170
+ readonly storage: "sha256-hash-only";
171
+ };
172
+ readonly signature: {
173
+ readonly algorithm: "HMAC-SHA256";
174
+ readonly encoding: "base64url";
175
+ readonly input: "payload-json";
176
+ readonly secret: "presented-runtime-token";
177
+ };
178
+ readonly payload: {
179
+ readonly method: "string";
180
+ readonly path: "string";
181
+ readonly rawDimsUnits: "Record<string, number>";
182
+ readonly measureContext: "Record<string, unknown>?";
183
+ readonly creditUnitsConsumed: "Record<string, number>?";
184
+ };
185
+ readonly errors: {
186
+ readonly missingToken: "missing_token";
187
+ readonly invalidMeterKey: "invalid_meter_key";
188
+ readonly invalidMeterValue: "invalid_meter_value";
189
+ };
190
+ readonly httpAdapter: {
191
+ readonly input: "Request";
192
+ readonly output: "Response";
193
+ readonly networkCalls: false;
194
+ readonly preserves: readonly ["body", "headers", "status", "statusText"];
195
+ readonly gatewayStripsInternalHeaders: true;
196
+ };
160
197
  };
161
198
  export declare const RUNTIME_HEALTH_CONTRACT: {
162
199
  readonly endpoint: "/v1/runtime/health";
@@ -16,7 +16,7 @@ export { createExpressMiddleware, type ExpressMiddleware, type ExpressRequestLik
16
16
  export { FS_RUNTIME_TOKEN_ENV, RUNTIME_TOKEN_PREFIXES, RUNTIME_TOKEN_CAPABILITIES, RUNTIME_HEADER_NAMES, RUNTIME_CLOCK_SKEW_SECONDS, RUNTIME_REPLAY_WINDOW_SECONDS, EMPTY_BODY_SHA256, STREAMING_EXEMPT_BODY_HASH, MAX_BODY_BYTES, type RuntimeErrorCode, type RuntimeTokenCapability, type CanonicalSigningInput, type RuntimeBootstrapResponse, type RuntimeMeteringEvent, type RuntimeHealthReport, type TransportMode, } from "./runtime-types.js";
17
17
  export { RUNTIME_ERROR_CODES } from "./generated/runtime-contract.js";
18
18
  export { hashBody, buildCanonicalSigningString, canonicalizeQuery, signCanonicalString, verifyCanonicalSignature, runtimeTokenKind, } from "./runtime-signing.js";
19
- export { createUsage, withUsage, MeteringError, METERING_PAYLOAD_HEADER, METERING_SIGNATURE_HEADER, METERING_TOKEN_HEADER, DEFAULT_TOKEN_ENV, type UsageMap, type UsageReporter, type MeteringOptions, } from "./legacy/metering.js";
19
+ export { createUsage, withUsage, MeteringError, METERING_PAYLOAD_HEADER, METERING_SIGNATURE_HEADER, METERING_TOKEN_HEADER, DEFAULT_TOKEN_ENV, type UsageMap, type UsageReporter, type MeteringOptions, } from "./response-metering.js";
20
20
  /**
21
21
  * The conceptual public entrypoint. `fartherShore.initFromEnv()` mirrors the
22
22
  * language-neutral spec. The returned instance is augmented with `middleware()`
@@ -0,0 +1,32 @@
1
+ declare const RESPONSE_METERING_ERROR_CODES: {
2
+ readonly missingToken: "missing_token";
3
+ readonly invalidMeterKey: "invalid_meter_key";
4
+ readonly invalidMeterValue: "invalid_meter_value";
5
+ };
6
+ type ResponseMeteringErrorCode = (typeof RESPONSE_METERING_ERROR_CODES)[keyof typeof RESPONSE_METERING_ERROR_CODES];
7
+ export declare const METERING_PAYLOAD_HEADER: "x-fs-metering";
8
+ export declare const METERING_SIGNATURE_HEADER: "x-fs-metering-sig";
9
+ export declare const METERING_TOKEN_HEADER: "x-fs-metering-token";
10
+ export declare const DEFAULT_TOKEN_ENV: "FS_RUNTIME_TOKEN";
11
+ export type UsageMap = Record<string, number>;
12
+ export type MeteringOptions = {
13
+ token?: string;
14
+ env?: Record<string, string | undefined>;
15
+ measureContext?: Record<string, unknown>;
16
+ creditUnitsConsumed?: UsageMap;
17
+ };
18
+ export type UsageWrapOptions = {
19
+ measureContext?: Record<string, unknown>;
20
+ creditUnitsConsumed?: UsageMap;
21
+ };
22
+ export type UsageReporter = {
23
+ report(meter: string, value: number): UsageReporter;
24
+ wrap(response: Response, options?: UsageWrapOptions): Promise<Response>;
25
+ };
26
+ export declare class MeteringError extends Error {
27
+ readonly code: ResponseMeteringErrorCode;
28
+ constructor(code: ResponseMeteringErrorCode, message: string);
29
+ }
30
+ export declare function createUsage(request: Request, options?: MeteringOptions): UsageReporter;
31
+ export declare function withUsage(request: Request, response: Response, usage: UsageMap, options?: MeteringOptions): Promise<Response>;
32
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@farthershore/backend",
3
- "version": "0.1.0",
4
- "description": "Farther Shore backend SDK: secure BYO-backend runtime fail-closed gateway request verification, metering, health, and lifecycle, all derived from FS_RUNTIME_TOKEN",
3
+ "version": "0.2.0",
4
+ "description": "Farther Shore backend SDK for builder upstreams: signed response usage, fail-closed gateway request verification, health, and lifecycle from FS_RUNTIME_TOKEN",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -1,36 +0,0 @@
1
- export declare const METERING_CONTRACT_VERSION: 1;
2
- export declare const METERING_HEADERS: {
3
- readonly payload: "x-fs-metering";
4
- readonly signature: "x-fs-metering-sig";
5
- readonly token: "x-fs-metering-token";
6
- };
7
- export declare const METERING_TOKEN_ENV: "FARTHERSHORE_METERING_TOKEN";
8
- export declare const METERING_TOKEN_CONTRACT: {
9
- readonly environmentVariable: "FARTHERSHORE_METERING_TOKEN";
10
- readonly presentation: "x-fs-metering-token";
11
- readonly storage: "sha256-hash-only";
12
- };
13
- export declare const METERING_SIGNATURE_CONTRACT: {
14
- readonly algorithm: "HMAC-SHA256";
15
- readonly encoding: "base64url";
16
- readonly input: "payload-json";
17
- readonly secret: "presented-metering-token";
18
- };
19
- export declare const METERING_ERROR_CODES: {
20
- readonly missingToken: "missing_token";
21
- readonly invalidMeterKey: "invalid_meter_key";
22
- readonly invalidMeterValue: "invalid_meter_value";
23
- };
24
- export type MeteringErrorCode = (typeof METERING_ERROR_CODES)[keyof typeof METERING_ERROR_CODES];
25
- export declare const METERING_HTTP_ADAPTER_CONTRACT: {
26
- readonly input: "Request";
27
- readonly output: "Response";
28
- readonly networkCalls: false;
29
- readonly preserves: readonly ["body", "headers", "status", "statusText"];
30
- readonly gatewayStripsInternalHeaders: true;
31
- };
32
- export type MeteringUsagePayload = {
33
- method: string;
34
- path: string;
35
- rawDimsUnits: Record<string, number>;
36
- };
@@ -1,20 +0,0 @@
1
- import { type MeteringErrorCode } from "../generated/metering-contract.js";
2
- export declare const METERING_PAYLOAD_HEADER: "x-fs-metering";
3
- export declare const METERING_SIGNATURE_HEADER: "x-fs-metering-sig";
4
- export declare const METERING_TOKEN_HEADER: "x-fs-metering-token";
5
- export declare const DEFAULT_TOKEN_ENV: "FARTHERSHORE_METERING_TOKEN";
6
- export type UsageMap = Record<string, number>;
7
- export type MeteringOptions = {
8
- token?: string;
9
- env?: Record<string, string | undefined>;
10
- };
11
- export type UsageReporter = {
12
- report(meter: string, value: number): UsageReporter;
13
- wrap(response: Response): Promise<Response>;
14
- };
15
- export declare class MeteringError extends Error {
16
- readonly code: MeteringErrorCode;
17
- constructor(code: MeteringErrorCode, message: string);
18
- }
19
- export declare function createUsage(request: Request, options?: MeteringOptions): UsageReporter;
20
- export declare function withUsage(request: Request, response: Response, usage: UsageMap, options?: MeteringOptions): Promise<Response>;