@webpieces/http-client-node 0.3.372 → 0.3.373

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,12 +9,13 @@ 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` is TYPICALLY the GCP Cloud Run service name, and MUST be when you omit `targetUrl`:
13
- we look your service up in the same project and region and form the URL from the container's own
14
- metadata, so you maintain no URL table. That works across demo/qa/prod as long as each
15
- environment has its own projectId, which is typical.
16
- - `targetUrl` overrides the lookup for another region, another project, or a non-Cloud-Run host.
17
- `svcName` is then used only for logging.
12
+ - `svcName` is TYPICALLY the GCP Cloud Run service name: on GCP we look your service up in the same
13
+ project and region and form the URL from the container's own metadata, so you maintain no URL
14
+ table. That works across demo/qa/prod as long as each environment has its own projectId.
15
+ - Anything the derivation cannot describe — a localhost port, another region, another project, a
16
+ non-Cloud-Run host — is a `ClientRegistry` mapping registered at startup (per environment), NOT a
17
+ per-client URL: `ClientRegistry.addMapping(svcName, port)` or `addUrlMapping(svcName, url)`. A
18
+ registered mapping wins over the GCP derivation.
18
19
 
19
20
  `ClientHttpFactory` injects a `Provider<NodeProxyClient>` and calls `get()` per contract.
20
21
  `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.372",
3
+ "version": "0.3.373",
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.372",
26
- "@webpieces/core-util": "0.3.372",
27
- "@webpieces/gcp-identity": "0.3.372",
28
- "@webpieces/http-client-core": "0.3.372",
25
+ "@webpieces/core-context": "0.3.373",
26
+ "@webpieces/core-util": "0.3.373",
27
+ "@webpieces/gcp-identity": "0.3.373",
28
+ "@webpieces/http-client-core": "0.3.373",
29
29
  "inversify": "7.10.4",
30
30
  "reflect-metadata": "0.2.2"
31
31
  }
@@ -5,44 +5,22 @@
5
5
  *
6
6
  * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of
7
7
  * {@link NodeProxyClient} and are shared by every client the factory builds. This is the RPC twin
8
- * of cloudtasks-client's TaskClientConfig, and takes the same two fields.
8
+ * of cloudtasks-client's TaskClientConfig, and takes the same single field.
9
9
  */
10
10
  export declare class ClientConfig {
11
11
  /**
12
- * TYPICALLY the GCP Cloud Run service name and it MUST be the Cloud Run service name
13
- * when you do not supply a `targetUrl`, because we derive the URL from it.
14
- *
15
- * We lookup your service in the same project, same region, and form the url from the
16
- * container information unless you pass in a targetUrl, so you do not have to maintain
17
- * targetUrls. This works across your demo, qa, prod environments as long as each
18
- * environment is in its own projectId, which is typical.
19
- *
20
- * When you DO supply a `targetUrl`, svcName is used only for logging, so any readable
21
- * name works.
12
+ * The service name. On GCP the URL is DERIVED from it (same project, same region the Cloud
13
+ * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything
14
+ * the derivation cannot describe — a localhost port, another region/project, a non-Cloud-Run
15
+ * host is a `ClientRegistry` mapping registered at startup, NOT a per-client URL.
22
16
  */
23
17
  readonly svcName: string;
24
- /**
25
- * Optional explicit base URL, for the cases lookup cannot describe: another region,
26
- * another project, or a host that is not Cloud Run at all. It wins over `svcName`.
27
- */
28
- readonly targetUrl?: string | undefined;
29
18
  constructor(
30
19
  /**
31
- * TYPICALLY the GCP Cloud Run service name and it MUST be the Cloud Run service name
32
- * when you do not supply a `targetUrl`, because we derive the URL from it.
33
- *
34
- * We lookup your service in the same project, same region, and form the url from the
35
- * container information unless you pass in a targetUrl, so you do not have to maintain
36
- * targetUrls. This works across your demo, qa, prod environments as long as each
37
- * environment is in its own projectId, which is typical.
38
- *
39
- * When you DO supply a `targetUrl`, svcName is used only for logging, so any readable
40
- * name works.
41
- */
42
- svcName: string,
43
- /**
44
- * Optional explicit base URL, for the cases lookup cannot describe: another region,
45
- * another project, or a host that is not Cloud Run at all. It wins over `svcName`.
20
+ * The service name. On GCP the URL is DERIVED from it (same project, same region the Cloud
21
+ * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything
22
+ * the derivation cannot describe — a localhost port, another region/project, a non-Cloud-Run
23
+ * host is a `ClientRegistry` mapping registered at startup, NOT a per-client URL.
46
24
  */
47
- targetUrl?: string | undefined);
25
+ svcName: string);
48
26
  }
@@ -8,32 +8,19 @@ exports.ClientConfig = void 0;
8
8
  *
9
9
  * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of
10
10
  * {@link NodeProxyClient} and are shared by every client the factory builds. This is the RPC twin
11
- * of cloudtasks-client's TaskClientConfig, and takes the same two fields.
11
+ * of cloudtasks-client's TaskClientConfig, and takes the same single field.
12
12
  */
13
13
  class ClientConfig {
14
14
  svcName;
15
- targetUrl;
16
15
  constructor(
17
16
  /**
18
- * TYPICALLY the GCP Cloud Run service name and it MUST be the Cloud Run service name
19
- * when you do not supply a `targetUrl`, because we derive the URL from it.
20
- *
21
- * We lookup your service in the same project, same region, and form the url from the
22
- * container information unless you pass in a targetUrl, so you do not have to maintain
23
- * targetUrls. This works across your demo, qa, prod environments as long as each
24
- * environment is in its own projectId, which is typical.
25
- *
26
- * When you DO supply a `targetUrl`, svcName is used only for logging, so any readable
27
- * name works.
17
+ * The service name. On GCP the URL is DERIVED from it (same project, same region the Cloud
18
+ * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything
19
+ * the derivation cannot describe — a localhost port, another region/project, a non-Cloud-Run
20
+ * host is a `ClientRegistry` mapping registered at startup, NOT a per-client URL.
28
21
  */
29
- svcName,
30
- /**
31
- * Optional explicit base URL, for the cases lookup cannot describe: another region,
32
- * another project, or a host that is not Cloud Run at all. It wins over `svcName`.
33
- */
34
- targetUrl) {
22
+ svcName) {
35
23
  this.svcName = svcName;
36
- this.targetUrl = targetUrl;
37
24
  }
38
25
  }
39
26
  exports.ClientConfig = ClientConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"ClientConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientConfig.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,YAAY;IAcD;IAMA;IAnBpB;IACI;;;;;;;;;;;OAWG;IACa,OAAe;IAE/B;;;OAGG;IACa,SAAkB;QANlB,YAAO,GAAP,OAAO,CAAQ;QAMf,cAAS,GAAT,SAAS,CAAS;IACnC,CAAC;CACP;AAtBD,oCAsBC","sourcesContent":["/**\n * Per-client STATE for a server-side HTTP client — nothing else. A plain class; it extends nothing\n * and is unrelated to the browser package's ClientConfig, because the two answer \"what URL?\"\n * differently and share nothing worth a base class.\n *\n * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of\n * {@link NodeProxyClient} and are shared by every client the factory builds. This is the RPC twin\n * of cloudtasks-client's TaskClientConfig, and takes the same two fields.\n */\nexport class ClientConfig {\n constructor(\n /**\n * TYPICALLY the GCP Cloud Run service name and it MUST be the Cloud Run service name\n * when you do not supply a `targetUrl`, because we derive the URL from it.\n *\n * We lookup your service in the same project, same region, and form the url from the\n * container information unless you pass in a targetUrl, so you do not have to maintain\n * targetUrls. This works across your demo, qa, prod environments as long as each\n * environment is in its own projectId, which is typical.\n *\n * When you DO supply a `targetUrl`, svcName is used only for logging, so any readable\n * name works.\n */\n public readonly svcName: string,\n\n /**\n * Optional explicit base URL, for the cases lookup cannot describe: another region,\n * another project, or a host that is not Cloud Run at all. It wins over `svcName`.\n */\n public readonly targetUrl?: string,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ClientConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientConfig.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,YAAY;IAQD;IAPpB;IACI;;;;;OAKG;IACa,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AAVD,oCAUC","sourcesContent":["/**\n * Per-client STATE for a server-side HTTP client — nothing else. A plain class; it extends nothing\n * and is unrelated to the browser package's ClientConfig, because the two answer \"what URL?\"\n * differently and share nothing worth a base class.\n *\n * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of\n * {@link NodeProxyClient} and are shared by every client the factory builds. This is the RPC twin\n * of cloudtasks-client's TaskClientConfig, and takes the same single field.\n */\nexport class ClientConfig {\n constructor(\n /**\n * The service name. On GCP the URL is DERIVED from it (same project, same region the Cloud\n * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything\n * the derivation cannot describe a localhost port, another region/project, a non-Cloud-Run\n * host is a `ClientRegistry` mapping registered at startup, NOT a per-client URL.\n */\n public readonly svcName: string,\n ) {}\n}\n"]}
@@ -15,8 +15,10 @@ import { NodeProxyClient } from './NodeProxyClient';
15
15
  * // same project + region as this container; the URL is derived, you maintain nothing
16
16
  * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
17
17
  *
18
- * // or point somewhere lookup cannot describe (other region/project, non-Cloud-Run)
19
- * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy', 'https://legacy.corp'));
18
+ * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),
19
+ * // register it once at startup — the client still carries only the svcName:
20
+ * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');
21
+ * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));
20
22
  *
21
23
  * const response = await server2.fetchValue(req); // inside a RequestContext
22
24
  * ```
@@ -37,7 +39,7 @@ export declare class ClientHttpFactory {
37
39
  * Create a type-safe RPC (HTTP) client for one API contract.
38
40
  *
39
41
  * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators
40
- * @param config - This client's state (its svcName, and optionally an explicit targetUrl)
42
+ * @param config - This client's state (its svcName)
41
43
  */
42
44
  createRpcClient<T extends object>(apiPrototype: ApiPrototype<T>, config: ClientConfig): T;
43
45
  }
@@ -23,8 +23,10 @@ const NodeProxyClient_1 = require("./NodeProxyClient");
23
23
  * // same project + region as this container; the URL is derived, you maintain nothing
24
24
  * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
25
25
  *
26
- * // or point somewhere lookup cannot describe (other region/project, non-Cloud-Run)
27
- * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy', 'https://legacy.corp'));
26
+ * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),
27
+ * // register it once at startup — the client still carries only the svcName:
28
+ * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');
29
+ * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));
28
30
  *
29
31
  * const response = await server2.fetchValue(req); // inside a RequestContext
30
32
  * ```
@@ -47,7 +49,7 @@ let ClientHttpFactory = class ClientHttpFactory {
47
49
  * Create a type-safe RPC (HTTP) client for one API contract.
48
50
  *
49
51
  * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators
50
- * @param config - This client's state (its svcName, and optionally an explicit targetUrl)
52
+ * @param config - This client's state (its svcName)
51
53
  */
52
54
  createRpcClient(apiPrototype, config) {
53
55
  // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this
@@ -1 +1 @@
1
- {"version":3,"file":"ClientHttpFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientHttpFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,oDAAsD;AACtD,0DAAqG;AAErG,kEAA+D;AAE/D,uDAAgF;AAEhF,iGAAiG;AACjG,6EAA6E;AAC7E,IAAA,oCAAqB,EAAC,4CAA0B,EAAE,iCAAe,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAII,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAE+B;IADzD,YACyD,mBAA8C;QAA9C,wBAAmB,GAAnB,mBAAmB,CAA2B;IACpG,CAAC;IAEJ;;;;;OAKG;IACH,eAAe,CAAmB,YAA6B,EAAE,MAAoB;QACjF,sFAAsF;QACtF,wEAAwE;QACxE,MAAM,WAAW,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;QACnD,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,IAAA,mCAAgB,EAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AAlBY,8CAAiB;4BAAjB,iBAAiB;IAH7B,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IAGJ,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CAAuC,uBAAQ;GAF7E,iBAAiB,CAkB7B","sourcesContent":["import { inject, injectable } from 'inversify';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { Provider, bindFrameworkProvider, provideFrameworkSingleton } from '@webpieces/core-context';\nimport type { ApiPrototype } from '@webpieces/http-client-core';\nimport { buildClientProxy } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\nimport { NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient } from './NodeProxyClient';\n\n// Teach the container how to hand out fresh NodeProxyClients. NodeProxyClient is bound TRANSIENT\n// (@provideFrameworkTransient), so each provider.get() constructs a new one.\nbindFrameworkProvider(NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient);\n\n/**\n * ClientHttpFactory - builds type-safe HTTP clients from API prototypes carrying\n * @ApiPath/@Endpoint decorators. The SERVER-side factory.\n *\n * This is the client-side equivalent of ApiRoutingFactory:\n * - Server routing: ApiRoutingFactory reads decorators -> routes HTTP requests to controllers\n * - Server client: ClientHttpFactory reads decorators -> generates HTTP requests from method calls\n *\n * Inject it and ask for a typed client per contract:\n * ```typescript\n * // same project + region as this container; the URL is derived, you maintain nothing\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n *\n * // or point somewhere lookup cannot describe (other region/project, non-Cloud-Run)\n * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy', 'https://legacy.corp'));\n *\n * const response = await server2.fetchValue(req); // inside a RequestContext\n * ```\n *\n * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the\n * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`\n * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so\n * the whole dependency graph is visible in this package's design.html.\n *\n * Unlike @webpieces/http-client-browser this package is Node-only, so the factory IS the inversify\n * entry point and the magic context is read straight from the RequestContext. A call made outside\n * `RequestContext.run(...)` throws rather than silently dropping the trace.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\n@injectable()\nexport class ClientHttpFactory {\n constructor(\n @inject(NODE_PROXY_CLIENT_PROVIDER) private readonly proxyClientProvider: Provider<NodeProxyClient>,\n ) {}\n\n /**\n * Create a type-safe RPC (HTTP) client for one API contract.\n *\n * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators\n * @param config - This client's state (its svcName, and optionally an explicit targetUrl)\n */\n createRpcClient<T extends object>(apiPrototype: ApiPrototype<T>, config: ClientConfig): T {\n // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this\n // contract + target; the collaborators already came from the container.\n const proxyClient = this.proxyClientProvider.get();\n proxyClient.init(apiPrototype, config);\n return buildClientProxy(apiPrototype, proxyClient);\n }\n}\n"]}
1
+ {"version":3,"file":"ClientHttpFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientHttpFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,oDAAsD;AACtD,0DAAqG;AAErG,kEAA+D;AAE/D,uDAAgF;AAEhF,iGAAiG;AACjG,6EAA6E;AAC7E,IAAA,oCAAqB,EAAC,4CAA0B,EAAE,iCAAe,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAII,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAE+B;IADzD,YACyD,mBAA8C;QAA9C,wBAAmB,GAAnB,mBAAmB,CAA2B;IACpG,CAAC;IAEJ;;;;;OAKG;IACH,eAAe,CAAmB,YAA6B,EAAE,MAAoB;QACjF,sFAAsF;QACtF,wEAAwE;QACxE,MAAM,WAAW,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;QACnD,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,IAAA,mCAAgB,EAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AAlBY,8CAAiB;4BAAjB,iBAAiB;IAH7B,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IAGJ,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CAAuC,uBAAQ;GAF7E,iBAAiB,CAkB7B","sourcesContent":["import { inject, injectable } from 'inversify';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { Provider, bindFrameworkProvider, provideFrameworkSingleton } from '@webpieces/core-context';\nimport type { ApiPrototype } from '@webpieces/http-client-core';\nimport { buildClientProxy } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\nimport { NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient } from './NodeProxyClient';\n\n// Teach the container how to hand out fresh NodeProxyClients. NodeProxyClient is bound TRANSIENT\n// (@provideFrameworkTransient), so each provider.get() constructs a new one.\nbindFrameworkProvider(NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient);\n\n/**\n * ClientHttpFactory - builds type-safe HTTP clients from API prototypes carrying\n * @ApiPath/@Endpoint decorators. The SERVER-side factory.\n *\n * This is the client-side equivalent of ApiRoutingFactory:\n * - Server routing: ApiRoutingFactory reads decorators -> routes HTTP requests to controllers\n * - Server client: ClientHttpFactory reads decorators -> generates HTTP requests from method calls\n *\n * Inject it and ask for a typed client per contract:\n * ```typescript\n * // same project + region as this container; the URL is derived, you maintain nothing\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n *\n * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),\n * // register it once at startup — the client still carries only the svcName:\n * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');\n * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));\n *\n * const response = await server2.fetchValue(req); // inside a RequestContext\n * ```\n *\n * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the\n * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`\n * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so\n * the whole dependency graph is visible in this package's design.html.\n *\n * Unlike @webpieces/http-client-browser this package is Node-only, so the factory IS the inversify\n * entry point and the magic context is read straight from the RequestContext. A call made outside\n * `RequestContext.run(...)` throws rather than silently dropping the trace.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\n@injectable()\nexport class ClientHttpFactory {\n constructor(\n @inject(NODE_PROXY_CLIENT_PROVIDER) private readonly proxyClientProvider: Provider<NodeProxyClient>,\n ) {}\n\n /**\n * Create a type-safe RPC (HTTP) client for one API contract.\n *\n * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators\n * @param config - This client's state (its svcName)\n */\n createRpcClient<T extends object>(apiPrototype: ApiPrototype<T>, config: ClientConfig): T {\n // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this\n // contract + target; the collaborators already came from the container.\n const proxyClient = this.proxyClientProvider.get();\n proxyClient.init(apiPrototype, config);\n return buildClientProxy(apiPrototype, proxyClient);\n }\n}\n"]}
@@ -21,7 +21,7 @@ export declare class NodeProxyClient extends ProxyClient {
21
21
  init(apiPrototype: ApiPrototype<object>, config: ClientConfig): void;
22
22
  /**
23
23
  * Resolved per call, never at construction, so building a client stays synchronous. Every GCP
24
- * metadata read beneath resolveTargetUrl is memoized process-wide, so only the first call pays.
24
+ * metadata read beneath resolveServiceUrl is memoized process-wide, so only the first call pays.
25
25
  */
26
26
  protected resolveBaseUrl(): Promise<string>;
27
27
  /** Straight from the RequestContext. Throws when there is no active request scope. */
@@ -33,10 +33,10 @@ let NodeProxyClient = class NodeProxyClient extends http_client_core_1.ProxyClie
33
33
  }
34
34
  /**
35
35
  * Resolved per call, never at construction, so building a client stays synchronous. Every GCP
36
- * metadata read beneath resolveTargetUrl is memoized process-wide, so only the first call pays.
36
+ * metadata read beneath resolveServiceUrl is memoized process-wide, so only the first call pays.
37
37
  */
38
38
  resolveBaseUrl() {
39
- return (0, gcp_identity_1.resolveTargetUrl)(this.config.svcName, this.config.targetUrl);
39
+ return (0, gcp_identity_1.resolveServiceUrl)(this.config.svcName);
40
40
  }
41
41
  /** Straight from the RequestContext. Throws when there is no active request scope. */
42
42
  outboundHeaders() {
@@ -1 +1 @@
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 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, resolveTargetUrl } 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 resolveTargetUrl is memoized process-wide, so only the first call pays.\n */\n protected override resolveBaseUrl(): Promise<string> {\n return resolveTargetUrl(this.config.svcName, this.config.targetUrl);\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 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"]}