@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 +17 -0
- package/package.json +26 -0
- package/src/ApiPrototype.d.ts +7 -0
- package/src/ApiPrototype.js +3 -0
- package/src/ApiPrototype.js.map +1 -0
- package/src/ClientErrorTranslator.d.ts +37 -0
- package/src/ClientErrorTranslator.js +72 -0
- package/src/ClientErrorTranslator.js.map +1 -0
- package/src/ProxyClient.d.ts +79 -0
- package/src/ProxyClient.js +161 -0
- package/src/ProxyClient.js.map +1 -0
- package/src/buildClientProxy.d.ts +8 -0
- package/src/buildClientProxy.js +67 -0
- package/src/buildClientProxy.js.map +1 -0
- package/src/index.d.ts +30 -0
- package/src/index.js +36 -0
- package/src/index.js.map +1 -0
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 @@
|
|
|
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
|
package/src/index.js.map
ADDED
|
@@ -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"]}
|