@farthershore/backend 0.19.0 → 0.21.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.
@@ -24,6 +24,7 @@ function statusForCode(code) {
24
24
  }
25
25
 
26
26
  // src/core/permissions.ts
27
+ var WILDCARD = "*";
27
28
  var FartherShorePermissionError = class extends Error {
28
29
  code = "permission_denied";
29
30
  status = 403;
@@ -35,6 +36,31 @@ var FartherShorePermissionError = class extends Error {
35
36
  this.requiredPermission = requiredPermission;
36
37
  }
37
38
  };
39
+ function permissionSatisfies(required, granted) {
40
+ if (granted === void 0) return true;
41
+ if (granted.includes(WILDCARD)) return true;
42
+ if (granted.includes(required)) return true;
43
+ const idx = required.indexOf(":");
44
+ if (idx > 0 && idx < required.length - 1) {
45
+ const subject = required.slice(0, idx);
46
+ if (subject !== WILDCARD && granted.includes(`${subject}:${WILDCARD}`))
47
+ return true;
48
+ if (required.slice(idx + 1) === WILDCARD && subject !== WILDCARD) {
49
+ const prefix = `${subject}:`;
50
+ return granted.some((permission) => permission.startsWith(prefix));
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+ function hasPermission(ctx, key) {
56
+ if (ctx.permissions === void 0) return false;
57
+ return permissionSatisfies(key, ctx.permissions);
58
+ }
59
+ function requirePermission(ctx, key) {
60
+ if (!hasPermission(ctx, key)) {
61
+ throw new FartherShorePermissionError(key);
62
+ }
63
+ }
38
64
 
39
65
  // src/generated/runtime-contract.ts
40
66
  var RUNTIME_BODY_HASH_CONTRACT = {
@@ -73,14 +99,17 @@ async function runMiddleware(fs, options, req, res, next) {
73
99
  const contentType = headerValue(req.headers, "content-type");
74
100
  const streamingExempt = isStreamingExempt(contentType);
75
101
  const body = streamingExempt ? null : extractRawBody(req);
76
- const ctx = await fs.verifyRequest({
77
- method: req.method,
78
- path,
79
- query,
80
- headers: req.headers,
81
- body,
82
- streamingExempt
83
- });
102
+ const ctx = await fs.verifyRequest(
103
+ {
104
+ method: req.method,
105
+ path,
106
+ query,
107
+ headers: req.headers,
108
+ body,
109
+ streamingExempt
110
+ },
111
+ { responseSink: expressResponseSink(res) }
112
+ );
84
113
  req.fartherShore = ctx;
85
114
  stripFartherShoreHeaders(req);
86
115
  next();
@@ -88,6 +117,16 @@ async function runMiddleware(fs, options, req, res, next) {
88
117
  fail(res, error, options, req);
89
118
  }
90
119
  }
120
+ function expressResponseSink(res) {
121
+ return {
122
+ canStampHeaders: () => res.headersSent !== true,
123
+ stampHeaders: (headers) => {
124
+ for (const [name, value] of Object.entries(headers)) {
125
+ res.setHeader(name, value);
126
+ }
127
+ }
128
+ };
129
+ }
91
130
  function fail(res, error, options, req) {
92
131
  const code = error instanceof FartherShoreError ? error.code : "bad_signature";
93
132
  const status = error instanceof FartherShoreError ? error.status : 401;
@@ -128,7 +167,12 @@ function stripFartherShoreHeaders(req) {
128
167
  withRaw.rawHeaders = cleaned;
129
168
  }
130
169
  }
131
- function createExpressHandler(handler) {
170
+ function createExpressHandler(optionsOrHandler, maybeHandler) {
171
+ const options = typeof optionsOrHandler === "function" ? {} : optionsOrHandler;
172
+ const handler = typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler;
173
+ if (typeof handler !== "function") {
174
+ throw new TypeError("fs.handler(options, cb) requires a handler callback");
175
+ }
132
176
  return (req, res, next) => {
133
177
  const ctx = req.fartherShore;
134
178
  if (!ctx) {
@@ -139,10 +183,22 @@ function createExpressHandler(handler) {
139
183
  res.status(401).json({ error: "principal_required" });
140
184
  return;
141
185
  }
186
+ if (!ctx.signedContext) {
187
+ res.status(401).json({ error: "context_unverified" });
188
+ return;
189
+ }
142
190
  const verified = ctx;
143
- void Promise.resolve().then(
144
- () => handler(verified, req, res, next)
145
- ).catch((error) => failHandler(res, next, error));
191
+ void Promise.resolve().then(() => {
192
+ if (options.permission !== void 0) {
193
+ requirePermission(verified, options.permission);
194
+ }
195
+ return handler(
196
+ verified,
197
+ req,
198
+ res,
199
+ next
200
+ );
201
+ }).catch((error) => failHandler(res, next, error));
146
202
  };
147
203
  }
148
204
  function failHandler(res, next, error) {
@@ -1,154 +1,6 @@
1
1
  import { createRequire as __createRequire } from "node:module";const require=__createRequire(import.meta.url);
2
2
 
3
3
  // src/generated/runtime-contract.ts
4
- var RUNTIME_CONTRACT_VERSION = 1;
5
- var RUNTIME_TOKEN_ENV = "FS_RUNTIME_TOKEN";
6
- var RUNTIME_TOKEN_CONTRACT = {
7
- environmentVariable: "FS_RUNTIME_TOKEN",
8
- prefixes: {
9
- live: "fsrt_live_",
10
- test: "fsrt_test_"
11
- },
12
- opaque: true,
13
- storage: "sha256-hash-only",
14
- lastFour: true,
15
- capabilities: ["gateway_verification", "metering", "health", "tunnel"]
16
- };
17
- var RUNTIME_BOOTSTRAP_CONTRACT = {
18
- method: "POST",
19
- path: "/v1/runtime/bootstrap",
20
- authorization: "Bearer fsrt_...",
21
- request: {
22
- instanceId: "string?",
23
- sdkVersion: "string?",
24
- sdkLanguage: "string?"
25
- },
26
- response: {
27
- product: {
28
- id: "string",
29
- slug: "string"
30
- },
31
- backend: {
32
- id: "string",
33
- slug: "string",
34
- name: "string"
35
- },
36
- environment: {
37
- id: "string?",
38
- kind: "live | test"
39
- },
40
- capabilities: "string[]",
41
- verification: {
42
- required: "boolean",
43
- jwksUrl: "string",
44
- clockSkewSeconds: "number",
45
- replayWindowSeconds: "number",
46
- headerNames: "object"
47
- },
48
- metering: {
49
- enabled: "boolean",
50
- endpoint: "string",
51
- credential: "string",
52
- allowedMeters: "string[]",
53
- allowedRoutes: "string[]",
54
- perEventMax: "number"
55
- },
56
- transport: {
57
- mode: "direct | tunnel",
58
- runner: "embedded | sidecar | null",
59
- originUrl: "string?",
60
- originHostname: "string?",
61
- localTarget: "string?",
62
- cloudflared: "object?"
63
- },
64
- routes: "object[]",
65
- policyVersion: "string",
66
- refreshAfterSeconds: "number"
67
- }
68
- };
69
- var RUNTIME_SIGNING_CONTRACT = {
70
- algorithm: "Ed25519",
71
- encoding: "base64url",
72
- keyMaterial: "service-jwt-jwks",
73
- canonicalString: {
74
- description: "Byte-exact, language-neutral serialization of the signed claim set. Fields are emitted in the FIXED order below, one per line, each as `name:value`, joined by a single newline (\\n, U+000A). NO trailing newline. The serialization depends on neither JSON key ordering nor any Node.js Buffer/serialization detail \u2014 only UTF-8 byte encoding of the field values. Empty/absent values are emitted as the empty string after the colon. Go/Python/Java/Rust reproduce this identically.",
75
- fieldSeparator: "\n",
76
- keyValueSeparator: ":",
77
- trailingNewline: false,
78
- fieldEncoding: "utf-8",
79
- fields: [
80
- "method",
81
- "path",
82
- "query",
83
- "body-hash",
84
- "request-id",
85
- "timestamp",
86
- "business-id",
87
- "backend-id",
88
- "route-id",
89
- "policy-version",
90
- "context-hash"
91
- ],
92
- fieldRules: {
93
- method: "Uppercased HTTP method (e.g. GET, POST).",
94
- path: "Request path, percent-encoded as received, no host, no query string. Always begins with '/'.",
95
- query: "The request's query string bytes VERBATIM, after the caller has stripped the URL's single leading '?' delimiter. Use the RAW wire query exactly as received \u2014 NO sorting, NO filtering, NO re-stripping a leading '?', NO dropping empty pairs, NO re-encoding. The signature binds to the exact query bytes: any reorder, added/removed/relocated pair, or re-encoding changes the bytes and MUST fail verification. Every SDK verifier MUST read the raw wire query (never a re-parsed/re-serialized form, which could reorder or re-encode and would then fail legitimate requests). Any normalization (sorting by name or value, dropping/moving empty pairs, re-stripping '?') is FORBIDDEN \u2014 each collapses distinct wire queries under one signature. Empty string when there is no query.",
96
- "body-hash": "Lowercase hex SHA-256 of the RAW request body bytes. For an empty body, the SHA-256 of zero bytes (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Streaming-exempt requests use the literal token 'STREAM'.",
97
- "request-id": "Opaque unique request id minted by the gateway (also the replay-cache nonce).",
98
- timestamp: "Integer Unix epoch seconds (UTC) at signing time, as a base-10 string with no padding.",
99
- "business-id": "Business id the request is routed to.",
100
- "backend-id": "Backend id the route binds to.",
101
- "route-id": "Resolved route id; empty string if the route is unresolved.",
102
- "policy-version": "Tenant artifact / policy version the gateway signed under.",
103
- "context-hash": "Lowercase hex SHA-256 of the presented X-Fs-Context JWT string (identity-context binding). For a request with no signed context, the SHA-256 of the empty string (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Binds the identity context into the request signature so one verification covers both."
104
- }
105
- }
106
- };
107
- var RUNTIME_CANONICAL_FIELDS = [
108
- "method",
109
- "path",
110
- "query",
111
- "body-hash",
112
- "request-id",
113
- "timestamp",
114
- "business-id",
115
- "backend-id",
116
- "route-id",
117
- "policy-version",
118
- "context-hash"
119
- ];
120
- var RUNTIME_BODY_HASH_CONTRACT = {
121
- algorithm: "SHA-256",
122
- encoding: "hex-lower",
123
- source: "raw-request-bytes",
124
- emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
125
- maxBodyBytes: 10485760,
126
- streamingExemptToken: "STREAM",
127
- streamingExemptContentTypes: [
128
- "text/event-stream",
129
- "application/octet-stream",
130
- "multipart/form-data"
131
- ],
132
- overMaxStatus: 413
133
- };
134
- var RUNTIME_HEADERS = {
135
- signature: "x-fs-signature",
136
- keyId: "x-fs-key-id",
137
- requestId: "x-fs-request-id",
138
- timestamp: "x-fs-timestamp",
139
- businessId: "x-fs-business-id",
140
- backendId: "x-fs-backend-id",
141
- routeId: "x-fs-route-id",
142
- policyVersion: "x-fs-policy-version",
143
- bodyHash: "x-fs-body-hash"
144
- };
145
- var RUNTIME_REPLAY_CONTRACT = {
146
- windowSeconds: 300,
147
- clockSkewSeconds: 5,
148
- nonce: "x-fs-request-id",
149
- nonceCache: "bounded-lru",
150
- policy: "fail-closed"
151
- };
152
4
  var RUNTIME_ERROR_CODES = {
153
5
  missingSignature: "missing_signature",
154
6
  malformedSignature: "malformed_signature",
@@ -169,40 +21,19 @@ var RUNTIME_ERROR_CODES = {
169
21
  serviceSubjectRequired: "service_subject_required",
170
22
  surfaceNotAllowed: "surface_not_allowed"
171
23
  };
172
- var RUNTIME_METERING_CONTRACT = {
173
- endpoint: "/v1/metering/events",
174
- method: "POST",
175
- credential: "reusable-bearer",
176
- event: {
177
- event_id: "string",
178
- business_id: "string",
179
- backend_id: "string",
180
- route_id: "string?",
181
- request_id: "string?",
182
- requestId: "string?",
183
- subscriptionId: "string",
184
- nonce: "string?",
185
- meter: "string",
186
- qty: "number",
187
- timestamp: "string"
188
- },
189
- postStreamEvent: {
190
- requestId: "string",
191
- subscriptionId: "string?",
192
- nonce: "string",
193
- meters: "Record<string, number>",
194
- creditUnitsConsumed: "Record<string, number>?",
195
- measureContext: "Record<string, unknown>?",
196
- signature: "string"
197
- },
198
- idempotencyKey: "event_id",
199
- delivery: "at-least-once",
200
- billingOnly: true,
201
- realtimeEnforced: false,
202
- postStreamBillingOnly: true,
203
- postStreamRealtimeEnforced: false,
204
- 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.",
205
- 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."
24
+ var RUNTIME_BODY_HASH_CONTRACT = {
25
+ algorithm: "SHA-256",
26
+ encoding: "hex-lower",
27
+ source: "raw-request-bytes",
28
+ emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
29
+ maxBodyBytes: 10485760,
30
+ streamingExemptToken: "STREAM",
31
+ streamingExemptContentTypes: [
32
+ "text/event-stream",
33
+ "application/octet-stream",
34
+ "multipart/form-data"
35
+ ],
36
+ overMaxStatus: 413
206
37
  };
207
38
  var RUNTIME_RESPONSE_METERING_CONTRACT = {
208
39
  headers: {
@@ -224,14 +55,18 @@ var RUNTIME_RESPONSE_METERING_CONTRACT = {
224
55
  payload: {
225
56
  method: "string",
226
57
  path: "string",
227
- rawDimsUnits: "Record<string, number>",
58
+ rawDimsUnits: "Record<string, number>?",
228
59
  measureContext: "Record<string, unknown>?",
229
- creditUnitsConsumed: "Record<string, number>?"
60
+ creditUnitsConsumed: "Record<string, number>?",
61
+ measurementsVersion: "1?",
62
+ measurements: "Array<{ meter: string; values: Record<string, number>; dims?: Record<string, string> }>?",
63
+ quote: "{ currency: string; amountNanos: string }?"
230
64
  },
231
65
  errors: {
232
66
  missingToken: "missing_token",
233
67
  invalidMeterKey: "invalid_meter_key",
234
- invalidMeterValue: "invalid_meter_value"
68
+ invalidMeterValue: "invalid_meter_value",
69
+ invalidQuote: "invalid_quote"
235
70
  },
236
71
  httpAdapter: {
237
72
  input: "Request",
@@ -241,58 +76,8 @@ var RUNTIME_RESPONSE_METERING_CONTRACT = {
241
76
  gatewayStripsInternalHeaders: true
242
77
  }
243
78
  };
244
- var RUNTIME_HEALTH_CONTRACT = {
245
- endpoint: "/v1/runtime/health",
246
- method: "POST",
247
- request: {
248
- instanceId: "string?",
249
- status: "starting | ready | degraded | stopping"
250
- },
251
- readinessStates: ["UNKNOWN", "WAITING", "READY", "DEGRADED", "OFFLINE"],
252
- checks: [
253
- "runtime_token_valid",
254
- "bootstrapped",
255
- "tunnel_running",
256
- "signed_request_2xx",
257
- "unsigned_request_401",
258
- "stale_signature_401",
259
- "wrong_route_401",
260
- "metering_observed"
261
- ],
262
- report: {
263
- runtimeToken: "boolean",
264
- bootstrap: "boolean",
265
- tunnel: "string?",
266
- verification: "boolean",
267
- metering: "boolean"
268
- }
269
- };
270
- var RUNTIME_TRANSPORT_CONTRACT = {
271
- modes: {
272
- direct: "Gateway fetches the builder's public origin URL; the SDK middleware fail-closed-verifies every request via Ed25519 request signing. Provisions zero Cloudflare objects. Available on all tiers; also the dev path.",
273
- tunnel: "Farther Shore provisions a private outbound Cloudflare Tunnel; no inbound port. The Production-secure tier. Consumes Cloudflare tunnel/route slots."
274
- },
275
- runners: {
276
- embedded: "fs.start() supervises cloudflared as a child process (default DX).",
277
- sidecar: "Vanilla cloudflare/cloudflared container beside the app (production / non-Node)."
278
- },
279
- channelTrust: ["tunnel"],
280
- requestTrust: "x-fs-signature",
281
- invariant: "Channel trust (tunnel) and request trust (the X-FS-* signature) are distinct layers; both always apply. CF-Access-* headers are transport-layer only and are IGNORED by the SDK."
282
- };
283
79
  export {
284
80
  RUNTIME_BODY_HASH_CONTRACT,
285
- RUNTIME_BOOTSTRAP_CONTRACT,
286
- RUNTIME_CANONICAL_FIELDS,
287
- RUNTIME_CONTRACT_VERSION,
288
81
  RUNTIME_ERROR_CODES,
289
- RUNTIME_HEADERS,
290
- RUNTIME_HEALTH_CONTRACT,
291
- RUNTIME_METERING_CONTRACT,
292
- RUNTIME_REPLAY_CONTRACT,
293
- RUNTIME_RESPONSE_METERING_CONTRACT,
294
- RUNTIME_SIGNING_CONTRACT,
295
- RUNTIME_TOKEN_CONTRACT,
296
- RUNTIME_TOKEN_ENV,
297
- RUNTIME_TRANSPORT_CONTRACT
82
+ RUNTIME_RESPONSE_METERING_CONTRACT
298
83
  };