@webpieces/http-client-core 0.4.760 → 0.4.762

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,5 +18,5 @@ nothing but `core-util`, so no node-only import can reach a browser bundle.
18
18
 
19
19
  `destination` is a `DestinationTrust`, derived here from the route's own `AuthMode`, and it decides
20
20
  whether TRUSTED context keys (`x-user-id`, `x-org-id`, `x-webpieces-roles`) ride along: only an
21
- endpoint that authenticates its caller (`@AuthOidc` / `@AuthSharedSecret`) receives them. Untrusted
21
+ endpoint that authenticates its caller (`@WpAuthOidc` / `@WpAuthSharedSecret`) receives them. Untrusted
22
22
  keys always travel. See `docs/architecture/context-propagation.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-client-core",
3
- "version": "0.4.760",
3
+ "version": "0.4.762",
4
4
  "description": "Isomorphic core of the webpieces HTTP client: the decorator-driven ProxyClient, error translation, and the Proxy trap shared by http-client-node and http-client-browser",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -21,6 +21,6 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "@webpieces/core-util": "0.4.760"
24
+ "@webpieces/core-util": "0.4.762"
25
25
  }
26
26
  }
@@ -84,7 +84,7 @@ export declare abstract class ProxyClient {
84
84
  protected execute(route: RouteMetadata, requestDto: unknown, method: () => Promise<unknown>): Promise<unknown>;
85
85
  /**
86
86
  * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot
87
- * mint the OIDC token an @AuthOidc endpoint demands. Surfacing it here beats failing on the
87
+ * mint the OIDC token an @WpAuthOidc endpoint demands. Surfacing it here beats failing on the
88
88
  * first call in production. The default accepts everything.
89
89
  */
90
90
  protected assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void;
@@ -83,7 +83,7 @@ class ProxyClient {
83
83
  }
84
84
  /**
85
85
  * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot
86
- * mint the OIDC token an @AuthOidc endpoint demands. Surfacing it here beats failing on the
86
+ * mint the OIDC token an @WpAuthOidc endpoint demands. Surfacing it here beats failing on the
87
87
  * first call in production. The default accepts everything.
88
88
  */
89
89
  assertEndpointSupported(_authMeta, _methodName) { }
@@ -149,7 +149,7 @@ class ProxyClient {
149
149
  for (const [methodName, endpointPath] of Object.entries(endpoints)) {
150
150
  const fullPath = basePath + endpointPath;
151
151
  // Capture the endpoint's auth mode so the client can mint delivery auth per
152
- // @AuthOidc / @AuthSharedSecret, exactly as the server verifies it.
152
+ // @WpAuthOidc / @WpAuthSharedSecret, exactly as the server verifies it.
153
153
  const authMeta = (0, core_util_1.getAuthMeta)(apiPrototype, methodName);
154
154
  this.assertEndpointSupported(authMeta, methodName);
155
155
  const formPost = (0, core_util_1.isFormPost)(apiPrototype, methodName);
@@ -209,15 +209,15 @@ class ProxyClient {
209
209
  `service-to-service client, set formPost:false (or remove it) so it uses JSON.`);
210
210
  }
211
211
  const authMode = route.authMeta?.mode;
212
- // @AuthApiKey: the credential is a CUSTOMER-held key, and the header carrying it is the app's
212
+ // @WpAuthApiKey: the credential is a CUSTOMER-held key, and the header carrying it is the app's
213
213
  // ApiKeyHook's choice, so this client has nothing to send and the call is a guaranteed 401.
214
214
  if (authMode?.kind === 'apikey') {
215
- throw new Error(`${this.apiName}.${route.methodName} is @AuthApiKey('${authMode.regime}') — only the partner ` +
215
+ throw new Error(`${this.apiName}.${route.methodName} is @WpAuthApiKey('${authMode.regime}') — only the partner ` +
216
216
  `holding that api key can call it, and the header carrying it is the app's ApiKeyHook's choice, ` +
217
217
  `so a webpieces client has no credential to send.`);
218
218
  }
219
- // @AuthWebhook is DELIBERATELY absent from this list. It used to be here, on the assumption
220
- // that the vendor is always somebody else — but `@AuthWebhook(name)` names a signing SCHEME,
219
+ // @WpAuthWebhook is DELIBERATELY absent from this list. It used to be here, on the assumption
220
+ // that the vendor is always somebody else — but `@WpAuthWebhook(name)` names a signing SCHEME,
221
221
  // not a direction, and for an OUTBOUND partner webhook WE are the vendor. The environment's
222
222
  // outbound-auth filter asks its bound signer to produce the signature, which is the exact
223
223
  // mirror of the inbound WebhookAuthCallback that verifies one.
@@ -1 +1 @@
1
- {"version":3,"file":"ProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n isFormPost,\n isRawBody,\n getMaskSpec,\n AuthMeta,\n DestinationTrust,\n RouteMetadata,\n LogApiCallImpl,\n ApiMethodInfo,\n toError,\n NetworkRejectClassifier,\n FilterChain,\n CallRegistry,\n CallDeadline,\n CallContext,\n} from '@webpieces/core-util';\nimport { ApiPrototype } from './ApiPrototype';\nimport { ClientFilterDefinition } from './ClientFilter';\nimport { ClientRequest } from './ClientRequest';\nimport { ClientErrorTranslator } from './ClientErrorTranslator';\nimport { HttpResponseDtoFactory } from './HttpResponseDtoFactory';\nimport { RequestOutcome } from './RequestOutcome';\nimport { ResponseBodyReader } from './ResponseBodyReader';\nimport { TranslatedFailure } from './TranslatedFailure';\n\n/**\n * ProxyClient - the HTTP call engine behind one API contract's client proxy.\n *\n * Contains ONLY what a browser can run: the route map built from the contract's decorators, URL\n * assembly, `fetch`, error translation, and logging. It holds no context object, no credentials,\n * and no recorder — it ASKS ITSELF for those through the hooks below, and each subclass answers\n * from its own environment.\n *\n * That is why the class is abstract rather than parameterized by a collaborator: a shared\n * header-provider seam would drag Node's AsyncLocalStorage vocabulary into a browser bundle and the\n * browser's store vocabulary into a server, and neither has any use for the other.\n *\n * NodeProxyClient (@webpieces/http-client-node) -> RequestContext, Secrets, mintIdToken, recording\n * BrowserProxyClient (@webpieces/http-client-browser) -> an app-held store, no credentials, no recording\n *\n * TWO-PHASE: collaborators arrive on the subclass constructor (so a DI container can supply them),\n * while the per-client state — which contract, which target — arrives on the subclass's `init`,\n * which calls {@link initRoutes}. That is what lets a factory hold a `Provider<ProxyClient>` and\n * hand out a fresh, independently-configured client per contract.\n */\nexport abstract class ProxyClient {\n // Assigned by initRoutes(), which every subclass's init() calls immediately after construction.\n private routeMap!: Map<string, RouteMetadata>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n\n /**\n * The OUTBOUND filter chain, built once at bind time from {@link clientFilters} and reused for\n * every call. Built once rather than per call because a filter is STATELESS by contract (the\n * per-call state is the {@link ClientRequest} the chain is handed), exactly as on the server.\n */\n private chain!: FilterChain<ClientRequest, Response>;\n\n /**\n * The app's own filters, as handed to `createRpcClient`. Set by {@link initRoutes} BEFORE it\n * calls {@link clientFilters}, so an environment's built-ins may read the app's intent off them\n * — @webpieces/http-client-node takes the SSRF policy from an installed `ContextBaseUrlFilter`\n * that way, which keeps the one legitimate relaxation at the same construction site as the\n * decision to be re-pointable at all.\n */\n protected appFilters: ClientFilterDefinition[] = [];\n\n // Stateless + dependency-free, so the browser bundle keeps no DI on the fetch path.\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n // Same shape and same reason: stateless, so it is constructed here rather than injected.\n private readonly bodyReader = new ResponseBodyReader();\n\n /**\n * fetch `Response` -> the transport-neutral {@link HttpResponseDto} an app's `ErrorTranslators`\n * sees. Normalising HERE is what makes `fromWire` receive the identical shape in node and in the\n * browser: both environments share this class, and this is the only place either builds a DTO.\n */\n private readonly responseDtoFactory = new HttpResponseDtoFactory();\n\n /**\n * @param logApiCall - built by the SUBCLASS's package around that environment's ApiCallContext\n * (node: RequestContextApiCallContext; browser: BrowserApiCallContext). REQUIRED, with no\n * default: core-util cannot construct either one, and a default here would have to reach for a\n * process-global — which is exactly the throw-on-first-call this parameter deleted.\n */\n constructor(protected readonly logApiCall: LogApiCallImpl) {}\n\n // ---------------------------------------------------------------- environment hooks\n\n /** The callee's base URL. Async because a server may derive it from container metadata. */\n protected abstract resolveBaseUrl(): Promise<string>;\n\n /**\n * Context headers to put on the wire. Server reads RequestContext; browser reads its store.\n *\n * `destination` is derived from THIS route's auth mode and decides whether TRUSTED context keys\n * (`x-user-id`, `x-org-id`, `x-webpieces-roles`) may ride along — see {@link DestinationTrust}.\n * It is a required argument on purpose: a defaulted \"send everything\" would put the permissive\n * answer one keystroke away and make the safe one opt-in.\n *\n * RENAMED from `outboundHeaders()` in the same change that added `destination`, and the rename IS\n * the migration. TypeScript accepts an override that declares FEWER parameters than its base, so a\n * downstream `protected override outboundHeaders(): Map<string, string>` would have kept compiling\n * and silently ignored the gate — the permissive behaviour surviving as a second spelling. Against\n * the NEW name that subclass fails twice over: `override` names a member the base no longer has,\n * and this abstract member is left unimplemented.\n */\n protected abstract outboundContextHeaders(destination: DestinationTrust): Map<string, string>;\n\n /**\n * Run the call. The default just logs it. Test-case RECORDING is a server concept, so\n * NodeProxyClient overrides this to capture the call when a recorder is in the context.\n *\n * Context fields are NOT passed in: a logging backend stamps them onto every record itself.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected async execute(\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n // apiClass = the CONTRACT name (this.apiName, e.g. 'SaveApi') so this client log line MATCHES\n // the server's for the same call. A client has no impl class, so controllerName is omitted.\n const info = new ApiMethodInfo(\n 'client',\n this.apiName,\n route.methodName,\n undefined,\n route.mask,\n );\n return this.logApiCall.execute(info, requestDto, method);\n }\n\n /**\n * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot\n * mint the OIDC token an @AuthOidc endpoint demands. Surfacing it here beats failing on the\n * first call in production. The default accepts everything.\n */\n protected assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n\n /**\n * The FRAMEWORK filters this environment installs on every client it builds, BENEATH whatever\n * the app passed to `createRpcClient`. The default installs none, so the browser runs the exact\n * code path it ran before the chain existed.\n *\n * \"Beneath\" is not a priority — see {@link initRoutes}. These are the filters that must judge\n * and sign what is ACTUALLY about to be sent, so no app priority may be allowed to get under\n * them: @webpieces/http-client-node installs its SSRF guard and its outbound-auth minter here,\n * and both would be defeated by an app filter that re-pointed the URL below them. Neither\n * concept can live in this class, because reading a RequestContext, resolving DNS and minting\n * an OIDC token are all things a browser bundle must never contain.\n */\n protected clientFilters(): ClientFilterDefinition[] {\n return [];\n }\n\n /**\n * Adapt a translated downstream failure into the error THIS environment's caller should see.\n *\n * THE INVARIANT, and the reason this hook exists at all:\n *\n * A status received from a downstream dependency describes OUR request to it. It is never the\n * status we return to OUR caller. The server that answered 404 is correct; the server that\n * asked for a route that does not exist is broken, and must say so as a 500.\n *\n * That invariant reads differently in the two environments, which is exactly why the ISOMORPHIC\n * {@link ClientErrorTranslator} cannot settle it:\n * - BROWSER: the client IS the end user's agent, so the downstream IS the answer. Pass it through\n * unchanged.\n * - NODE: server-to-server. A 4xx from a dependency is a caller-side defect (wrong path, wrong\n * base URL, an undeployed dependency, bad service credentials), so the caller owns it as a 500.\n *\n * ABSTRACT, not a defaulted pass-through, for the same reason\n * {@link outboundContextHeaders} takes a required `destination`: a permissive default puts the\n * wrong answer one keystroke away. A new environment subclass must SAY which of the two it is,\n * and there are exactly two subclasses in the repo, so the compile error is the migration.\n *\n * @param failure - the translated error, its provenance (app-registered vs built-in), and the\n * downstream status\n * @param callId - `ApiName.methodName`, so a rewritten message can still name the call\n */\n protected abstract adaptDownstreamFailure(failure: TranslatedFailure, callId: string): Error;\n\n /**\n * Fires before the logical call's attempts, once per RPC — the progress \"start marker\". Symmetric with\n * {@link onRequestEnd}: every start is followed by exactly one end, on every path, so a listener\n * can drive a counter (bar on / bar off) without leaking a permanently-spinning bar.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestStart(_route: RouteMetadata): void {}\n\n /**\n * Fires exactly ONCE after the call settles, on EVERY path (2xx, HTTP error, network reject) —\n * the \"stop marker\", carrying how it settled.\n *\n * Subsumes the older header-only hook: this is the ONLY place the `fetch` Response — and thus its\n * `Headers` — exists, so an app that needs to read a response header (e.g. a server-version stamp\n * for client↔server version matching) reads `outcome.headers` after settlement\n * and on both the ok and error paths. `outcome.ok`/`outcome.error` add the success-or-error\n * signal the header-only seam could not give.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestEnd(_route: RouteMetadata, _outcome: RequestOutcome): void {}\n\n // ---------------------------------------------------------------- contract binding\n\n /**\n * Bind this client to one API contract: read @ApiPath/@Endpoint/@Auth* off the prototype and\n * build the route map once. Each subclass's `init(api, config)` stores its own config, then\n * calls this.\n *\n * @param appFilters the app's OUTBOUND filters for this client, from `createRpcClient`. They are\n * merged with {@link clientFilters} and sorted by priority, highest OUTERMOST.\n * @throws Error if the prototype lacks @ApiPath, or declares an endpoint this environment\n * cannot satisfy (see {@link assertEndpointSupported}).\n */\n protected initRoutes(\n apiPrototype: ApiPrototype<object>,\n appFilters: ClientFilterDefinition[],\n ): void {\n this.appFilters = appFilters;\n this.apiClass = apiPrototype;\n if (!isApiPath(apiPrototype)) {\n const className = apiPrototype.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n const basePath = getApiPath(apiPrototype)!;\n const endpoints = getEndpoints(apiPrototype) || {};\n\n // apiName as the class name so client logs read \"SaveApi.save\", not \"undefined.save\"\n this.apiName = apiPrototype.name || 'UnknownApi';\n\n this.routeMap = new Map<string, RouteMetadata>();\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const fullPath = basePath + endpointPath;\n // Capture the endpoint's auth mode so the client can mint delivery auth per\n // @AuthOidc / @AuthSharedSecret, exactly as the server verifies it.\n const authMeta = getAuthMeta(apiPrototype, methodName);\n this.assertEndpointSupported(authMeta, methodName);\n const formPost = isFormPost(apiPrototype, methodName);\n this.routeMap.set(\n methodName,\n new RouteMetadata(\n 'POST',\n fullPath,\n methodName,\n this.apiName,\n authMeta,\n undefined,\n formPost,\n getMaskSpec(apiPrototype, methodName),\n isRawBody(apiPrototype, methodName),\n ),\n );\n }\n\n // APP filters first (highest priority OUTERMOST, matching the server's FilterMatcher), then\n // the framework built-ins, ALWAYS innermost. Two separate sorts rather than one over the\n // union, deliberately: an app priority orders app filters against each other and nothing\n // else, so no number an app can type — however large — gets underneath the SSRF guard or the\n // credential minter. A single sorted list would make \"displace the guard\" a matter of typing\n // a bigger integer, and a security control an app can outrank by accident is not a control.\n //\n // Sorted here, once, so FilterChain itself never sorts — priority lives on the DEFINITION,\n // not on the filter.\n const byPriority = (a: ClientFilterDefinition, b: ClientFilterDefinition): number =>\n b.priority - a.priority;\n const ordered = [\n ...[...this.appFilters].sort(byPriority),\n ...[...this.clientFilters()].sort(byPriority),\n ];\n this.chain = new FilterChain<ClientRequest, Response>(\n ordered.map((definition: ClientFilterDefinition) => definition.filter),\n );\n }\n\n /** The contract's class name, for logs and recordings. */\n protected contractName(): string {\n return this.apiName;\n }\n\n /** Check if a route exists for the given method name. */\n hasRoute(methodName: string): boolean {\n return this.routeMap.has(methodName);\n }\n\n /**\n * Get route metadata for a method name.\n * @throws Error if no route found\n */\n getRoute(methodName: string): RouteMetadata {\n const route = this.routeMap.get(methodName);\n if (!route) {\n throw new Error(`No route found for method ${methodName}`);\n }\n return route;\n }\n\n // ---------------------------------------------------------------- the call\n\n /**\n * FAIL FAST, PER METHOD, at call time: some endpoints exist for a caller that is not us, and this\n * proxy could only ever build a request they are obliged to reject. Refusing here rather than at\n * bind time means an api that MIXES such endpoints with normal ones still yields a working client\n * for the normal ones; only calling the un-callable method throws.\n *\n * @throws Error naming the endpoint, what it declared, and who its real caller is.\n */\n private refuseEndpointNoClientCanCall(route: RouteMetadata): void {\n // formPost exists ONLY for EXTERNAL inbound webhooks (e.g. Twilio is the caller). This proxy\n // JSON.stringifies the body, so calling one would silently send a wrong-encoded body.\n if (route.formPost) {\n throw new Error(\n `${this.apiName}.${route.methodName} is @Endpoint(..., { formPost: true }) — the ` +\n `webpieces client does not support calling form-encoded endpoints yet. formPost is ` +\n `for EXTERNAL inbound webhooks (e.g. Twilio) only. If this endpoint needs a ` +\n `service-to-service client, set formPost:false (or remove it) so it uses JSON.`,\n );\n }\n const authMode = route.authMeta?.mode;\n // @AuthApiKey: the credential is a CUSTOMER-held key, and the header carrying it is the app's\n // ApiKeyHook's choice, so this client has nothing to send and the call is a guaranteed 401.\n if (authMode?.kind === 'apikey') {\n throw new Error(\n `${this.apiName}.${route.methodName} is @AuthApiKey('${authMode.regime}') — only the partner ` +\n `holding that api key can call it, and the header carrying it is the app's ApiKeyHook's choice, ` +\n `so a webpieces client has no credential to send.`,\n );\n }\n // @AuthWebhook is DELIBERATELY absent from this list. It used to be here, on the assumption\n // that the vendor is always somebody else — but `@AuthWebhook(name)` names a signing SCHEME,\n // not a direction, and for an OUTBOUND partner webhook WE are the vendor. The environment's\n // outbound-auth filter asks its bound signer to produce the signature, which is the exact\n // mirror of the inbound WebhookAuthCallback that verifies one.\n }\n\n /** One logical call: one lifecycle pair and log entry across all strategy attempts. */\n // webpieces-disable no-any-unknown -- request and response DTOs are erased at the proxy boundary\n async makeRequest(route: RouteMetadata, args: unknown[]): Promise<unknown> {\n this.refuseEndpointNoClientCanCall(route);\n const requestDto = args[0];\n return this.execute(route, requestDto, () => this.executeCall(route, requestDto));\n }\n\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async executeCall(route: RouteMetadata, requestDto: unknown): Promise<unknown> {\n this.onRequestStart(route);\n let response: Response | undefined;\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n let result: unknown;\n // webpieces-disable no-unmanaged-exceptions -- report one logical END, preserving the original thrown value\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n result = await CallRegistry.execute(\n this.apiClass,\n route.methodName,\n (timeoutMs: number) => {\n response = undefined;\n return CallDeadline.run(\n timeoutMs,\n new CallContext(this.apiName, route.methodName),\n async (signal: AbortSignal) => {\n const request = await this.prepareRequest(route, requestDto);\n CallDeadline.throwIfAborted(signal);\n const received = await this.chain.execute(request, () =>\n this.sendOnce(request, signal),\n );\n CallDeadline.throwIfAborted(signal);\n response = received;\n return this.readResponse(received, route);\n },\n );\n },\n 30_000,\n );\n } catch (err: unknown) {\n const error = toError(err);\n this.onRequestEnd(\n route,\n new RequestOutcome(false, response?.status ?? 0, response?.headers, error),\n );\n throw err;\n }\n this.onRequestEnd(\n route,\n new RequestOutcome(true, response?.status ?? 0, response?.headers),\n );\n return result;\n }\n\n /** Fresh mutable request for every attempt, including URL, headers, auth and body. */\n // webpieces-disable no-any-unknown -- request DTO is erased at the proxy boundary\n private async prepareRequest(\n route: RouteMetadata,\n requestDto: unknown,\n ): Promise<ClientRequest> {\n const baseUrl = await this.resolveBaseUrl();\n const headers = new Map<string, string>([['Content-Type', 'application/json']]);\n const context = this.outboundContextHeaders(\n DestinationTrust.forAuthMode(route.authMeta?.mode),\n );\n for (const entry of context.entries()) headers.set(entry[0], entry[1]);\n return new ClientRequest(\n route,\n this.apiName,\n baseUrl,\n headers,\n JSON.stringify(requestDto),\n requestDto,\n );\n }\n\n /**\n * ONE transmission — the bottom of the filter chain, and the only place `fetch` is called.\n *\n * Everything it sends comes off the {@link ClientRequest} as the chain left it, so a filter's\n * edits to the url, the headers or the serialized body are exactly what goes on the wire. It may\n * run more than once for a single RPC when a filter follows a redirect.\n *\n * A network reject (offline, DNS, CORS preflight) is classified into a typed OfflineError here (a\n * genuine bug passes through untouched) so that filters above see the same typed error the caller\n * will, rather than a raw platform reject.\n */\n private async sendOnce(request: ClientRequest, signal: AbortSignal): Promise<Response> {\n CallDeadline.throwIfAborted(signal);\n const options: RequestInit = {\n method: request.route.httpMethod,\n signal,\n headers: request.headersAsRecord(),\n redirect: request.followRedirects ? 'follow' : 'manual',\n };\n if (request.body !== undefined) {\n options.body = request.body;\n }\n // webpieces-disable no-unmanaged-exceptions -- classify a network reject, then rethrow it typed\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-fetch -- this IS the generated-client implementation the rule points everyone to\n return await fetch(request.url, options);\n } catch (err: unknown) {\n const error = toError(err);\n throw this.networkRejectClassifier.toNetworkError(error, request.url);\n }\n }\n\n /** Body consumption is inside the attempt deadline, including non-JSON error bodies. */\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async readResponse(response: Response, route: RouteMetadata): Promise<unknown> {\n const callId = `${this.apiName}.${route.methodName}`;\n // 266 is protocol success, but its body represents an expected user exception.\n if (response.ok && response.status !== 266) {\n if (!this.bodyReader.isJson(response)) {\n throw new Error(\n this.bodyReader.describeForeignBody(response, callId, await response.text()),\n );\n }\n return response.json();\n }\n const protocolError = await this.bodyReader.readErrorBody(response, callId);\n const translated = ClientErrorTranslator.translateError(\n this.responseDtoFactory.fromFetch(response, protocolError),\n );\n throw this.adaptDownstreamFailure(translated, callId);\n }\n}\n"]}
1
+ {"version":3,"file":"ProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n isFormPost,\n isRawBody,\n getMaskSpec,\n AuthMeta,\n DestinationTrust,\n RouteMetadata,\n LogApiCallImpl,\n ApiMethodInfo,\n toError,\n NetworkRejectClassifier,\n FilterChain,\n CallRegistry,\n CallDeadline,\n CallContext,\n} from '@webpieces/core-util';\nimport { ApiPrototype } from './ApiPrototype';\nimport { ClientFilterDefinition } from './ClientFilter';\nimport { ClientRequest } from './ClientRequest';\nimport { ClientErrorTranslator } from './ClientErrorTranslator';\nimport { HttpResponseDtoFactory } from './HttpResponseDtoFactory';\nimport { RequestOutcome } from './RequestOutcome';\nimport { ResponseBodyReader } from './ResponseBodyReader';\nimport { TranslatedFailure } from './TranslatedFailure';\n\n/**\n * ProxyClient - the HTTP call engine behind one API contract's client proxy.\n *\n * Contains ONLY what a browser can run: the route map built from the contract's decorators, URL\n * assembly, `fetch`, error translation, and logging. It holds no context object, no credentials,\n * and no recorder — it ASKS ITSELF for those through the hooks below, and each subclass answers\n * from its own environment.\n *\n * That is why the class is abstract rather than parameterized by a collaborator: a shared\n * header-provider seam would drag Node's AsyncLocalStorage vocabulary into a browser bundle and the\n * browser's store vocabulary into a server, and neither has any use for the other.\n *\n * NodeProxyClient (@webpieces/http-client-node) -> RequestContext, Secrets, mintIdToken, recording\n * BrowserProxyClient (@webpieces/http-client-browser) -> an app-held store, no credentials, no recording\n *\n * TWO-PHASE: collaborators arrive on the subclass constructor (so a DI container can supply them),\n * while the per-client state — which contract, which target — arrives on the subclass's `init`,\n * which calls {@link initRoutes}. That is what lets a factory hold a `Provider<ProxyClient>` and\n * hand out a fresh, independently-configured client per contract.\n */\nexport abstract class ProxyClient {\n // Assigned by initRoutes(), which every subclass's init() calls immediately after construction.\n private routeMap!: Map<string, RouteMetadata>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n\n /**\n * The OUTBOUND filter chain, built once at bind time from {@link clientFilters} and reused for\n * every call. Built once rather than per call because a filter is STATELESS by contract (the\n * per-call state is the {@link ClientRequest} the chain is handed), exactly as on the server.\n */\n private chain!: FilterChain<ClientRequest, Response>;\n\n /**\n * The app's own filters, as handed to `createRpcClient`. Set by {@link initRoutes} BEFORE it\n * calls {@link clientFilters}, so an environment's built-ins may read the app's intent off them\n * — @webpieces/http-client-node takes the SSRF policy from an installed `ContextBaseUrlFilter`\n * that way, which keeps the one legitimate relaxation at the same construction site as the\n * decision to be re-pointable at all.\n */\n protected appFilters: ClientFilterDefinition[] = [];\n\n // Stateless + dependency-free, so the browser bundle keeps no DI on the fetch path.\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n // Same shape and same reason: stateless, so it is constructed here rather than injected.\n private readonly bodyReader = new ResponseBodyReader();\n\n /**\n * fetch `Response` -> the transport-neutral {@link HttpResponseDto} an app's `ErrorTranslators`\n * sees. Normalising HERE is what makes `fromWire` receive the identical shape in node and in the\n * browser: both environments share this class, and this is the only place either builds a DTO.\n */\n private readonly responseDtoFactory = new HttpResponseDtoFactory();\n\n /**\n * @param logApiCall - built by the SUBCLASS's package around that environment's ApiCallContext\n * (node: RequestContextApiCallContext; browser: BrowserApiCallContext). REQUIRED, with no\n * default: core-util cannot construct either one, and a default here would have to reach for a\n * process-global — which is exactly the throw-on-first-call this parameter deleted.\n */\n constructor(protected readonly logApiCall: LogApiCallImpl) {}\n\n // ---------------------------------------------------------------- environment hooks\n\n /** The callee's base URL. Async because a server may derive it from container metadata. */\n protected abstract resolveBaseUrl(): Promise<string>;\n\n /**\n * Context headers to put on the wire. Server reads RequestContext; browser reads its store.\n *\n * `destination` is derived from THIS route's auth mode and decides whether TRUSTED context keys\n * (`x-user-id`, `x-org-id`, `x-webpieces-roles`) may ride along — see {@link DestinationTrust}.\n * It is a required argument on purpose: a defaulted \"send everything\" would put the permissive\n * answer one keystroke away and make the safe one opt-in.\n *\n * RENAMED from `outboundHeaders()` in the same change that added `destination`, and the rename IS\n * the migration. TypeScript accepts an override that declares FEWER parameters than its base, so a\n * downstream `protected override outboundHeaders(): Map<string, string>` would have kept compiling\n * and silently ignored the gate — the permissive behaviour surviving as a second spelling. Against\n * the NEW name that subclass fails twice over: `override` names a member the base no longer has,\n * and this abstract member is left unimplemented.\n */\n protected abstract outboundContextHeaders(destination: DestinationTrust): Map<string, string>;\n\n /**\n * Run the call. The default just logs it. Test-case RECORDING is a server concept, so\n * NodeProxyClient overrides this to capture the call when a recorder is in the context.\n *\n * Context fields are NOT passed in: a logging backend stamps them onto every record itself.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected async execute(\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n // apiClass = the CONTRACT name (this.apiName, e.g. 'SaveApi') so this client log line MATCHES\n // the server's for the same call. A client has no impl class, so controllerName is omitted.\n const info = new ApiMethodInfo(\n 'client',\n this.apiName,\n route.methodName,\n undefined,\n route.mask,\n );\n return this.logApiCall.execute(info, requestDto, method);\n }\n\n /**\n * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot\n * mint the OIDC token an @WpAuthOidc endpoint demands. Surfacing it here beats failing on the\n * first call in production. The default accepts everything.\n */\n protected assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n\n /**\n * The FRAMEWORK filters this environment installs on every client it builds, BENEATH whatever\n * the app passed to `createRpcClient`. The default installs none, so the browser runs the exact\n * code path it ran before the chain existed.\n *\n * \"Beneath\" is not a priority — see {@link initRoutes}. These are the filters that must judge\n * and sign what is ACTUALLY about to be sent, so no app priority may be allowed to get under\n * them: @webpieces/http-client-node installs its SSRF guard and its outbound-auth minter here,\n * and both would be defeated by an app filter that re-pointed the URL below them. Neither\n * concept can live in this class, because reading a RequestContext, resolving DNS and minting\n * an OIDC token are all things a browser bundle must never contain.\n */\n protected clientFilters(): ClientFilterDefinition[] {\n return [];\n }\n\n /**\n * Adapt a translated downstream failure into the error THIS environment's caller should see.\n *\n * THE INVARIANT, and the reason this hook exists at all:\n *\n * A status received from a downstream dependency describes OUR request to it. It is never the\n * status we return to OUR caller. The server that answered 404 is correct; the server that\n * asked for a route that does not exist is broken, and must say so as a 500.\n *\n * That invariant reads differently in the two environments, which is exactly why the ISOMORPHIC\n * {@link ClientErrorTranslator} cannot settle it:\n * - BROWSER: the client IS the end user's agent, so the downstream IS the answer. Pass it through\n * unchanged.\n * - NODE: server-to-server. A 4xx from a dependency is a caller-side defect (wrong path, wrong\n * base URL, an undeployed dependency, bad service credentials), so the caller owns it as a 500.\n *\n * ABSTRACT, not a defaulted pass-through, for the same reason\n * {@link outboundContextHeaders} takes a required `destination`: a permissive default puts the\n * wrong answer one keystroke away. A new environment subclass must SAY which of the two it is,\n * and there are exactly two subclasses in the repo, so the compile error is the migration.\n *\n * @param failure - the translated error, its provenance (app-registered vs built-in), and the\n * downstream status\n * @param callId - `ApiName.methodName`, so a rewritten message can still name the call\n */\n protected abstract adaptDownstreamFailure(failure: TranslatedFailure, callId: string): Error;\n\n /**\n * Fires before the logical call's attempts, once per RPC — the progress \"start marker\". Symmetric with\n * {@link onRequestEnd}: every start is followed by exactly one end, on every path, so a listener\n * can drive a counter (bar on / bar off) without leaking a permanently-spinning bar.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestStart(_route: RouteMetadata): void {}\n\n /**\n * Fires exactly ONCE after the call settles, on EVERY path (2xx, HTTP error, network reject) —\n * the \"stop marker\", carrying how it settled.\n *\n * Subsumes the older header-only hook: this is the ONLY place the `fetch` Response — and thus its\n * `Headers` — exists, so an app that needs to read a response header (e.g. a server-version stamp\n * for client↔server version matching) reads `outcome.headers` after settlement\n * and on both the ok and error paths. `outcome.ok`/`outcome.error` add the success-or-error\n * signal the header-only seam could not give.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestEnd(_route: RouteMetadata, _outcome: RequestOutcome): void {}\n\n // ---------------------------------------------------------------- contract binding\n\n /**\n * Bind this client to one API contract: read @ApiPath/@Endpoint/@Auth* off the prototype and\n * build the route map once. Each subclass's `init(api, config)` stores its own config, then\n * calls this.\n *\n * @param appFilters the app's OUTBOUND filters for this client, from `createRpcClient`. They are\n * merged with {@link clientFilters} and sorted by priority, highest OUTERMOST.\n * @throws Error if the prototype lacks @ApiPath, or declares an endpoint this environment\n * cannot satisfy (see {@link assertEndpointSupported}).\n */\n protected initRoutes(\n apiPrototype: ApiPrototype<object>,\n appFilters: ClientFilterDefinition[],\n ): void {\n this.appFilters = appFilters;\n this.apiClass = apiPrototype;\n if (!isApiPath(apiPrototype)) {\n const className = apiPrototype.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n const basePath = getApiPath(apiPrototype)!;\n const endpoints = getEndpoints(apiPrototype) || {};\n\n // apiName as the class name so client logs read \"SaveApi.save\", not \"undefined.save\"\n this.apiName = apiPrototype.name || 'UnknownApi';\n\n this.routeMap = new Map<string, RouteMetadata>();\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const fullPath = basePath + endpointPath;\n // Capture the endpoint's auth mode so the client can mint delivery auth per\n // @WpAuthOidc / @WpAuthSharedSecret, exactly as the server verifies it.\n const authMeta = getAuthMeta(apiPrototype, methodName);\n this.assertEndpointSupported(authMeta, methodName);\n const formPost = isFormPost(apiPrototype, methodName);\n this.routeMap.set(\n methodName,\n new RouteMetadata(\n 'POST',\n fullPath,\n methodName,\n this.apiName,\n authMeta,\n undefined,\n formPost,\n getMaskSpec(apiPrototype, methodName),\n isRawBody(apiPrototype, methodName),\n ),\n );\n }\n\n // APP filters first (highest priority OUTERMOST, matching the server's FilterMatcher), then\n // the framework built-ins, ALWAYS innermost. Two separate sorts rather than one over the\n // union, deliberately: an app priority orders app filters against each other and nothing\n // else, so no number an app can type — however large — gets underneath the SSRF guard or the\n // credential minter. A single sorted list would make \"displace the guard\" a matter of typing\n // a bigger integer, and a security control an app can outrank by accident is not a control.\n //\n // Sorted here, once, so FilterChain itself never sorts — priority lives on the DEFINITION,\n // not on the filter.\n const byPriority = (a: ClientFilterDefinition, b: ClientFilterDefinition): number =>\n b.priority - a.priority;\n const ordered = [\n ...[...this.appFilters].sort(byPriority),\n ...[...this.clientFilters()].sort(byPriority),\n ];\n this.chain = new FilterChain<ClientRequest, Response>(\n ordered.map((definition: ClientFilterDefinition) => definition.filter),\n );\n }\n\n /** The contract's class name, for logs and recordings. */\n protected contractName(): string {\n return this.apiName;\n }\n\n /** Check if a route exists for the given method name. */\n hasRoute(methodName: string): boolean {\n return this.routeMap.has(methodName);\n }\n\n /**\n * Get route metadata for a method name.\n * @throws Error if no route found\n */\n getRoute(methodName: string): RouteMetadata {\n const route = this.routeMap.get(methodName);\n if (!route) {\n throw new Error(`No route found for method ${methodName}`);\n }\n return route;\n }\n\n // ---------------------------------------------------------------- the call\n\n /**\n * FAIL FAST, PER METHOD, at call time: some endpoints exist for a caller that is not us, and this\n * proxy could only ever build a request they are obliged to reject. Refusing here rather than at\n * bind time means an api that MIXES such endpoints with normal ones still yields a working client\n * for the normal ones; only calling the un-callable method throws.\n *\n * @throws Error naming the endpoint, what it declared, and who its real caller is.\n */\n private refuseEndpointNoClientCanCall(route: RouteMetadata): void {\n // formPost exists ONLY for EXTERNAL inbound webhooks (e.g. Twilio is the caller). This proxy\n // JSON.stringifies the body, so calling one would silently send a wrong-encoded body.\n if (route.formPost) {\n throw new Error(\n `${this.apiName}.${route.methodName} is @Endpoint(..., { formPost: true }) — the ` +\n `webpieces client does not support calling form-encoded endpoints yet. formPost is ` +\n `for EXTERNAL inbound webhooks (e.g. Twilio) only. If this endpoint needs a ` +\n `service-to-service client, set formPost:false (or remove it) so it uses JSON.`,\n );\n }\n const authMode = route.authMeta?.mode;\n // @WpAuthApiKey: the credential is a CUSTOMER-held key, and the header carrying it is the app's\n // ApiKeyHook's choice, so this client has nothing to send and the call is a guaranteed 401.\n if (authMode?.kind === 'apikey') {\n throw new Error(\n `${this.apiName}.${route.methodName} is @WpAuthApiKey('${authMode.regime}') — only the partner ` +\n `holding that api key can call it, and the header carrying it is the app's ApiKeyHook's choice, ` +\n `so a webpieces client has no credential to send.`,\n );\n }\n // @WpAuthWebhook is DELIBERATELY absent from this list. It used to be here, on the assumption\n // that the vendor is always somebody else — but `@WpAuthWebhook(name)` names a signing SCHEME,\n // not a direction, and for an OUTBOUND partner webhook WE are the vendor. The environment's\n // outbound-auth filter asks its bound signer to produce the signature, which is the exact\n // mirror of the inbound WebhookAuthCallback that verifies one.\n }\n\n /** One logical call: one lifecycle pair and log entry across all strategy attempts. */\n // webpieces-disable no-any-unknown -- request and response DTOs are erased at the proxy boundary\n async makeRequest(route: RouteMetadata, args: unknown[]): Promise<unknown> {\n this.refuseEndpointNoClientCanCall(route);\n const requestDto = args[0];\n return this.execute(route, requestDto, () => this.executeCall(route, requestDto));\n }\n\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async executeCall(route: RouteMetadata, requestDto: unknown): Promise<unknown> {\n this.onRequestStart(route);\n let response: Response | undefined;\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n let result: unknown;\n // webpieces-disable no-unmanaged-exceptions -- report one logical END, preserving the original thrown value\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n result = await CallRegistry.execute(\n this.apiClass,\n route.methodName,\n (timeoutMs: number) => {\n response = undefined;\n return CallDeadline.run(\n timeoutMs,\n new CallContext(this.apiName, route.methodName),\n async (signal: AbortSignal) => {\n const request = await this.prepareRequest(route, requestDto);\n CallDeadline.throwIfAborted(signal);\n const received = await this.chain.execute(request, () =>\n this.sendOnce(request, signal),\n );\n CallDeadline.throwIfAborted(signal);\n response = received;\n return this.readResponse(received, route);\n },\n );\n },\n 30_000,\n );\n } catch (err: unknown) {\n const error = toError(err);\n this.onRequestEnd(\n route,\n new RequestOutcome(false, response?.status ?? 0, response?.headers, error),\n );\n throw err;\n }\n this.onRequestEnd(\n route,\n new RequestOutcome(true, response?.status ?? 0, response?.headers),\n );\n return result;\n }\n\n /** Fresh mutable request for every attempt, including URL, headers, auth and body. */\n // webpieces-disable no-any-unknown -- request DTO is erased at the proxy boundary\n private async prepareRequest(\n route: RouteMetadata,\n requestDto: unknown,\n ): Promise<ClientRequest> {\n const baseUrl = await this.resolveBaseUrl();\n const headers = new Map<string, string>([['Content-Type', 'application/json']]);\n const context = this.outboundContextHeaders(\n DestinationTrust.forAuthMode(route.authMeta?.mode),\n );\n for (const entry of context.entries()) headers.set(entry[0], entry[1]);\n return new ClientRequest(\n route,\n this.apiName,\n baseUrl,\n headers,\n JSON.stringify(requestDto),\n requestDto,\n );\n }\n\n /**\n * ONE transmission — the bottom of the filter chain, and the only place `fetch` is called.\n *\n * Everything it sends comes off the {@link ClientRequest} as the chain left it, so a filter's\n * edits to the url, the headers or the serialized body are exactly what goes on the wire. It may\n * run more than once for a single RPC when a filter follows a redirect.\n *\n * A network reject (offline, DNS, CORS preflight) is classified into a typed OfflineError here (a\n * genuine bug passes through untouched) so that filters above see the same typed error the caller\n * will, rather than a raw platform reject.\n */\n private async sendOnce(request: ClientRequest, signal: AbortSignal): Promise<Response> {\n CallDeadline.throwIfAborted(signal);\n const options: RequestInit = {\n method: request.route.httpMethod,\n signal,\n headers: request.headersAsRecord(),\n redirect: request.followRedirects ? 'follow' : 'manual',\n };\n if (request.body !== undefined) {\n options.body = request.body;\n }\n // webpieces-disable no-unmanaged-exceptions -- classify a network reject, then rethrow it typed\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-fetch -- this IS the generated-client implementation the rule points everyone to\n return await fetch(request.url, options);\n } catch (err: unknown) {\n const error = toError(err);\n throw this.networkRejectClassifier.toNetworkError(error, request.url);\n }\n }\n\n /** Body consumption is inside the attempt deadline, including non-JSON error bodies. */\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async readResponse(response: Response, route: RouteMetadata): Promise<unknown> {\n const callId = `${this.apiName}.${route.methodName}`;\n // 266 is protocol success, but its body represents an expected user exception.\n if (response.ok && response.status !== 266) {\n if (!this.bodyReader.isJson(response)) {\n throw new Error(\n this.bodyReader.describeForeignBody(response, callId, await response.text()),\n );\n }\n return response.json();\n }\n const protocolError = await this.bodyReader.readErrorBody(response, callId);\n const translated = ClientErrorTranslator.translateError(\n this.responseDtoFactory.fromFetch(response, protocolError),\n );\n throw this.adaptDownstreamFailure(translated, callId);\n }\n}\n"]}