@farthershore/backend 0.12.0 → 0.13.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 +66 -1
- package/dist/index.js +798 -19
- package/dist/testing/index.js +2716 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/response-metering.d.ts +45 -0
- package/dist/types/testing/devGateway.d.ts +36 -0
- package/dist/types/testing/devRuntime.d.ts +68 -0
- package/dist/types/testing/index.d.ts +8 -0
- package/dist/types/testing/keysFile.d.ts +30 -0
- package/dist/types/testing/personas.d.ts +104 -0
- package/dist/types/testing/prodGuard.d.ts +12 -0
- package/dist/types/testing/signers.d.ts +89 -0
- package/dist/types/testing/traceSink.d.ts +67 -0
- package/dist/types/testing/usageSink.d.ts +31 -0
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1583,7 +1583,7 @@ function headerGetter(headers) {
|
|
|
1583
1583
|
|
|
1584
1584
|
// src/core/runtime.ts
|
|
1585
1585
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1586
|
-
var SDK_VERSION = "0.
|
|
1586
|
+
var SDK_VERSION = "0.13.0".length > 0 ? "0.13.0" : "0.0.0-dev";
|
|
1587
1587
|
var CONTRACTS_FP = "220bea90107ed396".length > 0 ? "220bea90107ed396" : "0000000000000000";
|
|
1588
1588
|
var FartherShore = class {
|
|
1589
1589
|
bootstrapClient;
|
|
@@ -1906,6 +1906,10 @@ function headerValue(headers, name) {
|
|
|
1906
1906
|
// src/response-metering.ts
|
|
1907
1907
|
var RESPONSE_METERING_HEADERS = RUNTIME_RESPONSE_METERING_CONTRACT.headers;
|
|
1908
1908
|
var RESPONSE_METERING_ERROR_CODES = RUNTIME_RESPONSE_METERING_CONTRACT.errors;
|
|
1909
|
+
var devMeteringHooks = null;
|
|
1910
|
+
function __setDevMeteringHooks(hooks) {
|
|
1911
|
+
devMeteringHooks = hooks;
|
|
1912
|
+
}
|
|
1909
1913
|
var METERING_PAYLOAD_HEADER = RESPONSE_METERING_HEADERS.payload;
|
|
1910
1914
|
var METERING_SIGNATURE_HEADER = RESPONSE_METERING_HEADERS.signature;
|
|
1911
1915
|
var METERING_TOKEN_HEADER = RESPONSE_METERING_HEADERS.token;
|
|
@@ -1939,19 +1943,60 @@ async function withUsage(request, response, usage, options = {}) {
|
|
|
1939
1943
|
return reporter.wrap(response);
|
|
1940
1944
|
}
|
|
1941
1945
|
async function signResponse(request, response, usage, options, wrapOptions) {
|
|
1942
|
-
const token = resolveToken(options);
|
|
1943
1946
|
const payload = buildPayload(request, usage, options, wrapOptions);
|
|
1944
|
-
const
|
|
1945
|
-
const headers =
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1947
|
+
const requestId = request.headers.get("x-fs-request-id") ?? void 0;
|
|
1948
|
+
const headers = await computeMeteringHeaders(payload, {
|
|
1949
|
+
...options.token !== void 0 ? { token: options.token } : {},
|
|
1950
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
1951
|
+
...requestId ? { requestId } : {},
|
|
1952
|
+
onSkip: () => {
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
if (Object.keys(headers).length === 0) {
|
|
1956
|
+
throw new MeteringError(
|
|
1957
|
+
RESPONSE_METERING_ERROR_CODES.missingToken,
|
|
1958
|
+
`${DEFAULT_TOKEN_ENV} is required to sign Farther Shore metering reports`
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
const merged = new Headers(response.headers);
|
|
1962
|
+
for (const [name, value] of Object.entries(headers)) merged.set(name, value);
|
|
1949
1963
|
return new Response(response.body, {
|
|
1950
1964
|
status: response.status,
|
|
1951
1965
|
statusText: response.statusText,
|
|
1952
|
-
headers
|
|
1966
|
+
headers: merged
|
|
1953
1967
|
});
|
|
1954
1968
|
}
|
|
1969
|
+
async function computeMeteringHeaders(payload, options = {}) {
|
|
1970
|
+
try {
|
|
1971
|
+
const token = resolveTokenSoft(options);
|
|
1972
|
+
if (!token) {
|
|
1973
|
+
skip(`${DEFAULT_TOKEN_ENV} is not set`, options);
|
|
1974
|
+
return {};
|
|
1975
|
+
}
|
|
1976
|
+
const json2 = JSON.stringify(payload);
|
|
1977
|
+
const signature = await signPayload(json2, token);
|
|
1978
|
+
devMeteringHooks?.record?.(payload, options.requestId);
|
|
1979
|
+
return {
|
|
1980
|
+
[METERING_PAYLOAD_HEADER]: json2,
|
|
1981
|
+
[METERING_SIGNATURE_HEADER]: signature,
|
|
1982
|
+
[METERING_TOKEN_HEADER]: token
|
|
1983
|
+
};
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
skip(error instanceof Error ? error.message : String(error), options);
|
|
1986
|
+
return {};
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
function skip(reason, options) {
|
|
1990
|
+
if (options.onSkip) {
|
|
1991
|
+
options.onSkip(reason);
|
|
1992
|
+
} else {
|
|
1993
|
+
console.warn(`metering headers skipped: ${reason}`);
|
|
1994
|
+
}
|
|
1995
|
+
devMeteringHooks?.onSkip?.(reason, options.requestId);
|
|
1996
|
+
}
|
|
1997
|
+
function resolveTokenSoft(options) {
|
|
1998
|
+
return options.token ?? options.env?.[DEFAULT_TOKEN_ENV] ?? processEnv(DEFAULT_TOKEN_ENV) ?? devMeteringHooks?.fallbackToken?.();
|
|
1999
|
+
}
|
|
1955
2000
|
function buildPayload(request, usage, options, wrapOptions) {
|
|
1956
2001
|
const url = new URL(request.url);
|
|
1957
2002
|
const measureContext = wrapOptions.measureContext ?? options.measureContext;
|
|
@@ -1971,7 +2016,7 @@ function buildPayload(request, usage, options, wrapOptions) {
|
|
|
1971
2016
|
...operationKey ? { operationKey: assertIdentifier(operationKey) } : {},
|
|
1972
2017
|
...usagePolicyId ? { usagePolicyId: assertIdentifier(usagePolicyId) } : {}
|
|
1973
2018
|
};
|
|
1974
|
-
return
|
|
2019
|
+
return payload;
|
|
1975
2020
|
}
|
|
1976
2021
|
function sortUsage(usage) {
|
|
1977
2022
|
return Object.fromEntries(
|
|
@@ -2013,16 +2058,6 @@ function assertIdentifier(value) {
|
|
|
2013
2058
|
}
|
|
2014
2059
|
return value;
|
|
2015
2060
|
}
|
|
2016
|
-
function resolveToken(options) {
|
|
2017
|
-
const token = options.token ?? options.env?.[DEFAULT_TOKEN_ENV] ?? processEnv(DEFAULT_TOKEN_ENV);
|
|
2018
|
-
if (!token) {
|
|
2019
|
-
throw new MeteringError(
|
|
2020
|
-
RESPONSE_METERING_ERROR_CODES.missingToken,
|
|
2021
|
-
`${DEFAULT_TOKEN_ENV} is required to sign Farther Shore metering reports`
|
|
2022
|
-
);
|
|
2023
|
-
}
|
|
2024
|
-
return token;
|
|
2025
|
-
}
|
|
2026
2061
|
function processEnv(key2) {
|
|
2027
2062
|
const maybeProcess = globalThis.process;
|
|
2028
2063
|
return maybeProcess?.env?.[key2];
|
|
@@ -2050,10 +2085,753 @@ function base64url(bytes) {
|
|
|
2050
2085
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2051
2086
|
}
|
|
2052
2087
|
|
|
2088
|
+
// src/testing/signers.ts
|
|
2089
|
+
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
2090
|
+
var TEST_KID = "fs-runtime-test-2026";
|
|
2091
|
+
var TEST_PRIVATE_JWK = {
|
|
2092
|
+
kty: "OKP",
|
|
2093
|
+
crv: "Ed25519",
|
|
2094
|
+
d: "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
|
|
2095
|
+
x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
|
|
2096
|
+
};
|
|
2097
|
+
var TEST_CONTEXT_SECRET = "fs-dev-context-secret-2026";
|
|
2098
|
+
var TEST_CONTEXT_KID = "fs-context-test-2026";
|
|
2099
|
+
async function makeSignedRequest(spec = {}) {
|
|
2100
|
+
const method = spec.method ?? "POST";
|
|
2101
|
+
const path = spec.path ?? "/v1/chat/completions";
|
|
2102
|
+
const query = spec.query ?? "";
|
|
2103
|
+
const body = spec.body ?? null;
|
|
2104
|
+
const streamingExempt = spec.streamingExempt ?? false;
|
|
2105
|
+
const privateJwk = spec.privateJwk ?? TEST_PRIVATE_JWK;
|
|
2106
|
+
const kid = spec.kid ?? TEST_KID;
|
|
2107
|
+
const bodyHash = streamingExempt ? "STREAM" : body && body.byteLength > 0 ? await hashBody2(body) : EMPTY_BODY_SHA256;
|
|
2108
|
+
const claim = {
|
|
2109
|
+
method,
|
|
2110
|
+
path,
|
|
2111
|
+
query,
|
|
2112
|
+
bodyHash,
|
|
2113
|
+
requestId: spec.requestId ?? `req_${cryptoRandom()}`,
|
|
2114
|
+
timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
2115
|
+
productId: spec.productId ?? "prod_test",
|
|
2116
|
+
backendId: spec.backendId ?? "be_test",
|
|
2117
|
+
routeId: spec.routeId ?? "route_test",
|
|
2118
|
+
policyVersion: spec.policyVersion ?? "pv_1"
|
|
2119
|
+
};
|
|
2120
|
+
const canonical = buildCanonicalSigningString2(claim);
|
|
2121
|
+
const signature = await signCanonicalString2(canonical, privateJwk);
|
|
2122
|
+
const headers = {
|
|
2123
|
+
[RUNTIME_HEADER_NAMES.signature]: signature,
|
|
2124
|
+
[RUNTIME_HEADER_NAMES.keyId]: kid,
|
|
2125
|
+
[RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
|
|
2126
|
+
[RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
|
|
2127
|
+
[RUNTIME_HEADER_NAMES.productId]: claim.productId,
|
|
2128
|
+
[RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
|
|
2129
|
+
[RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
|
|
2130
|
+
[RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
|
|
2131
|
+
[RUNTIME_HEADER_NAMES.bodyHash]: claim.bodyHash
|
|
2132
|
+
};
|
|
2133
|
+
return {
|
|
2134
|
+
input: { method, path, query, body, streamingExempt },
|
|
2135
|
+
claim,
|
|
2136
|
+
headers
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
function base64urlEncodeBytes(bytes) {
|
|
2140
|
+
let binary = "";
|
|
2141
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2142
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2143
|
+
}
|
|
2144
|
+
function base64urlEncodeJson(value) {
|
|
2145
|
+
return base64urlEncodeBytes(new TextEncoder().encode(JSON.stringify(value)));
|
|
2146
|
+
}
|
|
2147
|
+
async function signContextToken(claim, secret = TEST_CONTEXT_SECRET, kid = TEST_CONTEXT_KID) {
|
|
2148
|
+
const header = base64urlEncodeJson({ alg: "HS256", typ: "JWT", kid });
|
|
2149
|
+
const payload = base64urlEncodeJson({ cv: 1, ...claim });
|
|
2150
|
+
const signingInput = `${header}.${payload}`;
|
|
2151
|
+
const key2 = await crypto.subtle.importKey(
|
|
2152
|
+
"raw",
|
|
2153
|
+
new TextEncoder().encode(secret),
|
|
2154
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2155
|
+
false,
|
|
2156
|
+
["sign"]
|
|
2157
|
+
);
|
|
2158
|
+
const signature = await crypto.subtle.sign(
|
|
2159
|
+
"HMAC",
|
|
2160
|
+
key2,
|
|
2161
|
+
new TextEncoder().encode(signingInput)
|
|
2162
|
+
);
|
|
2163
|
+
return `${signingInput}.${base64urlEncodeBytes(new Uint8Array(signature))}`;
|
|
2164
|
+
}
|
|
2165
|
+
function generateDevSignerKeys() {
|
|
2166
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
2167
|
+
const privateJwk = privateKey.export({ format: "jwk" });
|
|
2168
|
+
const publicJwk = publicKey.export({ format: "jwk" });
|
|
2169
|
+
const kid = `fs-dev-${randomBytes(6).toString("hex")}`;
|
|
2170
|
+
const contextKid = `fs-dev-ctx-${randomBytes(6).toString("hex")}`;
|
|
2171
|
+
const contextSecret = randomBytes(32).toString("base64url");
|
|
2172
|
+
const runtimeToken = `${RUNTIME_TOKEN_PREFIXES.test}${randomBytes(24).toString("base64url")}`;
|
|
2173
|
+
return {
|
|
2174
|
+
kid,
|
|
2175
|
+
privateJwk,
|
|
2176
|
+
publicJwk,
|
|
2177
|
+
contextKid,
|
|
2178
|
+
contextSecret,
|
|
2179
|
+
runtimeToken
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
function cryptoRandom() {
|
|
2183
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/testing/personas.ts
|
|
2187
|
+
function definePersona(def) {
|
|
2188
|
+
return def;
|
|
2189
|
+
}
|
|
2190
|
+
var DEFAULT_PERSONAS = {
|
|
2191
|
+
owner: definePersona({
|
|
2192
|
+
name: "owner",
|
|
2193
|
+
permissions: ["*"],
|
|
2194
|
+
roles: ["owner"],
|
|
2195
|
+
subjectKey: "owner"
|
|
2196
|
+
}),
|
|
2197
|
+
admin: definePersona({
|
|
2198
|
+
name: "admin",
|
|
2199
|
+
permissions: ["*"],
|
|
2200
|
+
roles: ["admin"],
|
|
2201
|
+
subjectKey: "admin"
|
|
2202
|
+
}),
|
|
2203
|
+
member: definePersona({
|
|
2204
|
+
name: "member",
|
|
2205
|
+
permissions: [],
|
|
2206
|
+
roles: ["member"],
|
|
2207
|
+
subjectKey: "member"
|
|
2208
|
+
}),
|
|
2209
|
+
anonymous: definePersona({ name: "anonymous", anonymous: true })
|
|
2210
|
+
};
|
|
2211
|
+
function normalizeBody(body) {
|
|
2212
|
+
if (body === null || body === void 0) return null;
|
|
2213
|
+
if (typeof body === "string") return new TextEncoder().encode(body);
|
|
2214
|
+
if (body instanceof Uint8Array) return body;
|
|
2215
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
2216
|
+
return null;
|
|
2217
|
+
}
|
|
2218
|
+
function normalizeMethod(method) {
|
|
2219
|
+
return (method ?? "GET").toUpperCase();
|
|
2220
|
+
}
|
|
2221
|
+
function mergeHeaders(initHeaders, signedHeaders) {
|
|
2222
|
+
const headers = new Headers(initHeaders);
|
|
2223
|
+
for (const [name, value] of Object.entries(signedHeaders)) {
|
|
2224
|
+
headers.set(name, value);
|
|
2225
|
+
}
|
|
2226
|
+
return headers;
|
|
2227
|
+
}
|
|
2228
|
+
function buildContextClaim(persona, productId) {
|
|
2229
|
+
return {
|
|
2230
|
+
orgId: persona.orgId ?? "org_dev",
|
|
2231
|
+
actor: persona.actor ?? { type: "user", id: `user_${persona.name}` },
|
|
2232
|
+
// Product binding: the signed context productId MUST equal the signed
|
|
2233
|
+
// request productId or verifyRequest rejects it as tamper evidence.
|
|
2234
|
+
productId,
|
|
2235
|
+
compiledPlanId: persona.compiledPlanId ?? "plan_dev",
|
|
2236
|
+
subscriptionId: persona.subscriptionId ?? "sub_dev",
|
|
2237
|
+
subscriberId: persona.subscriberId ?? "subscriber_dev",
|
|
2238
|
+
environmentId: persona.environmentId ?? null,
|
|
2239
|
+
subjectKey: persona.subjectKey ?? persona.name,
|
|
2240
|
+
...persona.permissions !== void 0 ? { permissions: persona.permissions } : {},
|
|
2241
|
+
...persona.roles !== void 0 ? { roles: persona.roles } : {}
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function createPersonaClient(ctx) {
|
|
2245
|
+
function resolve(name) {
|
|
2246
|
+
const persona = ctx.personas.get(name);
|
|
2247
|
+
if (!persona) {
|
|
2248
|
+
throw new Error(
|
|
2249
|
+
`unknown persona "${name}" (known: ${[...ctx.personas.keys()].join(", ")})`
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
return persona;
|
|
2253
|
+
}
|
|
2254
|
+
async function buildHeaders(persona, spec) {
|
|
2255
|
+
const method = normalizeMethod(spec.method);
|
|
2256
|
+
const signed = await makeSignedRequest({
|
|
2257
|
+
method,
|
|
2258
|
+
path: spec.path ?? "/",
|
|
2259
|
+
query: spec.query ?? "",
|
|
2260
|
+
body: spec.body ?? null,
|
|
2261
|
+
streamingExempt: spec.streamingExempt ?? false,
|
|
2262
|
+
productId: ctx.productId,
|
|
2263
|
+
backendId: ctx.backendId,
|
|
2264
|
+
routeId: spec.routeId ?? "",
|
|
2265
|
+
privateJwk: ctx.keys.privateJwk,
|
|
2266
|
+
kid: ctx.keys.kid,
|
|
2267
|
+
...spec.requestId ? { requestId: spec.requestId } : {},
|
|
2268
|
+
...spec.timestamp !== void 0 ? { timestamp: spec.timestamp } : {}
|
|
2269
|
+
});
|
|
2270
|
+
const headers = { ...signed.headers };
|
|
2271
|
+
if (!persona.anonymous) {
|
|
2272
|
+
const claim = buildContextClaim(persona, ctx.productId);
|
|
2273
|
+
headers["x-fs-context"] = await signContextToken(
|
|
2274
|
+
claim,
|
|
2275
|
+
ctx.contextSecret,
|
|
2276
|
+
ctx.contextKid
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
return headers;
|
|
2280
|
+
}
|
|
2281
|
+
function asPersona(name) {
|
|
2282
|
+
const persona = resolve(name);
|
|
2283
|
+
return {
|
|
2284
|
+
persona: name,
|
|
2285
|
+
headers: (spec = {}) => buildHeaders(persona, spec),
|
|
2286
|
+
async fetch(url, init = {}) {
|
|
2287
|
+
const parsed = new URL(url);
|
|
2288
|
+
const method = normalizeMethod(init.method);
|
|
2289
|
+
const headers = await buildHeaders(persona, {
|
|
2290
|
+
method,
|
|
2291
|
+
path: parsed.pathname,
|
|
2292
|
+
query: parsed.search.replace(/^\?/, ""),
|
|
2293
|
+
body: normalizeBody(init.body)
|
|
2294
|
+
});
|
|
2295
|
+
const doFetch = ctx.fetchImpl ?? globalThis.fetch;
|
|
2296
|
+
return doFetch(url, {
|
|
2297
|
+
...init,
|
|
2298
|
+
method,
|
|
2299
|
+
headers: mergeHeaders(init.headers, headers)
|
|
2300
|
+
});
|
|
2301
|
+
},
|
|
2302
|
+
async inject(req, spec = {}) {
|
|
2303
|
+
const rawUrl = spec.path ?? req.url ?? req.path ?? "/";
|
|
2304
|
+
const qIndex = rawUrl.indexOf("?");
|
|
2305
|
+
const path = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
2306
|
+
const query = spec.query ?? (qIndex === -1 ? "" : rawUrl.slice(qIndex + 1));
|
|
2307
|
+
const method = normalizeMethod(spec.method ?? req.method);
|
|
2308
|
+
const headers = await buildHeaders(persona, {
|
|
2309
|
+
method,
|
|
2310
|
+
path,
|
|
2311
|
+
query,
|
|
2312
|
+
body: spec.body ?? null,
|
|
2313
|
+
...spec.streamingExempt !== void 0 ? { streamingExempt: spec.streamingExempt } : {},
|
|
2314
|
+
...spec.routeId ? { routeId: spec.routeId } : {}
|
|
2315
|
+
});
|
|
2316
|
+
for (const [field, value] of Object.entries(headers)) {
|
|
2317
|
+
req.set(field, value);
|
|
2318
|
+
}
|
|
2319
|
+
return req;
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
return { asPersona, personas: ctx.personas };
|
|
2324
|
+
}
|
|
2325
|
+
function buildPersonaMap(overrides) {
|
|
2326
|
+
const map = /* @__PURE__ */ new Map();
|
|
2327
|
+
for (const [name, def] of Object.entries(DEFAULT_PERSONAS)) {
|
|
2328
|
+
map.set(name, def);
|
|
2329
|
+
}
|
|
2330
|
+
if (Array.isArray(overrides)) {
|
|
2331
|
+
for (const def of overrides) map.set(def.name, def);
|
|
2332
|
+
} else if (overrides) {
|
|
2333
|
+
for (const [name, def] of Object.entries(overrides)) {
|
|
2334
|
+
map.set(name, { ...def, name });
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return map;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// src/testing/devGateway.ts
|
|
2341
|
+
var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
|
|
2342
|
+
var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
|
|
2343
|
+
var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
|
|
2344
|
+
function createDevGateway(options) {
|
|
2345
|
+
const productId = options.productId ?? "prod_dev";
|
|
2346
|
+
const backendId = options.backendId ?? "be_dev";
|
|
2347
|
+
const meterEvents = [];
|
|
2348
|
+
const bootstrap = {
|
|
2349
|
+
product: { id: productId, slug: options.productSlug ?? "dev-product" },
|
|
2350
|
+
backend: {
|
|
2351
|
+
id: backendId,
|
|
2352
|
+
slug: options.backendSlug ?? "dev-backend",
|
|
2353
|
+
name: "Dev Backend"
|
|
2354
|
+
},
|
|
2355
|
+
environment: { id: null, kind: "test" },
|
|
2356
|
+
capabilities: ["gateway_verification", "metering", "health"],
|
|
2357
|
+
verification: {
|
|
2358
|
+
required: options.mode === "simulated",
|
|
2359
|
+
jwksUrl: DEV_JWKS_URL,
|
|
2360
|
+
clockSkewSeconds: 5,
|
|
2361
|
+
replayWindowSeconds: 300,
|
|
2362
|
+
headerNames: RUNTIME_HEADER_NAMES
|
|
2363
|
+
},
|
|
2364
|
+
metering: {
|
|
2365
|
+
enabled: true,
|
|
2366
|
+
endpoint: DEV_METERING_ENDPOINT,
|
|
2367
|
+
credential: options.keys.runtimeToken,
|
|
2368
|
+
allowedMeters: [],
|
|
2369
|
+
allowedRoutes: [],
|
|
2370
|
+
perEventMax: 0
|
|
2371
|
+
},
|
|
2372
|
+
transport: { mode: "direct", runner: null },
|
|
2373
|
+
routes: (options.routeIds ?? []).map((id) => ({
|
|
2374
|
+
id,
|
|
2375
|
+
method: "POST",
|
|
2376
|
+
path: `/${id}`,
|
|
2377
|
+
backendId
|
|
2378
|
+
})),
|
|
2379
|
+
policyVersion: "pv_dev",
|
|
2380
|
+
refreshAfterSeconds: 3600
|
|
2381
|
+
};
|
|
2382
|
+
const jwksDoc = {
|
|
2383
|
+
keys: [{ ...options.keys.publicJwk, kid: options.keys.kid }]
|
|
2384
|
+
};
|
|
2385
|
+
const fetchImpl = async (input, init) => {
|
|
2386
|
+
const url = String(
|
|
2387
|
+
typeof input === "string" || input instanceof URL ? input : input.url
|
|
2388
|
+
);
|
|
2389
|
+
if (url.includes("/v1/runtime/bootstrap")) {
|
|
2390
|
+
return json(bootstrap);
|
|
2391
|
+
}
|
|
2392
|
+
if (url.includes("/.well-known/jwks.json") || url.includes("jwks")) {
|
|
2393
|
+
return json(jwksDoc);
|
|
2394
|
+
}
|
|
2395
|
+
if (url.includes("/v1/metering/events")) {
|
|
2396
|
+
const event = await readJsonBody(init, input);
|
|
2397
|
+
if (event) {
|
|
2398
|
+
meterEvents.push(event);
|
|
2399
|
+
options.onMeterEvent?.(event);
|
|
2400
|
+
}
|
|
2401
|
+
return json({ ok: true });
|
|
2402
|
+
}
|
|
2403
|
+
if (url.includes("/v1/runtime/health") || url.includes("/v1/runtime/drift")) {
|
|
2404
|
+
return json({ ok: true });
|
|
2405
|
+
}
|
|
2406
|
+
return new Response(JSON.stringify({ error: "not_found" }), {
|
|
2407
|
+
status: 404,
|
|
2408
|
+
headers: { "content-type": "application/json" }
|
|
2409
|
+
});
|
|
2410
|
+
};
|
|
2411
|
+
return {
|
|
2412
|
+
fetchImpl,
|
|
2413
|
+
bootstrap,
|
|
2414
|
+
meterEvents,
|
|
2415
|
+
productId,
|
|
2416
|
+
backendId,
|
|
2417
|
+
jwksUrl: DEV_JWKS_URL
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
function json(body, status = 200) {
|
|
2421
|
+
return new Response(JSON.stringify(body), {
|
|
2422
|
+
status,
|
|
2423
|
+
headers: { "content-type": "application/json" }
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
async function readJsonBody(init, input) {
|
|
2427
|
+
try {
|
|
2428
|
+
if (init?.body && typeof init.body === "string") {
|
|
2429
|
+
return JSON.parse(init.body);
|
|
2430
|
+
}
|
|
2431
|
+
if (input instanceof Request) {
|
|
2432
|
+
return await input.clone().json();
|
|
2433
|
+
}
|
|
2434
|
+
} catch {
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
return null;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/testing/devRuntime.ts
|
|
2441
|
+
import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
2442
|
+
import { dirname as dirname2 } from "node:path";
|
|
2443
|
+
|
|
2444
|
+
// src/testing/prodGuard.ts
|
|
2445
|
+
function isProductionEnv(env = readProcessEnv2()) {
|
|
2446
|
+
return (env.NODE_ENV ?? "").trim().toLowerCase() === "production";
|
|
2447
|
+
}
|
|
2448
|
+
var DevModeInProductionError = class extends Error {
|
|
2449
|
+
constructor(context) {
|
|
2450
|
+
super(
|
|
2451
|
+
`Farther Shore dev mode (${context}) is disabled in this process: NODE_ENV=production. Dev mode mints ephemeral signing keys and signed identity and must never run in production.`
|
|
2452
|
+
);
|
|
2453
|
+
this.name = "DevModeInProductionError";
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
function assertNotProduction(context, env = readProcessEnv2()) {
|
|
2457
|
+
if (isProductionEnv(env)) {
|
|
2458
|
+
throw new DevModeInProductionError(context);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
function readProcessEnv2() {
|
|
2462
|
+
const maybeProcess = globalThis.process;
|
|
2463
|
+
return maybeProcess?.env ?? {};
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// src/testing/usageSink.ts
|
|
2467
|
+
var DevUsageSink = class {
|
|
2468
|
+
events = [];
|
|
2469
|
+
/** Record a signed response-metering payload (withUsage / computeMeteringHeaders). */
|
|
2470
|
+
recordResponse(payload, requestId) {
|
|
2471
|
+
const raw = payload.rawDimsUnits;
|
|
2472
|
+
const meters = raw && typeof raw === "object" ? raw : {};
|
|
2473
|
+
this.events.push({
|
|
2474
|
+
source: "response",
|
|
2475
|
+
meters: { ...meters },
|
|
2476
|
+
payload,
|
|
2477
|
+
...requestId ? { requestId } : {},
|
|
2478
|
+
at: Date.now()
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
/** Record a background `fs.meter()` event captured by the dev gateway. */
|
|
2482
|
+
recordMeterEvent(event) {
|
|
2483
|
+
this.events.push({
|
|
2484
|
+
source: "meter",
|
|
2485
|
+
meters: { [event.meter]: event.qty },
|
|
2486
|
+
event,
|
|
2487
|
+
...event.request_id ? { requestId: event.request_id } : {},
|
|
2488
|
+
at: Date.now()
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
/** Total quantity per meter key across every recorded event. */
|
|
2492
|
+
byMeter() {
|
|
2493
|
+
const out = {};
|
|
2494
|
+
for (const evt of this.events) {
|
|
2495
|
+
for (const [meter, qty] of Object.entries(evt.meters)) {
|
|
2496
|
+
out[meter] = (out[meter] ?? 0) + qty;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return out;
|
|
2500
|
+
}
|
|
2501
|
+
/** Clear all recorded usage. */
|
|
2502
|
+
reset() {
|
|
2503
|
+
this.events.length = 0;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
// src/testing/traceSink.ts
|
|
2508
|
+
function redactValue(input) {
|
|
2509
|
+
return input.replace(/fsrt_(?:live|test)_[A-Za-z0-9_-]+/g, "fsrt_[redacted]").replace(/fsk_[A-Za-z0-9_-]+/g, "fsk_[redacted]").replace(/fsc_[A-Za-z0-9_-]+/g, "fsc_[redacted]").replace(/[Bb]earer\s+[A-Za-z0-9._-]+/g, "Bearer [redacted]");
|
|
2510
|
+
}
|
|
2511
|
+
var DevTraceSink = class {
|
|
2512
|
+
traces = /* @__PURE__ */ new Map();
|
|
2513
|
+
flushed = /* @__PURE__ */ new Set();
|
|
2514
|
+
appendLine;
|
|
2515
|
+
constructor(options = {}) {
|
|
2516
|
+
this.appendLine = options.appendLine;
|
|
2517
|
+
}
|
|
2518
|
+
ensure(requestId, mode) {
|
|
2519
|
+
let trace = this.traces.get(requestId);
|
|
2520
|
+
if (!trace) {
|
|
2521
|
+
trace = { requestId, mode, authz: [], metering: [] };
|
|
2522
|
+
this.traces.set(requestId, trace);
|
|
2523
|
+
}
|
|
2524
|
+
return trace;
|
|
2525
|
+
}
|
|
2526
|
+
/** Record how a request's signature/context verification resolved. */
|
|
2527
|
+
recordVerification(requestId, mode, outcome, fields = {}) {
|
|
2528
|
+
const trace = this.ensure(requestId, mode);
|
|
2529
|
+
if (fields.method) trace.method = fields.method;
|
|
2530
|
+
if (fields.path) trace.path = fields.path;
|
|
2531
|
+
if (fields.persona) trace.persona = fields.persona;
|
|
2532
|
+
trace.verification = {
|
|
2533
|
+
outcome,
|
|
2534
|
+
...fields.reason ? { reason: redactValue(fields.reason) } : {}
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
/** Record a single hasPermission / requirePermission decision. */
|
|
2538
|
+
recordAuthz(requestId, mode, entry) {
|
|
2539
|
+
const trace = this.ensure(requestId, mode);
|
|
2540
|
+
trace.authz.push({
|
|
2541
|
+
permission: entry.permission,
|
|
2542
|
+
decision: entry.decision,
|
|
2543
|
+
...entry.reason ? { reason: redactValue(entry.reason) } : {}
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
/** Record usage reported for this request. */
|
|
2547
|
+
recordMetering(requestId, mode, entry) {
|
|
2548
|
+
const trace = this.ensure(requestId, mode);
|
|
2549
|
+
trace.metering.push(entry);
|
|
2550
|
+
}
|
|
2551
|
+
/** Record the final response status and flush the trace as one JSONL line. */
|
|
2552
|
+
recordResponse(requestId, mode, status) {
|
|
2553
|
+
const trace = this.ensure(requestId, mode);
|
|
2554
|
+
trace.response = { status };
|
|
2555
|
+
this.flush(requestId);
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Append a trace's current state as one JSONL line (if a sink is wired).
|
|
2559
|
+
* Flushes at most ONCE per request id, so wrapping several response methods
|
|
2560
|
+
* (status/json/end) never produces duplicate JSONL lines.
|
|
2561
|
+
*/
|
|
2562
|
+
flush(requestId) {
|
|
2563
|
+
if (this.flushed.has(requestId)) return;
|
|
2564
|
+
const trace = this.traces.get(requestId);
|
|
2565
|
+
if (trace && this.appendLine) {
|
|
2566
|
+
this.appendLine(JSON.stringify(trace));
|
|
2567
|
+
this.flushed.add(requestId);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
/** The accumulated trace for a request id, or `undefined`. */
|
|
2571
|
+
forRequest(requestId) {
|
|
2572
|
+
return this.traces.get(requestId);
|
|
2573
|
+
}
|
|
2574
|
+
/** Every accumulated trace (insertion order). */
|
|
2575
|
+
all() {
|
|
2576
|
+
return [...this.traces.values()];
|
|
2577
|
+
}
|
|
2578
|
+
/** Clear all traces. */
|
|
2579
|
+
reset() {
|
|
2580
|
+
this.traces.clear();
|
|
2581
|
+
this.flushed.clear();
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
|
|
2585
|
+
// src/testing/keysFile.ts
|
|
2586
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2587
|
+
import { dirname } from "node:path";
|
|
2588
|
+
var DEFAULT_KEYS_FILE = ".farthershore/dev-keys.json";
|
|
2589
|
+
function writeDevKeysFile(path, contents) {
|
|
2590
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2591
|
+
writeFileSync(path, JSON.stringify(contents, null, 2), { mode: 384 });
|
|
2592
|
+
chmodSync(path, 384);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// src/testing/devRuntime.ts
|
|
2596
|
+
function createDevRuntime(options) {
|
|
2597
|
+
assertNotProduction("createDevRuntime", options.env);
|
|
2598
|
+
const mode = options.mode;
|
|
2599
|
+
const keys = options.keys ?? generateDevSignerKeys();
|
|
2600
|
+
const usage = new DevUsageSink();
|
|
2601
|
+
const trace = new DevTraceSink(
|
|
2602
|
+
options.traceJsonl ? { appendLine: options.traceJsonl } : {}
|
|
2603
|
+
);
|
|
2604
|
+
const gateway = createDevGateway({
|
|
2605
|
+
mode,
|
|
2606
|
+
keys,
|
|
2607
|
+
...options.productId ? { productId: options.productId } : {},
|
|
2608
|
+
...options.backendId ? { backendId: options.backendId } : {},
|
|
2609
|
+
...options.routes ? { routeIds: options.routes } : {},
|
|
2610
|
+
onMeterEvent: (event) => {
|
|
2611
|
+
usage.recordMeterEvent(event);
|
|
2612
|
+
options.usageJsonl?.(JSON.stringify({ source: "meter", event }));
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
__setDevMeteringHooks({
|
|
2616
|
+
fallbackToken: () => keys.runtimeToken,
|
|
2617
|
+
record: (payload, requestId) => {
|
|
2618
|
+
usage.recordResponse(payload, requestId);
|
|
2619
|
+
options.usageJsonl?.(JSON.stringify({ source: "response", payload }));
|
|
2620
|
+
if (requestId) {
|
|
2621
|
+
const raw = payload.rawDimsUnits;
|
|
2622
|
+
trace.recordMetering(requestId, mode, {
|
|
2623
|
+
meters: raw && typeof raw === "object" ? raw : {},
|
|
2624
|
+
source: "response"
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2629
|
+
const personas = buildPersonaMap(options.personas);
|
|
2630
|
+
const personaClient = createPersonaClient({
|
|
2631
|
+
keys,
|
|
2632
|
+
productId: gateway.productId,
|
|
2633
|
+
backendId: gateway.backendId,
|
|
2634
|
+
contextSecret: keys.contextSecret,
|
|
2635
|
+
contextKid: keys.contextKid,
|
|
2636
|
+
personas,
|
|
2637
|
+
mode,
|
|
2638
|
+
...options.appFetch ? { fetchImpl: options.appFetch } : {}
|
|
2639
|
+
});
|
|
2640
|
+
const fs = initFromEnv({
|
|
2641
|
+
runtimeToken: keys.runtimeToken,
|
|
2642
|
+
coreUrl: DEV_CORE_URL,
|
|
2643
|
+
fetchImpl: gateway.fetchImpl,
|
|
2644
|
+
contextSecrets: [keys.contextSecret],
|
|
2645
|
+
contextVerification: mode === "simulated" ? "required" : "preferred",
|
|
2646
|
+
env: {}
|
|
2647
|
+
});
|
|
2648
|
+
const tracedAuthz = {
|
|
2649
|
+
hasPermission(ctx, key2) {
|
|
2650
|
+
const decision = hasPermission(ctx, key2);
|
|
2651
|
+
recordDecision(ctx, key2, decision);
|
|
2652
|
+
return decision;
|
|
2653
|
+
},
|
|
2654
|
+
requirePermission(ctx, key2) {
|
|
2655
|
+
const decision = hasPermission(ctx, key2);
|
|
2656
|
+
recordDecision(ctx, key2, decision);
|
|
2657
|
+
if (!decision) throw new FartherShorePermissionError(key2);
|
|
2658
|
+
requirePermission(ctx, key2);
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
function recordDecision(ctx, key2, decision) {
|
|
2662
|
+
const requestId = ctx.requestId ?? "unknown";
|
|
2663
|
+
const subject = ctx.signedContext?.subjectKey ?? "unknown";
|
|
2664
|
+
trace.recordAuthz(requestId, mode, {
|
|
2665
|
+
permission: key2,
|
|
2666
|
+
decision: decision ? "allow" : "deny",
|
|
2667
|
+
...decision ? {} : { reason: `permission_denied: ${key2} (subject=${subject})` }
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
function middleware(mwOptions) {
|
|
2671
|
+
const inner = createExpressMiddleware(fs, mwOptions);
|
|
2672
|
+
return (req, res, next) => {
|
|
2673
|
+
const requestId = headerValue2(req.headers, "x-fs-request-id") ?? "unknown";
|
|
2674
|
+
const { path } = splitUrl2(req);
|
|
2675
|
+
const method = req.method;
|
|
2676
|
+
let nextCalled = false;
|
|
2677
|
+
let lastStatus = 200;
|
|
2678
|
+
const origStatus = res.status.bind(res);
|
|
2679
|
+
res.status = (code) => {
|
|
2680
|
+
lastStatus = code;
|
|
2681
|
+
return origStatus(code);
|
|
2682
|
+
};
|
|
2683
|
+
const origJson = res.json.bind(res);
|
|
2684
|
+
res.json = (body) => {
|
|
2685
|
+
if (!nextCalled) {
|
|
2686
|
+
const reason = typeof body === "object" && body !== null && "error" in body ? String(body.error) : void 0;
|
|
2687
|
+
trace.recordVerification(requestId, mode, "rejected", {
|
|
2688
|
+
method,
|
|
2689
|
+
path,
|
|
2690
|
+
...reason ? { reason } : {}
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
trace.recordResponse(requestId, mode, lastStatus);
|
|
2694
|
+
return origJson(body);
|
|
2695
|
+
};
|
|
2696
|
+
const wrappedNext = (err) => {
|
|
2697
|
+
nextCalled = true;
|
|
2698
|
+
trace.recordVerification(
|
|
2699
|
+
requestId,
|
|
2700
|
+
mode,
|
|
2701
|
+
mode === "simulated" ? "verified" : "passthrough",
|
|
2702
|
+
{ method, path }
|
|
2703
|
+
);
|
|
2704
|
+
next(err);
|
|
2705
|
+
};
|
|
2706
|
+
inner(req, res, wrappedNext);
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
fs.middleware = middleware;
|
|
2710
|
+
const devRuntime = {
|
|
2711
|
+
fs,
|
|
2712
|
+
asPersona: (name) => personaClient.asPersona(name),
|
|
2713
|
+
usage,
|
|
2714
|
+
trace,
|
|
2715
|
+
gateway,
|
|
2716
|
+
keys,
|
|
2717
|
+
personas,
|
|
2718
|
+
mode,
|
|
2719
|
+
bootstrap: gateway.bootstrap,
|
|
2720
|
+
authz: tracedAuthz,
|
|
2721
|
+
middleware,
|
|
2722
|
+
reset() {
|
|
2723
|
+
usage.reset();
|
|
2724
|
+
trace.reset();
|
|
2725
|
+
gateway.meterEvents.length = 0;
|
|
2726
|
+
}
|
|
2727
|
+
};
|
|
2728
|
+
fs.dev = devRuntime;
|
|
2729
|
+
return devRuntime;
|
|
2730
|
+
}
|
|
2731
|
+
var USAGE_JSONL_PATH = ".farthershore/dev-usage.jsonl";
|
|
2732
|
+
var TRACE_JSONL_DEFAULT = ".farthershore/dev-trace.jsonl";
|
|
2733
|
+
var PERSONAS_FILE = "fs.dev.personas.json";
|
|
2734
|
+
function devModeFromEnv(env) {
|
|
2735
|
+
const raw = (env.FS_DEV_MODE ?? "").trim().toLowerCase();
|
|
2736
|
+
if (raw === "passthrough" || raw === "simulated") return raw;
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
function createDevRuntimeFromEnv(env = readProcessEnv3()) {
|
|
2740
|
+
const mode = devModeFromEnv(env);
|
|
2741
|
+
if (!mode) {
|
|
2742
|
+
throw new Error(
|
|
2743
|
+
"createDevRuntimeFromEnv called without FS_DEV_MODE=passthrough|simulated"
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
assertNotProduction("FS_DEV_MODE", env);
|
|
2747
|
+
const keys = generateDevSignerKeys();
|
|
2748
|
+
const personas = loadPersonasFile();
|
|
2749
|
+
const tracePath = env.FS_DEV_TRACE?.trim() || TRACE_JSONL_DEFAULT;
|
|
2750
|
+
const runtime = createDevRuntime({
|
|
2751
|
+
mode,
|
|
2752
|
+
...personas ? { personas } : {},
|
|
2753
|
+
keys,
|
|
2754
|
+
env,
|
|
2755
|
+
usageJsonl: (line) => appendJsonl(USAGE_JSONL_PATH, line),
|
|
2756
|
+
traceJsonl: (line) => appendJsonl(tracePath, line)
|
|
2757
|
+
});
|
|
2758
|
+
const keysFile = {
|
|
2759
|
+
version: 1,
|
|
2760
|
+
mode,
|
|
2761
|
+
keys,
|
|
2762
|
+
productId: runtime.gateway.productId,
|
|
2763
|
+
backendId: runtime.gateway.backendId,
|
|
2764
|
+
personas: mapToRecord(runtime.personas)
|
|
2765
|
+
};
|
|
2766
|
+
writeDevKeysFile(DEFAULT_KEYS_FILE, keysFile);
|
|
2767
|
+
printBanner(mode, runtime, tracePath);
|
|
2768
|
+
return runtime.fs;
|
|
2769
|
+
}
|
|
2770
|
+
function printBanner(mode, runtime, tracePath) {
|
|
2771
|
+
const lines = [
|
|
2772
|
+
"============================================================",
|
|
2773
|
+
" \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
|
|
2774
|
+
` mode: ${mode.toUpperCase()}`,
|
|
2775
|
+
` product: ${runtime.gateway.productId}`,
|
|
2776
|
+
` backend: ${runtime.gateway.backendId}`,
|
|
2777
|
+
` personas: ${[...runtime.personas.keys()].join(", ")}`,
|
|
2778
|
+
` usage log: ${USAGE_JSONL_PATH}`,
|
|
2779
|
+
` trace log: ${tracePath}`,
|
|
2780
|
+
` dev keys: ${DEFAULT_KEYS_FILE} (mode 600, ephemeral)`,
|
|
2781
|
+
mode === "simulated" ? " verification REQUIRED \u2014 requests must carry a signed persona." : " verification OFF (passthrough) \u2014 middleware passes through.",
|
|
2782
|
+
"============================================================"
|
|
2783
|
+
];
|
|
2784
|
+
console.warn(lines.join("\n"));
|
|
2785
|
+
}
|
|
2786
|
+
function loadPersonasFile() {
|
|
2787
|
+
if (!existsSync(PERSONAS_FILE)) return void 0;
|
|
2788
|
+
try {
|
|
2789
|
+
return JSON.parse(readFileSync2(PERSONAS_FILE, "utf8"));
|
|
2790
|
+
} catch {
|
|
2791
|
+
return void 0;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
function appendJsonl(path, line) {
|
|
2795
|
+
try {
|
|
2796
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2797
|
+
appendFileSync(path, `${line}
|
|
2798
|
+
`);
|
|
2799
|
+
} catch {
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
function mapToRecord(map) {
|
|
2803
|
+
const out = {};
|
|
2804
|
+
for (const [name, def] of map) out[name] = def;
|
|
2805
|
+
return out;
|
|
2806
|
+
}
|
|
2807
|
+
function readProcessEnv3() {
|
|
2808
|
+
const maybeProcess = globalThis.process;
|
|
2809
|
+
return maybeProcess?.env ?? {};
|
|
2810
|
+
}
|
|
2811
|
+
function headerValue2(headers, name) {
|
|
2812
|
+
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
2813
|
+
if (Array.isArray(value)) return value[0];
|
|
2814
|
+
return value;
|
|
2815
|
+
}
|
|
2816
|
+
function splitUrl2(req) {
|
|
2817
|
+
const raw = req.originalUrl ?? req.url ?? req.path ?? "/";
|
|
2818
|
+
const qIndex = raw.indexOf("?");
|
|
2819
|
+
if (qIndex === -1) return { path: raw, query: "" };
|
|
2820
|
+
return { path: raw.slice(0, qIndex), query: raw.slice(qIndex + 1) };
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2053
2823
|
// src/index.ts
|
|
2824
|
+
function readProcessEnv4() {
|
|
2825
|
+
const maybeProcess = globalThis.process;
|
|
2826
|
+
return maybeProcess?.env ?? {};
|
|
2827
|
+
}
|
|
2054
2828
|
var fartherShore = {
|
|
2055
2829
|
/** Derive everything from FS_RUNTIME_TOKEN via bootstrap. */
|
|
2056
2830
|
initFromEnv(options = {}) {
|
|
2831
|
+
const env = options.env ?? readProcessEnv4();
|
|
2832
|
+
if (devModeFromEnv(env)) {
|
|
2833
|
+
return createDevRuntimeFromEnv(env);
|
|
2834
|
+
}
|
|
2057
2835
|
const fs = initFromEnv(options);
|
|
2058
2836
|
fs.middleware = (mwOptions) => createExpressMiddleware(fs, mwOptions);
|
|
2059
2837
|
return fs;
|
|
@@ -2093,6 +2871,7 @@ export {
|
|
|
2093
2871
|
buildCanonicalSigningString2 as buildCanonicalSigningString,
|
|
2094
2872
|
buildHealthReport,
|
|
2095
2873
|
canonicalizeQuery2 as canonicalizeQuery,
|
|
2874
|
+
computeMeteringHeaders,
|
|
2096
2875
|
createExpressMiddleware,
|
|
2097
2876
|
createUsage,
|
|
2098
2877
|
fartherShore,
|