@tabai/sdk 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/LICENSE +23 -0
- package/README.md +401 -0
- package/bin/tab.mjs +23 -0
- package/dist/_shared/abi.d.ts +150 -0
- package/dist/_shared/abi.d.ts.map +1 -0
- package/dist/_shared/abi.js +197 -0
- package/dist/_shared/abi.js.map +1 -0
- package/dist/_shared/chains.d.ts +118 -0
- package/dist/_shared/chains.d.ts.map +1 -0
- package/dist/_shared/chains.js +89 -0
- package/dist/_shared/chains.js.map +1 -0
- package/dist/_shared/hex.d.ts +35 -0
- package/dist/_shared/hex.d.ts.map +1 -0
- package/dist/_shared/hex.js +40 -0
- package/dist/_shared/hex.js.map +1 -0
- package/dist/_shared/index.d.ts +14 -0
- package/dist/_shared/index.d.ts.map +1 -0
- package/dist/_shared/index.js +14 -0
- package/dist/_shared/index.js.map +1 -0
- package/dist/_shared/keccak256.d.ts +29 -0
- package/dist/_shared/keccak256.d.ts.map +1 -0
- package/dist/_shared/keccak256.js +145 -0
- package/dist/_shared/keccak256.js.map +1 -0
- package/dist/_shared/result.d.ts +78 -0
- package/dist/_shared/result.d.ts.map +1 -0
- package/dist/_shared/result.js +61 -0
- package/dist/_shared/result.js.map +1 -0
- package/dist/cli/client-config.d.ts +155 -0
- package/dist/cli/client-config.d.ts.map +1 -0
- package/dist/cli/client-config.js +382 -0
- package/dist/cli/client-config.js.map +1 -0
- package/dist/cli/connect.d.ts +76 -0
- package/dist/cli/connect.d.ts.map +1 -0
- package/dist/cli/connect.js +158 -0
- package/dist/cli/connect.js.map +1 -0
- package/dist/cli/doctor.d.ts +57 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +253 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +45 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +371 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/http/client-402.d.ts +243 -0
- package/dist/http/client-402.d.ts.map +1 -0
- package/dist/http/client-402.js +515 -0
- package/dist/http/client-402.js.map +1 -0
- package/dist/http/headers.d.ts +173 -0
- package/dist/http/headers.d.ts.map +1 -0
- package/dist/http/headers.js +284 -0
- package/dist/http/headers.js.map +1 -0
- package/dist/http/index.d.ts +15 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +15 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/metering-claim.d.ts +82 -0
- package/dist/http/metering-claim.d.ts.map +1 -0
- package/dist/http/metering-claim.js +99 -0
- package/dist/http/metering-claim.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +40 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +50 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/assets.d.ts +31 -0
- package/dist/mcp/assets.d.ts.map +1 -0
- package/dist/mcp/assets.js +78 -0
- package/dist/mcp/assets.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/json-schema.d.ts +86 -0
- package/dist/mcp/json-schema.d.ts.map +1 -0
- package/dist/mcp/json-schema.js +215 -0
- package/dist/mcp/json-schema.js.map +1 -0
- package/dist/mcp/json.d.ts +43 -0
- package/dist/mcp/json.d.ts.map +1 -0
- package/dist/mcp/json.js +69 -0
- package/dist/mcp/json.js.map +1 -0
- package/dist/mcp/registry-client.d.ts +88 -0
- package/dist/mcp/registry-client.d.ts.map +1 -0
- package/dist/mcp/registry-client.js +158 -0
- package/dist/mcp/registry-client.js.map +1 -0
- package/dist/mcp/schemas.d.ts +82 -0
- package/dist/mcp/schemas.d.ts.map +1 -0
- package/dist/mcp/schemas.js +493 -0
- package/dist/mcp/schemas.js.map +1 -0
- package/dist/mcp/server.d.ts +97 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +285 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/settings.d.ts +90 -0
- package/dist/mcp/settings.d.ts.map +1 -0
- package/dist/mcp/settings.js +160 -0
- package/dist/mcp/settings.js.map +1 -0
- package/dist/mcp/toolset.d.ts +231 -0
- package/dist/mcp/toolset.d.ts.map +1 -0
- package/dist/mcp/toolset.js +760 -0
- package/dist/mcp/toolset.js.map +1 -0
- package/dist/payments/abi.d.ts +9 -0
- package/dist/payments/abi.d.ts.map +1 -0
- package/dist/payments/abi.js +17 -0
- package/dist/payments/abi.js.map +1 -0
- package/dist/payments/config.d.ts +199 -0
- package/dist/payments/config.d.ts.map +1 -0
- package/dist/payments/config.js +259 -0
- package/dist/payments/config.js.map +1 -0
- package/dist/payments/index.d.ts +13 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +13 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/kuru.d.ts +191 -0
- package/dist/payments/kuru.d.ts.map +1 -0
- package/dist/payments/kuru.js +377 -0
- package/dist/payments/kuru.js.map +1 -0
- package/dist/payments/monad.d.ts +69 -0
- package/dist/payments/monad.d.ts.map +1 -0
- package/dist/payments/monad.js +306 -0
- package/dist/payments/monad.js.map +1 -0
- package/dist/payments/permit2.d.ts +118 -0
- package/dist/payments/permit2.d.ts.map +1 -0
- package/dist/payments/permit2.js +366 -0
- package/dist/payments/permit2.js.map +1 -0
- package/dist/payments/registry.d.ts +119 -0
- package/dist/payments/registry.d.ts.map +1 -0
- package/dist/payments/registry.js +199 -0
- package/dist/payments/registry.js.map +1 -0
- package/dist/payments/strategy.d.ts +80 -0
- package/dist/payments/strategy.d.ts.map +1 -0
- package/dist/payments/strategy.js +103 -0
- package/dist/payments/strategy.js.map +1 -0
- package/dist/proxy/hooks.d.ts +90 -0
- package/dist/proxy/hooks.d.ts.map +1 -0
- package/dist/proxy/hooks.js +35 -0
- package/dist/proxy/hooks.js.map +1 -0
- package/dist/proxy/index.d.ts +9 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +9 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/proxy.d.ts +156 -0
- package/dist/proxy/proxy.d.ts.map +1 -0
- package/dist/proxy/proxy.js +366 -0
- package/dist/proxy/proxy.js.map +1 -0
- package/dist/server/adapters/express.d.ts +89 -0
- package/dist/server/adapters/express.d.ts.map +1 -0
- package/dist/server/adapters/express.js +215 -0
- package/dist/server/adapters/express.js.map +1 -0
- package/dist/server/adapters/hono.d.ts +52 -0
- package/dist/server/adapters/hono.d.ts.map +1 -0
- package/dist/server/adapters/hono.js +61 -0
- package/dist/server/adapters/hono.js.map +1 -0
- package/dist/server/adapters/next.d.ts +52 -0
- package/dist/server/adapters/next.d.ts.map +1 -0
- package/dist/server/adapters/next.js +56 -0
- package/dist/server/adapters/next.js.map +1 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +30 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/metering.d.ts +209 -0
- package/dist/server/metering.d.ts.map +1 -0
- package/dist/server/metering.js +365 -0
- package/dist/server/metering.js.map +1 -0
- package/dist/server/post-paid.d.ts +355 -0
- package/dist/server/post-paid.d.ts.map +1 -0
- package/dist/server/post-paid.js +512 -0
- package/dist/server/post-paid.js.map +1 -0
- package/dist/x402/client.d.ts +203 -0
- package/dist/x402/client.d.ts.map +1 -0
- package/dist/x402/client.js +337 -0
- package/dist/x402/client.js.map +1 -0
- package/dist/x402/hub.d.ts +79 -0
- package/dist/x402/hub.d.ts.map +1 -0
- package/dist/x402/hub.js +164 -0
- package/dist/x402/hub.js.map +1 -0
- package/dist/x402/index.d.ts +27 -0
- package/dist/x402/index.d.ts.map +1 -0
- package/dist/x402/index.js +27 -0
- package/dist/x402/index.js.map +1 -0
- package/dist/x402/proxy.d.ts +162 -0
- package/dist/x402/proxy.d.ts.map +1 -0
- package/dist/x402/proxy.js +198 -0
- package/dist/x402/proxy.js.map +1 -0
- package/dist/x402/server.d.ts +162 -0
- package/dist/x402/server.d.ts.map +1 -0
- package/dist/x402/server.js +306 -0
- package/dist/x402/server.js.map +1 -0
- package/dist/x402/wire.d.ts +104 -0
- package/dist/x402/wire.d.ts.map +1 -0
- package/dist/x402/wire.js +265 -0
- package/dist/x402/wire.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proxy layer: one handler that forwards a request upstream, meters the
|
|
3
|
+
* delivery through the post-paid plugin, and runs hooks around both (R23.4).
|
|
4
|
+
*
|
|
5
|
+
* ## Order of operations, which is the whole contract
|
|
6
|
+
*
|
|
7
|
+
* ```text
|
|
8
|
+
* before hooks, in registration order
|
|
9
|
+
* -> metering.execute( forward to upstream )
|
|
10
|
+
* after hooks, in reverse order
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* The forward runs *inside* the metering plugin's `execute`, so the plugin sees
|
|
14
|
+
* the upstream's response exactly as it sees a local handler's: it records the
|
|
15
|
+
* delivery once the response exists, attaches the charge block, and replaces
|
|
16
|
+
* the response with a 402 on `LimitExceeded` alone. Nothing about the plugin's
|
|
17
|
+
* ordering guarantee changes because the handler is a network hop, and the
|
|
18
|
+
* "never withhold pending payment" rule (R23.3) is inherited rather than
|
|
19
|
+
* re-implemented here.
|
|
20
|
+
*
|
|
21
|
+
* ## Streaming
|
|
22
|
+
*
|
|
23
|
+
* The upstream response body is passed through as the stream it arrived as.
|
|
24
|
+
* Nothing here reads it, and the charge headers are attached by rebuilding the
|
|
25
|
+
* response around the same stream. A hook that needs the body must `clone()` the
|
|
26
|
+
* response and read the copy, and a hook that does so pays the buffering it
|
|
27
|
+
* asked for and nobody else does. The request body is likewise forwarded as a
|
|
28
|
+
* stream where the host `fetch` supports one.
|
|
29
|
+
*
|
|
30
|
+
* ## What is not forwarded
|
|
31
|
+
*
|
|
32
|
+
* Hop-by-hop request headers, `host`, and `content-length` are dropped before
|
|
33
|
+
* forwarding, because they describe the connection the proxy is on rather than
|
|
34
|
+
* the request. On the way back, `content-encoding`, `content-length`, and
|
|
35
|
+
* `transfer-encoding` are dropped, because the host `fetch` already decoded the
|
|
36
|
+
* body and the outbound server re-frames it; forwarding those headers would tell
|
|
37
|
+
* the caller to decode a body that is already plain.
|
|
38
|
+
*
|
|
39
|
+
* ## Failures are responses, never throws
|
|
40
|
+
*
|
|
41
|
+
* An unreachable upstream is a 502 carrying a `TabError` body. It is not a
|
|
42
|
+
* delivery, so the plugin's default `billable` predicate does not meter it. A
|
|
43
|
+
* critical hook's `err` is a response at its category's status. The handler
|
|
44
|
+
* this factory returns never rejects.
|
|
45
|
+
*
|
|
46
|
+
* Requirements: 23.4, 23.3
|
|
47
|
+
*/
|
|
48
|
+
import { type Result, type TabError } from "../_shared/index.js";
|
|
49
|
+
import { type Logger } from "../logger.js";
|
|
50
|
+
import { type MeteredCharge, type MeteringOutcome, type PostPaidPlugin } from "../server/post-paid.js";
|
|
51
|
+
import type { ProxyHook, ProxyPhase } from "./hooks.js";
|
|
52
|
+
import type { SettlementView } from "./hooks.js";
|
|
53
|
+
/** Request headers that describe the hop rather than the request, per RFC 9110. */
|
|
54
|
+
export declare const HOP_BY_HOP_HEADERS: readonly string[];
|
|
55
|
+
/** Response headers the host `fetch` has already acted on and the outbound server re-frames. */
|
|
56
|
+
export declare const REFRAMED_RESPONSE_HEADERS: readonly string[];
|
|
57
|
+
/** The forward this proxy hands to its `fetch`. Structural, like `client-402.ts`. */
|
|
58
|
+
export interface ProxyForwardInit {
|
|
59
|
+
readonly method: string;
|
|
60
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
61
|
+
/** The request body stream, or undefined for a bodiless method. */
|
|
62
|
+
readonly body?: unknown;
|
|
63
|
+
/** Required by the host `fetch` to send a streamed request body. */
|
|
64
|
+
readonly duplex?: "half";
|
|
65
|
+
readonly signal?: AbortSignal;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `fetch` the proxy forwards with. The host's global `fetch` is the default.
|
|
69
|
+
*
|
|
70
|
+
* The third argument is the request as it reached the proxy, the same object
|
|
71
|
+
* the metering plugin later hands to `priceOf`. A forward that learns the price
|
|
72
|
+
* on the way, the x402-fronting one does, keys what it learned by it. The host
|
|
73
|
+
* `fetch` and every existing two-argument `fetchImpl` ignore it.
|
|
74
|
+
*/
|
|
75
|
+
export type ProxyFetch = (url: string, init: ProxyForwardInit, request: Request) => Promise<Response>;
|
|
76
|
+
/** One hook phase that returned an error or threw, in the order it happened. */
|
|
77
|
+
export interface HookFailure {
|
|
78
|
+
readonly hook: string;
|
|
79
|
+
readonly phase: ProxyPhase;
|
|
80
|
+
readonly critical: boolean;
|
|
81
|
+
readonly error: TabError;
|
|
82
|
+
}
|
|
83
|
+
export interface ProxyResult {
|
|
84
|
+
/** The response that goes out. */
|
|
85
|
+
readonly response: Response;
|
|
86
|
+
/**
|
|
87
|
+
* `delivered`: the upstream's response, metered or not. `refused`: the metering
|
|
88
|
+
* plugin replaced it, which is `LimitExceeded` and the two Agent-side refusals
|
|
89
|
+
* only. `failed`: a critical hook or the forward itself failed.
|
|
90
|
+
*/
|
|
91
|
+
readonly kind: "delivered" | "refused" | "failed";
|
|
92
|
+
/**
|
|
93
|
+
* Resolves once the delivery is recorded. Already resolved under the plugin's
|
|
94
|
+
* default `after-metering`; still pending under `before-metering`, where a
|
|
95
|
+
* host with a request lifetime hook passes it there.
|
|
96
|
+
*/
|
|
97
|
+
readonly metering: Promise<MeteringOutcome> | undefined;
|
|
98
|
+
/** The recorded charge, when the outcome was known before the hooks ran. */
|
|
99
|
+
readonly charge: MeteredCharge | undefined;
|
|
100
|
+
/** The Settlement a hook attached (R23.5). */
|
|
101
|
+
readonly settlement: SettlementView | undefined;
|
|
102
|
+
/** Every hook failure, critical or not. */
|
|
103
|
+
readonly hookFailures: readonly HookFailure[];
|
|
104
|
+
/** The error that produced a `failed` result. */
|
|
105
|
+
readonly failure: TabError | undefined;
|
|
106
|
+
/** The shared scratch the hooks used, for a Service that wants to log it. */
|
|
107
|
+
readonly state: ReadonlyMap<string, unknown>;
|
|
108
|
+
}
|
|
109
|
+
export interface TabProxyOptions {
|
|
110
|
+
/** Absolute base URL of the service being fronted. The request path and query are appended. */
|
|
111
|
+
readonly upstream: string;
|
|
112
|
+
readonly hooks?: readonly ProxyHook[];
|
|
113
|
+
/** The post-paid plugin of `server/post-paid.ts`. Metering runs on every request. */
|
|
114
|
+
readonly metering: PostPaidPlugin;
|
|
115
|
+
readonly fetchImpl?: ProxyFetch;
|
|
116
|
+
readonly logger?: Logger;
|
|
117
|
+
/** Aborts the forward after this long. No timeout when omitted. */
|
|
118
|
+
readonly timeoutMs?: number;
|
|
119
|
+
/** Request headers dropped before forwarding, on top of the hop-by-hop set and `host`. */
|
|
120
|
+
readonly dropRequestHeaders?: readonly string[];
|
|
121
|
+
/**
|
|
122
|
+
* A leading path segment removed before the path is appended to `upstream`.
|
|
123
|
+
* A proxy mounted at `/hub/nansen` in front of `https://api.nansen.ai/api/v1`
|
|
124
|
+
* strips `/hub/nansen`, so `/hub/nansen/query` reaches `.../api/v1/query`.
|
|
125
|
+
*/
|
|
126
|
+
readonly stripPrefix?: string;
|
|
127
|
+
/** Called with every result, so a Service logs which Settlement covered which request. */
|
|
128
|
+
readonly onResult?: (result: ProxyResult) => void;
|
|
129
|
+
}
|
|
130
|
+
/** A handler any web-standard host can mount, with the richer entry point beside it. */
|
|
131
|
+
export interface TabProxy {
|
|
132
|
+
(request: Request): Promise<Response>;
|
|
133
|
+
/** The same run, returning everything the handler discards. */
|
|
134
|
+
proxy(request: Request): Promise<ProxyResult>;
|
|
135
|
+
readonly hooks: readonly ProxyHook[];
|
|
136
|
+
readonly upstream: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Builds the proxy.
|
|
140
|
+
*
|
|
141
|
+
* Construction is total, as everywhere in this package. An unusable `upstream`
|
|
142
|
+
* is reported by the first request as a 500 carrying a `VALIDATION` error, which
|
|
143
|
+
* is the first point at which anybody is listening.
|
|
144
|
+
*/
|
|
145
|
+
export declare function createTabProxy(options: TabProxyOptions): TabProxy;
|
|
146
|
+
/**
|
|
147
|
+
* The request path and query, resolved against the upstream base.
|
|
148
|
+
*
|
|
149
|
+
* `stripPrefix`, when given, is removed from the front of the path first, and a
|
|
150
|
+
* path that does not start with it is refused rather than forwarded somewhere
|
|
151
|
+
* the mount did not intend.
|
|
152
|
+
*/
|
|
153
|
+
export declare function resolveUpstream(upstream: string, requestUrl: string, stripPrefix?: string): Result<string>;
|
|
154
|
+
/** A `TabError` as the response it becomes: its category's status, the error as JSON. */
|
|
155
|
+
export declare function errorResponse(error: TabError): Response;
|
|
156
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proxy layer: one handler that forwards a request upstream, meters the
|
|
3
|
+
* delivery through the post-paid plugin, and runs hooks around both (R23.4).
|
|
4
|
+
*
|
|
5
|
+
* ## Order of operations, which is the whole contract
|
|
6
|
+
*
|
|
7
|
+
* ```text
|
|
8
|
+
* before hooks, in registration order
|
|
9
|
+
* -> metering.execute( forward to upstream )
|
|
10
|
+
* after hooks, in reverse order
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* The forward runs *inside* the metering plugin's `execute`, so the plugin sees
|
|
14
|
+
* the upstream's response exactly as it sees a local handler's: it records the
|
|
15
|
+
* delivery once the response exists, attaches the charge block, and replaces
|
|
16
|
+
* the response with a 402 on `LimitExceeded` alone. Nothing about the plugin's
|
|
17
|
+
* ordering guarantee changes because the handler is a network hop, and the
|
|
18
|
+
* "never withhold pending payment" rule (R23.3) is inherited rather than
|
|
19
|
+
* re-implemented here.
|
|
20
|
+
*
|
|
21
|
+
* ## Streaming
|
|
22
|
+
*
|
|
23
|
+
* The upstream response body is passed through as the stream it arrived as.
|
|
24
|
+
* Nothing here reads it, and the charge headers are attached by rebuilding the
|
|
25
|
+
* response around the same stream. A hook that needs the body must `clone()` the
|
|
26
|
+
* response and read the copy, and a hook that does so pays the buffering it
|
|
27
|
+
* asked for and nobody else does. The request body is likewise forwarded as a
|
|
28
|
+
* stream where the host `fetch` supports one.
|
|
29
|
+
*
|
|
30
|
+
* ## What is not forwarded
|
|
31
|
+
*
|
|
32
|
+
* Hop-by-hop request headers, `host`, and `content-length` are dropped before
|
|
33
|
+
* forwarding, because they describe the connection the proxy is on rather than
|
|
34
|
+
* the request. On the way back, `content-encoding`, `content-length`, and
|
|
35
|
+
* `transfer-encoding` are dropped, because the host `fetch` already decoded the
|
|
36
|
+
* body and the outbound server re-frames it; forwarding those headers would tell
|
|
37
|
+
* the caller to decode a body that is already plain.
|
|
38
|
+
*
|
|
39
|
+
* ## Failures are responses, never throws
|
|
40
|
+
*
|
|
41
|
+
* An unreachable upstream is a 502 carrying a `TabError` body. It is not a
|
|
42
|
+
* delivery, so the plugin's default `billable` predicate does not meter it. A
|
|
43
|
+
* critical hook's `err` is a response at its category's status. The handler
|
|
44
|
+
* this factory returns never rejects.
|
|
45
|
+
*
|
|
46
|
+
* Requirements: 23.4, 23.3
|
|
47
|
+
*/
|
|
48
|
+
import { causeOf, httpStatusOf, ok, wrap } from "../_shared/index.js";
|
|
49
|
+
import { tabError } from "../errors.js";
|
|
50
|
+
import { defaultLogger } from "../logger.js";
|
|
51
|
+
import { jsonResponse, } from "../server/post-paid.js";
|
|
52
|
+
/** Request headers that describe the hop rather than the request, per RFC 9110. */
|
|
53
|
+
export const HOP_BY_HOP_HEADERS = [
|
|
54
|
+
"connection",
|
|
55
|
+
"keep-alive",
|
|
56
|
+
"proxy-authenticate",
|
|
57
|
+
"proxy-authorization",
|
|
58
|
+
"proxy-connection",
|
|
59
|
+
"te",
|
|
60
|
+
"trailer",
|
|
61
|
+
"transfer-encoding",
|
|
62
|
+
"upgrade",
|
|
63
|
+
];
|
|
64
|
+
/** Response headers the host `fetch` has already acted on and the outbound server re-frames. */
|
|
65
|
+
export const REFRAMED_RESPONSE_HEADERS = [
|
|
66
|
+
"content-encoding",
|
|
67
|
+
"content-length",
|
|
68
|
+
"transfer-encoding",
|
|
69
|
+
"connection",
|
|
70
|
+
"keep-alive",
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Builds the proxy.
|
|
74
|
+
*
|
|
75
|
+
* Construction is total, as everywhere in this package. An unusable `upstream`
|
|
76
|
+
* is reported by the first request as a 500 carrying a `VALIDATION` error, which
|
|
77
|
+
* is the first point at which anybody is listening.
|
|
78
|
+
*/
|
|
79
|
+
export function createTabProxy(options) {
|
|
80
|
+
const logger = options.logger ?? defaultLogger;
|
|
81
|
+
const hooks = [...(options.hooks ?? [])];
|
|
82
|
+
const dropped = new Set([
|
|
83
|
+
...HOP_BY_HOP_HEADERS,
|
|
84
|
+
"host",
|
|
85
|
+
"content-length",
|
|
86
|
+
...(options.dropRequestHeaders ?? []).map((name) => name.toLowerCase()),
|
|
87
|
+
]);
|
|
88
|
+
/**
|
|
89
|
+
* The forward. A failure becomes a response and is also recorded on `holder`,
|
|
90
|
+
* so the result can name the error without reading it back off the response.
|
|
91
|
+
*/
|
|
92
|
+
const forward = async (request, holder) => {
|
|
93
|
+
const failed = (error) => {
|
|
94
|
+
holder.failure = error;
|
|
95
|
+
return errorResponse(error);
|
|
96
|
+
};
|
|
97
|
+
const target = resolveUpstream(options.upstream, request.url, options.stripPrefix);
|
|
98
|
+
if (!target.ok)
|
|
99
|
+
return failed(target.error);
|
|
100
|
+
const send = options.fetchImpl ?? hostFetch();
|
|
101
|
+
if (send === undefined) {
|
|
102
|
+
return failed(tabError("UPSTREAM", "FETCH_UNAVAILABLE", "this host has no global fetch; supply fetchImpl, or run on Node 20.10 or later"));
|
|
103
|
+
}
|
|
104
|
+
const headers = {};
|
|
105
|
+
request.headers.forEach((value, name) => {
|
|
106
|
+
if (!dropped.has(name.toLowerCase()))
|
|
107
|
+
headers[name] = value;
|
|
108
|
+
});
|
|
109
|
+
const bodiless = request.method === "GET" || request.method === "HEAD";
|
|
110
|
+
const body = bodiless ? undefined : request.body;
|
|
111
|
+
const signal = forwardSignal(request, options.timeoutMs);
|
|
112
|
+
const init = {
|
|
113
|
+
method: request.method,
|
|
114
|
+
headers,
|
|
115
|
+
...(body === undefined || body === null ? {} : { body, duplex: "half" }),
|
|
116
|
+
...(signal === undefined ? {} : { signal }),
|
|
117
|
+
};
|
|
118
|
+
const sent = await wrap(async () => send(target.value, init, request), (error) => tabError("UPSTREAM", "PROXY_UPSTREAM_UNREACHABLE", `the upstream at ${target.value} did not answer`, {
|
|
119
|
+
retryable: true,
|
|
120
|
+
details: { upstream: target.value, method: request.method },
|
|
121
|
+
cause: causeOf(error),
|
|
122
|
+
}));
|
|
123
|
+
if (!sent.ok)
|
|
124
|
+
return failed(sent.error);
|
|
125
|
+
if (!looksLikeResponse(sent.value)) {
|
|
126
|
+
return failed(tabError("UPSTREAM", "PROXY_UPSTREAM_INVALID", `the fetch given to this proxy returned something that is not a response for ${target.value}`));
|
|
127
|
+
}
|
|
128
|
+
return reframe(sent.value);
|
|
129
|
+
};
|
|
130
|
+
const proxy = async (request) => {
|
|
131
|
+
const state = new Map();
|
|
132
|
+
const failures = [];
|
|
133
|
+
const base = { request, state, logger };
|
|
134
|
+
let settlement;
|
|
135
|
+
// ---- before, in registration order
|
|
136
|
+
for (const hook of hooks) {
|
|
137
|
+
if (hook.before === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
const context = { ...base, phase: "before" };
|
|
140
|
+
const outcome = await runPhase(hook, "before", context, logger);
|
|
141
|
+
if (outcome !== undefined) {
|
|
142
|
+
failures.push(outcome);
|
|
143
|
+
if (outcome.critical) {
|
|
144
|
+
return finish({
|
|
145
|
+
response: errorResponse(outcome.error),
|
|
146
|
+
kind: "failed",
|
|
147
|
+
metering: undefined,
|
|
148
|
+
charge: undefined,
|
|
149
|
+
settlement: context.settlement,
|
|
150
|
+
hookFailures: failures,
|
|
151
|
+
failure: outcome.error,
|
|
152
|
+
state,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
settlement = context.settlement ?? settlement;
|
|
157
|
+
}
|
|
158
|
+
// ---- the forward, under metering
|
|
159
|
+
const holder = {};
|
|
160
|
+
const execution = await options.metering.execute(request, () => forward(request, holder));
|
|
161
|
+
let response;
|
|
162
|
+
let kind;
|
|
163
|
+
let forwardThrew;
|
|
164
|
+
let metering;
|
|
165
|
+
let outcome;
|
|
166
|
+
if (execution.kind === "handler-failed") {
|
|
167
|
+
// `forward` never throws, so this is a plugin invariant rather than a path,
|
|
168
|
+
// and it is answered the same way a throwing hook is: as an INTERNAL error.
|
|
169
|
+
forwardThrew = tabError("INTERNAL", "PROXY_FORWARD_THREW", "the forward threw where it should have returned a response", {
|
|
170
|
+
cause: causeOf(execution.thrown),
|
|
171
|
+
});
|
|
172
|
+
response = errorResponse(forwardThrew);
|
|
173
|
+
kind = "failed";
|
|
174
|
+
}
|
|
175
|
+
else if (execution.kind === "refused") {
|
|
176
|
+
response = execution.response;
|
|
177
|
+
kind = "refused";
|
|
178
|
+
outcome = execution.outcome;
|
|
179
|
+
metering = Promise.resolve(execution.outcome);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
response = execution.response;
|
|
183
|
+
kind = holder.failure === undefined ? "delivered" : "failed";
|
|
184
|
+
metering = execution.metering;
|
|
185
|
+
if (options.metering.release === "after-metering")
|
|
186
|
+
outcome = await execution.metering;
|
|
187
|
+
}
|
|
188
|
+
const charge = outcome?.kind === "charged" ? outcome.charge : undefined;
|
|
189
|
+
// ---- after, in reverse order
|
|
190
|
+
for (const hook of [...hooks].reverse()) {
|
|
191
|
+
if (hook.after === undefined)
|
|
192
|
+
continue;
|
|
193
|
+
const context = {
|
|
194
|
+
...base,
|
|
195
|
+
phase: "after",
|
|
196
|
+
response,
|
|
197
|
+
...(charge === undefined ? {} : { charge }),
|
|
198
|
+
...(outcome === undefined ? {} : { metering: outcome }),
|
|
199
|
+
...(settlement === undefined ? {} : { settlement }),
|
|
200
|
+
};
|
|
201
|
+
const failed = await runPhase(hook, "after", context, logger);
|
|
202
|
+
settlement = context.settlement ?? settlement;
|
|
203
|
+
if (failed !== undefined) {
|
|
204
|
+
failures.push(failed);
|
|
205
|
+
if (failed.critical) {
|
|
206
|
+
discard(response, logger);
|
|
207
|
+
return finish({
|
|
208
|
+
response: errorResponse(failed.error),
|
|
209
|
+
kind: "failed",
|
|
210
|
+
metering,
|
|
211
|
+
charge,
|
|
212
|
+
settlement,
|
|
213
|
+
hookFailures: failures,
|
|
214
|
+
failure: failed.error,
|
|
215
|
+
state,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return finish({
|
|
221
|
+
response,
|
|
222
|
+
kind,
|
|
223
|
+
metering,
|
|
224
|
+
charge,
|
|
225
|
+
settlement,
|
|
226
|
+
hookFailures: failures,
|
|
227
|
+
failure: kind === "failed" ? (holder.failure ?? forwardThrew) : undefined,
|
|
228
|
+
state,
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
const finish = (result) => {
|
|
232
|
+
if (options.onResult !== undefined) {
|
|
233
|
+
try {
|
|
234
|
+
options.onResult(result);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
logger.warn("onResult threw and was ignored", causeOf(error));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
const handler = async (request) => (await proxy(request)).response;
|
|
243
|
+
return Object.assign(handler, { proxy, hooks, upstream: options.upstream });
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Runs one phase of one hook without letting it throw or reject into the proxy.
|
|
247
|
+
* Returns the failure, or undefined when the phase succeeded.
|
|
248
|
+
*/
|
|
249
|
+
async function runPhase(hook, phase, context, logger) {
|
|
250
|
+
const run = phase === "before" ? hook.before : hook.after;
|
|
251
|
+
if (run === undefined)
|
|
252
|
+
return undefined;
|
|
253
|
+
let result;
|
|
254
|
+
try {
|
|
255
|
+
result = await run.call(hook, context);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
result = { ok: false, error: tabError("INTERNAL", "HOOK_THREW", `hook ${hook.name} threw in its ${phase} phase`, { cause: causeOf(error) }) };
|
|
259
|
+
}
|
|
260
|
+
if (result.ok)
|
|
261
|
+
return undefined;
|
|
262
|
+
const critical = hook.critical === true;
|
|
263
|
+
logger[critical ? "error" : "warn"](critical ? `hook ${hook.name} failed its ${phase} phase and the request fails with it` : `hook ${hook.name} failed its ${phase} phase and was skipped`, { hook: hook.name, phase, code: result.error.code, message: result.error.message });
|
|
264
|
+
return { hook: hook.name, phase, critical, error: result.error };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The request path and query, resolved against the upstream base.
|
|
268
|
+
*
|
|
269
|
+
* `stripPrefix`, when given, is removed from the front of the path first, and a
|
|
270
|
+
* path that does not start with it is refused rather than forwarded somewhere
|
|
271
|
+
* the mount did not intend.
|
|
272
|
+
*/
|
|
273
|
+
export function resolveUpstream(upstream, requestUrl, stripPrefix) {
|
|
274
|
+
const base = safeUrl(upstream.endsWith("/") ? upstream : `${upstream}/`);
|
|
275
|
+
if (base === undefined || (base.protocol !== "http:" && base.protocol !== "https:")) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
error: tabError("VALIDATION", "UPSTREAM_INVALID", `upstream \`${upstream}\` is not an absolute http or https URL`),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const incoming = safeUrl(requestUrl, base);
|
|
282
|
+
if (incoming === undefined) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
error: tabError("VALIDATION", "REQUEST_URL_INVALID", `the request URL \`${requestUrl}\` does not parse`),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
let pathname = incoming.pathname;
|
|
289
|
+
if (stripPrefix !== undefined && stripPrefix.length > 0) {
|
|
290
|
+
const prefix = `/${stripPrefix.replace(/^\/+|\/+$/g, "")}`;
|
|
291
|
+
if (pathname !== prefix && !pathname.startsWith(`${prefix}/`)) {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
error: tabError("VALIDATION", "REQUEST_PATH_OUTSIDE_MOUNT", `the request path \`${pathname}\` does not start with the mount \`${prefix}\``, {
|
|
295
|
+
details: { path: pathname, prefix },
|
|
296
|
+
}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
pathname = pathname.slice(prefix.length);
|
|
300
|
+
}
|
|
301
|
+
const relative = `${pathname.replace(/^\/+/, "")}${incoming.search}`;
|
|
302
|
+
return ok(new URL(relative, base).toString());
|
|
303
|
+
}
|
|
304
|
+
function safeUrl(value, base) {
|
|
305
|
+
try {
|
|
306
|
+
return base === undefined ? new URL(value) : new URL(value, base);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** A signal that fires on the caller's abort or on the timeout, whichever comes first. */
|
|
313
|
+
function forwardSignal(request, timeoutMs) {
|
|
314
|
+
const timeout = timeoutMs === undefined ? undefined : AbortSignal.timeout(timeoutMs);
|
|
315
|
+
const caller = request.signal;
|
|
316
|
+
if (timeout === undefined)
|
|
317
|
+
return caller;
|
|
318
|
+
const any = AbortSignal.any;
|
|
319
|
+
return typeof any === "function" ? any([caller, timeout]) : timeout;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Rebuilds the upstream response around its own body stream, minus the headers
|
|
323
|
+
* the host `fetch` has already consumed. No byte of the body is read here.
|
|
324
|
+
*/
|
|
325
|
+
function reframe(upstream) {
|
|
326
|
+
const headers = new Headers();
|
|
327
|
+
upstream.headers.forEach((value, name) => {
|
|
328
|
+
if (!REFRAMED_RESPONSE_HEADERS.includes(name.toLowerCase()))
|
|
329
|
+
headers.append(name, value);
|
|
330
|
+
});
|
|
331
|
+
return new Response(upstream.body, {
|
|
332
|
+
status: upstream.status,
|
|
333
|
+
statusText: upstream.statusText,
|
|
334
|
+
headers,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/** A `TabError` as the response it becomes: its category's status, the error as JSON. */
|
|
338
|
+
export function errorResponse(error) {
|
|
339
|
+
return jsonResponse(httpStatusOf(error), {}, { ok: false, error });
|
|
340
|
+
}
|
|
341
|
+
function looksLikeResponse(value) {
|
|
342
|
+
if (typeof value !== "object" || value === null)
|
|
343
|
+
return false;
|
|
344
|
+
const candidate = value;
|
|
345
|
+
return typeof candidate.status === "number" && typeof candidate.headers === "object" && candidate.headers !== null;
|
|
346
|
+
}
|
|
347
|
+
/** Releases a response that is not going out, so its connection does not wait for the collector. */
|
|
348
|
+
function discard(response, logger) {
|
|
349
|
+
const body = response.body;
|
|
350
|
+
if (body === null || response.bodyUsed)
|
|
351
|
+
return;
|
|
352
|
+
try {
|
|
353
|
+
body.cancel().catch((error) => {
|
|
354
|
+
logger.debug("discarded response body could not be cancelled", causeOf(error));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
logger.debug("discarded response body could not be cancelled", causeOf(error));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** The host's global `fetch`, behind a runtime check so its absence is an error and not a TypeError. */
|
|
362
|
+
function hostFetch() {
|
|
363
|
+
const candidate = globalThis.fetch;
|
|
364
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/proxy/proxy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The proxy layer: one handler that forwards a request upstream, meters the\n * delivery through the post-paid plugin, and runs hooks around both (R23.4).\n *\n * ## Order of operations, which is the whole contract\n *\n * ```text\n * before hooks, in registration order\n * -> metering.execute( forward to upstream )\n * after hooks, in reverse order\n * ```\n *\n * The forward runs *inside* the metering plugin's `execute`, so the plugin sees\n * the upstream's response exactly as it sees a local handler's: it records the\n * delivery once the response exists, attaches the charge block, and replaces\n * the response with a 402 on `LimitExceeded` alone. Nothing about the plugin's\n * ordering guarantee changes because the handler is a network hop, and the\n * \"never withhold pending payment\" rule (R23.3) is inherited rather than\n * re-implemented here.\n *\n * ## Streaming\n *\n * The upstream response body is passed through as the stream it arrived as.\n * Nothing here reads it, and the charge headers are attached by rebuilding the\n * response around the same stream. A hook that needs the body must `clone()` the\n * response and read the copy, and a hook that does so pays the buffering it\n * asked for and nobody else does. The request body is likewise forwarded as a\n * stream where the host `fetch` supports one.\n *\n * ## What is not forwarded\n *\n * Hop-by-hop request headers, `host`, and `content-length` are dropped before\n * forwarding, because they describe the connection the proxy is on rather than\n * the request. On the way back, `content-encoding`, `content-length`, and\n * `transfer-encoding` are dropped, because the host `fetch` already decoded the\n * body and the outbound server re-frames it; forwarding those headers would tell\n * the caller to decode a body that is already plain.\n *\n * ## Failures are responses, never throws\n *\n * An unreachable upstream is a 502 carrying a `TabError` body. It is not a\n * delivery, so the plugin's default `billable` predicate does not meter it. A\n * critical hook's `err` is a response at its category's status. The handler\n * this factory returns never rejects.\n *\n * Requirements: 23.4, 23.3\n */\n\nimport { causeOf, httpStatusOf, ok, wrap, type Result, type TabError } from \"../_shared/index.js\";\n\nimport { tabError } from \"../errors.js\";\nimport { defaultLogger, type Logger } from \"../logger.js\";\nimport {\n jsonResponse,\n type MeteredCharge,\n type MeteringOutcome,\n type PostPaidPlugin,\n} from \"../server/post-paid.js\";\nimport type { ProxyHook, ProxyHookContext, ProxyPhase } from \"./hooks.js\";\nimport type { SettlementView } from \"./hooks.js\";\n\n/** Request headers that describe the hop rather than the request, per RFC 9110. */\nexport const HOP_BY_HOP_HEADERS: readonly string[] = [\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"proxy-connection\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n];\n\n/** Response headers the host `fetch` has already acted on and the outbound server re-frames. */\nexport const REFRAMED_RESPONSE_HEADERS: readonly string[] = [\n \"content-encoding\",\n \"content-length\",\n \"transfer-encoding\",\n \"connection\",\n \"keep-alive\",\n];\n\n/** The forward this proxy hands to its `fetch`. Structural, like `client-402.ts`. */\nexport interface ProxyForwardInit {\n readonly method: string;\n readonly headers: Readonly<Record<string, string>>;\n /** The request body stream, or undefined for a bodiless method. */\n readonly body?: unknown;\n /** Required by the host `fetch` to send a streamed request body. */\n readonly duplex?: \"half\";\n readonly signal?: AbortSignal;\n}\n\n/**\n * The `fetch` the proxy forwards with. The host's global `fetch` is the default.\n *\n * The third argument is the request as it reached the proxy, the same object\n * the metering plugin later hands to `priceOf`. A forward that learns the price\n * on the way, the x402-fronting one does, keys what it learned by it. The host\n * `fetch` and every existing two-argument `fetchImpl` ignore it.\n */\nexport type ProxyFetch = (url: string, init: ProxyForwardInit, request: Request) => Promise<Response>;\n\n/** One hook phase that returned an error or threw, in the order it happened. */\nexport interface HookFailure {\n readonly hook: string;\n readonly phase: ProxyPhase;\n readonly critical: boolean;\n readonly error: TabError;\n}\n\nexport interface ProxyResult {\n /** The response that goes out. */\n readonly response: Response;\n /**\n * `delivered`: the upstream's response, metered or not. `refused`: the metering\n * plugin replaced it, which is `LimitExceeded` and the two Agent-side refusals\n * only. `failed`: a critical hook or the forward itself failed.\n */\n readonly kind: \"delivered\" | \"refused\" | \"failed\";\n /**\n * Resolves once the delivery is recorded. Already resolved under the plugin's\n * default `after-metering`; still pending under `before-metering`, where a\n * host with a request lifetime hook passes it there.\n */\n readonly metering: Promise<MeteringOutcome> | undefined;\n /** The recorded charge, when the outcome was known before the hooks ran. */\n readonly charge: MeteredCharge | undefined;\n /** The Settlement a hook attached (R23.5). */\n readonly settlement: SettlementView | undefined;\n /** Every hook failure, critical or not. */\n readonly hookFailures: readonly HookFailure[];\n /** The error that produced a `failed` result. */\n readonly failure: TabError | undefined;\n /** The shared scratch the hooks used, for a Service that wants to log it. */\n readonly state: ReadonlyMap<string, unknown>;\n}\n\nexport interface TabProxyOptions {\n /** Absolute base URL of the service being fronted. The request path and query are appended. */\n readonly upstream: string;\n readonly hooks?: readonly ProxyHook[];\n /** The post-paid plugin of `server/post-paid.ts`. Metering runs on every request. */\n readonly metering: PostPaidPlugin;\n readonly fetchImpl?: ProxyFetch;\n readonly logger?: Logger;\n /** Aborts the forward after this long. No timeout when omitted. */\n readonly timeoutMs?: number;\n /** Request headers dropped before forwarding, on top of the hop-by-hop set and `host`. */\n readonly dropRequestHeaders?: readonly string[];\n /**\n * A leading path segment removed before the path is appended to `upstream`.\n * A proxy mounted at `/hub/nansen` in front of `https://api.nansen.ai/api/v1`\n * strips `/hub/nansen`, so `/hub/nansen/query` reaches `.../api/v1/query`.\n */\n readonly stripPrefix?: string;\n /** Called with every result, so a Service logs which Settlement covered which request. */\n readonly onResult?: (result: ProxyResult) => void;\n}\n\n/** A handler any web-standard host can mount, with the richer entry point beside it. */\nexport interface TabProxy {\n (request: Request): Promise<Response>;\n /** The same run, returning everything the handler discards. */\n proxy(request: Request): Promise<ProxyResult>;\n readonly hooks: readonly ProxyHook[];\n readonly upstream: string;\n}\n\n/**\n * Builds the proxy.\n *\n * Construction is total, as everywhere in this package. An unusable `upstream`\n * is reported by the first request as a 500 carrying a `VALIDATION` error, which\n * is the first point at which anybody is listening.\n */\nexport function createTabProxy(options: TabProxyOptions): TabProxy {\n const logger = options.logger ?? defaultLogger;\n const hooks = [...(options.hooks ?? [])];\n const dropped = new Set([\n ...HOP_BY_HOP_HEADERS,\n \"host\",\n \"content-length\",\n ...(options.dropRequestHeaders ?? []).map((name) => name.toLowerCase()),\n ]);\n\n /**\n * The forward. A failure becomes a response and is also recorded on `holder`,\n * so the result can name the error without reading it back off the response.\n */\n const forward = async (request: Request, holder: { failure?: TabError }): Promise<Response> => {\n const failed = (error: TabError): Response => {\n holder.failure = error;\n return errorResponse(error);\n };\n const target = resolveUpstream(options.upstream, request.url, options.stripPrefix);\n if (!target.ok) return failed(target.error);\n\n const send = options.fetchImpl ?? hostFetch();\n if (send === undefined) {\n return failed(\n tabError(\"UPSTREAM\", \"FETCH_UNAVAILABLE\", \"this host has no global fetch; supply fetchImpl, or run on Node 20.10 or later\"),\n );\n }\n\n const headers: Record<string, string> = {};\n request.headers.forEach((value, name) => {\n if (!dropped.has(name.toLowerCase())) headers[name] = value;\n });\n\n const bodiless = request.method === \"GET\" || request.method === \"HEAD\";\n const body = bodiless ? undefined : request.body;\n const signal = forwardSignal(request, options.timeoutMs);\n const init: ProxyForwardInit = {\n method: request.method,\n headers,\n ...(body === undefined || body === null ? {} : { body, duplex: \"half\" as const }),\n ...(signal === undefined ? {} : { signal }),\n };\n\n const sent = await wrap(\n async () => send(target.value, init, request),\n (error) =>\n tabError(\"UPSTREAM\", \"PROXY_UPSTREAM_UNREACHABLE\", `the upstream at ${target.value} did not answer`, {\n retryable: true,\n details: { upstream: target.value, method: request.method },\n cause: causeOf(error),\n }),\n );\n if (!sent.ok) return failed(sent.error);\n if (!looksLikeResponse(sent.value)) {\n return failed(\n tabError(\"UPSTREAM\", \"PROXY_UPSTREAM_INVALID\", `the fetch given to this proxy returned something that is not a response for ${target.value}`),\n );\n }\n return reframe(sent.value);\n };\n\n const proxy = async (request: Request): Promise<ProxyResult> => {\n const state = new Map<string, unknown>();\n const failures: HookFailure[] = [];\n const base = { request, state, logger } as const;\n let settlement: SettlementView | undefined;\n\n // ---- before, in registration order\n for (const hook of hooks) {\n if (hook.before === undefined) continue;\n const context: ProxyHookContext = { ...base, phase: \"before\" };\n const outcome = await runPhase(hook, \"before\", context, logger);\n if (outcome !== undefined) {\n failures.push(outcome);\n if (outcome.critical) {\n return finish({\n response: errorResponse(outcome.error),\n kind: \"failed\",\n metering: undefined,\n charge: undefined,\n settlement: context.settlement,\n hookFailures: failures,\n failure: outcome.error,\n state,\n });\n }\n }\n settlement = context.settlement ?? settlement;\n }\n\n // ---- the forward, under metering\n const holder: { failure?: TabError } = {};\n const execution = await options.metering.execute(request, () => forward(request, holder));\n\n let response: Response;\n let kind: ProxyResult[\"kind\"];\n let forwardThrew: TabError | undefined;\n let metering: Promise<MeteringOutcome> | undefined;\n let outcome: MeteringOutcome | undefined;\n if (execution.kind === \"handler-failed\") {\n // `forward` never throws, so this is a plugin invariant rather than a path,\n // and it is answered the same way a throwing hook is: as an INTERNAL error.\n forwardThrew = tabError(\"INTERNAL\", \"PROXY_FORWARD_THREW\", \"the forward threw where it should have returned a response\", {\n cause: causeOf(execution.thrown),\n });\n response = errorResponse(forwardThrew);\n kind = \"failed\";\n } else if (execution.kind === \"refused\") {\n response = execution.response;\n kind = \"refused\";\n outcome = execution.outcome;\n metering = Promise.resolve(execution.outcome);\n } else {\n response = execution.response;\n kind = holder.failure === undefined ? \"delivered\" : \"failed\";\n metering = execution.metering;\n if (options.metering.release === \"after-metering\") outcome = await execution.metering;\n }\n const charge = outcome?.kind === \"charged\" ? outcome.charge : undefined;\n\n // ---- after, in reverse order\n for (const hook of [...hooks].reverse()) {\n if (hook.after === undefined) continue;\n const context: ProxyHookContext = {\n ...base,\n phase: \"after\",\n response,\n ...(charge === undefined ? {} : { charge }),\n ...(outcome === undefined ? {} : { metering: outcome }),\n ...(settlement === undefined ? {} : { settlement }),\n };\n const failed = await runPhase(hook, \"after\", context, logger);\n settlement = context.settlement ?? settlement;\n if (failed !== undefined) {\n failures.push(failed);\n if (failed.critical) {\n discard(response, logger);\n return finish({\n response: errorResponse(failed.error),\n kind: \"failed\",\n metering,\n charge,\n settlement,\n hookFailures: failures,\n failure: failed.error,\n state,\n });\n }\n }\n }\n\n return finish({\n response,\n kind,\n metering,\n charge,\n settlement,\n hookFailures: failures,\n failure: kind === \"failed\" ? (holder.failure ?? forwardThrew) : undefined,\n state,\n });\n };\n\n const finish = (result: ProxyResult): ProxyResult => {\n if (options.onResult !== undefined) {\n try {\n options.onResult(result);\n } catch (error) {\n logger.warn(\"onResult threw and was ignored\", causeOf(error));\n }\n }\n return result;\n };\n\n const handler = async (request: Request): Promise<Response> => (await proxy(request)).response;\n return Object.assign(handler, { proxy, hooks, upstream: options.upstream });\n}\n\n/**\n * Runs one phase of one hook without letting it throw or reject into the proxy.\n * Returns the failure, or undefined when the phase succeeded.\n */\nasync function runPhase(\n hook: ProxyHook,\n phase: ProxyPhase,\n context: ProxyHookContext,\n logger: Logger,\n): Promise<HookFailure | undefined> {\n const run = phase === \"before\" ? hook.before : hook.after;\n if (run === undefined) return undefined;\n let result: Result<void>;\n try {\n result = await run.call(hook, context);\n } catch (error) {\n result = { ok: false, error: tabError(\"INTERNAL\", \"HOOK_THREW\", `hook ${hook.name} threw in its ${phase} phase`, { cause: causeOf(error) }) };\n }\n if (result.ok) return undefined;\n const critical = hook.critical === true;\n logger[critical ? \"error\" : \"warn\"](\n critical ? `hook ${hook.name} failed its ${phase} phase and the request fails with it` : `hook ${hook.name} failed its ${phase} phase and was skipped`,\n { hook: hook.name, phase, code: result.error.code, message: result.error.message },\n );\n return { hook: hook.name, phase, critical, error: result.error };\n}\n\n/**\n * The request path and query, resolved against the upstream base.\n *\n * `stripPrefix`, when given, is removed from the front of the path first, and a\n * path that does not start with it is refused rather than forwarded somewhere\n * the mount did not intend.\n */\nexport function resolveUpstream(upstream: string, requestUrl: string, stripPrefix?: string): Result<string> {\n const base = safeUrl(upstream.endsWith(\"/\") ? upstream : `${upstream}/`);\n if (base === undefined || (base.protocol !== \"http:\" && base.protocol !== \"https:\")) {\n return {\n ok: false,\n error: tabError(\"VALIDATION\", \"UPSTREAM_INVALID\", `upstream \\`${upstream}\\` is not an absolute http or https URL`),\n };\n }\n const incoming = safeUrl(requestUrl, base);\n if (incoming === undefined) {\n return {\n ok: false,\n error: tabError(\"VALIDATION\", \"REQUEST_URL_INVALID\", `the request URL \\`${requestUrl}\\` does not parse`),\n };\n }\n let pathname = incoming.pathname;\n if (stripPrefix !== undefined && stripPrefix.length > 0) {\n const prefix = `/${stripPrefix.replace(/^\\/+|\\/+$/g, \"\")}`;\n if (pathname !== prefix && !pathname.startsWith(`${prefix}/`)) {\n return {\n ok: false,\n error: tabError(\"VALIDATION\", \"REQUEST_PATH_OUTSIDE_MOUNT\", `the request path \\`${pathname}\\` does not start with the mount \\`${prefix}\\``, {\n details: { path: pathname, prefix },\n }),\n };\n }\n pathname = pathname.slice(prefix.length);\n }\n const relative = `${pathname.replace(/^\\/+/, \"\")}${incoming.search}`;\n return ok(new URL(relative, base).toString());\n}\n\nfunction safeUrl(value: string, base?: URL): URL | undefined {\n try {\n return base === undefined ? new URL(value) : new URL(value, base);\n } catch {\n return undefined;\n }\n}\n\n/** A signal that fires on the caller's abort or on the timeout, whichever comes first. */\nfunction forwardSignal(request: Request, timeoutMs: number | undefined): AbortSignal | undefined {\n const timeout = timeoutMs === undefined ? undefined : AbortSignal.timeout(timeoutMs);\n const caller = request.signal;\n if (timeout === undefined) return caller;\n const any = (AbortSignal as { any?: (signals: AbortSignal[]) => AbortSignal }).any;\n return typeof any === \"function\" ? any([caller, timeout]) : timeout;\n}\n\n/**\n * Rebuilds the upstream response around its own body stream, minus the headers\n * the host `fetch` has already consumed. No byte of the body is read here.\n */\nfunction reframe(upstream: Response): Response {\n const headers = new Headers();\n upstream.headers.forEach((value, name) => {\n if (!REFRAMED_RESPONSE_HEADERS.includes(name.toLowerCase())) headers.append(name, value);\n });\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers,\n });\n}\n\n/** A `TabError` as the response it becomes: its category's status, the error as JSON. */\nexport function errorResponse(error: TabError): Response {\n return jsonResponse(httpStatusOf(error), {}, { ok: false, error });\n}\n\nfunction looksLikeResponse(value: unknown): value is Response {\n if (typeof value !== \"object\" || value === null) return false;\n const candidate = value as { status?: unknown; headers?: unknown };\n return typeof candidate.status === \"number\" && typeof candidate.headers === \"object\" && candidate.headers !== null;\n}\n\n/** Releases a response that is not going out, so its connection does not wait for the collector. */\nfunction discard(response: Response, logger: Logger): void {\n const body = response.body;\n if (body === null || response.bodyUsed) return;\n try {\n body.cancel().catch((error: unknown) => {\n logger.debug(\"discarded response body could not be cancelled\", causeOf(error));\n });\n } catch (error) {\n logger.debug(\"discarded response body could not be cancelled\", causeOf(error));\n }\n}\n\n/** The host's global `fetch`, behind a runtime check so its absence is an error and not a TypeError. */\nfunction hostFetch(): ProxyFetch | undefined {\n const candidate = (globalThis as { fetch?: unknown }).fetch;\n return typeof candidate === \"function\" ? (candidate as ProxyFetch) : undefined;\n}\n"]}
|