@webpieces/http-client-core 0.0.0-dev

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 ADDED
@@ -0,0 +1,17 @@
1
+ # @webpieces/http-client-core
2
+
3
+ The isomorphic engine behind the webpieces HTTP clients. You almost certainly want one of:
4
+
5
+ - **[@webpieces/http-client-node](../http-client-node)** — server-side: inversify-wired, reads
6
+ `RequestContext`, mints OIDC / shared-secret delivery auth, resolves Cloud Run URLs from a
7
+ service name.
8
+ - **[@webpieces/http-client-browser](../http-client-browser)** — browser: DI-free (React or
9
+ Angular), app-managed context store, no AsyncLocalStorage.
10
+
11
+ This package holds what they share: `ProxyClient` (contract decorators → HTTP request),
12
+ `buildClientProxy` (the typed `Proxy` trap), `ClientTarget`, and `ClientErrorTranslator`.
13
+
14
+ The one seam between the two environments is `OutboundHeaders` (in `@webpieces/core-util`):
15
+ `RequestContextHeaders` on the server, `ContextMgr` in a browser. Keeping that a seam is what
16
+ lets this package depend on nothing but `core-util`, so no node-only import can reach a browser
17
+ bundle.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@webpieces/http-client-core",
3
+ "version": "0.0.0-dev",
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
+ "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "author": "Dean Hiller",
9
+ "license": "Apache-2.0",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/deanhiller/webpieces-ts.git",
13
+ "directory": "packages/http/http-client-core"
14
+ },
15
+ "keywords": [
16
+ "webpieces",
17
+ "http",
18
+ "client"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@webpieces/core-util": "workspace:*"
25
+ }
26
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Type representing a class constructor whose prototype is T — the shared API contract class,
3
+ * carrying the @ApiPath/@Endpoint/@Auth* decorators that both the client and the server read.
4
+ */
5
+ export type ApiPrototype<T> = Function & {
6
+ prototype: T;
7
+ };
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=ApiPrototype.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiPrototype.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ApiPrototype.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Type representing a class constructor whose prototype is T — the shared API contract class,\n * carrying the @ApiPath/@Endpoint/@Auth* decorators that both the client and the server read.\n */\nexport type ApiPrototype<T> = Function & { prototype: T };\n"]}
@@ -0,0 +1,37 @@
1
+ import { ProtocolError } from '@webpieces/core-util';
2
+ /**
3
+ * ClientErrorTranslator - Translates HTTP error responses to HttpError exceptions.
4
+ *
5
+ * This is the CLIENT-SIDE reverse of ExpressWrapper.handleError() on the server.
6
+ * It reconstructs typed HttpError exceptions from ProtocolError JSON responses.
7
+ *
8
+ * Architecture:
9
+ * - Server: HttpError → ExpressWrapper.handleError() → ProtocolError JSON
10
+ * - Client: ProtocolError JSON → ClientErrorTranslator.translateError() → HttpError
11
+ *
12
+ * This achieves symmetric error handling - server throws typed exceptions,
13
+ * client receives typed exceptions.
14
+ */
15
+ export declare class ClientErrorTranslator {
16
+ /**
17
+ * Parse error response and reconstruct appropriate HttpError subclass.
18
+ *
19
+ * Maps HTTP status codes to error types (symmetric with server):
20
+ * - 400 → HttpBadRequestError (with field, guiAlertMessage)
21
+ * - 266 → HttpUserError (with errorCode) - 2xx code for user validation
22
+ * - 401 → HttpUnauthorizedError
23
+ * - 403 → HttpForbiddenError
24
+ * - 404 → HttpNotFoundError
25
+ * - 408 → HttpTimeoutError
26
+ * - 500 → HttpInternalServerError
27
+ * - 502 → HttpBadGatewayError
28
+ * - 504 → HttpGatewayTimeoutError
29
+ * - 598 → HttpVendorError (with waitSeconds) - custom status code
30
+ * - other → generic HttpError
31
+ *
32
+ * @param response - Fetch Response object
33
+ * @param protocolError - Parsed ProtocolError from response body
34
+ * @returns HttpError subclass instance
35
+ */
36
+ static translateError(response: Response, protocolError: ProtocolError): Error;
37
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClientErrorTranslator = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
5
+ /**
6
+ * ClientErrorTranslator - Translates HTTP error responses to HttpError exceptions.
7
+ *
8
+ * This is the CLIENT-SIDE reverse of ExpressWrapper.handleError() on the server.
9
+ * It reconstructs typed HttpError exceptions from ProtocolError JSON responses.
10
+ *
11
+ * Architecture:
12
+ * - Server: HttpError → ExpressWrapper.handleError() → ProtocolError JSON
13
+ * - Client: ProtocolError JSON → ClientErrorTranslator.translateError() → HttpError
14
+ *
15
+ * This achieves symmetric error handling - server throws typed exceptions,
16
+ * client receives typed exceptions.
17
+ */
18
+ class ClientErrorTranslator {
19
+ /**
20
+ * Parse error response and reconstruct appropriate HttpError subclass.
21
+ *
22
+ * Maps HTTP status codes to error types (symmetric with server):
23
+ * - 400 → HttpBadRequestError (with field, guiAlertMessage)
24
+ * - 266 → HttpUserError (with errorCode) - 2xx code for user validation
25
+ * - 401 → HttpUnauthorizedError
26
+ * - 403 → HttpForbiddenError
27
+ * - 404 → HttpNotFoundError
28
+ * - 408 → HttpTimeoutError
29
+ * - 500 → HttpInternalServerError
30
+ * - 502 → HttpBadGatewayError
31
+ * - 504 → HttpGatewayTimeoutError
32
+ * - 598 → HttpVendorError (with waitSeconds) - custom status code
33
+ * - other → generic HttpError
34
+ *
35
+ * @param response - Fetch Response object
36
+ * @param protocolError - Parsed ProtocolError from response body
37
+ * @returns HttpError subclass instance
38
+ */
39
+ static translateError(response, protocolError) {
40
+ const statusCode = response.status;
41
+ const message = protocolError.message || response.statusText || 'Unknown error';
42
+ const subType = protocolError.subType;
43
+ // Map status codes to error types (symmetric with server's ExpressWrapper.handleError())
44
+ switch (statusCode) {
45
+ case 400:
46
+ return new core_util_1.HttpBadRequestError(message, protocolError.field, protocolError.guiAlertMessage);
47
+ case 266: // HttpUserError - 2xx code for user validation errors
48
+ return new core_util_1.HttpUserError(message, protocolError.errorCode);
49
+ case 401:
50
+ return new core_util_1.HttpUnauthorizedError(message, subType);
51
+ case 403:
52
+ return new core_util_1.HttpForbiddenError(message);
53
+ case 404:
54
+ return new core_util_1.HttpNotFoundError(message);
55
+ case 408:
56
+ return new core_util_1.HttpTimeoutError(message);
57
+ case 500:
58
+ return new core_util_1.HttpInternalServerError(message);
59
+ case 502:
60
+ return new core_util_1.HttpBadGatewayError(message);
61
+ case 504:
62
+ return new core_util_1.HttpGatewayTimeoutError(message);
63
+ case 598: // HttpVendorError - custom status code for vendor/external service errors
64
+ return new core_util_1.HttpVendorError(message, protocolError.waitSeconds);
65
+ default:
66
+ // Unknown status code - return generic HttpError
67
+ return new Error(` could not translate statusCode=${statusCode}`);
68
+ }
69
+ }
70
+ }
71
+ exports.ClientErrorTranslator = ClientErrorTranslator;
72
+ //# sourceMappingURL=ClientErrorTranslator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ClientErrorTranslator.ts"],"names":[],"mappings":";;;AAAA,oDAa8B;AAE9B;;;;;;;;;;;;GAYG;AACH,MAAa,qBAAqB;IAC9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,cAAc,CAAC,QAAkB,EAAE,aAA4B;QAClE,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC;QACnC,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,QAAQ,CAAC,UAAU,IAAI,eAAe,CAAC;QAChF,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC;QAEtC,yFAAyF;QACzF,QAAQ,UAAU,EAAE,CAAC;YACjB,KAAK,GAAG;gBACJ,OAAO,IAAI,+BAAmB,CAC1B,OAAO,EACP,aAAa,CAAC,KAAK,EACnB,aAAa,CAAC,eAAe,CAChC,CAAC;YAEN,KAAK,GAAG,EAAE,sDAAsD;gBAC5D,OAAO,IAAI,yBAAa,CAAC,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;YAE/D,KAAK,GAAG;gBACJ,OAAO,IAAI,iCAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAEvD,KAAK,GAAG;gBACJ,OAAO,IAAI,8BAAkB,CAAC,OAAO,CAAC,CAAC;YAE3C,KAAK,GAAG;gBACJ,OAAO,IAAI,6BAAiB,CAAC,OAAO,CAAC,CAAC;YAE1C,KAAK,GAAG;gBACJ,OAAO,IAAI,4BAAgB,CAAC,OAAO,CAAC,CAAC;YAEzC,KAAK,GAAG;gBACJ,OAAO,IAAI,mCAAuB,CAAC,OAAO,CAAC,CAAC;YAEhD,KAAK,GAAG;gBACJ,OAAO,IAAI,+BAAmB,CAAC,OAAO,CAAC,CAAC;YAE5C,KAAK,GAAG;gBACJ,OAAO,IAAI,mCAAuB,CAAC,OAAO,CAAC,CAAC;YAEhD,KAAK,GAAG,EAAE,0EAA0E;gBAChF,OAAO,IAAI,2BAAe,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;YAEnE;gBACI,iDAAiD;gBACjD,OAAO,IAAI,KAAK,CAAC,mCAAmC,UAAU,EAAE,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;CACJ;AAnED,sDAmEC","sourcesContent":["import {\n ProtocolError,\n HttpError,\n HttpBadRequestError,\n HttpUserError,\n HttpVendorError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpTimeoutError,\n HttpInternalServerError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n} from '@webpieces/core-util';\n\n/**\n * ClientErrorTranslator - Translates HTTP error responses to HttpError exceptions.\n *\n * This is the CLIENT-SIDE reverse of ExpressWrapper.handleError() on the server.\n * It reconstructs typed HttpError exceptions from ProtocolError JSON responses.\n *\n * Architecture:\n * - Server: HttpError → ExpressWrapper.handleError() → ProtocolError JSON\n * - Client: ProtocolError JSON → ClientErrorTranslator.translateError() → HttpError\n *\n * This achieves symmetric error handling - server throws typed exceptions,\n * client receives typed exceptions.\n */\nexport class ClientErrorTranslator {\n /**\n * Parse error response and reconstruct appropriate HttpError subclass.\n *\n * Maps HTTP status codes to error types (symmetric with server):\n * - 400 → HttpBadRequestError (with field, guiAlertMessage)\n * - 266 → HttpUserError (with errorCode) - 2xx code for user validation\n * - 401 → HttpUnauthorizedError\n * - 403 → HttpForbiddenError\n * - 404 → HttpNotFoundError\n * - 408 → HttpTimeoutError\n * - 500 → HttpInternalServerError\n * - 502 → HttpBadGatewayError\n * - 504 → HttpGatewayTimeoutError\n * - 598 → HttpVendorError (with waitSeconds) - custom status code\n * - other → generic HttpError\n *\n * @param response - Fetch Response object\n * @param protocolError - Parsed ProtocolError from response body\n * @returns HttpError subclass instance\n */\n static translateError(response: Response, protocolError: ProtocolError): Error {\n const statusCode = response.status;\n const message = protocolError.message || response.statusText || 'Unknown error';\n const subType = protocolError.subType;\n\n // Map status codes to error types (symmetric with server's ExpressWrapper.handleError())\n switch (statusCode) {\n case 400:\n return new HttpBadRequestError(\n message,\n protocolError.field,\n protocolError.guiAlertMessage,\n );\n\n case 266: // HttpUserError - 2xx code for user validation errors\n return new HttpUserError(message, protocolError.errorCode);\n\n case 401:\n return new HttpUnauthorizedError(message, subType);\n\n case 403:\n return new HttpForbiddenError(message);\n\n case 404:\n return new HttpNotFoundError(message);\n\n case 408:\n return new HttpTimeoutError(message);\n\n case 500:\n return new HttpInternalServerError(message);\n\n case 502:\n return new HttpBadGatewayError(message);\n\n case 504:\n return new HttpGatewayTimeoutError(message);\n\n case 598: // HttpVendorError - custom status code for vendor/external service errors\n return new HttpVendorError(message, protocolError.waitSeconds);\n\n default:\n // Unknown status code - return generic HttpError\n return new Error(` could not translate statusCode=${statusCode}`);\n }\n }\n}\n"]}
@@ -0,0 +1,79 @@
1
+ import { AuthMeta, RouteMetadata, LogApiCall } from '@webpieces/core-util';
2
+ import { ApiPrototype } from './ApiPrototype';
3
+ /**
4
+ * ProxyClient - the HTTP call engine behind one API contract's client proxy.
5
+ *
6
+ * Contains ONLY what a browser can run: the route map built from the contract's decorators, URL
7
+ * assembly, `fetch`, error translation, and logging. It holds no context object, no credentials,
8
+ * and no recorder — it ASKS ITSELF for those through the hooks below, and each subclass answers
9
+ * from its own environment.
10
+ *
11
+ * That is why the class is abstract rather than parameterized by a collaborator: a shared
12
+ * header-provider seam would drag Node's AsyncLocalStorage vocabulary into a browser bundle and the
13
+ * browser's store vocabulary into a server, and neither has any use for the other.
14
+ *
15
+ * NodeProxyClient (@webpieces/http-client-node) -> RequestContext, Secrets, mintIdToken, recording
16
+ * BrowserProxyClient (@webpieces/http-client-browser) -> an app-held store, no credentials, no recording
17
+ *
18
+ * TWO-PHASE: collaborators arrive on the subclass constructor (so a DI container can supply them),
19
+ * while the per-client state — which contract, which target — arrives on the subclass's `init`,
20
+ * which calls {@link initRoutes}. That is what lets a factory hold a `Provider<ProxyClient>` and
21
+ * hand out a fresh, independently-configured client per contract.
22
+ */
23
+ export declare abstract class ProxyClient {
24
+ protected readonly logApiCall: LogApiCall;
25
+ private routeMap;
26
+ private apiName;
27
+ constructor(logApiCall?: LogApiCall);
28
+ /** The callee's base URL. Async because a server may derive it from container metadata. */
29
+ protected abstract resolveBaseUrl(): Promise<string>;
30
+ /** Context headers to put on the wire. Server reads RequestContext; browser reads its store. */
31
+ protected abstract outboundHeaders(): Map<string, string>;
32
+ /**
33
+ * Attach the endpoint's outbound credential. Service-to-service auth (@AuthOidc bearer,
34
+ * @AuthSharedSecret value) is a SERVER concept; a browser has neither a minter nor a Secrets
35
+ * store, so it attaches nothing and its user JWT simply travels as a transferred context key.
36
+ */
37
+ protected attachOutboundAuth(_route: RouteMetadata, _baseUrl: string, _httpHeaders: Record<string, string>): Promise<void>;
38
+ /**
39
+ * Run the call. The default just logs it. Test-case RECORDING is a server concept, so
40
+ * NodeProxyClient overrides this to capture the call when a recorder is in the context.
41
+ *
42
+ * Context fields are NOT passed in: a logging backend stamps them onto every record itself.
43
+ */
44
+ protected execute(route: RouteMetadata, requestDto: unknown, method: () => Promise<unknown>): Promise<unknown>;
45
+ /**
46
+ * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot
47
+ * mint the OIDC token an @AuthOidc endpoint demands. Surfacing it here beats failing on the
48
+ * first call in production. The default accepts everything.
49
+ */
50
+ protected assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void;
51
+ /**
52
+ * Bind this client to one API contract: read @ApiPath/@Endpoint/@Auth* off the prototype and
53
+ * build the route map once. Each subclass's `init(api, config)` stores its own config, then
54
+ * calls this.
55
+ *
56
+ * @throws Error if the prototype lacks @ApiPath, or declares an endpoint this environment
57
+ * cannot satisfy (see {@link assertEndpointSupported}).
58
+ */
59
+ protected initRoutes(apiPrototype: ApiPrototype<object>): void;
60
+ /** The contract's class name, for logs and recordings. */
61
+ protected contractName(): string;
62
+ /** Check if a route exists for the given method name. */
63
+ hasRoute(methodName: string): boolean;
64
+ /**
65
+ * Get route metadata for a method name.
66
+ * @throws Error if no route found
67
+ */
68
+ getRoute(methodName: string): RouteMetadata;
69
+ /**
70
+ * Make an HTTP request based on route metadata and arguments.
71
+ *
72
+ * All endpoints are POST-only. The request body is the first argument.
73
+ */
74
+ makeRequest(route: RouteMetadata, args: any[]): Promise<any>;
75
+ /**
76
+ * Execute the fetch request and handle response.
77
+ */
78
+ private executeFetch;
79
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProxyClient = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
5
+ const ClientErrorTranslator_1 = require("./ClientErrorTranslator");
6
+ /**
7
+ * ProxyClient - the HTTP call engine behind one API contract's client proxy.
8
+ *
9
+ * Contains ONLY what a browser can run: the route map built from the contract's decorators, URL
10
+ * assembly, `fetch`, error translation, and logging. It holds no context object, no credentials,
11
+ * and no recorder — it ASKS ITSELF for those through the hooks below, and each subclass answers
12
+ * from its own environment.
13
+ *
14
+ * That is why the class is abstract rather than parameterized by a collaborator: a shared
15
+ * header-provider seam would drag Node's AsyncLocalStorage vocabulary into a browser bundle and the
16
+ * browser's store vocabulary into a server, and neither has any use for the other.
17
+ *
18
+ * NodeProxyClient (@webpieces/http-client-node) -> RequestContext, Secrets, mintIdToken, recording
19
+ * BrowserProxyClient (@webpieces/http-client-browser) -> an app-held store, no credentials, no recording
20
+ *
21
+ * TWO-PHASE: collaborators arrive on the subclass constructor (so a DI container can supply them),
22
+ * while the per-client state — which contract, which target — arrives on the subclass's `init`,
23
+ * which calls {@link initRoutes}. That is what lets a factory hold a `Provider<ProxyClient>` and
24
+ * hand out a fresh, independently-configured client per contract.
25
+ */
26
+ class ProxyClient {
27
+ logApiCall;
28
+ // Assigned by initRoutes(), which every subclass's init() calls immediately after construction.
29
+ routeMap;
30
+ apiName;
31
+ constructor(logApiCall = new core_util_1.LogApiCall()) {
32
+ this.logApiCall = logApiCall;
33
+ }
34
+ /**
35
+ * Attach the endpoint's outbound credential. Service-to-service auth (@AuthOidc bearer,
36
+ * @AuthSharedSecret value) is a SERVER concept; a browser has neither a minter nor a Secrets
37
+ * store, so it attaches nothing and its user JWT simply travels as a transferred context key.
38
+ */
39
+ async attachOutboundAuth(_route, _baseUrl, _httpHeaders) { }
40
+ /**
41
+ * Run the call. The default just logs it. Test-case RECORDING is a server concept, so
42
+ * NodeProxyClient overrides this to capture the call when a recorder is in the context.
43
+ *
44
+ * Context fields are NOT passed in: a logging backend stamps them onto every record itself.
45
+ */
46
+ // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary
47
+ async execute(route, requestDto,
48
+ // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary
49
+ method) {
50
+ return this.logApiCall.execute('CLIENT', route, requestDto, method);
51
+ }
52
+ /**
53
+ * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot
54
+ * mint the OIDC token an @AuthOidc endpoint demands. Surfacing it here beats failing on the
55
+ * first call in production. The default accepts everything.
56
+ */
57
+ assertEndpointSupported(_authMeta, _methodName) { }
58
+ // ---------------------------------------------------------------- contract binding
59
+ /**
60
+ * Bind this client to one API contract: read @ApiPath/@Endpoint/@Auth* off the prototype and
61
+ * build the route map once. Each subclass's `init(api, config)` stores its own config, then
62
+ * calls this.
63
+ *
64
+ * @throws Error if the prototype lacks @ApiPath, or declares an endpoint this environment
65
+ * cannot satisfy (see {@link assertEndpointSupported}).
66
+ */
67
+ initRoutes(apiPrototype) {
68
+ if (!(0, core_util_1.isApiPath)(apiPrototype)) {
69
+ const className = apiPrototype.name || 'Unknown';
70
+ throw new Error(`Class ${className} must be decorated with @ApiPath()`);
71
+ }
72
+ const basePath = (0, core_util_1.getApiPath)(apiPrototype);
73
+ const endpoints = (0, core_util_1.getEndpoints)(apiPrototype) || {};
74
+ // apiName as the class name so client logs read "SaveApi.save", not "undefined.save"
75
+ this.apiName = apiPrototype.name || 'UnknownApi';
76
+ this.routeMap = new Map();
77
+ for (const [methodName, endpointPath] of Object.entries(endpoints)) {
78
+ const fullPath = basePath + endpointPath;
79
+ // Capture the endpoint's auth mode so the client can mint delivery auth per
80
+ // @AuthOidc / @AuthSharedSecret, exactly as the server verifies it.
81
+ const authMeta = (0, core_util_1.getAuthMeta)(apiPrototype, methodName);
82
+ this.assertEndpointSupported(authMeta, methodName);
83
+ this.routeMap.set(methodName, new core_util_1.RouteMetadata('POST', fullPath, methodName, this.apiName, authMeta));
84
+ }
85
+ }
86
+ /** The contract's class name, for logs and recordings. */
87
+ contractName() {
88
+ return this.apiName;
89
+ }
90
+ /** Check if a route exists for the given method name. */
91
+ hasRoute(methodName) {
92
+ return this.routeMap.has(methodName);
93
+ }
94
+ /**
95
+ * Get route metadata for a method name.
96
+ * @throws Error if no route found
97
+ */
98
+ getRoute(methodName) {
99
+ const route = this.routeMap.get(methodName);
100
+ if (!route) {
101
+ throw new Error(`No route found for method ${methodName}`);
102
+ }
103
+ return route;
104
+ }
105
+ // ---------------------------------------------------------------- the call
106
+ /**
107
+ * Make an HTTP request based on route metadata and arguments.
108
+ *
109
+ * All endpoints are POST-only. The request body is the first argument.
110
+ */
111
+ // webpieces-disable no-any-unknown -- proxy method: the request DTO (args) + response are erased at the client boundary
112
+ async makeRequest(route, args) {
113
+ // Resolved per call (memoized underneath on a server), so building a client stayed synchronous.
114
+ const baseUrl = await this.resolveBaseUrl();
115
+ const url = `${baseUrl}${route.path}`;
116
+ const httpHeaders = {
117
+ 'Content-Type': 'application/json',
118
+ };
119
+ // Transferred context, request-id chained. The server impl throws here when there is no
120
+ // active RequestContext — an outbound call with no trace is a bug, not a default.
121
+ const outboundHeaders = this.outboundHeaders();
122
+ for (const entry of outboundHeaders.entries()) {
123
+ httpHeaders[entry[0]] = entry[1];
124
+ }
125
+ await this.attachOutboundAuth(route, baseUrl, httpHeaders);
126
+ const options = {
127
+ method: route.httpMethod,
128
+ headers: httpHeaders,
129
+ };
130
+ // POST body is the first argument as JSON
131
+ // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary
132
+ let requestDto;
133
+ if (args.length > 0) {
134
+ requestDto = args[0];
135
+ options.body = JSON.stringify(requestDto);
136
+ }
137
+ // Wrap fetch in a method for LogApiCall.execute
138
+ // webpieces-disable no-any-unknown -- the response DTO's type is erased at the proxy boundary
139
+ const method = async () => {
140
+ return this.executeFetch(url, options);
141
+ };
142
+ return await this.execute(route, requestDto, method);
143
+ }
144
+ /**
145
+ * Execute the fetch request and handle response.
146
+ */
147
+ // webpieces-disable no-any-unknown -- the response DTO's type is erased at the proxy boundary
148
+ async executeFetch(url, options) {
149
+ // webpieces-disable no-fetch -- this IS the generated-client implementation the rule points everyone to
150
+ const response = await fetch(url, options);
151
+ if (response.ok) {
152
+ return await response.json();
153
+ }
154
+ // Handle errors (non-2xx responses): parse ProtocolError from the response body and
155
+ // reconstruct the appropriate HttpError subclass.
156
+ const protocolError = (await response.json());
157
+ throw ClientErrorTranslator_1.ClientErrorTranslator.translateError(response, protocolError);
158
+ }
159
+ }
160
+ exports.ProxyClient = ProxyClient;
161
+ //# sourceMappingURL=ProxyClient.js.map
@@ -0,0 +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 AuthMeta,\n RouteMetadata,\n ProtocolError,\n LogApiCall,\n} from '@webpieces/core-util';\nimport { ApiPrototype } from './ApiPrototype';\nimport { ClientErrorTranslator } from './ClientErrorTranslator';\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\n constructor(protected readonly logApiCall: LogApiCall = new LogApiCall()) {}\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 /** Context headers to put on the wire. Server reads RequestContext; browser reads its store. */\n protected abstract outboundHeaders(): Map<string, string>;\n\n /**\n * Attach the endpoint's outbound credential. Service-to-service auth (@AuthOidc bearer,\n * @AuthSharedSecret value) is a SERVER concept; a browser has neither a minter nor a Secrets\n * store, so it attaches nothing and its user JWT simply travels as a transferred context key.\n */\n protected async attachOutboundAuth(\n _route: RouteMetadata,\n _baseUrl: string,\n _httpHeaders: Record<string, string>,\n ): Promise<void> {}\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 return this.logApiCall.execute('CLIENT', route, 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 // ---------------------------------------------------------------- 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 * @throws Error if the prototype lacks @ApiPath, or declares an endpoint this environment\n * cannot satisfy (see {@link assertEndpointSupported}).\n */\n protected initRoutes(apiPrototype: ApiPrototype<object>): void {\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 this.routeMap.set(methodName, new RouteMetadata('POST', fullPath, methodName, this.apiName, authMeta));\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 * Make an HTTP request based on route metadata and arguments.\n *\n * All endpoints are POST-only. The request body is the first argument.\n */\n // webpieces-disable no-any-unknown -- proxy method: the request DTO (args) + response are erased at the client boundary\n async makeRequest(route: RouteMetadata, args: any[]): Promise<any> {\n // Resolved per call (memoized underneath on a server), so building a client stayed synchronous.\n const baseUrl = await this.resolveBaseUrl();\n const url = `${baseUrl}${route.path}`;\n\n const httpHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Transferred context, request-id chained. The server impl throws here when there is no\n // active RequestContext — an outbound call with no trace is a bug, not a default.\n const outboundHeaders = this.outboundHeaders();\n for (const entry of outboundHeaders.entries()) {\n httpHeaders[entry[0]] = entry[1];\n }\n\n await this.attachOutboundAuth(route, baseUrl, httpHeaders);\n\n const options: RequestInit = {\n method: route.httpMethod,\n headers: httpHeaders,\n };\n\n // POST body is the first argument as JSON\n // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary\n let requestDto: unknown;\n if (args.length > 0) {\n requestDto = args[0];\n options.body = JSON.stringify(requestDto);\n }\n\n // Wrap fetch in a method for LogApiCall.execute\n // webpieces-disable no-any-unknown -- the response DTO's type is erased at the proxy boundary\n const method = async (): Promise<unknown> => {\n return this.executeFetch(url, options);\n };\n\n return await this.execute(route, requestDto, method);\n }\n\n /**\n * Execute the fetch request and handle response.\n */\n // webpieces-disable no-any-unknown -- the response DTO's type is erased at the proxy boundary\n private async executeFetch(url: string, options: RequestInit): Promise<unknown> {\n // webpieces-disable no-fetch -- this IS the generated-client implementation the rule points everyone to\n const response = await fetch(url, options);\n\n if (response.ok) {\n return await response.json();\n }\n\n // Handle errors (non-2xx responses): parse ProtocolError from the response body and\n // reconstruct the appropriate HttpError subclass.\n const protocolError = (await response.json()) as ProtocolError;\n throw ClientErrorTranslator.translateError(response, protocolError);\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { ApiPrototype } from './ApiPrototype';
2
+ import { ProxyClient } from './ProxyClient';
3
+ /**
4
+ * Wrap an already-init'd {@link ProxyClient} in the typed Proxy that IS the client: every
5
+ * `@Endpoint` method call becomes an HTTP request. Shared by the browser and Node factories so
6
+ * the two environments cannot drift on framework-inspection behavior.
7
+ */
8
+ export declare function buildClientProxy<T extends object>(apiPrototype: ApiPrototype<T>, proxyClient: ProxyClient): T;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildClientProxy = buildClientProxy;
4
+ /**
5
+ * Properties accessed by DI frameworks (Angular, Vue), debuggers, Promise checks, and serializers.
6
+ * These should return undefined instead of throwing, allowing frameworks to inspect the proxy.
7
+ *
8
+ * Why this exists:
9
+ * - Angular's injector profiler accesses `constructor` after useFactory returns
10
+ * - Promise.resolve() checks for `then` to detect thenables
11
+ * - JSON.stringify checks for `toJSON`
12
+ * - Debuggers access `prototype`, `__proto__`, etc.
13
+ */
14
+ const FRAMEWORK_INSPECTION_PROPERTIES = new Set([
15
+ 'constructor', // Angular DI profiler, class inspection
16
+ 'prototype', // Prototype chain inspection
17
+ '__proto__', // Legacy prototype access
18
+ 'name', // Angular isNotFound() check, function/class name inspection
19
+ 'then', // Promise/thenable detection
20
+ 'catch', // Promise check
21
+ 'finally', // Promise check
22
+ 'toJSON', // JSON.stringify
23
+ 'valueOf', // Type coercion
24
+ 'toString', // String coercion
25
+ 'nodeType', // DOM element check
26
+ 'tagName', // DOM element check
27
+ '$$typeof', // React element/component check
28
+ '$typeof', // React internal
29
+ '_isVue', // Vue internal
30
+ 'ngOnInit', // Angular lifecycle hook check
31
+ 'ngOnDestroy', // Angular lifecycle hook check
32
+ 'ngOnChanges', // Angular lifecycle hook check
33
+ 'asymmetricMatch', // Jest matcher protocol
34
+ ]);
35
+ /**
36
+ * Wrap an already-init'd {@link ProxyClient} in the typed Proxy that IS the client: every
37
+ * `@Endpoint` method call becomes an HTTP request. Shared by the browser and Node factories so
38
+ * the two environments cannot drift on framework-inspection behavior.
39
+ */
40
+ // webpieces-disable no-function-outside-class -- returns a Proxy; it holds no state a class could own
41
+ function buildClientProxy(apiPrototype, proxyClient) {
42
+ return new Proxy({}, {
43
+ get(target, prop) {
44
+ // Symbols (Symbol.toStringTag, Symbol.iterator, etc.) - throw for now to learn if this happens
45
+ if (typeof prop !== 'string') {
46
+ throw new Error(`Proxy accessed with non-string property: ${String(prop)} (type: ${typeof prop}). ` +
47
+ `Please report this so we can add it to the whitelist.`);
48
+ }
49
+ // Framework inspection properties - return undefined to allow inspection WITHOUT
50
+ // throwing. This is critical for Angular DI, Promise checks, etc.
51
+ if (FRAMEWORK_INSPECTION_PROPERTIES.has(prop)) {
52
+ return undefined;
53
+ }
54
+ if (!proxyClient.hasRoute(prop)) {
55
+ // For unknown properties (likely typos), throw a helpful error
56
+ throw new Error(`No route found for method '${prop}' on ${apiPrototype.name || 'Unknown'}. ` +
57
+ `Check for typos or ensure the method has @Endpoint() decorator.`);
58
+ }
59
+ const route = proxyClient.getRoute(prop);
60
+ // webpieces-disable no-any-unknown -- request DTO types are erased at the proxy boundary
61
+ return async (...args) => {
62
+ return proxyClient.makeRequest(route, args);
63
+ };
64
+ },
65
+ });
66
+ }
67
+ //# sourceMappingURL=buildClientProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildClientProxy.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/buildClientProxy.ts"],"names":[],"mappings":";;AA+CA,4CAiCC;AAvED;;;;;;;;;GASG;AACH,MAAM,+BAA+B,GAAG,IAAI,GAAG,CAAC;IAC5C,aAAa,EAAM,wCAAwC;IAC3D,WAAW,EAAQ,6BAA6B;IAChD,WAAW,EAAQ,0BAA0B;IAC7C,MAAM,EAAa,6DAA6D;IAChF,MAAM,EAAa,6BAA6B;IAChD,OAAO,EAAY,gBAAgB;IACnC,SAAS,EAAU,gBAAgB;IACnC,QAAQ,EAAW,iBAAiB;IACpC,SAAS,EAAU,gBAAgB;IACnC,UAAU,EAAS,kBAAkB;IACrC,UAAU,EAAS,oBAAoB;IACvC,SAAS,EAAU,oBAAoB;IACvC,UAAU,EAAS,gCAAgC;IACnD,SAAS,EAAU,iBAAiB;IACpC,QAAQ,EAAW,eAAe;IAClC,UAAU,EAAS,+BAA+B;IAClD,aAAa,EAAM,+BAA+B;IAClD,aAAa,EAAM,+BAA+B;IAClD,iBAAiB,EAAE,wBAAwB;CAC9C,CAAC,CAAC;AAEH;;;;GAIG;AACH,sGAAsG;AACtG,SAAgB,gBAAgB,CAAmB,YAA6B,EAAE,WAAwB;IACtG,OAAO,IAAI,KAAK,CAAC,EAAO,EAAE;QACtB,GAAG,CAAC,MAAS,EAAE,IAAqB;YAChC,+FAA+F;YAC/F,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CACX,4CAA4C,MAAM,CAAC,IAAI,CAAC,WAAW,OAAO,IAAI,KAAK;oBACnF,uDAAuD,CAC1D,CAAC;YACN,CAAC;YAED,iFAAiF;YACjF,kEAAkE;YAClE,IAAI,+BAA+B,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5C,OAAO,SAAS,CAAC;YACrB,CAAC;YAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC9B,+DAA+D;gBAC/D,MAAM,IAAI,KAAK,CACX,8BAA8B,IAAI,QAAQ,YAAY,CAAC,IAAI,IAAI,SAAS,IAAI;oBAC5E,iEAAiE,CACpE,CAAC;YACN,CAAC;YAED,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAEzC,yFAAyF;YACzF,OAAO,KAAK,EAAE,GAAG,IAAW,EAAE,EAAE;gBAC5B,OAAO,WAAW,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAChD,CAAC,CAAC;QACN,CAAC;KACJ,CAAC,CAAC;AACP,CAAC","sourcesContent":["import { ApiPrototype } from './ApiPrototype';\nimport { ProxyClient } from './ProxyClient';\n\n/**\n * Return type for the Proxy get trap — either an async method or undefined for framework inspection.\n */\n// webpieces-disable no-any-unknown -- Proxy get trap returns generic response promises\ntype ProxyGetResult = (...args: never[]) => Promise<unknown>;\n\n/**\n * Properties accessed by DI frameworks (Angular, Vue), debuggers, Promise checks, and serializers.\n * These should return undefined instead of throwing, allowing frameworks to inspect the proxy.\n *\n * Why this exists:\n * - Angular's injector profiler accesses `constructor` after useFactory returns\n * - Promise.resolve() checks for `then` to detect thenables\n * - JSON.stringify checks for `toJSON`\n * - Debuggers access `prototype`, `__proto__`, etc.\n */\nconst FRAMEWORK_INSPECTION_PROPERTIES = new Set([\n 'constructor', // Angular DI profiler, class inspection\n 'prototype', // Prototype chain inspection\n '__proto__', // Legacy prototype access\n 'name', // Angular isNotFound() check, function/class name inspection\n 'then', // Promise/thenable detection\n 'catch', // Promise check\n 'finally', // Promise check\n 'toJSON', // JSON.stringify\n 'valueOf', // Type coercion\n 'toString', // String coercion\n 'nodeType', // DOM element check\n 'tagName', // DOM element check\n '$$typeof', // React element/component check\n '$typeof', // React internal\n '_isVue', // Vue internal\n 'ngOnInit', // Angular lifecycle hook check\n 'ngOnDestroy', // Angular lifecycle hook check\n 'ngOnChanges', // Angular lifecycle hook check\n 'asymmetricMatch', // Jest matcher protocol\n]);\n\n/**\n * Wrap an already-init'd {@link ProxyClient} in the typed Proxy that IS the client: every\n * `@Endpoint` method call becomes an HTTP request. Shared by the browser and Node factories so\n * the two environments cannot drift on framework-inspection behavior.\n */\n// webpieces-disable no-function-outside-class -- returns a Proxy; it holds no state a class could own\nexport function buildClientProxy<T extends object>(apiPrototype: ApiPrototype<T>, proxyClient: ProxyClient): T {\n return new Proxy({} as T, {\n get(target: T, prop: string | symbol): ProxyGetResult | undefined {\n // Symbols (Symbol.toStringTag, Symbol.iterator, etc.) - throw for now to learn if this happens\n if (typeof prop !== 'string') {\n throw new Error(\n `Proxy accessed with non-string property: ${String(prop)} (type: ${typeof prop}). ` +\n `Please report this so we can add it to the whitelist.`\n );\n }\n\n // Framework inspection properties - return undefined to allow inspection WITHOUT\n // throwing. This is critical for Angular DI, Promise checks, etc.\n if (FRAMEWORK_INSPECTION_PROPERTIES.has(prop)) {\n return undefined;\n }\n\n if (!proxyClient.hasRoute(prop)) {\n // For unknown properties (likely typos), throw a helpful error\n throw new Error(\n `No route found for method '${prop}' on ${apiPrototype.name || 'Unknown'}. ` +\n `Check for typos or ensure the method has @Endpoint() decorator.`\n );\n }\n\n const route = proxyClient.getRoute(prop);\n\n // webpieces-disable no-any-unknown -- request DTO types are erased at the proxy boundary\n return async (...args: any[]) => {\n return proxyClient.makeRequest(route, args);\n };\n },\n });\n}\n"]}
package/src/index.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @webpieces/http-client-core
3
+ *
4
+ * The ISOMORPHIC core of the webpieces HTTP client — everything that reads an API contract's
5
+ * decorators and turns a method call into an HTTP request, with no opinion about where the
6
+ * magic context comes from or whether a DI container exists.
7
+ *
8
+ * You almost certainly want one of its two environment packages instead:
9
+ * - Server: @webpieces/http-client-node (inversify-wired, reads RequestContext, mints OIDC)
10
+ * - Browser: @webpieces/http-client-browser (no DI — React or Angular, app-managed context store)
11
+ *
12
+ * Architecture:
13
+ * ```
14
+ * http-api (defines the contract)
15
+ * ^
16
+ * +-- http-routing (server: contract -> handlers)
17
+ * +-- http-client-core (contract -> HTTP requests) <- YOU ARE HERE
18
+ * +-- http-client-node (RequestContext + Secrets + OIDC + inversify factory)
19
+ * +-- http-client-browser (app-held store + plain factory, no DI)
20
+ * ```
21
+ *
22
+ * There is no context/credential/recording seam here at all: ProxyClient is ABSTRACT and asks its
23
+ * subclass for the base URL, the context headers, the log map, the outbound credential, and the
24
+ * recorder. Nothing server-only (RequestContext, Secrets, mintIdToken, TestCaseRecorder) can reach
25
+ * a browser bundle, and nothing browser-only (a ContextReader store) reaches a server.
26
+ */
27
+ export { ProxyClient } from './ProxyClient';
28
+ export type { ApiPrototype } from './ApiPrototype';
29
+ export { buildClientProxy } from './buildClientProxy';
30
+ export { ClientErrorTranslator } from './ClientErrorTranslator';
package/src/index.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * @webpieces/http-client-core
4
+ *
5
+ * The ISOMORPHIC core of the webpieces HTTP client — everything that reads an API contract's
6
+ * decorators and turns a method call into an HTTP request, with no opinion about where the
7
+ * magic context comes from or whether a DI container exists.
8
+ *
9
+ * You almost certainly want one of its two environment packages instead:
10
+ * - Server: @webpieces/http-client-node (inversify-wired, reads RequestContext, mints OIDC)
11
+ * - Browser: @webpieces/http-client-browser (no DI — React or Angular, app-managed context store)
12
+ *
13
+ * Architecture:
14
+ * ```
15
+ * http-api (defines the contract)
16
+ * ^
17
+ * +-- http-routing (server: contract -> handlers)
18
+ * +-- http-client-core (contract -> HTTP requests) <- YOU ARE HERE
19
+ * +-- http-client-node (RequestContext + Secrets + OIDC + inversify factory)
20
+ * +-- http-client-browser (app-held store + plain factory, no DI)
21
+ * ```
22
+ *
23
+ * There is no context/credential/recording seam here at all: ProxyClient is ABSTRACT and asks its
24
+ * subclass for the base URL, the context headers, the log map, the outbound credential, and the
25
+ * recorder. Nothing server-only (RequestContext, Secrets, mintIdToken, TestCaseRecorder) can reach
26
+ * a browser bundle, and nothing browser-only (a ContextReader store) reaches a server.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.ClientErrorTranslator = exports.buildClientProxy = exports.ProxyClient = void 0;
30
+ var ProxyClient_1 = require("./ProxyClient");
31
+ Object.defineProperty(exports, "ProxyClient", { enumerable: true, get: function () { return ProxyClient_1.ProxyClient; } });
32
+ var buildClientProxy_1 = require("./buildClientProxy");
33
+ Object.defineProperty(exports, "buildClientProxy", { enumerable: true, get: function () { return buildClientProxy_1.buildClientProxy; } });
34
+ var ClientErrorTranslator_1 = require("./ClientErrorTranslator");
35
+ Object.defineProperty(exports, "ClientErrorTranslator", { enumerable: true, get: function () { return ClientErrorTranslator_1.ClientErrorTranslator; } });
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;;AAEH,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA","sourcesContent":["/**\n * @webpieces/http-client-core\n *\n * The ISOMORPHIC core of the webpieces HTTP client — everything that reads an API contract's\n * decorators and turns a method call into an HTTP request, with no opinion about where the\n * magic context comes from or whether a DI container exists.\n *\n * You almost certainly want one of its two environment packages instead:\n * - Server: @webpieces/http-client-node (inversify-wired, reads RequestContext, mints OIDC)\n * - Browser: @webpieces/http-client-browser (no DI — React or Angular, app-managed context store)\n *\n * Architecture:\n * ```\n * http-api (defines the contract)\n * ^\n * +-- http-routing (server: contract -> handlers)\n * +-- http-client-core (contract -> HTTP requests) <- YOU ARE HERE\n * +-- http-client-node (RequestContext + Secrets + OIDC + inversify factory)\n * +-- http-client-browser (app-held store + plain factory, no DI)\n * ```\n *\n * There is no context/credential/recording seam here at all: ProxyClient is ABSTRACT and asks its\n * subclass for the base URL, the context headers, the log map, the outbound credential, and the\n * recorder. Nothing server-only (RequestContext, Secrets, mintIdToken, TestCaseRecorder) can reach\n * a browser bundle, and nothing browser-only (a ContextReader store) reaches a server.\n */\n\nexport { ProxyClient } from './ProxyClient';\nexport type { ApiPrototype } from './ApiPrototype';\nexport { buildClientProxy } from './buildClientProxy';\nexport { ClientErrorTranslator } from './ClientErrorTranslator';\n"]}