@webpieces/http-client-node 0.3.380 → 0.3.382
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 +12 -7
- package/package.json +5 -5
- package/src/NodeProxyClient.d.ts +6 -2
- package/src/NodeProxyClient.js +7 -3
- package/src/NodeProxyClient.js.map +1 -1
package/README.md
CHANGED
|
@@ -9,13 +9,18 @@ const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'))
|
|
|
9
9
|
const res = await server2.fetchValue(req); // inside a RequestContext
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
- `svcName`
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
- `svcName` becomes a URL through `ClientRegistry.resolve` — ONE chain, the same one the browser
|
|
13
|
+
client and Cloud Tasks run:
|
|
14
|
+
1. a registered mapping wins: `ClientRegistry.addMapping(svcName, port)` (localhost) or
|
|
15
|
+
`addUrlMapping(svcName, url)` (anything else — AWS, another region/project, an external API)
|
|
16
|
+
2. else the installed deriver, if any: `ClientRegistry.setDeriver(gcpCloudRunDeriver())` on GCP
|
|
17
|
+
(`svcName` is the Cloud Run service name, so same-project/same-region peers need no mapping at
|
|
18
|
+
all), or `templateDeriver('https://{svc}.example.com')` for any predictable-DNS environment
|
|
19
|
+
3. else it THROWS, naming both fixes. A server has no "own origin" to fall back to, so an
|
|
20
|
+
unresolvable peer is a setup bug, not a silent mis-route. (The BROWSER client differs here and
|
|
21
|
+
only here: it goes relative — same origin.)
|
|
22
|
+
- The deriver is optional. Registering every svcName is a first-class, sufficient setup — which is
|
|
23
|
+
what localhost and tests do, since per-service ports are inherently a table, not a formula.
|
|
19
24
|
|
|
20
25
|
`ClientHttpFactory` injects a `Provider<NodeProxyClient>` and calls `get()` per contract.
|
|
21
26
|
`NodeProxyClient` is bound TRANSIENT, so each client gets its own — the provider caches nothing,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-client-node",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.382",
|
|
4
4
|
"description": "Server-side HTTP client for webpieces: inversify-wired, reads RequestContext directly, mints OIDC/shared-secret delivery auth, resolves Cloud Run URLs from a service name",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-context": "0.3.
|
|
26
|
-
"@webpieces/core-util": "0.3.
|
|
27
|
-
"@webpieces/gcp-identity": "0.3.
|
|
28
|
-
"@webpieces/http-client-core": "0.3.
|
|
25
|
+
"@webpieces/core-context": "0.3.382",
|
|
26
|
+
"@webpieces/core-util": "0.3.382",
|
|
27
|
+
"@webpieces/gcp-identity": "0.3.382",
|
|
28
|
+
"@webpieces/http-client-core": "0.3.382",
|
|
29
29
|
"inversify": "7.10.4",
|
|
30
30
|
"reflect-metadata": "0.2.2"
|
|
31
31
|
}
|
package/src/NodeProxyClient.d.ts
CHANGED
|
@@ -20,8 +20,12 @@ export declare class NodeProxyClient extends ProxyClient {
|
|
|
20
20
|
/** Bind this client to one API contract + target. */
|
|
21
21
|
init(apiPrototype: ApiPrototype<object>, config: ClientConfig): void;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* The same chain every client runs — a ClientRegistry mapping, else the installed deriver — but
|
|
24
|
+
* with NODE's fallback: THROW. A server has no "own origin" to fall back to the way a browser
|
|
25
|
+
* does, so an unresolvable peer is a setup bug and must fail loudly (the error names the fixes).
|
|
26
|
+
*
|
|
27
|
+
* Resolved per call, never at construction, so building a client stays synchronous. Any metadata
|
|
28
|
+
* read beneath a deriver is memoized process-wide, so only the first call pays.
|
|
25
29
|
*/
|
|
26
30
|
protected resolveBaseUrl(): Promise<string>;
|
|
27
31
|
/** Straight from the RequestContext. Throws when there is no active request scope. */
|
package/src/NodeProxyClient.js
CHANGED
|
@@ -32,11 +32,15 @@ let NodeProxyClient = class NodeProxyClient extends http_client_core_1.ProxyClie
|
|
|
32
32
|
this.initRoutes(apiPrototype);
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* The same chain every client runs — a ClientRegistry mapping, else the installed deriver — but
|
|
36
|
+
* with NODE's fallback: THROW. A server has no "own origin" to fall back to the way a browser
|
|
37
|
+
* does, so an unresolvable peer is a setup bug and must fail loudly (the error names the fixes).
|
|
38
|
+
*
|
|
39
|
+
* Resolved per call, never at construction, so building a client stays synchronous. Any metadata
|
|
40
|
+
* read beneath a deriver is memoized process-wide, so only the first call pays.
|
|
37
41
|
*/
|
|
38
42
|
resolveBaseUrl() {
|
|
39
|
-
return
|
|
43
|
+
return core_util_1.ClientRegistry.resolve(this.config.svcName);
|
|
40
44
|
}
|
|
41
45
|
/** Straight from the RequestContext. Throws when there is no active request scope. */
|
|
42
46
|
outboundHeaders() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/NodeProxyClient.ts"],"names":[],"mappings":";;;;AAAA,yCAAyD;AACzD,oDAQ8B;AAC9B,0DAA2G;AAC3G,0DAAqE;AACrE,kEAAwE;AAGxE;;;;;;;GAOG;AAGI,IAAM,eAAe,GAArB,MAAM,eAAgB,SAAQ,8BAAW;IAKQ;IAEd;IAGY;IAT1C,MAAM,CAAgB;IAE9B,YAEoD,OAA8B,EAE5C,OAAgB,EAGJ,OAAiB;QAE/D,KAAK,EAAE,CAAC;QAPwC,YAAO,GAAP,OAAO,CAAuB;QAE5C,YAAO,GAAP,OAAO,CAAS;QAGJ,YAAO,GAAP,OAAO,CAAU;IAGnE,CAAC;IAED,qDAAqD;IACrD,IAAI,CAAC,YAAkC,EAAE,MAAoB;QACzD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IAClC,CAAC;IAED;;;OAGG;IACgB,cAAc;QAC7B,OAAO,IAAA,gCAAiB,EAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;IAED,sFAAsF;IACnE,eAAe;QAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAC/C,CAAC;IAED;;;;;;;OAOG;IACgB,KAAK,CAAC,kBAAkB,CACvC,KAAoB,EACpB,OAAe,EACf,WAAmC;QAEnC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC;QAClC,IAAI,IAAI,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;YACxB,WAAW,CAAC,eAAe,CAAC,GAAG,UAAU,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACvF,CAAC;aAAM,IAAI,IAAI,EAAE,IAAI,KAAK,eAAe,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACjD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CACX,sDAAsD,IAAI,CAAC,SAAS,eAAe,KAAK,CAAC,UAAU,EAAE,CACxG,CAAC;YACN,CAAC;YACD,gFAAgF;YAChF,4DAA4D;YAC5D,WAAW,CAAC,eAAe,CAAC,GAAG,aAAa,MAAM,EAAE,CAAC;QACzD,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,iFAAiF;IAC9D,KAAK,CAAC,OAAO,CAC5B,KAAoB,EACpB,UAAmB;IACnB,iFAAiF;IACjF,MAA8B;QAG9B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IAED;;;;;OAKG;IACH,iFAAiF;IACzE,KAAK,CAAC,UAAU,CACpB,QAA0B,EAC1B,KAAoB,EACpB,UAAmB;IACnB,iFAAiF;IACjF,MAA8B;QAG9B,MAAM,WAAW,GAA2B,EAAE,CAAC;QAC/C,KAAK,MAAM,KAAK,IAAI,6BAAc,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;YAC5D,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,4BAAgB,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;QACxG,QAAQ,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAEnC,4HAA4H;QAC5H,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;YAChE,QAAQ,CAAC,eAAe,GAAG,QAAQ,CAAC;YACpC,OAAO,QAAQ,CAAC;QACpB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,QAAQ,CAAC,eAAe,GAAG,IAAI,yBAAa,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YACxE,MAAM,GAAG,CAAC;QACd,CAAC;IACL,CAAC;IAED,iFAAiF;IAC9D,uBAAuB,CAAC,SAA+B,EAAE,WAAmB,IAAS,CAAC;CAC5G,CAAA;AAvHY,0CAAe;0BAAf,eAAe;IAF3B,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IAMJ,mBAAA,IAAA,kBAAM,EAAC,oCAAqB,CAAC,CAAA;IAE7B,mBAAA,IAAA,kBAAM,EAAC,sBAAO,CAAC,CAAA;IAGf,mBAAA,IAAA,oBAAQ,GAAE,CAAA;IAAE,mBAAA,IAAA,kBAAM,EAAC,mBAAO,CAAC,CAAA;6CAL6B,oCAAqB;QAEnC,sBAAO;QAGM,mBAAO;GAV1D,eAAe,CAuH3B;AAED;;;;;;;GAOG;AACH,gGAAgG;AACnF,QAAA,0BAA0B,GAAG,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC","sourcesContent":["import { inject, injectable, optional } from 'inversify';\nimport {\n AuthMeta,\n RecordedEndpoint,\n RecordedError,\n RouteMetadata,\n Secrets,\n TestCaseRecorder,\n toError,\n} from '@webpieces/core-util';\nimport { RequestContext, RequestContextHeaders, provideFrameworkTransient } from '@webpieces/core-context';\nimport { GcpOidc, resolveServiceUrl } from '@webpieces/gcp-identity';\nimport { ApiPrototype, ProxyClient } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\n\n/**\n * The server-side {@link ProxyClient}. Everything a browser cannot do lives here: reading the\n * ambient RequestContext, minting OIDC tokens, holding shared secrets, and recording test cases.\n *\n * TRANSIENT on purpose. Every `createRpcClient(api, config)` needs its own instance, because `init()`\n * binds one instance to exactly one API contract and one target. {@link ProxyClientProvider} hands\n * them out — see its doc.\n */\n@provideFrameworkTransient()\n@injectable()\nexport class NodeProxyClient extends ProxyClient {\n private config!: ClientConfig;\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only @AuthSharedSecret endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(Secrets) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n /** Bind this client to one API contract + target. */\n init(apiPrototype: ApiPrototype<object>, config: ClientConfig): void {\n this.config = config;\n this.initRoutes(apiPrototype);\n }\n\n /**\n * Resolved per call, never at construction, so building a client stays synchronous. Every GCP\n * metadata read beneath resolveServiceUrl is memoized process-wide, so only the first call pays.\n */\n protected override resolveBaseUrl(): Promise<string> {\n return resolveServiceUrl(this.config.svcName);\n }\n\n /** Straight from the RequestContext. Throws when there is no active request scope. */\n protected override outboundHeaders(): Map<string, string> {\n return this.headers.buildOutboundHeaders();\n }\n\n /**\n * Attach the outbound credential for the endpoint's AuthMode: an @AuthOidc bearer minted as\n * this caller's runtime SA (audience = the callee base URL — the server verifies the signature\n * + caller allow-list), or the @AuthSharedSecret(key) value THIS client sends from its bound\n * {@link Secrets}. Both ride in the ONE `Authorization` header under their own scheme —\n * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so it cannot leak onto\n * the next hop. Never reads process.env.\n */\n protected override async attachOutboundAuth(\n route: RouteMetadata,\n baseUrl: string,\n httpHeaders: Record<string, string>,\n ): Promise<void> {\n const mode = route.authMeta?.mode;\n if (mode?.kind === 'oidc') {\n httpHeaders['Authorization'] = `Bearer ${await this.gcpOidc.mintIdToken(baseUrl)}`;\n } else if (mode?.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (!secret) {\n throw new Error(\n `No shared secret configured for @AuthSharedSecret('${mode.secretKey}') endpoint ${route.methodName}`,\n );\n }\n // Same header as a JWT/OIDC token, but its OWN scheme, so a secret can never be\n // mistaken for a token nor accepted where one was expected.\n httpHeaders['Authorization'] = `Webpieces ${secret}`;\n }\n }\n\n /**\n * Test-case recording hook (mirror of Java HttpsJsonClientInvokeHandler): if a recorder is\n * travelling in the magic context, capture this outbound call + its result so it becomes a mock\n * in the generated test. Absent a recorder this is exactly the base behavior.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected override 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 const recorder = this.headers.findRecorder();\n if (!recorder) {\n return super.execute(route, requestDto, method);\n }\n return this.recordCall(recorder, route, requestDto, method);\n }\n\n /**\n * Execute the call while recording it (args + masked ctx snapshot + result).\n *\n * The snapshot is a FIXTURE field, not a log line, so it is built here rather than handed down\n * from the call path — a logging backend stamps its own fields and never sees this.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n private async recordCall(\n recorder: TestCaseRecorder,\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 const ctxSnapshot: Record<string, string> = {};\n for (const entry of RequestContext.buildLogFields().entries()) {\n ctxSnapshot[entry[0]] = entry[1];\n }\n const recorded = new RecordedEndpoint(this.contractName(), route.methodName, [requestDto], ctxSnapshot);\n recorder.addEndpointInfo(recorded);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await super.execute(route, requestDto, method);\n recorded.successResponse = response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n recorded.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n }\n }\n\n /** A server can satisfy every auth mode, so nothing is rejected at bind time. */\n protected override assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n}\n\n/**\n * DI token for the `Provider<NodeProxyClient>` that hands out RPC clients — one per API contract.\n * `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because NodeProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would instead hand back one lazily-created\n * instance — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const NODE_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<NodeProxyClient>');\n"]}
|
|
1
|
+
{"version":3,"file":"NodeProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/NodeProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, injectable, optional } from 'inversify';\nimport {\n AuthMeta,\n ClientRegistry,\n RecordedEndpoint,\n RecordedError,\n RouteMetadata,\n Secrets,\n TestCaseRecorder,\n toError,\n} from '@webpieces/core-util';\nimport { RequestContext, RequestContextHeaders, provideFrameworkTransient } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport { ApiPrototype, ProxyClient } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\n\n/**\n * The server-side {@link ProxyClient}. Everything a browser cannot do lives here: reading the\n * ambient RequestContext, minting OIDC tokens, holding shared secrets, and recording test cases.\n *\n * TRANSIENT on purpose. Every `createRpcClient(api, config)` needs its own instance, because `init()`\n * binds one instance to exactly one API contract and one target. {@link ProxyClientProvider} hands\n * them out — see its doc.\n */\n@provideFrameworkTransient()\n@injectable()\nexport class NodeProxyClient extends ProxyClient {\n private config!: ClientConfig;\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only @AuthSharedSecret endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(Secrets) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n /** Bind this client to one API contract + target. */\n init(apiPrototype: ApiPrototype<object>, config: ClientConfig): void {\n this.config = config;\n this.initRoutes(apiPrototype);\n }\n\n /**\n * The same chain every client runs — a ClientRegistry mapping, else the installed deriver — but\n * with NODE's fallback: THROW. A server has no \"own origin\" to fall back to the way a browser\n * does, so an unresolvable peer is a setup bug and must fail loudly (the error names the fixes).\n *\n * Resolved per call, never at construction, so building a client stays synchronous. Any metadata\n * read beneath a deriver is memoized process-wide, so only the first call pays.\n */\n protected override resolveBaseUrl(): Promise<string> {\n return ClientRegistry.resolve(this.config.svcName);\n }\n\n /** Straight from the RequestContext. Throws when there is no active request scope. */\n protected override outboundHeaders(): Map<string, string> {\n return this.headers.buildOutboundHeaders();\n }\n\n /**\n * Attach the outbound credential for the endpoint's AuthMode: an @AuthOidc bearer minted as\n * this caller's runtime SA (audience = the callee base URL — the server verifies the signature\n * + caller allow-list), or the @AuthSharedSecret(key) value THIS client sends from its bound\n * {@link Secrets}. Both ride in the ONE `Authorization` header under their own scheme —\n * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so it cannot leak onto\n * the next hop. Never reads process.env.\n */\n protected override async attachOutboundAuth(\n route: RouteMetadata,\n baseUrl: string,\n httpHeaders: Record<string, string>,\n ): Promise<void> {\n const mode = route.authMeta?.mode;\n if (mode?.kind === 'oidc') {\n httpHeaders['Authorization'] = `Bearer ${await this.gcpOidc.mintIdToken(baseUrl)}`;\n } else if (mode?.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (!secret) {\n throw new Error(\n `No shared secret configured for @AuthSharedSecret('${mode.secretKey}') endpoint ${route.methodName}`,\n );\n }\n // Same header as a JWT/OIDC token, but its OWN scheme, so a secret can never be\n // mistaken for a token nor accepted where one was expected.\n httpHeaders['Authorization'] = `Webpieces ${secret}`;\n }\n }\n\n /**\n * Test-case recording hook (mirror of Java HttpsJsonClientInvokeHandler): if a recorder is\n * travelling in the magic context, capture this outbound call + its result so it becomes a mock\n * in the generated test. Absent a recorder this is exactly the base behavior.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected override 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 const recorder = this.headers.findRecorder();\n if (!recorder) {\n return super.execute(route, requestDto, method);\n }\n return this.recordCall(recorder, route, requestDto, method);\n }\n\n /**\n * Execute the call while recording it (args + masked ctx snapshot + result).\n *\n * The snapshot is a FIXTURE field, not a log line, so it is built here rather than handed down\n * from the call path — a logging backend stamps its own fields and never sees this.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n private async recordCall(\n recorder: TestCaseRecorder,\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 const ctxSnapshot: Record<string, string> = {};\n for (const entry of RequestContext.buildLogFields().entries()) {\n ctxSnapshot[entry[0]] = entry[1];\n }\n const recorded = new RecordedEndpoint(this.contractName(), route.methodName, [requestDto], ctxSnapshot);\n recorder.addEndpointInfo(recorded);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await super.execute(route, requestDto, method);\n recorded.successResponse = response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n recorded.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n }\n }\n\n /** A server can satisfy every auth mode, so nothing is rejected at bind time. */\n protected override assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n}\n\n/**\n * DI token for the `Provider<NodeProxyClient>` that hands out RPC clients — one per API contract.\n * `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because NodeProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would instead hand back one lazily-created\n * instance — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const NODE_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<NodeProxyClient>');\n"]}
|