@webpieces/core-util 0.4.956 → 0.4.958

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.
Files changed (94) hide show
  1. package/package.json +1 -1
  2. package/src/http/ApiCallInfo.d.ts +71 -11
  3. package/src/http/ApiCallInfo.js +81 -7
  4. package/src/http/ApiCallInfo.js.map +1 -1
  5. package/src/http/ApiMethodInfo.d.ts +14 -0
  6. package/src/http/ApiMethodInfo.js +3 -0
  7. package/src/http/ApiMethodInfo.js.map +1 -1
  8. package/src/http/CallRegistry.d.ts +5 -10
  9. package/src/http/CallRegistry.js +21 -43
  10. package/src/http/CallRegistry.js.map +1 -1
  11. package/src/http/CallRegistryCompileAssertions.js +1 -1
  12. package/src/http/CallRegistryCompileAssertions.js.map +1 -1
  13. package/src/http/CallScope.d.ts +33 -0
  14. package/src/http/CallScope.js +70 -0
  15. package/src/http/CallScope.js.map +1 -0
  16. package/src/http/CallStrategy.d.ts +13 -1
  17. package/src/http/CallStrategy.js +4 -1
  18. package/src/http/CallStrategy.js.map +1 -1
  19. package/src/http/ClientRegistry.d.ts +54 -58
  20. package/src/http/ClientRegistry.js +72 -73
  21. package/src/http/ClientRegistry.js.map +1 -1
  22. package/src/http/ContextMgr.js +12 -2
  23. package/src/http/ContextMgr.js.map +1 -1
  24. package/src/http/DeletedAuthCompileAssertions.js +3 -1
  25. package/src/http/DeletedAuthCompileAssertions.js.map +1 -1
  26. package/src/http/DestinationTrust.d.ts +33 -4
  27. package/src/http/DestinationTrust.js +54 -11
  28. package/src/http/DestinationTrust.js.map +1 -1
  29. package/src/http/EndpointCompileAssertions.js +12 -6
  30. package/src/http/EndpointCompileAssertions.js.map +1 -1
  31. package/src/http/ErrorClassifier.d.ts +20 -27
  32. package/src/http/ErrorClassifier.js +1 -15
  33. package/src/http/ErrorClassifier.js.map +1 -1
  34. package/src/http/ErrorLadderCompileAssertions.d.ts +1 -0
  35. package/src/http/ErrorLadderCompileAssertions.js +42 -0
  36. package/src/http/ErrorLadderCompileAssertions.js.map +1 -0
  37. package/src/http/ErrorTranslator.d.ts +8 -4
  38. package/src/http/ErrorTranslator.js.map +1 -1
  39. package/src/http/ExternalContextKeys.d.ts +31 -0
  40. package/src/http/ExternalContextKeys.js +77 -0
  41. package/src/http/ExternalContextKeys.js.map +1 -0
  42. package/src/http/HttpContract.d.ts +45 -7
  43. package/src/http/HttpContract.js +84 -6
  44. package/src/http/HttpContract.js.map +1 -1
  45. package/src/http/HttpEndpointOptions.d.ts +22 -3
  46. package/src/http/HttpEndpointOptions.js +8 -1
  47. package/src/http/HttpEndpointOptions.js.map +1 -1
  48. package/src/http/LogApiCall.d.ts +4 -1
  49. package/src/http/LogApiCall.js +9 -7
  50. package/src/http/LogApiCall.js.map +1 -1
  51. package/src/http/QueryParams.d.ts +47 -0
  52. package/src/http/QueryParams.js +89 -0
  53. package/src/http/QueryParams.js.map +1 -0
  54. package/src/http/RetryStrategy.js +1 -1
  55. package/src/http/RetryStrategy.js.map +1 -1
  56. package/src/http/RouteMetadata.d.ts +16 -2
  57. package/src/http/RouteMetadata.js +19 -3
  58. package/src/http/RouteMetadata.js.map +1 -1
  59. package/src/http/RouteMetadataFactory.d.ts +25 -0
  60. package/src/http/RouteMetadataFactory.js +97 -12
  61. package/src/http/RouteMetadataFactory.js.map +1 -1
  62. package/src/http/StreamErrorControl.d.ts +9 -1
  63. package/src/http/StreamErrorControl.js +14 -3
  64. package/src/http/StreamErrorControl.js.map +1 -1
  65. package/src/http/WebpiecesDefaultErrorClassifier.d.ts +2 -2
  66. package/src/http/WebpiecesDefaultErrorClassifier.js +2 -2
  67. package/src/http/WebpiecesDefaultErrorClassifier.js.map +1 -1
  68. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +2 -1
  69. package/src/http/WebpiecesDefaultErrorTranslator.js +2 -1
  70. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  71. package/src/http/WpAuthApiKeyCompileAssertions.js +2 -2
  72. package/src/http/WpAuthApiKeyCompileAssertions.js.map +1 -1
  73. package/src/http/api-kind.d.ts +3 -2
  74. package/src/http/api-kind.js +5 -4
  75. package/src/http/api-kind.js.map +1 -1
  76. package/src/http/auth-mode.d.ts +8 -2
  77. package/src/http/auth-mode.js +11 -5
  78. package/src/http/auth-mode.js.map +1 -1
  79. package/src/http/decorators.d.ts +39 -21
  80. package/src/http/decorators.js +69 -37
  81. package/src/http/decorators.js.map +1 -1
  82. package/src/http/external-caller.d.ts +12 -17
  83. package/src/http/external-caller.js +12 -17
  84. package/src/http/external-caller.js.map +1 -1
  85. package/src/http/http-parameter-decorators.d.ts +17 -3
  86. package/src/http/http-parameter-decorators.js +27 -6
  87. package/src/http/http-parameter-decorators.js.map +1 -1
  88. package/src/index.d.ts +14 -9
  89. package/src/index.js +32 -13
  90. package/src/index.js.map +1 -1
  91. package/src/ipc/IpcConnection.js +1 -1
  92. package/src/ipc/IpcConnection.js.map +1 -1
  93. package/src/ipc/IpcLogging.js +1 -1
  94. package/src/ipc/IpcLogging.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":";;;AAEA,uFAAuF;AACvF,uFAAuF;AAiBvF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAa,cAAc;IACvB,0FAA0F;IAClF,MAAM,CAAU,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE7D,gGAAgG;IACxF,MAAM,CAAC,OAAO,CAAgC;IAEtD;;;;;;;;;;;;;OAaG;IACK,MAAM,CAAC,eAAe,GAAoB,oEAAkC,CAAC;IAErF;;;;;;OAMG;IACK,MAAM,CAAC,yBAAyB,GAAoB,oEAAkC,CAAC;IAE/F;;;;;OAKG;IACK,MAAM,CAAU,0BAA0B,GAAG,IAAI,GAAG,EAA2B,CAAC;IAExF,uDAAuD;IACvD,wJAAwJ;IACxJ,MAAM,CAAC,UAAU,CAAC,OAAe,EAAE,IAAY;QAC3C,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,oBAAoB,IAAI,EAAE,CAAC,CAAC;IACrE,CAAC;IAED;;;;;OAKG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,aAAa,CAAC,OAAe,EAAE,GAAW;QAC7C,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED;;;OAGG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,UAAU,CAAC,EAAqB;QACnC,cAAc,CAAC,OAAO,GAAG,EAAE,CAAC;IAChC,CAAC;IAED,kGAAkG;IAClG,wJAAwJ;IACxJ,MAAM,CAAC,SAAS,CAAC,OAAe;QAC5B,OAAO,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChD,CAAC;IAED;;;OAGG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,MAAM,CAAC,OAAe;QACzB,MAAM,GAAG,GAAG,cAAc,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACX,kCAAkC,OAAO,6BAA6B;gBAClE,oEAAoE;gBACpE,iEAAiE,CACxE,CAAC;QACN,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,UAAU,CAAC,OAAe;QAC7B,MAAM,QAAQ,GAAG,cAAc,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;YAC1B,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAe;QAChC,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACX,uBAAuB,OAAO,MAAM;gBAChC,iDAAiD,OAAO,YAAY;gBACpE,oDAAoD,OAAO,qBAAqB;gBAChF,gFAAgF;gBAChF,2FAA2F;gBAC3F,yFAAyF;gBACzF,2FAA2F,CAClG,CAAC;QACN,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB,CAAC,UAA2B;QACjD,cAAc,CAAC,eAAe,GAAG,UAAU,CAAC;IAChD,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB;QACrB,OAAO,cAAc,CAAC,eAAe,CAAC;IAC1C,CAAC;IAED;;;;;OAKG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,yBAAyB,CAAC,UAA2B;QACxD,cAAc,CAAC,yBAAyB,GAAG,UAAU,CAAC;IAC1D,CAAC;IAED;;;;OAIG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB,CAAC,KAA2B;QACjD,cAAc,CAAC,0BAA0B,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACpF,CAAC;IAED;;;;;;;;;OASG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,QAAQ,CAAC,KAAY,EAAE,UAAyB;QACnD,MAAM,SAAS,GAAG,cAAc,CAAC,0BAA0B,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACrF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YACtD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,OAAO,CAAC;YACnB,CAAC;QACL,CAAC;QACD,MAAM,OAAO,GAAG,cAAc,CAAC,yBAAyB,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACrF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,OAAO,CAAC;QACnB,CAAC;QACD,OAAO,oEAAkC,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAC1E,CAAC;IAED;;;;;;;;OAQG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,aAAa;QAChB,cAAc,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAChC,cAAc,CAAC,OAAO,GAAG,SAAS,CAAC;QACnC,cAAc,CAAC,eAAe,GAAG,oEAAkC,CAAC;QACpE,cAAc,CAAC,yBAAyB,GAAG,oEAAkC,CAAC;QAC9E,cAAc,CAAC,0BAA0B,CAAC,KAAK,EAAE,CAAC;IACtD,CAAC;;AA5NL,wCA6NC","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { CallOutcome, ErrorClassifier, KeyedErrorClassifier } from './ErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_CLASSIFIER } from './WebpiecesDefaultErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * The process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}\n * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same\n * no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * There is no \"did anyone register one\" question anywhere in the framework because this field\n * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an\n * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.\n *\n * ONE, not a list, on purpose: an app that has several layers of error policy composes them\n * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit\n * in the order two unrelated startup paths happened to register.\n */\n private static errorTranslator: ErrorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n\n /**\n * The app/company DEFAULT {@link ErrorClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single classifier covers the server router AND all internal clients. NEVER undefined: it\n * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}) and\n * {@link ClientRegistry.setDefaultErrorClassifier} replaces it. Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultErrorClassifier: ErrorClassifier = WEBPIECES_DEFAULT_ERROR_CLASSIFIER;\n\n /**\n * Per-EXTERNAL-client {@link ErrorClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classify}.\n */\n private static readonly errorClassifiersByApiClass = new Map<string, ErrorClassifier>();\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n /**\n * Install the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for\n * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping\n * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring\n * {@link ClientRegistry.addMapping}.\n *\n * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslator(translator: ErrorTranslator): void {\n ClientRegistry.errorTranslator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.\n *\n * Until an app calls {@link ClientRegistry.setErrorTranslator} this is\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and\n * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),\n * so every call site in the framework is one unconditional line.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(): ErrorTranslator {\n return ClientRegistry.errorTranslator;\n }\n\n /**\n * Set the app/company DEFAULT error classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDefaultErrorClassifier(classifier: ErrorClassifier): void {\n ClientRegistry.appDefaultErrorClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client error classifier, keyed by its\n * {@link KeyedErrorClassifier.apiClass} (e.g. 'FirestoreAdminClient'). Consulted BEFORE the\n * default tier for calls with that apiClass. Call ONCE at startup. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addErrorClassifier(keyed: KeyedErrorClassifier): void {\n ClientRegistry.errorClassifiersByApiClass.set(keyed.apiClass, keyed.classifier);\n }\n\n /**\n * The {@link CallOutcome} of a thrown API-call error. Resolved most-specific-first, and ALWAYS\n * definitive:\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier — unless it defers;\n * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.\n *\n * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt\n * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n const perClient = ClientRegistry.errorClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n const verdict = ClientRegistry.appDefaultErrorClassifier.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n return WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next.\n *\n * It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole\n * reason it is not called `clear()` any more: `getErrorTranslator()` and the default error\n * classifier are non-optional now, so \"cleared\" is not a state they have. A spec that wants\n * webpieces' own behaviour back asks for exactly that.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n ClientRegistry.appDefaultErrorClassifier = WEBPIECES_DEFAULT_ERROR_CLASSIFIER;\n ClientRegistry.errorClassifiersByApiClass.clear();\n }\n}\n"]}
1
+ {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { CallOutcome, ErrorClassifier } from './ErrorClassifier';\nimport { CallScope, CallScopeLadder } from './CallScope';\nimport { WEBPIECES_DEFAULT_ERROR_CLASSIFIER } from './WebpiecesDefaultErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/** One {@link CallScope} rung of the error-translator ladder. Data only, so a class. */\nclass TranslatorSlot {\n translator?: ErrorTranslator;\n}\n\n/** One {@link CallScope} rung of the error-classifier ladder. Data only, so a class. */\nclass ClassifierSlot {\n classifier?: ErrorClassifier;\n}\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ClientRegistry.getErrorTranslator} is\n * never undefined. Process-global, set once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * ONE translator per scope, not a list, on purpose: an app that has several layers of error\n * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written\n * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a\n * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an\n * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.\n */\n private static readonly errorTranslators = new CallScopeLadder<TranslatorSlot>(\n () => new TranslatorSlot(),\n );\n\n /**\n * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through\n * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers\n * the server router AND every internal client; an SDK-wrapping api-client registers its own at\n * its abstract `XxxApi` class. See {@link ClientRegistry.classify}.\n */\n private static readonly errorClassifiers = new CallScopeLadder<ClassifierSlot>(\n () => new ClassifierSlot(),\n );\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n /**\n * Install an {@link ErrorTranslator} for one {@link CallScope}: `'ALL'` (the app's translator,\n * server side AND every client side), one contract CLASS, or one method of one contract. The\n * scope is REQUIRED — there is no unscoped spelling. Call at startup, on the server AND in the\n * browser, mirroring {@link ClientRegistry.addMapping}.\n *\n * ```ts\n * ClientRegistry.setErrorTranslator(new AppErrorTranslator(), 'ALL');\n * ClientRegistry.setErrorTranslator(new VendorErrorTranslator(), VendorApi); // one contract\n * ClientRegistry.setErrorTranslator(new LegacyErrorTranslator(), VendorApi, 'sync'); // one method\n * ```\n *\n * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator\n * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next\n * rung — exactly as before.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslator(translator: ErrorTranslator, ...scope: CallScope): void {\n ClientRegistry.errorTranslators.at(...scope).translator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.\n *\n * Both arguments are required so every call site states what it knows: the client and the server\n * pass the route's contract class and method; a failure with no route (an unmatched request, a\n * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(api: Function | undefined, methodName: string | undefined): ErrorTranslator {\n const registered = ClientRegistry.errorTranslators\n .rungs(api, methodName)\n .find((slot: TranslatorSlot) => slot.translator !== undefined)?.translator;\n return registered ?? WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n }\n\n /**\n * Install an {@link ErrorClassifier} for one {@link CallScope}: `'ALL'` (the app/company\n * classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router and every\n * internal client), one contract CLASS (an SDK-wrapping api-client keys by its abstract `XxxApi`),\n * or one method of one contract. The scope is REQUIRED. Call at startup, on the server AND in the\n * browser. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorClassifier(classifier: ErrorClassifier, ...scope: CallScope): void {\n ClientRegistry.errorClassifiers.at(...scope).classifier = classifier;\n }\n\n /**\n * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of\n * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:\n * 1. the method classifier — unless it defers;\n * 2. else the API classifier — unless it defers;\n * 3. else the ALL classifier — unless it defers;\n * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.\n *\n * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt\n * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n const rungs = ClientRegistry.errorClassifiers.rungs(methodInfo.contract, methodInfo.methodName);\n for (const slot of rungs) {\n const verdict = slot.classifier?.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n return WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next.\n *\n * It empties every translator and classifier rung, which RESTORES the webpieces defaults:\n * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so\n * \"cleared\" is not a state they have. A spec that wants webpieces' own behaviour back asks for\n * exactly that.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslators.clear();\n ClientRegistry.errorClassifiers.clear();\n }\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContextMgr = void 0;
4
+ const ApiError_1 = require("../errors/ApiError");
4
5
  const HeaderRegistry_1 = require("./HeaderRegistry");
5
6
  const ServiceInfo_1 = require("./ServiceInfo");
6
7
  const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
@@ -59,7 +60,9 @@ class ContextMgr {
59
60
  */
60
61
  buildOutboundHeaders(destination) {
61
62
  const outbound = new Map();
62
- for (const key of HeaderRegistry_1.HeaderRegistry.get().getTransferredKeys()) {
63
+ // An EXTERNAL_OUTBOUND destination lists EXACTLY what it sends, and each listed key is required.
64
+ const listed = destination.listedKeys();
65
+ for (const key of listed ?? HeaderRegistry_1.HeaderRegistry.get().getTransferredKeys()) {
63
66
  if (!destination.allows(key)) {
64
67
  continue;
65
68
  }
@@ -67,6 +70,13 @@ class ContextMgr {
67
70
  if (value !== undefined && value !== null && value !== '') {
68
71
  outbound.set(key.httpHeader, value);
69
72
  }
73
+ else if (listed !== undefined) {
74
+ throw new ApiError_1.ApiImplementationError(`Context key '${key.name}' (header ${key.httpHeader}) is listed in @ExternalContextKeys for this ` +
75
+ `outbound call but has no value in the context store. Set it before calling — nothing was sent.`);
76
+ }
77
+ }
78
+ if (!destination.allows(WebpiecesCoreHeaders_1.WebpiecesCoreHeaders.CLIENT_VERSION)) {
79
+ return outbound;
70
80
  }
71
81
  // CLIENT_VERSION: a browser ORIGINATES a call, so it sends its OWN app build version (from
72
82
  // ServiceInfo) and the server logs which client build called it — same rule as the server-side
@@ -103,7 +113,7 @@ class ContextMgr {
103
113
  if (value === null || value === '') {
104
114
  continue;
105
115
  }
106
- if (!destination.allows(key) || key.isTrusted()) {
116
+ if (!destination.believesResponse(key) || key.isTrusted()) {
107
117
  // DROPPED: a browser proves nothing, so it may never hold a trusted value.
108
118
  continue;
109
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMgr.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ContextMgr.ts"],"names":[],"mappings":";;;AAGA,qDAAkD;AAClD,+CAA4C;AAC5C,iEAA8D;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,UAAU;IAGC;IAFpB;IACI,iFAAiF;IACjE,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAC3C,CAAC;IAEJ;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,oBAAoB,CAAC,WAA6B;QAC9C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAE3C,KAAK,MAAM,GAAG,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC;YAC1D,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3B,SAAS;YACb,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACxD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,UAAW,EAAE,KAAK,CAAC,CAAC;YACzC,CAAC;QACL,CAAC;QAED,2FAA2F;QAC3F,+FAA+F;QAC/F,gGAAgG;QAChG,MAAM,SAAS,GAAG,yBAAW,CAAC,UAAU,EAAE,CAAC;QAC3C,MAAM,mBAAmB,GAAG,2CAAoB,CAAC,cAAc,CAAC,UAAW,CAAC;QAC5E,IAAI,SAAS,EAAE,CAAC;YACZ,QAAQ,CAAC,GAAG,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACzC,CAAC;QAED,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAAqB,CAAC,OAAgB,EAAE,WAA6B;QACjE,KAAK,MAAM,GAAG,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,cAAe,CAAC,CAAC;YAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACjC,SAAS;YACb,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;gBAC9C,2EAA2E;gBAC3E,SAAS;YACb,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QAClG,CAAC;IACL,CAAC;CACJ;AArFD,gCAqFC","sourcesContent":["import { ContextKey } from '../ContextKey';\nimport { ContextStore } from './ContextWriter';\nimport { DestinationTrust } from './DestinationTrust';\nimport { HeaderRegistry } from './HeaderRegistry';\nimport { ServiceInfo } from './ServiceInfo';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\n\n/**\n * ContextMgr - propagates the magic context onto outbound BROWSER requests.\n *\n * BROWSER-ONLY. Only @webpieces/http-client-browser may name this class. The server reads\n * `RequestContext` directly through `RequestContextHeaders` (in @webpieces/core-context) — a\n * `ContextReader` indirection buys a server nothing, because there is exactly one right answer there.\n *\n * Browsers have no AsyncLocalStorage, so the app holds a `MutableContextStore` and sets values as\n * they become known (login token, tenant). Every transferred key (httpHeader set) in the GLOBAL\n * {@link HeaderRegistry} is read from it and added to outbound requests. The registry is a process\n * global configured once at startup (like LogManager) and is browser-safe: it is the key SCHEMA,\n * not the value store.\n *\n * Example usage:\n * ```typescript\n * // startup, before bootstrap:\n * HeaderRegistry.configure(CompanyHeaders.ALL_HEADERS, true);\n *\n * const store = new MutableContextStore();\n * const factory = new ClientHttpBrowserFactory(store);\n * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com', ClientRole.SERVER));\n * ```\n */\nexport class ContextMgr {\n constructor(\n /** The app-held store context-key values are read from AND written back into. */\n public readonly contextStore: ContextStore,\n ) {}\n\n /**\n * Build the headers to send on an outbound request: every transferred key (httpHeader set)\n * with a non-empty value THAT THIS DESTINATION MAY RECEIVE, emitted under its `httpHeader`\n * wire name.\n *\n * NO request-id chaining. A browser ORIGINATES a trace — it has no inbound request to point\n * back at. If the app puts an `x-request-id` on the store it goes out as-is, and the server's\n * inbound transfer adopts it as hop 1's own id. Chaining is a server concern; see\n * RequestContextHeaders.\n *\n * `destination` gates TRUSTED keys exactly as it does on the server side (see\n * {@link DestinationTrust}). In practice a browser never reaches the permissive branch — twice\n * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an OIDC (`oidcSameProject()` /\n * `oidcSvcs(...)`) or `sharedSecret(...)` contract at all, so every browser destination is `jwt()` or `@WpAuthPublic`.\n * The rule is applied here anyway rather than argued away, because the OTHER guarantee people\n * reach for — \"`MutableContextStore.set` only accepts an untrusted key, so a browser store\n * cannot HOLD a trusted value\" — is true of that store and NOT of the seam: {@link ContextMgr}\n * takes any app-supplied {@link ContextStore}, whose `read` is handed an `AnyContextKey`. One\n * enforced rule in both builders beats a browser-only exemption resting on an implementation\n * detail of one implementation.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n const outbound = new Map<string, string>();\n\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n const value = this.contextStore.read(key);\n if (value !== undefined && value !== null && value !== '') {\n outbound.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION: a browser ORIGINATES a call, so it sends its OWN app build version (from\n // ServiceInfo) and the server logs which client build called it — same rule as the server-side\n // RequestContextHeaders. Our version wins; absent if this app was never identified via setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n outbound.set(clientVersionHeader, myVersion);\n } else {\n outbound.delete(clientVersionHeader);\n }\n\n return outbound;\n }\n\n /**\n * RESPONSE -> the store: every key that declares a `responseHeader` and appears on a response\n * this browser just received, merged into the app-held store per the key's own\n * {@link ContextKey.responseMerge}.\n *\n * The browser twin of `RequestContextHeaders.acceptResponseHeaders`, and the reason a value set\n * by a server reaches the page without the page naming a header.\n *\n * TRUSTED keys are DROPPED here, unconditionally and without consulting `destination`, and the\n * `if` is what makes that a compile-time fact rather than a convention: {@link ContextWriter.set}\n * takes an {@link AnyUntrustedContextKey}, so a browser store CANNOT hold a proven value. That is\n * the same rule as the outbound direction reaches by a different route — every browser\n * destination is `jwt()` or `@WpAuthPublic`, so `destination.allows` would refuse them\n * anyway — and `destination` is still threaded and still applied, so a future browser destination\n * that DID authenticate its caller does not silently start believing response headers.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.allows(key) || key.isTrusted()) {\n // DROPPED: a browser proves nothing, so it may never hold a trusted value.\n continue;\n }\n this.contextStore.set(key, key.mergeResponseValue(this.contextStore.readValue?.(key), value));\n }\n }\n}\n"]}
1
+ {"version":3,"file":"ContextMgr.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ContextMgr.ts"],"names":[],"mappings":";;;AACA,iDAA4D;AAG5D,qDAAkD;AAClD,+CAA4C;AAC5C,iEAA8D;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,UAAU;IAGC;IAFpB;IACI,iFAAiF;IACjE,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAC3C,CAAC;IAEJ;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,oBAAoB,CAAC,WAA6B;QAC9C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAE3C,iGAAiG;QACjG,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,EAAE,CAAC;QACxC,KAAK,MAAM,GAAG,IAAI,MAAM,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC;YACpE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3B,SAAS;YACb,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACxD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,UAAW,EAAE,KAAK,CAAC,CAAC;YACzC,CAAC;iBAAM,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC9B,MAAM,IAAI,iCAAsB,CAC5B,gBAAgB,GAAG,CAAC,IAAI,aAAa,GAAG,CAAC,UAAU,+CAA+C;oBAC9F,gGAAgG,CACvG,CAAC;YACN,CAAC;QACL,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,2CAAoB,CAAC,cAAc,CAAC,EAAE,CAAC;YAC3D,OAAO,QAAQ,CAAC;QACpB,CAAC;QAED,2FAA2F;QAC3F,+FAA+F;QAC/F,gGAAgG;QAChG,MAAM,SAAS,GAAG,yBAAW,CAAC,UAAU,EAAE,CAAC;QAC3C,MAAM,mBAAmB,GAAG,2CAAoB,CAAC,cAAc,CAAC,UAAW,CAAC;QAC5E,IAAI,SAAS,EAAE,CAAC;YACZ,QAAQ,CAAC,GAAG,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACzC,CAAC;QAED,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAAqB,CAAC,OAAgB,EAAE,WAA6B;QACjE,KAAK,MAAM,GAAG,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,cAAe,CAAC,CAAC;YAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACjC,SAAS;YACb,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;gBACxD,2EAA2E;gBAC3E,SAAS;YACb,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QAClG,CAAC;IACL,CAAC;CACJ;AA/FD,gCA+FC","sourcesContent":["import { ContextKey } from '../ContextKey';\nimport { ApiImplementationError } from '../errors/ApiError';\nimport { ContextStore } from './ContextWriter';\nimport { DestinationTrust } from './DestinationTrust';\nimport { HeaderRegistry } from './HeaderRegistry';\nimport { ServiceInfo } from './ServiceInfo';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\n\n/**\n * ContextMgr - propagates the magic context onto outbound BROWSER requests.\n *\n * BROWSER-ONLY. Only @webpieces/http-client-browser may name this class. The server reads\n * `RequestContext` directly through `RequestContextHeaders` (in @webpieces/core-context) — a\n * `ContextReader` indirection buys a server nothing, because there is exactly one right answer there.\n *\n * Browsers have no AsyncLocalStorage, so the app holds a `MutableContextStore` and sets values as\n * they become known (login token, tenant). Every transferred key (httpHeader set) in the GLOBAL\n * {@link HeaderRegistry} is read from it and added to outbound requests. The registry is a process\n * global configured once at startup (like LogManager) and is browser-safe: it is the key SCHEMA,\n * not the value store.\n *\n * Example usage:\n * ```typescript\n * // startup, before bootstrap:\n * HeaderRegistry.configure(CompanyHeaders.ALL_HEADERS, true);\n *\n * const store = new MutableContextStore();\n * const factory = new ClientHttpBrowserFactory(store);\n * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com', ClientRole.SERVER));\n * ```\n */\nexport class ContextMgr {\n constructor(\n /** The app-held store context-key values are read from AND written back into. */\n public readonly contextStore: ContextStore,\n ) {}\n\n /**\n * Build the headers to send on an outbound request: every transferred key (httpHeader set)\n * with a non-empty value THAT THIS DESTINATION MAY RECEIVE, emitted under its `httpHeader`\n * wire name.\n *\n * NO request-id chaining. A browser ORIGINATES a trace — it has no inbound request to point\n * back at. If the app puts an `x-request-id` on the store it goes out as-is, and the server's\n * inbound transfer adopts it as hop 1's own id. Chaining is a server concern; see\n * RequestContextHeaders.\n *\n * `destination` gates TRUSTED keys exactly as it does on the server side (see\n * {@link DestinationTrust}). In practice a browser never reaches the permissive branch — twice\n * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an OIDC (`oidcSameProject()` /\n * `oidcSvcs(...)`) or `sharedSecret(...)` contract at all, so every browser destination is `jwt()` or `@WpAuthPublic`.\n * The rule is applied here anyway rather than argued away, because the OTHER guarantee people\n * reach for — \"`MutableContextStore.set` only accepts an untrusted key, so a browser store\n * cannot HOLD a trusted value\" — is true of that store and NOT of the seam: {@link ContextMgr}\n * takes any app-supplied {@link ContextStore}, whose `read` is handed an `AnyContextKey`. One\n * enforced rule in both builders beats a browser-only exemption resting on an implementation\n * detail of one implementation.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n const outbound = new Map<string, string>();\n\n // An EXTERNAL_OUTBOUND destination lists EXACTLY what it sends, and each listed key is required.\n const listed = destination.listedKeys();\n for (const key of listed ?? HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n const value = this.contextStore.read(key);\n if (value !== undefined && value !== null && value !== '') {\n outbound.set(key.httpHeader!, value);\n } else if (listed !== undefined) {\n throw new ApiImplementationError(\n `Context key '${key.name}' (header ${key.httpHeader}) is listed in @ExternalContextKeys for this ` +\n `outbound call but has no value in the context store. Set it before calling — nothing was sent.`,\n );\n }\n }\n if (!destination.allows(WebpiecesCoreHeaders.CLIENT_VERSION)) {\n return outbound;\n }\n\n // CLIENT_VERSION: a browser ORIGINATES a call, so it sends its OWN app build version (from\n // ServiceInfo) and the server logs which client build called it — same rule as the server-side\n // RequestContextHeaders. Our version wins; absent if this app was never identified via setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n outbound.set(clientVersionHeader, myVersion);\n } else {\n outbound.delete(clientVersionHeader);\n }\n\n return outbound;\n }\n\n /**\n * RESPONSE -> the store: every key that declares a `responseHeader` and appears on a response\n * this browser just received, merged into the app-held store per the key's own\n * {@link ContextKey.responseMerge}.\n *\n * The browser twin of `RequestContextHeaders.acceptResponseHeaders`, and the reason a value set\n * by a server reaches the page without the page naming a header.\n *\n * TRUSTED keys are DROPPED here, unconditionally and without consulting `destination`, and the\n * `if` is what makes that a compile-time fact rather than a convention: {@link ContextWriter.set}\n * takes an {@link AnyUntrustedContextKey}, so a browser store CANNOT hold a proven value. That is\n * the same rule as the outbound direction reaches by a different route — every browser\n * destination is `jwt()` or `@WpAuthPublic`, so `destination.allows` would refuse them\n * anyway — and `destination` is still threaded and still applied, so a future browser destination\n * that DID authenticate its caller does not silently start believing response headers.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.believesResponse(key) || key.isTrusted()) {\n // DROPPED: a browser proves nothing, so it may never hold a trusted value.\n continue;\n }\n this.contextStore.set(key, key.mergeResponseValue(this.contextStore.readValue?.(key), value));\n }\n }\n}\n"]}
@@ -14,8 +14,10 @@ class DeletedAuthCompileAssertions {
14
14
  void CoreUtil.oidc;
15
15
  // @ts-expect-error use WpAuth([sharedSecret(...)]) and WpAuthorization
16
16
  void CoreUtil.WpAuthSharedSecret;
17
- // @ts-expect-error use WpAuth([webhook(...)]) and WpAuthorization
17
+ // @ts-expect-error use WpAuth([vendorAuth(...)]) and WpAuthorization
18
18
  void CoreUtil.WpAuthWebhook;
19
+ // @ts-expect-error webhook(name) is renamed vendorAuth(name): one VendorAuthHook mints, refreshes and verifies
20
+ void CoreUtil.webhook;
19
21
  // @ts-expect-error use WpAuth([apiKey(...)]) and WpAuthorization
20
22
  void CoreUtil.WpAuthApiKey;
21
23
  // @ts-expect-error use separate WpLocalOnly, authentication and authorization
@@ -1 +1 @@
1
- {"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAAA,2DAAqC;AAErC,4FAA4F;AAC5F,MAAa,4BAA4B;IACrC,cAAc;QACV,2DAA2D;QAC3D,KAAK,QAAQ,CAAC,SAAS,CAAC;QACxB,oGAAoG;QACpG,KAAK,QAAQ,CAAC,UAAU,CAAC;QACzB,4HAA4H;QAC5H,KAAK,QAAQ,CAAC,IAAI,CAAC;QACnB,uEAAuE;QACvE,KAAK,QAAQ,CAAC,kBAAkB,CAAC;QACjC,kEAAkE;QAClE,KAAK,QAAQ,CAAC,aAAa,CAAC;QAC5B,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,8EAA8E;QAC9E,KAAK,QAAQ,CAAC,eAAe,CAAC;QAC9B,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,oEAAoE;QACpE,KAAK,QAAQ,CAAC,WAAW,CAAC;QAC1B,kEAAkE;QAClE,KAAK,QAAQ,CAAC,aAAa,CAAC;IAChC,CAAC;CACJ;AAvBD,oEAuBC","sourcesContent":["import * as CoreUtil from '../index';\n\n/** Each removed spelling has its own assertion so one missing export cannot mask a shim. */\nexport class DeletedAuthCompileAssertions {\n removedExports(): void {\n // @ts-expect-error use WpAuth([jwt()]) and WpAuthorization\n void CoreUtil.WpAuthJwt;\n // @ts-expect-error use WpAuth([oidcSameProject()]) or WpAuth([oidcSvcs([...])]) and WpAuthorization\n void CoreUtil.WpAuthOidc;\n // @ts-expect-error oidc(...) is removed: oidcSameProject() for this GCP project, oidcSvcs([...]) for named service accounts\n void CoreUtil.oidc;\n // @ts-expect-error use WpAuth([sharedSecret(...)]) and WpAuthorization\n void CoreUtil.WpAuthSharedSecret;\n // @ts-expect-error use WpAuth([webhook(...)]) and WpAuthorization\n void CoreUtil.WpAuthWebhook;\n // @ts-expect-error use WpAuth([apiKey(...)]) and WpAuthorization\n void CoreUtil.WpAuthApiKey;\n // @ts-expect-error use separate WpLocalOnly, authentication and authorization\n void CoreUtil.WpAuthLocalOnly;\n // @ts-expect-error MCP shares receiving endpoint WpAuthorization\n void CoreUtil.WpMcpAuthJwt;\n // @ts-expect-error unresolved credentials are read with getAuthMeta\n void CoreUtil.getAuthMode;\n // @ts-expect-error roles belong to common operation authorization\n void CoreUtil.rolesRequired;\n }\n}\n"]}
1
+ {"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAAA,2DAAqC;AAErC,4FAA4F;AAC5F,MAAa,4BAA4B;IACrC,cAAc;QACV,2DAA2D;QAC3D,KAAK,QAAQ,CAAC,SAAS,CAAC;QACxB,oGAAoG;QACpG,KAAK,QAAQ,CAAC,UAAU,CAAC;QACzB,4HAA4H;QAC5H,KAAK,QAAQ,CAAC,IAAI,CAAC;QACnB,uEAAuE;QACvE,KAAK,QAAQ,CAAC,kBAAkB,CAAC;QACjC,qEAAqE;QACrE,KAAK,QAAQ,CAAC,aAAa,CAAC;QAC5B,+GAA+G;QAC/G,KAAK,QAAQ,CAAC,OAAO,CAAC;QACtB,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,8EAA8E;QAC9E,KAAK,QAAQ,CAAC,eAAe,CAAC;QAC9B,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,oEAAoE;QACpE,KAAK,QAAQ,CAAC,WAAW,CAAC;QAC1B,kEAAkE;QAClE,KAAK,QAAQ,CAAC,aAAa,CAAC;IAChC,CAAC;CACJ;AAzBD,oEAyBC","sourcesContent":["import * as CoreUtil from '../index';\n\n/** Each removed spelling has its own assertion so one missing export cannot mask a shim. */\nexport class DeletedAuthCompileAssertions {\n removedExports(): void {\n // @ts-expect-error use WpAuth([jwt()]) and WpAuthorization\n void CoreUtil.WpAuthJwt;\n // @ts-expect-error use WpAuth([oidcSameProject()]) or WpAuth([oidcSvcs([...])]) and WpAuthorization\n void CoreUtil.WpAuthOidc;\n // @ts-expect-error oidc(...) is removed: oidcSameProject() for this GCP project, oidcSvcs([...]) for named service accounts\n void CoreUtil.oidc;\n // @ts-expect-error use WpAuth([sharedSecret(...)]) and WpAuthorization\n void CoreUtil.WpAuthSharedSecret;\n // @ts-expect-error use WpAuth([vendorAuth(...)]) and WpAuthorization\n void CoreUtil.WpAuthWebhook;\n // @ts-expect-error webhook(name) is renamed vendorAuth(name): one VendorAuthHook mints, refreshes and verifies\n void CoreUtil.webhook;\n // @ts-expect-error use WpAuth([apiKey(...)]) and WpAuthorization\n void CoreUtil.WpAuthApiKey;\n // @ts-expect-error use separate WpLocalOnly, authentication and authorization\n void CoreUtil.WpAuthLocalOnly;\n // @ts-expect-error MCP shares receiving endpoint WpAuthorization\n void CoreUtil.WpMcpAuthJwt;\n // @ts-expect-error unresolved credentials are read with getAuthMeta\n void CoreUtil.getAuthMode;\n // @ts-expect-error roles belong to common operation authorization\n void CoreUtil.rolesRequired;\n }\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { AnyContextKey } from '../ContextKey';
2
2
  import { AuthMode } from './auth-mode';
3
+ import { RouteMetadata } from './RouteMetadata';
3
4
  /**
4
5
  * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key
5
6
  * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?
@@ -32,6 +33,12 @@ import { AuthMode } from './auth-mode';
32
33
  */
33
34
  export declare class DestinationTrust {
34
35
  private readonly verifiesCaller;
36
+ /**
37
+ * Set ONLY for an `EXTERNAL_OUTBOUND` destination: the `@ExternalContextKeys` list, which is
38
+ * then the WHOLE of what may be sent. Undefined for every destination in this repo, whose wire
39
+ * is governed by {@link forAuthMode} exactly as before.
40
+ */
41
+ private readonly externalKeys;
35
42
  /**
36
43
  * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to
37
44
  * believe context WE vouch for — this is the service-to-service identity propagation that trusted
@@ -39,12 +46,26 @@ export declare class DestinationTrust {
39
46
  */
40
47
  private static readonly VERIFIES_CALLER;
41
48
  /**
42
- * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / webhook(...) /
49
+ * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / vendorAuth(...) /
43
50
  * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.
44
51
  * Untrusted keys still travel.
45
52
  */
46
53
  private static readonly CANNOT_VERIFY_CALLER;
47
54
  private constructor();
55
+ /**
56
+ * The destination of ONE route. An `EXTERNAL_OUTBOUND` route (an outside system's API) gets a
57
+ * destination that sends ONLY its `@ExternalContextKeys` and believes NO response header: a vendor
58
+ * is somebody else's process, so our `x-request-id`, `x-webpieces-*` and app keys stay home unless
59
+ * the contract lists them, and nothing it answers may write into our context. Every other route is
60
+ * {@link forAuthMode}, unchanged — so the wire to our own servers is identical.
61
+ */
62
+ static forRoute(route: RouteMetadata): DestinationTrust;
63
+ /**
64
+ * The keys an `EXTERNAL_OUTBOUND` destination sends — every one is REQUIRED, so a builder throws
65
+ * before any I/O when one has no value. Undefined for every other destination, whose builder
66
+ * iterates the registry's transferred keys and filters them with {@link allows}.
67
+ */
68
+ listedKeys(): readonly AnyContextKey[] | undefined;
48
69
  /**
49
70
  * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an
50
71
  * endpoint that declared no mode) is treated as un-verifying, i.e. the SAFE answer — an absent
@@ -65,9 +86,17 @@ export declare class DestinationTrust {
65
86
  * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is
66
87
  * somebody else's process whose response headers are an assertion, not a proof.
67
88
  *
68
- * A second, identically-bodied `believesResponsesFrom(key)` would be two spellings of one
69
- * decision — the shim shape `.claude/rules/no-backwards-compat.md` rejects — so the direction
70
- * lives in the CALLER's name and the rule lives here, once.
89
+ * For every destination in this repo the two directions are one question, so the response side
90
+ * ({@link believesResponse}) delegates here; only an `EXTERNAL_OUTBOUND` destination answers them
91
+ * differently (it sends listed keys and believes none).
71
92
  */
72
93
  allows(key: AnyContextKey): boolean;
94
+ /**
95
+ * May a key that came BACK on this destination's response be merged into our context? Exactly
96
+ * {@link allows} for a destination in this repo — and NOTHING from an `EXTERNAL_OUTBOUND` one,
97
+ * whose response headers are an outside system's, not a peer's. A separate method because for
98
+ * that destination the two directions genuinely differ: a key we LIST to send (`x-request-id`)
99
+ * is not thereby a key we let the vendor write back.
100
+ */
101
+ believesResponse(key: AnyContextKey): boolean;
73
102
  }
@@ -33,20 +33,50 @@ exports.DestinationTrust = void 0;
33
33
  */
34
34
  class DestinationTrust {
35
35
  verifiesCaller;
36
+ externalKeys;
36
37
  /**
37
38
  * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to
38
39
  * believe context WE vouch for — this is the service-to-service identity propagation that trusted
39
40
  * keys keep an `httpHeader` for.
40
41
  */
41
- static VERIFIES_CALLER = new DestinationTrust(true);
42
+ static VERIFIES_CALLER = new DestinationTrust(true, undefined);
42
43
  /**
43
- * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / webhook(...) /
44
+ * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / vendorAuth(...) /
44
45
  * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.
45
46
  * Untrusted keys still travel.
46
47
  */
47
- static CANNOT_VERIFY_CALLER = new DestinationTrust(false);
48
- constructor(verifiesCaller) {
48
+ static CANNOT_VERIFY_CALLER = new DestinationTrust(false, undefined);
49
+ constructor(verifiesCaller,
50
+ /**
51
+ * Set ONLY for an `EXTERNAL_OUTBOUND` destination: the `@ExternalContextKeys` list, which is
52
+ * then the WHOLE of what may be sent. Undefined for every destination in this repo, whose wire
53
+ * is governed by {@link forAuthMode} exactly as before.
54
+ */
55
+ externalKeys) {
49
56
  this.verifiesCaller = verifiesCaller;
57
+ this.externalKeys = externalKeys;
58
+ }
59
+ /**
60
+ * The destination of ONE route. An `EXTERNAL_OUTBOUND` route (an outside system's API) gets a
61
+ * destination that sends ONLY its `@ExternalContextKeys` and believes NO response header: a vendor
62
+ * is somebody else's process, so our `x-request-id`, `x-webpieces-*` and app keys stay home unless
63
+ * the contract lists them, and nothing it answers may write into our context. Every other route is
64
+ * {@link forAuthMode}, unchanged — so the wire to our own servers is identical.
65
+ */
66
+ // webpieces-disable no-function-outside-class -- static factory beside forAuthMode, for the same reason
67
+ static forRoute(route) {
68
+ if (route.externalOutbound) {
69
+ return new DestinationTrust(false, route.externalContextKeys);
70
+ }
71
+ return DestinationTrust.forAuthMode(route.authMeta?.methods[0]);
72
+ }
73
+ /**
74
+ * The keys an `EXTERNAL_OUTBOUND` destination sends — every one is REQUIRED, so a builder throws
75
+ * before any I/O when one has no value. Undefined for every other destination, whose builder
76
+ * iterates the registry's transferred keys and filters them with {@link allows}.
77
+ */
78
+ listedKeys() {
79
+ return this.externalKeys;
50
80
  }
51
81
  /**
52
82
  * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an
@@ -64,12 +94,11 @@ class DestinationTrust {
64
94
  return DestinationTrust.VERIFIES_CALLER;
65
95
  case 'jwt':
66
96
  case 'public':
67
- // webhook(...) authenticates an OUTSIDE VENDOR, which is not the same thing as
97
+ // vendorAuth(...) authenticates an OUTSIDE VENDOR, which is not the same thing as
68
98
  // authenticating a peer in this repo. The vendor knows nothing of webpieces context
69
99
  // headers and would never send one, so there is no identity to propagate in either
70
- // direction — and a webpieces client cannot call such an endpoint anyway (it cannot mint
71
- // the vendor's signature). Trusted keys stay home.
72
- case 'webhook':
100
+ // direction. Trusted keys stay home.
101
+ case 'vendorAuth':
73
102
  // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is
74
103
  // another company's codebase, so nothing it forwards may be believed, and no webpieces
75
104
  // client can call it anyway (the framework extracts no api-key header — the app's hook
@@ -97,13 +126,27 @@ class DestinationTrust {
97
126
  * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is
98
127
  * somebody else's process whose response headers are an assertion, not a proof.
99
128
  *
100
- * A second, identically-bodied `believesResponsesFrom(key)` would be two spellings of one
101
- * decision — the shim shape `.claude/rules/no-backwards-compat.md` rejects — so the direction
102
- * lives in the CALLER's name and the rule lives here, once.
129
+ * For every destination in this repo the two directions are one question, so the response side
130
+ * ({@link believesResponse}) delegates here; only an `EXTERNAL_OUTBOUND` destination answers them
131
+ * differently (it sends listed keys and believes none).
103
132
  */
104
133
  allows(key) {
134
+ if (this.externalKeys !== undefined)
135
+ return this.externalKeys.includes(key);
105
136
  return this.verifiesCaller || !key.isTrusted();
106
137
  }
138
+ /**
139
+ * May a key that came BACK on this destination's response be merged into our context? Exactly
140
+ * {@link allows} for a destination in this repo — and NOTHING from an `EXTERNAL_OUTBOUND` one,
141
+ * whose response headers are an outside system's, not a peer's. A separate method because for
142
+ * that destination the two directions genuinely differ: a key we LIST to send (`x-request-id`)
143
+ * is not thereby a key we let the vendor write back.
144
+ */
145
+ believesResponse(key) {
146
+ if (this.externalKeys !== undefined)
147
+ return false;
148
+ return this.allows(key);
149
+ }
107
150
  }
108
151
  exports.DestinationTrust = DestinationTrust;
109
152
  //# sourceMappingURL=DestinationTrust.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,gBAAgB;IAeY;IAdrC;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErE;;;;OAIG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAE3E,YAAqC,cAAuB;QAAvB,mBAAc,GAAd,cAAc,CAAS;IAAG,CAAC;IAEhE;;;;OAIG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,+EAA+E;YAC/E,oFAAoF;YACpF,mFAAmF;YACnF,yFAAyF;YACzF,mDAAmD;YACnD,KAAK,SAAS,CAAC;YACf,qFAAqF;YACrF,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,iCAAiC;YACjC,KAAK,QAAQ;gBACb,4EAA4E;gBAC5E,uFAAuF;gBACvF,wFAAwF;gBACxF,iFAAiF;gBAC7E,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAkB;QACrB,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;;AAzEL,4CA0EC","sourcesContent":["import { AnyContextKey } from '../ContextKey';\nimport { AuthMode } from './auth-mode';\n\n/**\n * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key\n * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?\n *\n * ## Why the client has to answer this at all\n *\n * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a\n * route that verified WHO called it — `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`\n * route the same header must match what the authenticator independently derived, or the request is\n * REJECTED with a 401.\n *\n * That rule is correct, and it means a client that ships `x-user-id` to a `@WpAuthPublic` endpoint is\n * building a request the callee is obliged to reject. Before this class the outbound builders\n * forwarded EVERY transferred key with no idea what the destination was, so an internal service\n * calling another service's public or JWT endpoint 401'd itself. The fix belongs on the producing\n * side: don't send what cannot possibly be believed.\n *\n * ## Why it is a class with a private constructor and no boolean parameter\n *\n * `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the\n * \"widening that is an ABSENCE rather than a token\" `.claude/rules/no-backwards-compat.md` rejects — the\n * permissive answer would be\n * what you get by not typing anything. There is no way to build a DestinationTrust except from the\n * destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does\n * not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is\n * asked. The two instances are PRIVATE for the same reason: exposing them would be a second spelling\n * that skips the derivation.\n *\n * Per CLAUDE.md: data-only structure, so a class rather than an interface or a bare boolean.\n */\nexport class DestinationTrust {\n /**\n * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to\n * believe context WE vouch for — this is the service-to-service identity propagation that trusted\n * keys keep an `httpHeader` for.\n */\n private static readonly VERIFIES_CALLER = new DestinationTrust(true);\n\n /**\n * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / webhook(...) /\n * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.\n * Untrusted keys still travel.\n */\n private static readonly CANNOT_VERIFY_CALLER = new DestinationTrust(false);\n\n private constructor(private readonly verifiesCaller: boolean) {}\n\n /**\n * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an\n * endpoint that declared no mode) is treated as un-verifying, i.e. the SAFE answer — an absent\n * declaration must never be the widest one.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor, exactly as ContextKey.trusted/untrusted do: the destination's auth mode must be part of the CALL, and a DI-injected instance method would let a caller hold one without ever naming a route\n static forAuthMode(mode: AuthMode | undefined): DestinationTrust {\n if (mode === undefined) {\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return DestinationTrust.VERIFIES_CALLER;\n case 'jwt':\n case 'public':\n // webhook(...) authenticates an OUTSIDE VENDOR, which is not the same thing as\n // authenticating a peer in this repo. The vendor knows nothing of webpieces context\n // headers and would never send one, so there is no identity to propagate in either\n // direction — and a webpieces client cannot call such an endpoint anyway (it cannot mint\n // the vendor's signature). Trusted keys stay home.\n case 'webhook':\n // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is\n // another company's codebase, so nothing it forwards may be believed, and no webpieces\n // client can call it anyway (the framework extracts no api-key header — the app's hook\n // owns which headers carry the credential; the contract's `credentials` list only DESCRIBES\n // them). Trusted keys stay home.\n case 'apikey':\n // @WpLocalOnly authenticates NOBODY — it gates on the environment, not on a\n // credential — so a browser with curl on the same laptop is indistinguishable from us.\n // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to\n // AuthMode is a compile error here rather than a silent permissive fallthrough.)\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n }\n\n /**\n * May this key cross the boundary between us and this destination? Untrusted keys always may —\n * nobody was ever going to make a security decision on them. A trusted key may only when we and\n * the destination authenticate each other.\n *\n * ONE method answers BOTH directions, deliberately, because it is one question:\n *\n * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination\n * will authenticate US, because that is the only case its `AuthFilter` will admit it.\n * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a\n * destination this client authenticated to — an `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` peer is a\n * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is\n * somebody else's process whose response headers are an assertion, not a proof.\n *\n * A second, identically-bodied `believesResponsesFrom(key)` would be two spellings of one\n * decision — the shim shape `.claude/rules/no-backwards-compat.md` rejects — so the direction\n * lives in the CALLER's name and the rule lives here, once.\n */\n allows(key: AnyContextKey): boolean {\n return this.verifiesCaller || !key.isTrusted();\n }\n}\n"]}
1
+ {"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,gBAAgB;IAgBJ;IAMA;IArBrB;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAEhF;;;;OAIG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAEtF,YACqB,cAAuB;IACxC;;;;OAIG;IACc,YAAkD;QANlD,mBAAc,GAAd,cAAc,CAAS;QAMvB,iBAAY,GAAZ,YAAY,CAAsC;IACpE,CAAC;IAEJ;;;;;;OAMG;IACH,wGAAwG;IACxG,MAAM,CAAC,QAAQ,CAAC,KAAoB;QAChC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;YACzB,OAAO,IAAI,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAED;;;;OAIG;IACH,UAAU;QACN,OAAO,IAAI,CAAC,YAAY,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,kFAAkF;YAClF,oFAAoF;YACpF,mFAAmF;YACnF,qCAAqC;YACrC,KAAK,YAAY,CAAC;YAClB,qFAAqF;YACrF,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,iCAAiC;YACjC,KAAK,QAAQ;gBACb,4EAA4E;gBAC5E,uFAAuF;gBACvF,wFAAwF;gBACxF,iFAAiF;gBAC7E,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAkB;QACrB,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5E,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;IAED;;;;;;OAMG;IACH,gBAAgB,CAAC,GAAkB;QAC/B,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAClD,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;;AArHL,4CAsHC","sourcesContent":["import { AnyContextKey } from '../ContextKey';\nimport { AuthMode } from './auth-mode';\nimport { RouteMetadata } from './RouteMetadata';\n\n/**\n * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key\n * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?\n *\n * ## Why the client has to answer this at all\n *\n * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a\n * route that verified WHO called it — `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`\n * route the same header must match what the authenticator independently derived, or the request is\n * REJECTED with a 401.\n *\n * That rule is correct, and it means a client that ships `x-user-id` to a `@WpAuthPublic` endpoint is\n * building a request the callee is obliged to reject. Before this class the outbound builders\n * forwarded EVERY transferred key with no idea what the destination was, so an internal service\n * calling another service's public or JWT endpoint 401'd itself. The fix belongs on the producing\n * side: don't send what cannot possibly be believed.\n *\n * ## Why it is a class with a private constructor and no boolean parameter\n *\n * `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the\n * \"widening that is an ABSENCE rather than a token\" `.claude/rules/no-backwards-compat.md` rejects — the\n * permissive answer would be\n * what you get by not typing anything. There is no way to build a DestinationTrust except from the\n * destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does\n * not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is\n * asked. The two instances are PRIVATE for the same reason: exposing them would be a second spelling\n * that skips the derivation.\n *\n * Per CLAUDE.md: data-only structure, so a class rather than an interface or a bare boolean.\n */\nexport class DestinationTrust {\n /**\n * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to\n * believe context WE vouch for — this is the service-to-service identity propagation that trusted\n * keys keep an `httpHeader` for.\n */\n private static readonly VERIFIES_CALLER = new DestinationTrust(true, undefined);\n\n /**\n * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / vendorAuth(...) /\n * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.\n * Untrusted keys still travel.\n */\n private static readonly CANNOT_VERIFY_CALLER = new DestinationTrust(false, undefined);\n\n private constructor(\n private readonly verifiesCaller: boolean,\n /**\n * Set ONLY for an `EXTERNAL_OUTBOUND` destination: the `@ExternalContextKeys` list, which is\n * then the WHOLE of what may be sent. Undefined for every destination in this repo, whose wire\n * is governed by {@link forAuthMode} exactly as before.\n */\n private readonly externalKeys: readonly AnyContextKey[] | undefined,\n ) {}\n\n /**\n * The destination of ONE route. An `EXTERNAL_OUTBOUND` route (an outside system's API) gets a\n * destination that sends ONLY its `@ExternalContextKeys` and believes NO response header: a vendor\n * is somebody else's process, so our `x-request-id`, `x-webpieces-*` and app keys stay home unless\n * the contract lists them, and nothing it answers may write into our context. Every other route is\n * {@link forAuthMode}, unchanged — so the wire to our own servers is identical.\n */\n // webpieces-disable no-function-outside-class -- static factory beside forAuthMode, for the same reason\n static forRoute(route: RouteMetadata): DestinationTrust {\n if (route.externalOutbound) {\n return new DestinationTrust(false, route.externalContextKeys);\n }\n return DestinationTrust.forAuthMode(route.authMeta?.methods[0]);\n }\n\n /**\n * The keys an `EXTERNAL_OUTBOUND` destination sends — every one is REQUIRED, so a builder throws\n * before any I/O when one has no value. Undefined for every other destination, whose builder\n * iterates the registry's transferred keys and filters them with {@link allows}.\n */\n listedKeys(): readonly AnyContextKey[] | undefined {\n return this.externalKeys;\n }\n\n /**\n * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an\n * endpoint that declared no mode) is treated as un-verifying, i.e. the SAFE answer — an absent\n * declaration must never be the widest one.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor, exactly as ContextKey.trusted/untrusted do: the destination's auth mode must be part of the CALL, and a DI-injected instance method would let a caller hold one without ever naming a route\n static forAuthMode(mode: AuthMode | undefined): DestinationTrust {\n if (mode === undefined) {\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return DestinationTrust.VERIFIES_CALLER;\n case 'jwt':\n case 'public':\n // vendorAuth(...) authenticates an OUTSIDE VENDOR, which is not the same thing as\n // authenticating a peer in this repo. The vendor knows nothing of webpieces context\n // headers and would never send one, so there is no identity to propagate in either\n // direction. Trusted keys stay home.\n case 'vendorAuth':\n // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is\n // another company's codebase, so nothing it forwards may be believed, and no webpieces\n // client can call it anyway (the framework extracts no api-key header — the app's hook\n // owns which headers carry the credential; the contract's `credentials` list only DESCRIBES\n // them). Trusted keys stay home.\n case 'apikey':\n // @WpLocalOnly authenticates NOBODY — it gates on the environment, not on a\n // credential — so a browser with curl on the same laptop is indistinguishable from us.\n // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to\n // AuthMode is a compile error here rather than a silent permissive fallthrough.)\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n }\n\n /**\n * May this key cross the boundary between us and this destination? Untrusted keys always may —\n * nobody was ever going to make a security decision on them. A trusted key may only when we and\n * the destination authenticate each other.\n *\n * ONE method answers BOTH directions, deliberately, because it is one question:\n *\n * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination\n * will authenticate US, because that is the only case its `AuthFilter` will admit it.\n * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a\n * destination this client authenticated to — an `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` peer is a\n * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is\n * somebody else's process whose response headers are an assertion, not a proof.\n *\n * For every destination in this repo the two directions are one question, so the response side\n * ({@link believesResponse}) delegates here; only an `EXTERNAL_OUTBOUND` destination answers them\n * differently (it sends listed keys and believes none).\n */\n allows(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return this.externalKeys.includes(key);\n return this.verifiesCaller || !key.isTrusted();\n }\n\n /**\n * May a key that came BACK on this destination's response be merged into our context? Exactly\n * {@link allows} for a destination in this repo — and NOTHING from an `EXTERNAL_OUTBOUND` one,\n * whose response headers are an outside system's, not a peer's. A separate method because for\n * that destination the two directions genuinely differ: a key we LIST to send (`x-request-id`)\n * is not thereby a key we let the vendor write back.\n */\n believesResponse(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return false;\n return this.allows(key);\n }\n}\n"]}
@@ -2,13 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const decorators_1 = require("./decorators");
4
4
  (0, decorators_1.Endpoint)(decorators_1.POST, '/valid-rpc', decorators_1.WRITE, decorators_1.RPC);
5
- (0, decorators_1.Endpoint)(decorators_1.POST, '/valid-external', decorators_1.WRITE, decorators_1.EXTERNAL, { calledBy: 'twilio' });
6
- // @ts-expect-error external endpoints require options containing calledBy
7
- (0, decorators_1.Endpoint)(decorators_1.POST, '/missing-external-options', decorators_1.WRITE, decorators_1.EXTERNAL);
8
- // @ts-expect-error external endpoint options require calledBy
9
- (0, decorators_1.Endpoint)(decorators_1.POST, '/missing-external-caller', decorators_1.WRITE, decorators_1.EXTERNAL, { formPost: true });
5
+ (0, decorators_1.Endpoint)(decorators_1.PUT, '/valid-put', decorators_1.WRITE_IDEMPOTENT, decorators_1.RPC);
6
+ (0, decorators_1.Endpoint)(decorators_1.PATCH, '/valid-patch', decorators_1.WRITE, decorators_1.RPC);
7
+ (0, decorators_1.Endpoint)(decorators_1.DELETE, '/valid-delete', decorators_1.WRITE_IDEMPOTENT, decorators_1.RPC);
8
+ (0, decorators_1.Endpoint)(decorators_1.POST, '/valid-external', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND, { calledBy: 'twilio' });
9
+ (0, decorators_1.Endpoint)(decorators_1.GET, '/valid-outbound', decorators_1.READ, decorators_1.EXTERNAL_OUTBOUND);
10
+ // @ts-expect-error inbound external endpoints require options containing calledBy
11
+ (0, decorators_1.Endpoint)(decorators_1.POST, '/missing-external-options', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND);
12
+ // @ts-expect-error inbound external endpoint options require calledBy
13
+ (0, decorators_1.Endpoint)(decorators_1.POST, '/missing-external-caller', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND, { formPost: true });
10
14
  // @ts-expect-error callerKind is a closed enum-backed union rather than free text
11
- (0, decorators_1.Endpoint)(decorators_1.POST, '/invalid-caller-kind', decorators_1.WRITE, decorators_1.EXTERNAL, {
15
+ (0, decorators_1.Endpoint)(decorators_1.POST, '/invalid-caller-kind', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND, {
12
16
  calledBy: 'twilio',
13
17
  callerKind: 'vendor',
14
18
  });
@@ -18,4 +22,6 @@ const decorators_1 = require("./decorators");
18
22
  (0, decorators_1.Endpoint)('POST', '/raw-method', decorators_1.WRITE, decorators_1.RPC);
19
23
  // @ts-expect-error callers must import the enum-backed trigger-kind constant
20
24
  (0, decorators_1.Endpoint)(decorators_1.POST, '/raw-trigger', decorators_1.WRITE, 'rpc');
25
+ // @ts-expect-error callers must import the enum-backed HTTP method constant, for the new verbs too
26
+ (0, decorators_1.Endpoint)('PUT', '/raw-put', decorators_1.WRITE, decorators_1.RPC);
21
27
  //# sourceMappingURL=EndpointCompileAssertions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EndpointCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/EndpointCompileAssertions.ts"],"names":[],"mappings":";;AAAA,6CAAoE;AAEpE,IAAA,qBAAQ,EAAC,iBAAI,EAAE,YAAY,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AACzC,IAAA,qBAAQ,EAAC,iBAAI,EAAE,iBAAiB,EAAE,kBAAK,EAAE,qBAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;AAE3E,0EAA0E;AAC1E,IAAA,qBAAQ,EAAC,iBAAI,EAAE,2BAA2B,EAAE,kBAAK,EAAE,qBAAQ,CAAC,CAAC;AAE7D,8DAA8D;AAC9D,IAAA,qBAAQ,EAAC,iBAAI,EAAE,0BAA0B,EAAE,kBAAK,EAAE,qBAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAEhF,kFAAkF;AAClF,IAAA,qBAAQ,EAAC,iBAAI,EAAE,sBAAsB,EAAE,kBAAK,EAAE,qBAAQ,EAAE;IACpD,QAAQ,EAAE,QAAQ;IAClB,UAAU,EAAE,QAAQ;CACvB,CAAC,CAAC;AAEH,0EAA0E;AAC1E,IAAA,qBAAQ,EAAC,iBAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAG,CAAC,CAAC;AAE/C,4EAA4E;AAC5E,IAAA,qBAAQ,EAAC,MAAM,EAAE,aAAa,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AAE5C,6EAA6E;AAC7E,IAAA,qBAAQ,EAAC,iBAAI,EAAE,cAAc,EAAE,kBAAK,EAAE,KAAK,CAAC,CAAC","sourcesContent":["import { Endpoint, EXTERNAL, POST, RPC, WRITE } from './decorators';\n\nEndpoint(POST, '/valid-rpc', WRITE, RPC);\nEndpoint(POST, '/valid-external', WRITE, EXTERNAL, { calledBy: 'twilio' });\n\n// @ts-expect-error external endpoints require options containing calledBy\nEndpoint(POST, '/missing-external-options', WRITE, EXTERNAL);\n\n// @ts-expect-error external endpoint options require calledBy\nEndpoint(POST, '/missing-external-caller', WRITE, EXTERNAL, { formPost: true });\n\n// @ts-expect-error callerKind is a closed enum-backed union rather than free text\nEndpoint(POST, '/invalid-caller-kind', WRITE, EXTERNAL, {\n calledBy: 'twilio',\n callerKind: 'vendor',\n});\n\n// @ts-expect-error callers must import the enum-backed operation constant\nEndpoint(POST, '/raw-operation', 'write', RPC);\n\n// @ts-expect-error callers must import the enum-backed HTTP method constant\nEndpoint('POST', '/raw-method', WRITE, RPC);\n\n// @ts-expect-error callers must import the enum-backed trigger-kind constant\nEndpoint(POST, '/raw-trigger', WRITE, 'rpc');\n"]}
1
+ {"version":3,"file":"EndpointCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/EndpointCompileAssertions.ts"],"names":[],"mappings":";;AAAA,6CAAgJ;AAEhJ,IAAA,qBAAQ,EAAC,iBAAI,EAAE,YAAY,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AACzC,IAAA,qBAAQ,EAAC,gBAAG,EAAE,YAAY,EAAE,6BAAgB,EAAE,gBAAG,CAAC,CAAC;AACnD,IAAA,qBAAQ,EAAC,kBAAK,EAAE,cAAc,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AAC5C,IAAA,qBAAQ,EAAC,mBAAM,EAAE,eAAe,EAAE,6BAAgB,EAAE,gBAAG,CAAC,CAAC;AACzD,IAAA,qBAAQ,EAAC,iBAAI,EAAE,iBAAiB,EAAE,kBAAK,EAAE,6BAAgB,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;AACnF,IAAA,qBAAQ,EAAC,gBAAG,EAAE,iBAAiB,EAAE,iBAAI,EAAE,8BAAiB,CAAC,CAAC;AAE1D,kFAAkF;AAClF,IAAA,qBAAQ,EAAC,iBAAI,EAAE,2BAA2B,EAAE,kBAAK,EAAE,6BAAgB,CAAC,CAAC;AAErE,sEAAsE;AACtE,IAAA,qBAAQ,EAAC,iBAAI,EAAE,0BAA0B,EAAE,kBAAK,EAAE,6BAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAExF,kFAAkF;AAClF,IAAA,qBAAQ,EAAC,iBAAI,EAAE,sBAAsB,EAAE,kBAAK,EAAE,6BAAgB,EAAE;IAC5D,QAAQ,EAAE,QAAQ;IAClB,UAAU,EAAE,QAAQ;CACvB,CAAC,CAAC;AAEH,0EAA0E;AAC1E,IAAA,qBAAQ,EAAC,iBAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAG,CAAC,CAAC;AAE/C,4EAA4E;AAC5E,IAAA,qBAAQ,EAAC,MAAM,EAAE,aAAa,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AAE5C,6EAA6E;AAC7E,IAAA,qBAAQ,EAAC,iBAAI,EAAE,cAAc,EAAE,kBAAK,EAAE,KAAK,CAAC,CAAC;AAE7C,mGAAmG;AACnG,IAAA,qBAAQ,EAAC,KAAK,EAAE,UAAU,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC","sourcesContent":["import { DELETE, GET, READ, Endpoint, EXTERNAL_INBOUND, EXTERNAL_OUTBOUND, PATCH, POST, PUT, RPC, WRITE, WRITE_IDEMPOTENT } from './decorators';\n\nEndpoint(POST, '/valid-rpc', WRITE, RPC);\nEndpoint(PUT, '/valid-put', WRITE_IDEMPOTENT, RPC);\nEndpoint(PATCH, '/valid-patch', WRITE, RPC);\nEndpoint(DELETE, '/valid-delete', WRITE_IDEMPOTENT, RPC);\nEndpoint(POST, '/valid-external', WRITE, EXTERNAL_INBOUND, { calledBy: 'twilio' });\nEndpoint(GET, '/valid-outbound', READ, EXTERNAL_OUTBOUND);\n\n// @ts-expect-error inbound external endpoints require options containing calledBy\nEndpoint(POST, '/missing-external-options', WRITE, EXTERNAL_INBOUND);\n\n// @ts-expect-error inbound external endpoint options require calledBy\nEndpoint(POST, '/missing-external-caller', WRITE, EXTERNAL_INBOUND, { formPost: true });\n\n// @ts-expect-error callerKind is a closed enum-backed union rather than free text\nEndpoint(POST, '/invalid-caller-kind', WRITE, EXTERNAL_INBOUND, {\n calledBy: 'twilio',\n callerKind: 'vendor',\n});\n\n// @ts-expect-error callers must import the enum-backed operation constant\nEndpoint(POST, '/raw-operation', 'write', RPC);\n\n// @ts-expect-error callers must import the enum-backed HTTP method constant\nEndpoint('POST', '/raw-method', WRITE, RPC);\n\n// @ts-expect-error callers must import the enum-backed trigger-kind constant\nEndpoint(POST, '/raw-trigger', WRITE, 'rpc');\n\n// @ts-expect-error callers must import the enum-backed HTTP method constant, for the new verbs too\nEndpoint('PUT', '/raw-put', WRITE, RPC);\n"]}
@@ -26,39 +26,32 @@ export declare enum CallOutcome {
26
26
  * rather than a bare function — so "find usages" in an IDE lands on every implementation.
27
27
  *
28
28
  * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once
29
- * singleton that owns URL mappings and the {@link ErrorTranslator}. Resolved most-specific first by
30
- * {@link ClientRegistry.classify}:
29
+ * singleton that owns URL mappings and the {@link ErrorTranslator} — with
30
+ * `ClientRegistry.setErrorClassifier(c, ...scope)`, on the same `CallScope` ladder as
31
+ * `CallRegistry`'s strategy and timeout. {@link ClientRegistry.classify} resolves it most specific
32
+ * first:
31
33
  *
32
- * 1. `ClientRegistry.addErrorClassifier(new KeyedErrorClassifier(apiClass, c))` — per EXTERNAL
33
- * client, keyed by {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi',
34
- * 'TwilioApi'). Each external API qualifies its errors differently (a Firestore 404 miss is
35
- * EXPECTED; a Twilio 429 is TRANSIENT), so it overrides the tiers below for that apiClass only.
36
- * 2. `ClientRegistry.setDefaultErrorClassifier(c)` — ONE per app/company. It reads
37
- * {@link ApiMethodInfo.side}, so a single classifier covers the SERVER router AND every INTERNAL
38
- * client (webpieces http client, cloud tasks).
39
- * 3. {@link WebpiecesDefaultErrorClassifier} — the terminal tier, which never defers.
34
+ * 1. `setErrorClassifier(c, XxxApi, 'method')` — one method of one contract.
35
+ * 2. `setErrorClassifier(c, XxxApi)` — one contract, keyed by its CLASS (never a name). An
36
+ * SDK-wrapping api-client keys by its abstract `XxxApi`: each external API qualifies its errors
37
+ * differently (a document store's 404 miss is EXPECTED; a messaging vendor's 429 is TRANSIENT),
38
+ * so it overrides the rungs below for that contract only.
39
+ * 3. `setErrorClassifier(c, 'ALL')` — ONE per app/company. It reads {@link ApiMethodInfo.side}, so a
40
+ * single classifier covers the SERVER router AND every INTERNAL client (webpieces http client,
41
+ * cloud tasks).
42
+ * 4. {@link WebpiecesDefaultErrorClassifier} — the terminal rung, which never defers.
40
43
  *
41
- * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal.
42
- * An external client that forgets to register is FAIL-SAFE: it falls to the tiers below, where every
43
- * client-side error except a 266 is a failure.
44
+ * Internal clients and the server register NOTHING per contract — the ABSENCE of a contract rung is
45
+ * the signal. An external client that forgets to register is FAIL-SAFE: it falls to the rungs below,
46
+ * where every client-side error except a 266 is a failure.
44
47
  */
45
48
  export interface ErrorClassifier {
46
49
  /**
47
50
  * @param error - the already-normalized thrown error (callers pass `toError(err)`)
48
- * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client
49
- * @returns the outcome, or `undefined` to DEFER to the next tier (a per-apiClass classifier defers
50
- * to the app default; the app default defers to the webpieces built-in). Deferring is what
51
- * makes classifiers additive AND override-capable.
51
+ * @param methodInfo - the call identity; `side` distinguishes server/client, `contract` the API
52
+ * @returns the outcome, or `undefined` to DEFER to the next rung (method defers to API, API to
53
+ * ALL, ALL to the webpieces built-in). Deferring is what makes classifiers additive AND
54
+ * override-capable.
52
55
  */
53
56
  classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome | undefined;
54
57
  }
55
- /**
56
- * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
57
- * {@link ErrorClassifier} to apply for it. Data only, so it is a class (not an inline object literal),
58
- * per CLAUDE.md — handed to {@link ClientRegistry.addErrorClassifier}.
59
- */
60
- export declare class KeyedErrorClassifier {
61
- readonly apiClass: string;
62
- readonly classifier: ErrorClassifier;
63
- constructor(apiClass: string, classifier: ErrorClassifier);
64
- }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KeyedErrorClassifier = exports.CallOutcome = void 0;
3
+ exports.CallOutcome = void 0;
4
4
  /**
5
5
  * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table
6
6
  * answering "is this error expected, and is it worth repeating?":
@@ -22,18 +22,4 @@ var CallOutcome;
22
22
  /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */
23
23
  CallOutcome["FAILURE"] = "failure";
24
24
  })(CallOutcome || (exports.CallOutcome = CallOutcome = {}));
25
- /**
26
- * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
27
- * {@link ErrorClassifier} to apply for it. Data only, so it is a class (not an inline object literal),
28
- * per CLAUDE.md — handed to {@link ClientRegistry.addErrorClassifier}.
29
- */
30
- class KeyedErrorClassifier {
31
- apiClass;
32
- classifier;
33
- constructor(apiClass, classifier) {
34
- this.apiClass = apiClass;
35
- this.classifier = classifier;
36
- }
37
- }
38
- exports.KeyedErrorClassifier = KeyedErrorClassifier;
39
25
  //# sourceMappingURL=ErrorClassifier.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorClassifier.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;GAWG;AACH,IAAY,WAOX;AAPD,WAAY,WAAW;IACnB,qGAAqG;IACrG,oCAAqB,CAAA;IACrB,qGAAqG;IACrG,8CAA+B,CAAA;IAC/B,6GAA6G;IAC7G,kCAAmB,CAAA;AACvB,CAAC,EAPW,WAAW,2BAAX,WAAW,QAOtB;AAoCD;;;;GAIG;AACH,MAAa,oBAAoB;IAET;IACA;IAFpB,YACoB,QAAgB,EAChB,UAA2B;QAD3B,aAAQ,GAAR,QAAQ,CAAQ;QAChB,eAAU,GAAV,UAAU,CAAiB;IAC5C,CAAC;CACP;AALD,oDAKC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table\n * answering \"is this error expected, and is it worth repeating?\":\n *\n * - `RetryStrategy` retries only {@link CallOutcome.TRANSIENT_FAILURE} (and only on a `READ` /\n * `WRITE_IDEMPOTENT` endpoint — the ENDPOINT's safety is `CallContext.operation`'s question, never\n * the classifier's);\n * - `LogApiCall` logs {@link CallOutcome.EXPECTED} as `[API-*-resp-OTHER]` (`result='success'`) and\n * both failure outcomes as `[API-*-resp-FAIL]` (`result='failure'`);\n * - the MCP tool-call renderer derives its model-visible `retry` advice from the outcome plus the\n * endpoint's operation.\n */\nexport enum CallOutcome {\n /** It threw, but things are WORKING: a 266, a server-side 4xx, an external client's \"404 = miss\". */\n EXPECTED = 'expected',\n /** Broken NOW, may work if repeated: unavailable, connection, timeouts, 429/backoff, bad-gateway. */\n TRANSIENT_FAILURE = 'transient',\n /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */\n FAILURE = 'failure',\n}\n\n/**\n * Classifies a thrown API-call error into a {@link CallOutcome}.\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and the {@link ErrorTranslator}. Resolved most-specific first by\n * {@link ClientRegistry.classify}:\n *\n * 1. `ClientRegistry.addErrorClassifier(new KeyedErrorClassifier(apiClass, c))` — per EXTERNAL\n * client, keyed by {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi',\n * 'TwilioApi'). Each external API qualifies its errors differently (a Firestore 404 miss is\n * EXPECTED; a Twilio 429 is TRANSIENT), so it overrides the tiers below for that apiClass only.\n * 2. `ClientRegistry.setDefaultErrorClassifier(c)` — ONE per app/company. It reads\n * {@link ApiMethodInfo.side}, so a single classifier covers the SERVER router AND every INTERNAL\n * client (webpieces http client, cloud tasks).\n * 3. {@link WebpiecesDefaultErrorClassifier} — the terminal tier, which never defers.\n *\n * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal.\n * An external client that forgets to register is FAIL-SAFE: it falls to the tiers below, where every\n * client-side error except a 266 is a failure.\n */\nexport interface ErrorClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client\n * @returns the outcome, or `undefined` to DEFER to the next tier (a per-apiClass classifier defers\n * to the app default; the app default defers to the webpieces built-in). Deferring is what\n * makes classifiers additive AND override-capable.\n */\n classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome | undefined;\n}\n\n/**\n * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the\n * {@link ErrorClassifier} to apply for it. Data only, so it is a class (not an inline object literal),\n * per CLAUDE.md — handed to {@link ClientRegistry.addErrorClassifier}.\n */\nexport class KeyedErrorClassifier {\n constructor(\n public readonly apiClass: string,\n public readonly classifier: ErrorClassifier,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorClassifier.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;GAWG;AACH,IAAY,WAOX;AAPD,WAAY,WAAW;IACnB,qGAAqG;IACrG,oCAAqB,CAAA;IACrB,qGAAqG;IACrG,8CAA+B,CAAA;IAC/B,6GAA6G;IAC7G,kCAAmB,CAAA;AACvB,CAAC,EAPW,WAAW,2BAAX,WAAW,QAOtB","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table\n * answering \"is this error expected, and is it worth repeating?\":\n *\n * - `RetryStrategy` retries only {@link CallOutcome.TRANSIENT_FAILURE} (and only on a `READ` /\n * `WRITE_IDEMPOTENT` endpoint — the ENDPOINT's safety is `CallContext.operation`'s question, never\n * the classifier's);\n * - `LogApiCall` logs {@link CallOutcome.EXPECTED} as `[API-*-resp-OTHER]` (`result='success'`) and\n * both failure outcomes as `[API-*-resp-FAIL]` (`result='failure'`);\n * - the MCP tool-call renderer derives its model-visible `retry` advice from the outcome plus the\n * endpoint's operation.\n */\nexport enum CallOutcome {\n /** It threw, but things are WORKING: a 266, a server-side 4xx, an external client's \"404 = miss\". */\n EXPECTED = 'expected',\n /** Broken NOW, may work if repeated: unavailable, connection, timeouts, 429/backoff, bad-gateway. */\n TRANSIENT_FAILURE = 'transient',\n /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */\n FAILURE = 'failure',\n}\n\n/**\n * Classifies a thrown API-call error into a {@link CallOutcome}.\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and the {@link ErrorTranslator} — with\n * `ClientRegistry.setErrorClassifier(c, ...scope)`, on the same `CallScope` ladder as\n * `CallRegistry`'s strategy and timeout. {@link ClientRegistry.classify} resolves it most specific\n * first:\n *\n * 1. `setErrorClassifier(c, XxxApi, 'method')` — one method of one contract.\n * 2. `setErrorClassifier(c, XxxApi)` — one contract, keyed by its CLASS (never a name). An\n * SDK-wrapping api-client keys by its abstract `XxxApi`: each external API qualifies its errors\n * differently (a document store's 404 miss is EXPECTED; a messaging vendor's 429 is TRANSIENT),\n * so it overrides the rungs below for that contract only.\n * 3. `setErrorClassifier(c, 'ALL')` — ONE per app/company. It reads {@link ApiMethodInfo.side}, so a\n * single classifier covers the SERVER router AND every INTERNAL client (webpieces http client,\n * cloud tasks).\n * 4. {@link WebpiecesDefaultErrorClassifier} — the terminal rung, which never defers.\n *\n * Internal clients and the server register NOTHING per contract — the ABSENCE of a contract rung is\n * the signal. An external client that forgets to register is FAIL-SAFE: it falls to the rungs below,\n * where every client-side error except a 266 is a failure.\n */\nexport interface ErrorClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `contract` the API\n * @returns the outcome, or `undefined` to DEFER to the next rung (method defers to API, API to\n * ALL, ALL to the webpieces built-in). Deferring is what makes classifiers additive AND\n * override-capable.\n */\n classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome | undefined;\n}\n\n"]}
@@ -0,0 +1 @@
1
+ export {};