@webpieces/core-util 0.4.863 → 0.4.865

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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/ContextKey.d.ts +3 -3
  3. package/src/ContextKey.js +3 -3
  4. package/src/ContextKey.js.map +1 -1
  5. package/src/http/ContextMgr.d.ts +3 -3
  6. package/src/http/ContextMgr.js +3 -3
  7. package/src/http/ContextMgr.js.map +1 -1
  8. package/src/http/DeletedAuthCompileAssertions.d.ts +4 -0
  9. package/src/http/DeletedAuthCompileAssertions.js +30 -0
  10. package/src/http/DeletedAuthCompileAssertions.js.map +1 -0
  11. package/src/http/DestinationTrust.d.ts +6 -6
  12. package/src/http/DestinationTrust.js +12 -13
  13. package/src/http/DestinationTrust.js.map +1 -1
  14. package/src/http/HttpEndpointOptions.d.ts +2 -2
  15. package/src/http/HttpEndpointOptions.js.map +1 -1
  16. package/src/http/RouteMetadata.d.ts +6 -1
  17. package/src/http/RouteMetadata.js +14 -1
  18. package/src/http/RouteMetadata.js.map +1 -1
  19. package/src/http/RouteMetadataFactory.js +9 -3
  20. package/src/http/RouteMetadataFactory.js.map +1 -1
  21. package/src/http/RuntimeLocality.d.ts +3 -3
  22. package/src/http/RuntimeLocality.js +1 -1
  23. package/src/http/RuntimeLocality.js.map +1 -1
  24. package/src/http/Secrets.d.ts +2 -2
  25. package/src/http/Secrets.js +2 -2
  26. package/src/http/Secrets.js.map +1 -1
  27. package/src/http/Surface.d.ts +4 -4
  28. package/src/http/Surface.js.map +1 -1
  29. package/src/http/WebpiecesCoreHeaders.d.ts +3 -3
  30. package/src/http/WebpiecesCoreHeaders.js +3 -3
  31. package/src/http/WebpiecesCoreHeaders.js.map +1 -1
  32. package/src/http/WpAuthApiKeyCompileAssertions.d.ts +4 -8
  33. package/src/http/WpAuthApiKeyCompileAssertions.js +8 -13
  34. package/src/http/WpAuthApiKeyCompileAssertions.js.map +1 -1
  35. package/src/http/WpAuthJwtCompileAssertions.d.ts +1 -27
  36. package/src/http/WpAuthJwtCompileAssertions.js +13 -48
  37. package/src/http/WpAuthJwtCompileAssertions.js.map +1 -1
  38. package/src/http/WpAuthorizationCompileAssertions.d.ts +1 -0
  39. package/src/http/WpAuthorizationCompileAssertions.js +26 -0
  40. package/src/http/WpAuthorizationCompileAssertions.js.map +1 -0
  41. package/src/http/auth-mode.d.ts +22 -130
  42. package/src/http/auth-mode.js +100 -26
  43. package/src/http/auth-mode.js.map +1 -1
  44. package/src/http/authorization.d.ts +38 -0
  45. package/src/http/authorization.js +71 -0
  46. package/src/http/authorization.js.map +1 -0
  47. package/src/http/decorators.d.ts +11 -114
  48. package/src/http/decorators.js +47 -157
  49. package/src/http/decorators.js.map +1 -1
  50. package/src/index.d.ts +5 -4
  51. package/src/index.js +21 -19
  52. package/src/index.js.map +1 -1
  53. package/src/mcp/McpAuthMetadata.d.ts +0 -9
  54. package/src/mcp/McpAuthMetadata.js +0 -31
  55. package/src/mcp/McpAuthMetadata.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.863",
3
+ "version": "0.4.865",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -73,9 +73,9 @@
73
73
  *
74
74
  * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified
75
75
  * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on
76
- * the endpoint's own auth mode: a route that verified WHO called it (`@WpAuthOidc`,
77
- * `@WpAuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a
78
- * browser (`@WpAuthJwt`, public) does not.
76
+ * the endpoint's own auth mode: a route that verified WHO called it (`oidc(...)`,
77
+ * `sharedSecret(...)`) accepts the caller's trusted headers; a route reachable by a
78
+ * browser (`jwt()`, public) does not.
79
79
  *
80
80
  * Per CLAUDE.md: data-only structures are classes, not interfaces.
81
81
  *
package/src/ContextKey.js CHANGED
@@ -74,9 +74,9 @@
74
74
  *
75
75
  * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified
76
76
  * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on
77
- * the endpoint's own auth mode: a route that verified WHO called it (`@WpAuthOidc`,
78
- * `@WpAuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a
79
- * browser (`@WpAuthJwt`, public) does not.
77
+ * the endpoint's own auth mode: a route that verified WHO called it (`oidc(...)`,
78
+ * `sharedSecret(...)`) accepts the caller's trusted headers; a route reachable by a
79
+ * browser (`jwt()`, public) does not.
80
80
  *
81
81
  * Per CLAUDE.md: data-only structures are classes, not interfaces.
82
82
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ContextKey - a single key that travels in the request's \"magic context\"\n * (RequestContext on the server, MutableContextStore in the browser).\n *\n * This ONE class replaces the old split of `Header` (interface) + `PlatformHeader`\n * (class) + `ContextKey` (class). Every context value — whether it rides over HTTP\n * (request-id, tenant, authorization) or stays in-process (method-meta, the\n * TestCaseRecorder) — is a `ContextKey`.\n *\n * The fields are named for what they DO (flipped from the old model):\n * - `name` ALWAYS set. The context storage key, the log/MDC key, and the\n * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.\n * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under\n * this HTTP header name (inbound request -> context, and context ->\n * outbound request). e.g. 'x-request-id'. When UNSET, the key is\n * context-only and never leaves the process (method-meta, recorder).\n * - `responseHeader` OPTIONAL, and the exact MIRROR of `httpHeader`. When set, this key is written\n * back onto every RESPONSE under this header name, and a webpieces client reads it\n * off the response into the CALLER's context — so a value travels UP the call tree\n * hop by hop. Independent of `httpHeader`: a key may ride requests, responses,\n * both, or neither.\n * - `responseMerge` HOW a response value combines with what the caller already holds — `collect`\n * (the default), `first` or `last`. See {@link ResponseMerge}.\n * - `trust` REQUIRED, and stated by WHICH FACTORY you call. See below.\n * - `maskInLogs` When true, the value is masked (partially) in logs. This is about\n * LOG REDACTION and has NOTHING to do with `trust` — a userId is\n * trusted AND fully logged; a bearer token is untrusted AND masked.\n * (Formerly `isSecured`, renamed because sitting next to `trust` the\n * old name read as \"this value is secure\", which it never meant.)\n * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for\n * object-valued/internal keys like the recorder or method-meta that\n * must not be serialized into log lines).\n *\n * ## TRUST — the whole point of this class\n *\n * A context value is either something the framework PROVED (`trusted`) or something a\n * caller merely ASSERTED (`untrusted`). That distinction is invisible in a `Map<string,\n * string>`, so code — human- or AI-written — routinely reads a spoofable header as if it\n * were an authenticated fact. `userId` is the canonical example: it is a verified JWT\n * claim on one route and an attacker-supplied `x-user-id` on the next, and nothing in the\n * old API told them apart.\n *\n * So trust is declared ON THE KEY, at the single place the key is defined, and it is\n * enforced at BOTH ends:\n *\n * ContextKey.trusted<string>('userId', 'jwt claim `sub`, stamped by AuthFilter', 'x-user-id')\n * ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid')\n *\n * `grep -rn \"ContextKey.trusted\"` therefore enumerates every high-assurance field in the\n * codebase, each with its `provenance` string on the same line saying WHY it is trusted.\n * That is the AI-facing payoff: the answer is one grep, not an audit.\n *\n * The constructor is PRIVATE — you cannot make a key without picking a branch, and there\n * is no default. A default would make the permissive branch the shortest thing to type,\n * which is exactly the \"widening that is an ABSENCE rather than a token\" that\n * `.claude/rules/no-backwards-compat.md`\n * rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so\n * \"trusted with no stated reason\" cannot be written down.\n *\n * ## What makes the `trusted` label HONEST at runtime\n *\n * The label would be a lie if anything could write a trusted key from an unverified\n * source. Three enforced facts prevent that:\n *\n * 1. WRITES are typed: only `RequestContext.putTrusted(key, value)` accepts a trusted key,\n * and it is a distinct, greppable verb an app has to type on purpose.\n * 2. INBOUND wire values for trusted keys never enter the context directly.\n * `RequestContextHeaders.fillFromRequest` stashes them as PENDING, and `AuthFilter`\n * admits them only after it knows who the caller is (see {@link PendingWireTrust}).\n * 3. READS are typed: `getTrusted(key)` does not compile for an untrusted key, and\n * `getUntrusted(key)` does not compile for a trusted one. Picking a verb is\n * unavoidable, so a reader always knows which kind of value it is holding.\n *\n * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified\n * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on\n * the endpoint's own auth mode: a route that verified WHO called it (`@WpAuthOidc`,\n * `@WpAuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a\n * browser (`@WpAuthJwt`, public) does not.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n *\n * The type parameter `V` is the TYPE OF THE VALUE stored under this key — `string` for the wire/log\n * keys (requestId, tenantId, ...), `ApiCallInfo` for the structured api tag, `TestCaseRecorder` for\n * the recorder. It is REQUIRED (no default): every key must state what it holds. A heterogeneous\n * store CANNOT be a `Record<string, string>` — the recorder and the api payload are not strings — so\n * instead each KEY carries its own value type, and the typed accessors INFER it from the key. That\n * keeps the backing Map honestly type-erased while the public surface stays fully typed: a caller\n * never asserts a value type, the key already declares it. A genuinely mixed collection of keys is\n * spelled explicitly as `AnyContextKey[]`, so \"I mean a mixed bag\" is a visible, deliberate\n * statement, never a default.\n *\n * The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can\n * reject the wrong kind of key at COMPILE time rather than throwing at runtime (`.claude/rules/no-backwards-compat.md`\n * treats a\n * runtime throw standing in for an expressible type as a defect).\n */\n\n/** The two kinds of context value. See the {@link ContextKey} class doc. */\nexport type Trust = 'trusted' | 'untrusted';\n\n/**\n * A trusted key of any value type — what {@link ContextTuple} carries, and what the trusted write\n * verb accepts when the value type is not statically known.\n */\n// webpieces-disable no-any-unknown -- the ONE sanctioned `unknown`: a key whose value type is deliberately unconstrained (mixed-bag collections / key-agnostic code). Every other site names one of these aliases instead of repeating it.\nexport type AnyTrustedContextKey = ContextKey<unknown, 'trusted'>;\n\n/**\n * An untrusted key of any value type — what the {@link ApiCallContext} seam stamps, so that seam\n * cannot be used as a side door to forge a trusted value.\n */\n// webpieces-disable no-any-unknown -- same sanctioned mixed-bag alias, narrowed to the untrusted branch\nexport type AnyUntrustedContextKey = ContextKey<unknown, 'untrusted'>;\n\n/**\n * A ContextKey whose value type is intentionally UNCONSTRAINED — a \"key of any value type\". Use this\n * (never a bare `ContextKey`, which no longer compiles) for genuinely mixed-bag collections and\n * key-agnostic code: `ALL_HEADERS: AnyContextKey[]`, the {@link HeaderRegistry}'s key arrays, a\n * reader that takes whatever key it is handed. Naming the mixed case makes \"I mean any key\" a visible,\n * deliberate statement.\n *\n * It is a UNION of the two branches, not `ContextKey<unknown, Trust>`, and that is load-bearing rather\n * than cosmetic. Trust is BINARY, so `if (key.isTrusted())` should type BOTH of its branches — and it\n * does only against a union: TypeScript narrows the negative of a `this is X` predicate by dropping the\n * union constituents assignable to `X`, so the `else` here lands on {@link AnyUntrustedContextKey} and\n * goes straight to `putUntrusted` with no cast. Written as one type with a mixed `Trust` parameter\n * there would be nothing to drop, the `else` would stay mixed, and the class would need a second\n * `isUntrusted()` predicate to type the branch its own negative already decided — one runtime question\n * with two spellings, which is the shim shape `.claude/rules/no-backwards-compat.md` rejects.\n *\n * Mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)` takes one, but\n * no WRITE verb does. A write must name the trust level, which is what keeps the `trusted` label\n * honest.\n */\nexport type AnyContextKey = AnyTrustedContextKey | AnyUntrustedContextKey;\n\n/**\n * HOW a RESPONSE value merges with whatever the caller's context already holds for the same key.\n *\n * Declared per KEY, because fan-out is the normal case and not the exception: one inbound request\n * that calls five services produces FIVE values for one response key, and a silent last-write-wins\n * keeps one of them and throws four away without saying so. Naming the policy on the key is what\n * makes that choice visible at the definition rather than emergent at runtime.\n *\n * - `collect` APPEND to a list (the value type is therefore `string[]`). The default, and the right\n * answer for every DIAGNOSTIC key — cache status, backend version, a downstream warning\n * — where \"all five\" is the interesting answer.\n * - `first` KEEP what is already there; the first value seen wins. This is the policy for a key\n * whose value belongs to THIS hop and must not be replaced by a callee's echo of it —\n * {@link WebpiecesCoreHeaders.REQUEST_ID} is exactly that.\n * - `last` OVERWRITE. The last response seen wins. Use it only where a single, latest value is\n * genuinely meant.\n */\nexport type ResponseMerge = 'collect' | 'first' | 'last';\n\nexport class ContextKey<V, T extends Trust = Trust> {\n /**\n * Phantom marker carrying the value type {@link V}. It has no runtime existence (`declare`, never\n * assigned) — it exists ONLY so the read verbs return `V` and the write verbs check `value`\n * against `V`, both inferred straight from the key. Optional, so `ContextKey<A>` stays assignable\n * to `AnyContextKey` (i.e. `ContextKey<unknown>`) — arrays of mixed keys keep working.\n */\n declare readonly __valueType?: V;\n\n /**\n * Phantom marker carrying the trust level {@link T} — the reason `getTrusted(SOME_UNTRUSTED_KEY)`\n * is a COMPILE error and not a runtime throw. Like `__valueType` it never exists at runtime; the\n * runtime answer is the {@link trust} field below, which the fill/reconcile path reads.\n */\n declare readonly __trust?: T;\n\n /** Context storage key + log/MDC key + recorder name. Always set. */\n readonly name: string;\n\n /**\n * HTTP header name when this key is transferred over the wire (e.g.\n * 'x-request-id'). Undefined = context-only, never transferred.\n */\n readonly httpHeader?: string;\n\n /**\n * HTTP header name this key is written back on the RESPONSE under (e.g. 'x-request-id').\n * Undefined = this key never rides a response.\n *\n * It is the exact mirror of {@link httpHeader}, and the two are INDEPENDENT: a key may travel\n * out on requests only, back on responses only, both, or neither. The server writes every key\n * that sets it onto every response from one choke point; a webpieces client reads it back off\n * the response into the CALLER's context, which is what lets a value climb the call tree hop by\n * hop with nothing in between mentioning HTTP.\n */\n readonly responseHeader?: string;\n\n /**\n * How a response value merges with what the context already holds — see {@link ResponseMerge}.\n * Only consulted when {@link responseHeader} is set; ignored otherwise.\n */\n readonly responseMerge: ResponseMerge;\n\n /** The runtime twin of the phantom {@link __trust}. See the class doc. */\n readonly trust: Trust;\n\n /**\n * WHY this key is trusted, in prose — 'jwt claim `sub`, stamped by AuthFilter', or\n * 'whatsapp webhook phone number -> user lookup'. Required on a trusted key, absent on an\n * untrusted one. It exists so that grepping the trusted keys also tells you what proves each\n * one, without opening another file.\n */\n readonly provenance?: string;\n\n /** Mask this value (partially) in logs. Log redaction only — unrelated to {@link trust}. */\n readonly maskInLogs: boolean;\n\n /** Whether this key is logged at all. Default true; false = never logged. */\n readonly isLogged: boolean;\n\n /**\n * PRIVATE — use {@link trusted} or {@link untrusted}. There is deliberately no way to build a key\n * without stating its trust level: a defaulted trust argument would make the permissive branch\n * the shortest thing to type and impossible to grep.\n */\n private constructor(\n name: string,\n trust: Trust,\n provenance: string | undefined,\n httpHeader: string | undefined,\n maskInLogs: boolean,\n isLogged: boolean,\n responseHeader: string | undefined,\n responseMerge: ResponseMerge,\n ) {\n this.name = name;\n this.trust = trust;\n this.provenance = provenance;\n this.httpHeader = httpHeader;\n this.maskInLogs = maskInLogs;\n this.isLogged = isLogged;\n this.responseHeader = responseHeader;\n this.responseMerge = responseMerge;\n }\n\n /**\n * A key whose value the framework PROVED — a verified JWT claim, or a fact an app derived from a\n * verified credential (a Twilio/WhatsApp webhook's signed phone number looked up to a userId).\n *\n * Only `RequestContext.putTrusted` can write one, only `RequestContext.getTrusted` can read one,\n * and an inbound wire value for one is held PENDING until `AuthFilter` knows who the caller is.\n *\n * @param provenance WHY it is trusted, in prose. Required — see {@link ContextKey.provenance}.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; the trust branch must be part of the call, not a defaulted argument\n static trusted<V>(\n name: string,\n provenance: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n responseHeader?: string,\n responseMerge: ResponseMerge = 'collect',\n ): ContextKey<V, 'trusted'> {\n return new ContextKey<V, 'trusted'>(\n name,\n 'trusted',\n provenance,\n httpHeader,\n maskInLogs,\n isLogged,\n responseHeader,\n responseMerge,\n );\n }\n\n /**\n * A key whose value is merely ASSERTED by whoever sent it — a browser-minted actionId, a\n * client-supplied recording flag, an in-process log tag. Perfectly fine to use; just never\n * an input to an authorization decision.\n *\n * This is the DEFAULT choice in the sense that most keys are this — but it is never the default\n * VALUE: you still type the word, so reading a key definition always tells you which it is.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; see trusted()\n static untrusted<V>(\n name: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n responseHeader?: string,\n responseMerge: ResponseMerge = 'collect',\n ): ContextKey<V, 'untrusted'> {\n return new ContextKey<V, 'untrusted'>(\n name,\n 'untrusted',\n undefined,\n httpHeader,\n maskInLogs,\n isLogged,\n responseHeader,\n responseMerge,\n );\n }\n\n /** True when this key is transferred over HTTP (has an httpHeader). */\n isTransferred(): boolean {\n return this.httpHeader !== undefined;\n }\n\n /** True when this key rides back on the RESPONSE (has a responseHeader). */\n isResponseTransferred(): boolean {\n return this.responseHeader !== undefined;\n }\n\n /**\n * The value to STORE for this key given what the context already holds and what one response\n * just carried, per this key's {@link responseMerge}.\n *\n * It lives on the KEY rather than in the two client packages because the policy is a property of\n * the declaration, and because node and browser must not be able to answer it differently.\n *\n * `collect` returns an ARRAY, which is why a collecting key's value type is `string[]`: a\n * non-array `existing` (somebody put a scalar under a collecting key) is folded in as the first\n * element rather than thrown away, so the fan-out answer is never silently truncated.\n */\n // webpieces-disable no-any-unknown -- the context store is honestly type-erased; the key's own V types the public accessors\n mergeResponseValue(existing: unknown, incoming: string): unknown {\n switch (this.responseMerge) {\n case 'first':\n return existing === undefined ? incoming : existing;\n case 'last':\n return incoming;\n case 'collect':\n if (Array.isArray(existing)) {\n // webpieces-disable no-any-unknown -- a collect key's accumulated list is honestly type-erased here, exactly like every other key-agnostic serialization read\n return [...(existing as unknown[]), incoming];\n }\n return existing === undefined ? [incoming] : [existing, incoming];\n }\n }\n\n /**\n * True for a key built by {@link trusted}. Trust is BINARY, so this ONE predicate answers it in\n * both directions and there is deliberately no `isUntrusted()` twin. The RUNTIME check used by the\n * inbound fill and the AuthFilter reconciliation; ordinary application code should never need it,\n * because the typed verbs already made the decision at compile time.\n *\n * It is a TYPE PREDICATE, so BOTH branches are typed, with NO cast on either side: the `if` holds a\n * `ContextKey<V, 'trusted'>` for `putTrusted` / `PendingWireTrust.stash`, and the `else` holds a\n * `ContextKey<V, 'untrusted'>` for `putUntrusted`. The `else` types only because\n * {@link AnyContextKey} is a UNION of the two branches — see that alias for why the negative of a\n * predicate needs something to drop. Before this, every such site wrote `key as\n * AnyTrustedContextKey`; a cast is exactly the thing an agent copies to the one place it is not\n * warranted, so the runtime check produces the type it proves instead.\n *\n * That is what makes a loop over a mixed `AnyContextKey[]` — the {@link HeaderRegistry} arrays, a\n * browser-log payload re-stated into a detached scope — safe BY CONSTRUCTION: the loop can only\n * write a key whose trust it has just tested, and a trusted key cannot reach `putUntrusted` at all,\n * so a loop fed by a source that proves nothing cannot fabricate a proven value, and nobody has to\n * remember to filter. That is a limit on the SOURCE, not on the key: a trusted key is written all\n * the time via `putTrusted`, by an authenticator or by app code that proved the value out of band.\n */\n isTrusted(): this is ContextKey<V, 'trusted'> {\n return this.trust === 'trusted';\n }\n\n /**\n * The value as it should appear in a log line: returned as-is for a normal\n * key, partially masked when this key sets `maskInLogs`. Masking is length-based:\n * - Length > 15: first 3 + \"...\" + last 3\n * - Length 8-15: first 2 + \"...\"\n * - Length < 8: \"<secure key too short to log>\"\n */\n maskForLogs(value: string): string {\n if (!this.maskInLogs) {\n return value;\n }\n const len = value.length;\n if (len < 8) {\n return '<secure key too short to log>';\n } else if (len <= 15) {\n return `${value.substring(0, 2)}...`;\n } else {\n return `${value.substring(0, 3)}...${value.substring(len - 3)}`;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+FG;;;AA4DH,MAAa,UAAU;IAgBnB,qEAAqE;IAC5D,IAAI,CAAS;IAEtB;;;OAGG;IACM,UAAU,CAAU;IAE7B;;;;;;;;;OASG;IACM,cAAc,CAAU;IAEjC;;;OAGG;IACM,aAAa,CAAgB;IAEtC,0EAA0E;IACjE,KAAK,CAAQ;IAEtB;;;;;OAKG;IACM,UAAU,CAAU;IAE7B,4FAA4F;IACnF,UAAU,CAAU;IAE7B,6EAA6E;IACpE,QAAQ,CAAU;IAE3B;;;;OAIG;IACH,YACI,IAAY,EACZ,KAAY,EACZ,UAA8B,EAC9B,UAA8B,EAC9B,UAAmB,EACnB,QAAiB,EACjB,cAAkC,EAClC,aAA4B;QAE5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACvC,CAAC;IAED;;;;;;;;OAQG;IACH,6KAA6K;IAC7K,MAAM,CAAC,OAAO,CACV,IAAY,EACZ,UAAkB,EAClB,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI,EACf,cAAuB,EACvB,gBAA+B,SAAS;QAExC,OAAO,IAAI,UAAU,CACjB,IAAI,EACJ,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,cAAc,EACd,aAAa,CAChB,CAAC;IACN,CAAC;IAED;;;;;;;OAOG;IACH,uHAAuH;IACvH,MAAM,CAAC,SAAS,CACZ,IAAY,EACZ,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI,EACf,cAAuB,EACvB,gBAA+B,SAAS;QAExC,OAAO,IAAI,UAAU,CACjB,IAAI,EACJ,WAAW,EACX,SAAS,EACT,UAAU,EACV,UAAU,EACV,QAAQ,EACR,cAAc,EACd,aAAa,CAChB,CAAC;IACN,CAAC;IAED,uEAAuE;IACvE,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC;IACzC,CAAC;IAED,4EAA4E;IAC5E,qBAAqB;QACjB,OAAO,IAAI,CAAC,cAAc,KAAK,SAAS,CAAC;IAC7C,CAAC;IAED;;;;;;;;;;OAUG;IACH,4HAA4H;IAC5H,kBAAkB,CAAC,QAAiB,EAAE,QAAgB;QAClD,QAAQ,IAAI,CAAC,aAAa,EAAE,CAAC;YACzB,KAAK,OAAO;gBACR,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;YACxD,KAAK,MAAM;gBACP,OAAO,QAAQ,CAAC;YACpB,KAAK,SAAS;gBACV,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAC1B,8JAA8J;oBAC9J,OAAO,CAAC,GAAI,QAAsB,EAAE,QAAQ,CAAC,CAAC;gBAClD,CAAC;gBACD,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,SAAS;QACL,OAAO,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC;IACpC,CAAC;IAED;;;;;;OAMG;IACH,WAAW,CAAC,KAAa;QACrB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACnB,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;QACzB,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACV,OAAO,+BAA+B,CAAC;QAC3C,CAAC;aAAM,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC;YACnB,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC;QACzC,CAAC;aAAM,CAAC;YACJ,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;QACpE,CAAC;IACL,CAAC;CACJ;AAnOD,gCAmOC","sourcesContent":["/**\n * ContextKey - a single key that travels in the request's \"magic context\"\n * (RequestContext on the server, MutableContextStore in the browser).\n *\n * This ONE class replaces the old split of `Header` (interface) + `PlatformHeader`\n * (class) + `ContextKey` (class). Every context value — whether it rides over HTTP\n * (request-id, tenant, authorization) or stays in-process (method-meta, the\n * TestCaseRecorder) — is a `ContextKey`.\n *\n * The fields are named for what they DO (flipped from the old model):\n * - `name` ALWAYS set. The context storage key, the log/MDC key, and the\n * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.\n * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under\n * this HTTP header name (inbound request -> context, and context ->\n * outbound request). e.g. 'x-request-id'. When UNSET, the key is\n * context-only and never leaves the process (method-meta, recorder).\n * - `responseHeader` OPTIONAL, and the exact MIRROR of `httpHeader`. When set, this key is written\n * back onto every RESPONSE under this header name, and a webpieces client reads it\n * off the response into the CALLER's context — so a value travels UP the call tree\n * hop by hop. Independent of `httpHeader`: a key may ride requests, responses,\n * both, or neither.\n * - `responseMerge` HOW a response value combines with what the caller already holds — `collect`\n * (the default), `first` or `last`. See {@link ResponseMerge}.\n * - `trust` REQUIRED, and stated by WHICH FACTORY you call. See below.\n * - `maskInLogs` When true, the value is masked (partially) in logs. This is about\n * LOG REDACTION and has NOTHING to do with `trust` — a userId is\n * trusted AND fully logged; a bearer token is untrusted AND masked.\n * (Formerly `isSecured`, renamed because sitting next to `trust` the\n * old name read as \"this value is secure\", which it never meant.)\n * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for\n * object-valued/internal keys like the recorder or method-meta that\n * must not be serialized into log lines).\n *\n * ## TRUST — the whole point of this class\n *\n * A context value is either something the framework PROVED (`trusted`) or something a\n * caller merely ASSERTED (`untrusted`). That distinction is invisible in a `Map<string,\n * string>`, so code — human- or AI-written — routinely reads a spoofable header as if it\n * were an authenticated fact. `userId` is the canonical example: it is a verified JWT\n * claim on one route and an attacker-supplied `x-user-id` on the next, and nothing in the\n * old API told them apart.\n *\n * So trust is declared ON THE KEY, at the single place the key is defined, and it is\n * enforced at BOTH ends:\n *\n * ContextKey.trusted<string>('userId', 'jwt claim `sub`, stamped by AuthFilter', 'x-user-id')\n * ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid')\n *\n * `grep -rn \"ContextKey.trusted\"` therefore enumerates every high-assurance field in the\n * codebase, each with its `provenance` string on the same line saying WHY it is trusted.\n * That is the AI-facing payoff: the answer is one grep, not an audit.\n *\n * The constructor is PRIVATE — you cannot make a key without picking a branch, and there\n * is no default. A default would make the permissive branch the shortest thing to type,\n * which is exactly the \"widening that is an ABSENCE rather than a token\" that\n * `.claude/rules/no-backwards-compat.md`\n * rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so\n * \"trusted with no stated reason\" cannot be written down.\n *\n * ## What makes the `trusted` label HONEST at runtime\n *\n * The label would be a lie if anything could write a trusted key from an unverified\n * source. Three enforced facts prevent that:\n *\n * 1. WRITES are typed: only `RequestContext.putTrusted(key, value)` accepts a trusted key,\n * and it is a distinct, greppable verb an app has to type on purpose.\n * 2. INBOUND wire values for trusted keys never enter the context directly.\n * `RequestContextHeaders.fillFromRequest` stashes them as PENDING, and `AuthFilter`\n * admits them only after it knows who the caller is (see {@link PendingWireTrust}).\n * 3. READS are typed: `getTrusted(key)` does not compile for an untrusted key, and\n * `getUntrusted(key)` does not compile for a trusted one. Picking a verb is\n * unavoidable, so a reader always knows which kind of value it is holding.\n *\n * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified\n * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on\n * the endpoint's own auth mode: a route that verified WHO called it (`oidc(...)`,\n * `sharedSecret(...)`) accepts the caller's trusted headers; a route reachable by a\n * browser (`jwt()`, public) does not.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n *\n * The type parameter `V` is the TYPE OF THE VALUE stored under this key — `string` for the wire/log\n * keys (requestId, tenantId, ...), `ApiCallInfo` for the structured api tag, `TestCaseRecorder` for\n * the recorder. It is REQUIRED (no default): every key must state what it holds. A heterogeneous\n * store CANNOT be a `Record<string, string>` — the recorder and the api payload are not strings — so\n * instead each KEY carries its own value type, and the typed accessors INFER it from the key. That\n * keeps the backing Map honestly type-erased while the public surface stays fully typed: a caller\n * never asserts a value type, the key already declares it. A genuinely mixed collection of keys is\n * spelled explicitly as `AnyContextKey[]`, so \"I mean a mixed bag\" is a visible, deliberate\n * statement, never a default.\n *\n * The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can\n * reject the wrong kind of key at COMPILE time rather than throwing at runtime (`.claude/rules/no-backwards-compat.md`\n * treats a\n * runtime throw standing in for an expressible type as a defect).\n */\n\n/** The two kinds of context value. See the {@link ContextKey} class doc. */\nexport type Trust = 'trusted' | 'untrusted';\n\n/**\n * A trusted key of any value type — what {@link ContextTuple} carries, and what the trusted write\n * verb accepts when the value type is not statically known.\n */\n// webpieces-disable no-any-unknown -- the ONE sanctioned `unknown`: a key whose value type is deliberately unconstrained (mixed-bag collections / key-agnostic code). Every other site names one of these aliases instead of repeating it.\nexport type AnyTrustedContextKey = ContextKey<unknown, 'trusted'>;\n\n/**\n * An untrusted key of any value type — what the {@link ApiCallContext} seam stamps, so that seam\n * cannot be used as a side door to forge a trusted value.\n */\n// webpieces-disable no-any-unknown -- same sanctioned mixed-bag alias, narrowed to the untrusted branch\nexport type AnyUntrustedContextKey = ContextKey<unknown, 'untrusted'>;\n\n/**\n * A ContextKey whose value type is intentionally UNCONSTRAINED — a \"key of any value type\". Use this\n * (never a bare `ContextKey`, which no longer compiles) for genuinely mixed-bag collections and\n * key-agnostic code: `ALL_HEADERS: AnyContextKey[]`, the {@link HeaderRegistry}'s key arrays, a\n * reader that takes whatever key it is handed. Naming the mixed case makes \"I mean any key\" a visible,\n * deliberate statement.\n *\n * It is a UNION of the two branches, not `ContextKey<unknown, Trust>`, and that is load-bearing rather\n * than cosmetic. Trust is BINARY, so `if (key.isTrusted())` should type BOTH of its branches — and it\n * does only against a union: TypeScript narrows the negative of a `this is X` predicate by dropping the\n * union constituents assignable to `X`, so the `else` here lands on {@link AnyUntrustedContextKey} and\n * goes straight to `putUntrusted` with no cast. Written as one type with a mixed `Trust` parameter\n * there would be nothing to drop, the `else` would stay mixed, and the class would need a second\n * `isUntrusted()` predicate to type the branch its own negative already decided — one runtime question\n * with two spellings, which is the shim shape `.claude/rules/no-backwards-compat.md` rejects.\n *\n * Mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)` takes one, but\n * no WRITE verb does. A write must name the trust level, which is what keeps the `trusted` label\n * honest.\n */\nexport type AnyContextKey = AnyTrustedContextKey | AnyUntrustedContextKey;\n\n/**\n * HOW a RESPONSE value merges with whatever the caller's context already holds for the same key.\n *\n * Declared per KEY, because fan-out is the normal case and not the exception: one inbound request\n * that calls five services produces FIVE values for one response key, and a silent last-write-wins\n * keeps one of them and throws four away without saying so. Naming the policy on the key is what\n * makes that choice visible at the definition rather than emergent at runtime.\n *\n * - `collect` APPEND to a list (the value type is therefore `string[]`). The default, and the right\n * answer for every DIAGNOSTIC key — cache status, backend version, a downstream warning\n * — where \"all five\" is the interesting answer.\n * - `first` KEEP what is already there; the first value seen wins. This is the policy for a key\n * whose value belongs to THIS hop and must not be replaced by a callee's echo of it —\n * {@link WebpiecesCoreHeaders.REQUEST_ID} is exactly that.\n * - `last` OVERWRITE. The last response seen wins. Use it only where a single, latest value is\n * genuinely meant.\n */\nexport type ResponseMerge = 'collect' | 'first' | 'last';\n\nexport class ContextKey<V, T extends Trust = Trust> {\n /**\n * Phantom marker carrying the value type {@link V}. It has no runtime existence (`declare`, never\n * assigned) — it exists ONLY so the read verbs return `V` and the write verbs check `value`\n * against `V`, both inferred straight from the key. Optional, so `ContextKey<A>` stays assignable\n * to `AnyContextKey` (i.e. `ContextKey<unknown>`) — arrays of mixed keys keep working.\n */\n declare readonly __valueType?: V;\n\n /**\n * Phantom marker carrying the trust level {@link T} — the reason `getTrusted(SOME_UNTRUSTED_KEY)`\n * is a COMPILE error and not a runtime throw. Like `__valueType` it never exists at runtime; the\n * runtime answer is the {@link trust} field below, which the fill/reconcile path reads.\n */\n declare readonly __trust?: T;\n\n /** Context storage key + log/MDC key + recorder name. Always set. */\n readonly name: string;\n\n /**\n * HTTP header name when this key is transferred over the wire (e.g.\n * 'x-request-id'). Undefined = context-only, never transferred.\n */\n readonly httpHeader?: string;\n\n /**\n * HTTP header name this key is written back on the RESPONSE under (e.g. 'x-request-id').\n * Undefined = this key never rides a response.\n *\n * It is the exact mirror of {@link httpHeader}, and the two are INDEPENDENT: a key may travel\n * out on requests only, back on responses only, both, or neither. The server writes every key\n * that sets it onto every response from one choke point; a webpieces client reads it back off\n * the response into the CALLER's context, which is what lets a value climb the call tree hop by\n * hop with nothing in between mentioning HTTP.\n */\n readonly responseHeader?: string;\n\n /**\n * How a response value merges with what the context already holds — see {@link ResponseMerge}.\n * Only consulted when {@link responseHeader} is set; ignored otherwise.\n */\n readonly responseMerge: ResponseMerge;\n\n /** The runtime twin of the phantom {@link __trust}. See the class doc. */\n readonly trust: Trust;\n\n /**\n * WHY this key is trusted, in prose — 'jwt claim `sub`, stamped by AuthFilter', or\n * 'whatsapp webhook phone number -> user lookup'. Required on a trusted key, absent on an\n * untrusted one. It exists so that grepping the trusted keys also tells you what proves each\n * one, without opening another file.\n */\n readonly provenance?: string;\n\n /** Mask this value (partially) in logs. Log redaction only — unrelated to {@link trust}. */\n readonly maskInLogs: boolean;\n\n /** Whether this key is logged at all. Default true; false = never logged. */\n readonly isLogged: boolean;\n\n /**\n * PRIVATE — use {@link trusted} or {@link untrusted}. There is deliberately no way to build a key\n * without stating its trust level: a defaulted trust argument would make the permissive branch\n * the shortest thing to type and impossible to grep.\n */\n private constructor(\n name: string,\n trust: Trust,\n provenance: string | undefined,\n httpHeader: string | undefined,\n maskInLogs: boolean,\n isLogged: boolean,\n responseHeader: string | undefined,\n responseMerge: ResponseMerge,\n ) {\n this.name = name;\n this.trust = trust;\n this.provenance = provenance;\n this.httpHeader = httpHeader;\n this.maskInLogs = maskInLogs;\n this.isLogged = isLogged;\n this.responseHeader = responseHeader;\n this.responseMerge = responseMerge;\n }\n\n /**\n * A key whose value the framework PROVED — a verified JWT claim, or a fact an app derived from a\n * verified credential (a Twilio/WhatsApp webhook's signed phone number looked up to a userId).\n *\n * Only `RequestContext.putTrusted` can write one, only `RequestContext.getTrusted` can read one,\n * and an inbound wire value for one is held PENDING until `AuthFilter` knows who the caller is.\n *\n * @param provenance WHY it is trusted, in prose. Required — see {@link ContextKey.provenance}.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; the trust branch must be part of the call, not a defaulted argument\n static trusted<V>(\n name: string,\n provenance: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n responseHeader?: string,\n responseMerge: ResponseMerge = 'collect',\n ): ContextKey<V, 'trusted'> {\n return new ContextKey<V, 'trusted'>(\n name,\n 'trusted',\n provenance,\n httpHeader,\n maskInLogs,\n isLogged,\n responseHeader,\n responseMerge,\n );\n }\n\n /**\n * A key whose value is merely ASSERTED by whoever sent it — a browser-minted actionId, a\n * client-supplied recording flag, an in-process log tag. Perfectly fine to use; just never\n * an input to an authorization decision.\n *\n * This is the DEFAULT choice in the sense that most keys are this — but it is never the default\n * VALUE: you still type the word, so reading a key definition always tells you which it is.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; see trusted()\n static untrusted<V>(\n name: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n responseHeader?: string,\n responseMerge: ResponseMerge = 'collect',\n ): ContextKey<V, 'untrusted'> {\n return new ContextKey<V, 'untrusted'>(\n name,\n 'untrusted',\n undefined,\n httpHeader,\n maskInLogs,\n isLogged,\n responseHeader,\n responseMerge,\n );\n }\n\n /** True when this key is transferred over HTTP (has an httpHeader). */\n isTransferred(): boolean {\n return this.httpHeader !== undefined;\n }\n\n /** True when this key rides back on the RESPONSE (has a responseHeader). */\n isResponseTransferred(): boolean {\n return this.responseHeader !== undefined;\n }\n\n /**\n * The value to STORE for this key given what the context already holds and what one response\n * just carried, per this key's {@link responseMerge}.\n *\n * It lives on the KEY rather than in the two client packages because the policy is a property of\n * the declaration, and because node and browser must not be able to answer it differently.\n *\n * `collect` returns an ARRAY, which is why a collecting key's value type is `string[]`: a\n * non-array `existing` (somebody put a scalar under a collecting key) is folded in as the first\n * element rather than thrown away, so the fan-out answer is never silently truncated.\n */\n // webpieces-disable no-any-unknown -- the context store is honestly type-erased; the key's own V types the public accessors\n mergeResponseValue(existing: unknown, incoming: string): unknown {\n switch (this.responseMerge) {\n case 'first':\n return existing === undefined ? incoming : existing;\n case 'last':\n return incoming;\n case 'collect':\n if (Array.isArray(existing)) {\n // webpieces-disable no-any-unknown -- a collect key's accumulated list is honestly type-erased here, exactly like every other key-agnostic serialization read\n return [...(existing as unknown[]), incoming];\n }\n return existing === undefined ? [incoming] : [existing, incoming];\n }\n }\n\n /**\n * True for a key built by {@link trusted}. Trust is BINARY, so this ONE predicate answers it in\n * both directions and there is deliberately no `isUntrusted()` twin. The RUNTIME check used by the\n * inbound fill and the AuthFilter reconciliation; ordinary application code should never need it,\n * because the typed verbs already made the decision at compile time.\n *\n * It is a TYPE PREDICATE, so BOTH branches are typed, with NO cast on either side: the `if` holds a\n * `ContextKey<V, 'trusted'>` for `putTrusted` / `PendingWireTrust.stash`, and the `else` holds a\n * `ContextKey<V, 'untrusted'>` for `putUntrusted`. The `else` types only because\n * {@link AnyContextKey} is a UNION of the two branches — see that alias for why the negative of a\n * predicate needs something to drop. Before this, every such site wrote `key as\n * AnyTrustedContextKey`; a cast is exactly the thing an agent copies to the one place it is not\n * warranted, so the runtime check produces the type it proves instead.\n *\n * That is what makes a loop over a mixed `AnyContextKey[]` — the {@link HeaderRegistry} arrays, a\n * browser-log payload re-stated into a detached scope — safe BY CONSTRUCTION: the loop can only\n * write a key whose trust it has just tested, and a trusted key cannot reach `putUntrusted` at all,\n * so a loop fed by a source that proves nothing cannot fabricate a proven value, and nobody has to\n * remember to filter. That is a limit on the SOURCE, not on the key: a trusted key is written all\n * the time via `putTrusted`, by an authenticator or by app code that proved the value out of band.\n */\n isTrusted(): this is ContextKey<V, 'trusted'> {\n return this.trust === 'trusted';\n }\n\n /**\n * The value as it should appear in a log line: returned as-is for a normal\n * key, partially masked when this key sets `maskInLogs`. Masking is length-based:\n * - Length > 15: first 3 + \"...\" + last 3\n * - Length 8-15: first 2 + \"...\"\n * - Length < 8: \"<secure key too short to log>\"\n */\n maskForLogs(value: string): string {\n if (!this.maskInLogs) {\n return value;\n }\n const len = value.length;\n if (len < 8) {\n return '<secure key too short to log>';\n } else if (len <= 15) {\n return `${value.substring(0, 2)}...`;\n } else {\n return `${value.substring(0, 3)}...${value.substring(len - 3)}`;\n }\n }\n}\n"]}
@@ -41,8 +41,8 @@ export declare class ContextMgr {
41
41
  *
42
42
  * `destination` gates TRUSTED keys exactly as it does on the server side (see
43
43
  * {@link DestinationTrust}). In practice a browser never reaches the permissive branch — twice
44
- * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an `@WpAuthOidc` /
45
- * `@WpAuthSharedSecret` contract at all, so every browser destination is `@WpAuthJwt` or `@WpAuthPublic`.
44
+ * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an `oidc(...)` /
45
+ * `sharedSecret(...)` contract at all, so every browser destination is `jwt()` or `@WpAuthPublic`.
46
46
  * The rule is applied here anyway rather than argued away, because the OTHER guarantee people
47
47
  * reach for — "`MutableContextStore.set` only accepts an untrusted key, so a browser store
48
48
  * cannot HOLD a trusted value" — is true of that store and NOT of the seam: {@link ContextMgr}
@@ -65,7 +65,7 @@ export declare class ContextMgr {
65
65
  * `if` is what makes that a compile-time fact rather than a convention: {@link ContextWriter.set}
66
66
  * takes an {@link AnyUntrustedContextKey}, so a browser store CANNOT hold a proven value. That is
67
67
  * the same rule as the outbound direction reaches by a different route — every browser
68
- * destination is `@WpAuthJwt` or `@WpAuthPublic`, so `destination.allows` would refuse them
68
+ * destination is `jwt()` or `@WpAuthPublic`, so `destination.allows` would refuse them
69
69
  * anyway — and `destination` is still threaded and still applied, so a future browser destination
70
70
  * that DID authenticate its caller does not silently start believing response headers.
71
71
  */
@@ -46,8 +46,8 @@ class ContextMgr {
46
46
  *
47
47
  * `destination` gates TRUSTED keys exactly as it does on the server side (see
48
48
  * {@link DestinationTrust}). In practice a browser never reaches the permissive branch — twice
49
- * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an `@WpAuthOidc` /
50
- * `@WpAuthSharedSecret` contract at all, so every browser destination is `@WpAuthJwt` or `@WpAuthPublic`.
49
+ * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an `oidc(...)` /
50
+ * `sharedSecret(...)` contract at all, so every browser destination is `jwt()` or `@WpAuthPublic`.
51
51
  * The rule is applied here anyway rather than argued away, because the OTHER guarantee people
52
52
  * reach for — "`MutableContextStore.set` only accepts an untrusted key, so a browser store
53
53
  * cannot HOLD a trusted value" — is true of that store and NOT of the seam: {@link ContextMgr}
@@ -93,7 +93,7 @@ class ContextMgr {
93
93
  * `if` is what makes that a compile-time fact rather than a convention: {@link ContextWriter.set}
94
94
  * takes an {@link AnyUntrustedContextKey}, so a browser store CANNOT hold a proven value. That is
95
95
  * the same rule as the outbound direction reaches by a different route — every browser
96
- * destination is `@WpAuthJwt` or `@WpAuthPublic`, so `destination.allows` would refuse them
96
+ * destination is `jwt()` or `@WpAuthPublic`, so `destination.allows` would refuse them
97
97
  * anyway — and `destination` is still threaded and still applied, so a future browser destination
98
98
  * that DID authenticate its caller does not silently start believing response headers.
99
99
  */
@@ -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'));\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 `@WpAuthOidc` /\n * `@WpAuthSharedSecret` contract at all, so every browser destination is `@WpAuthJwt` 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 `@WpAuthJwt` 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":";;;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'));\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(...)` /\n * `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"]}
@@ -0,0 +1,4 @@
1
+ /** Each removed spelling has its own assertion so one missing export cannot mask a shim. */
2
+ export declare class DeletedAuthCompileAssertions {
3
+ removedExports(): void;
4
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeletedAuthCompileAssertions = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const CoreUtil = tslib_1.__importStar(require("../index"));
6
+ /** Each removed spelling has its own assertion so one missing export cannot mask a shim. */
7
+ class DeletedAuthCompileAssertions {
8
+ removedExports() {
9
+ // @ts-expect-error use WpAuth([jwt()]) and WpAuthorization
10
+ void CoreUtil.WpAuthJwt;
11
+ // @ts-expect-error use WpAuth([oidc(...)]) and WpAuthorization
12
+ void CoreUtil.WpAuthOidc;
13
+ // @ts-expect-error use WpAuth([sharedSecret(...)]) and WpAuthorization
14
+ void CoreUtil.WpAuthSharedSecret;
15
+ // @ts-expect-error use WpAuth([webhook(...)]) and WpAuthorization
16
+ void CoreUtil.WpAuthWebhook;
17
+ // @ts-expect-error use WpAuth([apiKey(...)]) and WpAuthorization
18
+ void CoreUtil.WpAuthApiKey;
19
+ // @ts-expect-error use separate WpLocalOnly, authentication and authorization
20
+ void CoreUtil.WpAuthLocalOnly;
21
+ // @ts-expect-error MCP shares receiving endpoint WpAuthorization
22
+ void CoreUtil.WpMcpAuthJwt;
23
+ // @ts-expect-error unresolved credentials are read with getAuthMeta
24
+ void CoreUtil.getAuthMode;
25
+ // @ts-expect-error roles belong to common operation authorization
26
+ void CoreUtil.rolesRequired;
27
+ }
28
+ }
29
+ exports.DeletedAuthCompileAssertions = DeletedAuthCompileAssertions;
30
+ //# sourceMappingURL=DeletedAuthCompileAssertions.js.map
@@ -0,0 +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,+DAA+D;QAC/D,KAAK,QAAQ,CAAC,UAAU,CAAC;QACzB,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;AArBD,oEAqBC","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([oidc(...)]) and WpAuthorization\n void CoreUtil.WpAuthOidc;\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"]}
@@ -7,7 +7,7 @@ import { AuthMode } from './auth-mode';
7
7
  * ## Why the client has to answer this at all
8
8
  *
9
9
  * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a
10
- * route that verified WHO called it — `@WpAuthOidc` / `@WpAuthSharedSecret`. On a `@WpAuthJwt` or `@WpAuthPublic`
10
+ * route that verified WHO called it — `oidc(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`
11
11
  * route the same header must match what the authenticator independently derived, or the request is
12
12
  * REJECTED with a 401.
13
13
  *
@@ -33,14 +33,14 @@ import { AuthMode } from './auth-mode';
33
33
  export declare class DestinationTrust {
34
34
  private readonly verifiesCaller;
35
35
  /**
36
- * The destination authenticates its CALLER (@WpAuthOidc / @WpAuthSharedSecret), so it is entitled to
36
+ * The destination authenticates its CALLER (oidc(...) / sharedSecret(...)), so it is entitled to
37
37
  * believe context WE vouch for — this is the service-to-service identity propagation that trusted
38
38
  * keys keep an `httpHeader` for.
39
39
  */
40
40
  private static readonly VERIFIES_CALLER;
41
41
  /**
42
- * The destination cannot tell us from a browser with curl (@WpAuthJwt / @WpAuthPublic / @WpAuthWebhook /
43
- * @WpAuthApiKey / @WpAuthLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.
42
+ * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / webhook(...) /
43
+ * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.
44
44
  * Untrusted keys still travel.
45
45
  */
46
46
  private static readonly CANNOT_VERIFY_CALLER;
@@ -61,8 +61,8 @@ export declare class DestinationTrust {
61
61
  * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination
62
62
  * will authenticate US, because that is the only case its `AuthFilter` will admit it.
63
63
  * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a
64
- * destination this client authenticated to — an `@WpAuthOidc` / `@WpAuthSharedSecret` peer is a
65
- * service in our own trust domain, while a `@WpAuthPublic` / `@WpAuthJwt` / partner endpoint is
64
+ * destination this client authenticated to — an `oidc(...)` / `sharedSecret(...)` peer is a
65
+ * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is
66
66
  * somebody else's process whose response headers are an assertion, not a proof.
67
67
  *
68
68
  * A second, identically-bodied `believesResponsesFrom(key)` would be two spellings of one
@@ -8,7 +8,7 @@ exports.DestinationTrust = void 0;
8
8
  * ## Why the client has to answer this at all
9
9
  *
10
10
  * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a
11
- * route that verified WHO called it — `@WpAuthOidc` / `@WpAuthSharedSecret`. On a `@WpAuthJwt` or `@WpAuthPublic`
11
+ * route that verified WHO called it — `oidc(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`
12
12
  * route the same header must match what the authenticator independently derived, or the request is
13
13
  * REJECTED with a 401.
14
14
  *
@@ -34,14 +34,14 @@ exports.DestinationTrust = void 0;
34
34
  class DestinationTrust {
35
35
  verifiesCaller;
36
36
  /**
37
- * The destination authenticates its CALLER (@WpAuthOidc / @WpAuthSharedSecret), so it is entitled to
37
+ * The destination authenticates its CALLER (oidc(...) / sharedSecret(...)), so it is entitled to
38
38
  * believe context WE vouch for — this is the service-to-service identity propagation that trusted
39
39
  * keys keep an `httpHeader` for.
40
40
  */
41
41
  static VERIFIES_CALLER = new DestinationTrust(true);
42
42
  /**
43
- * The destination cannot tell us from a browser with curl (@WpAuthJwt / @WpAuthPublic / @WpAuthWebhook /
44
- * @WpAuthApiKey / @WpAuthLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.
43
+ * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / webhook(...) /
44
+ * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.
45
45
  * Untrusted keys still travel.
46
46
  */
47
47
  static CANNOT_VERIFY_CALLER = new DestinationTrust(false);
@@ -64,23 +64,22 @@ class DestinationTrust {
64
64
  return DestinationTrust.VERIFIES_CALLER;
65
65
  case 'jwt':
66
66
  case 'public':
67
- // @WpAuthWebhook authenticates an OUTSIDE VENDOR, which is not the same thing as
67
+ // webhook(...) authenticates an OUTSIDE VENDOR, which is not the same thing as
68
68
  // authenticating a peer in this repo. The vendor knows nothing of webpieces context
69
69
  // headers and would never send one, so there is no identity to propagate in either
70
70
  // direction — and a webpieces client cannot call such an endpoint anyway (it cannot mint
71
71
  // the vendor's signature). Trusted keys stay home.
72
72
  case 'webhook':
73
- // @WpAuthApiKey authenticates a CUSTOMER, not a peer service. The holder of the key is
73
+ // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is
74
74
  // another company's codebase, so nothing it forwards may be believed, and no webpieces
75
75
  // client can call it anyway (the framework extracts no api-key header — the app's hook
76
76
  // owns which headers carry the credential; the contract's `credentials` list only DESCRIBES
77
77
  // them). Trusted keys stay home.
78
78
  case 'apikey':
79
- // @WpAuthLocalOnly authenticates NOBODY — it gates on the environment, not on a
80
- // credential — so a browser with curl on the same laptop is indistinguishable from us.
81
- // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to
82
- // AuthMode is a compile error here rather than a silent permissive fallthrough.)
83
- case 'local-only':
79
+ // @WpLocalOnly authenticates NOBODY — it gates on the environment, not on a
80
+ // credential — so a browser with curl on the same laptop is indistinguishable from us.
81
+ // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to
82
+ // AuthMode is a compile error here rather than a silent permissive fallthrough.)
84
83
  return DestinationTrust.CANNOT_VERIFY_CALLER;
85
84
  }
86
85
  }
@@ -94,8 +93,8 @@ class DestinationTrust {
94
93
  * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination
95
94
  * will authenticate US, because that is the only case its `AuthFilter` will admit it.
96
95
  * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a
97
- * destination this client authenticated to — an `@WpAuthOidc` / `@WpAuthSharedSecret` peer is a
98
- * service in our own trust domain, while a `@WpAuthPublic` / `@WpAuthJwt` / partner endpoint is
96
+ * destination this client authenticated to — an `oidc(...)` / `sharedSecret(...)` peer is a
97
+ * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is
99
98
  * somebody else's process whose response headers are an assertion, not a proof.
100
99
  *
101
100
  * A second, identically-bodied `believesResponsesFrom(key)` would be two spellings of one
@@ -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,iFAAiF;YACjF,oFAAoF;YACpF,mFAAmF;YACnF,yFAAyF;YACzF,mDAAmD;YACnD,KAAK,SAAS,CAAC;YACf,uFAAuF;YACvF,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,iCAAiC;YACjC,KAAK,QAAQ,CAAC;YACd,gFAAgF;YAChF,uFAAuF;YACvF,wFAAwF;YACxF,iFAAiF;YACjF,KAAK,YAAY;gBACb,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;;AA1EL,4CA2EC","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 — `@WpAuthOidc` / `@WpAuthSharedSecret`. On a `@WpAuthJwt` 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 (@WpAuthOidc / @WpAuthSharedSecret), 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 (@WpAuthJwt / @WpAuthPublic / @WpAuthWebhook /\n * @WpAuthApiKey / @WpAuthLocalOnly / 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 // @WpAuthWebhook 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 // @WpAuthApiKey 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 // @WpAuthLocalOnly 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 case 'local-only':\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 `@WpAuthOidc` / `@WpAuthSharedSecret` peer is a\n * service in our own trust domain, while a `@WpAuthPublic` / `@WpAuthJwt` / 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":";;;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 — `oidc(...)` / `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 (oidc(...) / 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 `oidc(...)` / `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"]}
@@ -22,10 +22,10 @@ export interface EndpointOptions {
22
22
  */
23
23
  formPost?: boolean;
24
24
  /**
25
- * Retain the verbatim request bytes and absolute URL for a `@WpAuthWebhook` callback to verify.
25
+ * Retain the verbatim request bytes and absolute URL for a `webhook(...)` callback to verify.
26
26
  * This is retention, not new buffering: the Express adapter already accumulates the body.
27
27
  *
28
- * `@WpAuthWebhook` requires this option at wiring time. It combines with `formPost` for vendors
28
+ * `webhook(...)` requires this option at wiring time. It combines with `formPost` for vendors
29
29
  * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.
30
30
  */
31
31
  rawBody?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":";;;AAGA,8EAA8E;AAC9E,IAAK,sBAIJ;AAJD,WAAK,sBAAsB;IACvB,uCAAa,CAAA;IACb,+DAAqC,CAAA;IACrC,yCAAe,CAAA;AACnB,CAAC,EAJI,sBAAsB,KAAtB,sBAAsB,QAI1B;AAED,wDAAwD;AAC3C,QAAA,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;AACnC,QAAA,gBAAgB,GAAG,sBAAsB,CAAC,gBAAgB,CAAC;AAC3D,QAAA,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport { EndpointResponseType } from './HttpContract';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum EndpointOperationValue {\n READ = 'read',\n WRITE_IDEMPOTENT = 'write-idempotent',\n WRITE = 'write',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const READ = EndpointOperationValue.READ;\nexport const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;\nexport const WRITE = EndpointOperationValue.WRITE;\n\n/** The side-effect contract callers use to decide whether repeating an operation is safe. */\nexport type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;\n\n/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */\nexport interface EndpointOptions {\n /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */\n responseType?: EndpointResponseType;\n /**\n * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request\n * DTO must be flat; urlencoded has no nesting. Default false = JSON.\n */\n formPost?: boolean;\n /**\n * Retain the verbatim request bytes and absolute URL for a `@WpAuthWebhook` callback to verify.\n * This is retention, not new buffering: the Express adapter already accumulates the body.\n *\n * `@WpAuthWebhook` requires this option at wiring time. It combines with `formPost` for vendors\n * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.\n */\n rawBody?: boolean;\n /**\n * This route is plumbing the USER did not ask for — a log shipper, a heartbeat, a telemetry\n * flush. It is not progress to report, and it must not be logged as an api call, because the\n * line it would write is itself the thing being shipped.\n *\n * Rides the route metadata for the same reason {@link formPost} and {@link rawBody} do: the\n * consumer branches on the ROUTE, without knowing the apiClass/methodName. Two consumers read\n * it today — {@link LogApiCallImpl} emits no `[API-*-req]`/`[API-*-resp-*]` line for the call,\n * and an app's `RequestLifecycleListener` can skip its progress bar and its own RPC\n * instrumentation.\n *\n * WHY a declaration and not a suppression switch: shipping a log over a logged transport logs\n * about shipping logs. The two mechanisms that do not know the endpoint both failed in prod\n * (see #976) — a wall-clock \"we are shipping\" boolean swallowed unrelated lines including an\n * error alarm, and a logger-name allowlist was incomplete by construction the moment anyone\n * added a logger, producing a steady-state 4-lines-per-second request loop on an idle page.\n * The ENDPOINT is the only stable, declarative fact, so it is where the decision belongs.\n *\n * Default false: absent means the ordinary, fully-logged route. The quiet path is the one you\n * have to say out loud, so `grep -rn background` lists every endpoint webpieces stops logging.\n */\n background?: boolean;\n /**\n * This route is NOT part of the CUSTOMER-facing contract. `public-openapi.json` — the document a\n * customer gets, and the only one a publish path uploads — contains no path, no operation, no\n * schema and no prose for it. It is not \"rendered with a flag set\", it is absent.\n *\n * It is still in `full-private-openapi.json`, and still in `mcp-openapi.json` when it carries\n * `@WpMcpTool`. The reason it is withheld belongs in the method's JSDoc, which is where every\n * other piece of its documentation already lives.\n *\n * WHY A BOOLEAN HERE IS NOT SHIM SHAPE #5, given that `@ApiType` exists precisely to avoid a\n * falsy default meaning \"publish\": the two operate at different levels and the default here is\n * NOT the permissive one. A contract reaches customers only by declaring\n * `@ApiType(..., EXTERNAL_CUSTOMER)` — one deliberate token per contract — and `hidden` only ever\n * SUBTRACTS from what that token already granted. Omitting it on a contract nobody published to\n * customers publishes nothing. The per-method case it exists for is real and cannot be said at\n * class level: a NEW endpoint on an already-published API, built on main before it is announced.\n *\n * Default false, and `grep -rn \"hidden: true\"` lists every method withheld from customers.\n *\n * HIDING IS A DOCUMENTATION DECISION AND NEVER AN ACCESS-CONTROL ONE. The route is served exactly\n * as before and is still reachable by anything holding its credential — including an agent, if it\n * also carries `@WpMcpTool`. Declare its authorization as loudly as a published one.\n */\n hidden?: boolean;\n /**\n * This operation may touch systems OUTSIDE this service — a payment processor, a partner's API,\n * the physical world. It feeds MCP's `openWorldHint`, and it is rendered as a sentence in the\n * operation's description so a human integrating against the document sees it too.\n *\n * DEFAULT FALSE, deliberately unlike the MCP specification's own `true` default. MCP defaults to\n * \"open world\" because it cannot know; webpieces CAN know, because the author of the endpoint is\n * right here. Defaulting to `true` would make the interesting case the silent one and the boring\n * case the one you have to type, which is backwards — and an agent told that every endpoint might\n * reach the outside world has been told nothing. `grep -rn \"openWorld: true\"` lists the ones that\n * genuinely do.\n */\n openWorld?: boolean;\n}\n\n/** An external endpoint must also identify the outside system that calls it. */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system (`'twilio'`) — graph identity, not display text. */\n calledBy: string;\n /** What that caller is; picks its graph shape. Defaults to `'saas'`. */\n callerKind?: ExternalSystemKind;\n}\n"]}
1
+ {"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":";;;AAGA,8EAA8E;AAC9E,IAAK,sBAIJ;AAJD,WAAK,sBAAsB;IACvB,uCAAa,CAAA;IACb,+DAAqC,CAAA;IACrC,yCAAe,CAAA;AACnB,CAAC,EAJI,sBAAsB,KAAtB,sBAAsB,QAI1B;AAED,wDAAwD;AAC3C,QAAA,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;AACnC,QAAA,gBAAgB,GAAG,sBAAsB,CAAC,gBAAgB,CAAC;AAC3D,QAAA,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport { EndpointResponseType } from './HttpContract';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum EndpointOperationValue {\n READ = 'read',\n WRITE_IDEMPOTENT = 'write-idempotent',\n WRITE = 'write',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const READ = EndpointOperationValue.READ;\nexport const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;\nexport const WRITE = EndpointOperationValue.WRITE;\n\n/** The side-effect contract callers use to decide whether repeating an operation is safe. */\nexport type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;\n\n/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */\nexport interface EndpointOptions {\n /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */\n responseType?: EndpointResponseType;\n /**\n * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request\n * DTO must be flat; urlencoded has no nesting. Default false = JSON.\n */\n formPost?: boolean;\n /**\n * Retain the verbatim request bytes and absolute URL for a `webhook(...)` callback to verify.\n * This is retention, not new buffering: the Express adapter already accumulates the body.\n *\n * `webhook(...)` requires this option at wiring time. It combines with `formPost` for vendors\n * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.\n */\n rawBody?: boolean;\n /**\n * This route is plumbing the USER did not ask for — a log shipper, a heartbeat, a telemetry\n * flush. It is not progress to report, and it must not be logged as an api call, because the\n * line it would write is itself the thing being shipped.\n *\n * Rides the route metadata for the same reason {@link formPost} and {@link rawBody} do: the\n * consumer branches on the ROUTE, without knowing the apiClass/methodName. Two consumers read\n * it today — {@link LogApiCallImpl} emits no `[API-*-req]`/`[API-*-resp-*]` line for the call,\n * and an app's `RequestLifecycleListener` can skip its progress bar and its own RPC\n * instrumentation.\n *\n * WHY a declaration and not a suppression switch: shipping a log over a logged transport logs\n * about shipping logs. The two mechanisms that do not know the endpoint both failed in prod\n * (see #976) — a wall-clock \"we are shipping\" boolean swallowed unrelated lines including an\n * error alarm, and a logger-name allowlist was incomplete by construction the moment anyone\n * added a logger, producing a steady-state 4-lines-per-second request loop on an idle page.\n * The ENDPOINT is the only stable, declarative fact, so it is where the decision belongs.\n *\n * Default false: absent means the ordinary, fully-logged route. The quiet path is the one you\n * have to say out loud, so `grep -rn background` lists every endpoint webpieces stops logging.\n */\n background?: boolean;\n /**\n * This route is NOT part of the CUSTOMER-facing contract. `public-openapi.json` — the document a\n * customer gets, and the only one a publish path uploads — contains no path, no operation, no\n * schema and no prose for it. It is not \"rendered with a flag set\", it is absent.\n *\n * It is still in `full-private-openapi.json`, and still in `mcp-openapi.json` when it carries\n * `@WpMcpTool`. The reason it is withheld belongs in the method's JSDoc, which is where every\n * other piece of its documentation already lives.\n *\n * WHY A BOOLEAN HERE IS NOT SHIM SHAPE #5, given that `@ApiType` exists precisely to avoid a\n * falsy default meaning \"publish\": the two operate at different levels and the default here is\n * NOT the permissive one. A contract reaches customers only by declaring\n * `@ApiType(..., EXTERNAL_CUSTOMER)` — one deliberate token per contract — and `hidden` only ever\n * SUBTRACTS from what that token already granted. Omitting it on a contract nobody published to\n * customers publishes nothing. The per-method case it exists for is real and cannot be said at\n * class level: a NEW endpoint on an already-published API, built on main before it is announced.\n *\n * Default false, and `grep -rn \"hidden: true\"` lists every method withheld from customers.\n *\n * HIDING IS A DOCUMENTATION DECISION AND NEVER AN ACCESS-CONTROL ONE. The route is served exactly\n * as before and is still reachable by anything holding its credential — including an agent, if it\n * also carries `@WpMcpTool`. Declare its authorization as loudly as a published one.\n */\n hidden?: boolean;\n /**\n * This operation may touch systems OUTSIDE this service — a payment processor, a partner's API,\n * the physical world. It feeds MCP's `openWorldHint`, and it is rendered as a sentence in the\n * operation's description so a human integrating against the document sees it too.\n *\n * DEFAULT FALSE, deliberately unlike the MCP specification's own `true` default. MCP defaults to\n * \"open world\" because it cannot know; webpieces CAN know, because the author of the endpoint is\n * right here. Defaulting to `true` would make the interesting case the silent one and the boring\n * case the one you have to type, which is backwards — and an agent told that every endpoint might\n * reach the outside world has been told nothing. `grep -rn \"openWorld: true\"` lists the ones that\n * genuinely do.\n */\n openWorld?: boolean;\n}\n\n/** An external endpoint must also identify the outside system that calls it. */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system (`'twilio'`) — graph identity, not display text. */\n calledBy: string;\n /** What that caller is; picks its graph shape. Defaults to `'saas'`. */\n callerKind?: ExternalSystemKind;\n}\n"]}
@@ -23,6 +23,11 @@ export declare class RouteMetadata {
23
23
  readonly responseType: EndpointResponseType;
24
24
  /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
25
25
  readonly streaming?: StreamingEndpointMetadata | undefined;
26
+ apiClass?: Function;
27
+ authorization?: import('./authorization').AuthorizationRequirement<unknown>;
28
+ localOnly: boolean;
29
+ /** Clients clone metadata per call so a concrete winning mode governs both trust directions. */
30
+ withSelectedAuth(method: import('./auth-mode').AuthMode): RouteMetadata;
26
31
  httpMethod: string;
27
32
  path: string;
28
33
  methodName: string;
@@ -44,7 +49,7 @@ export declare class RouteMetadata {
44
49
  readonly mask?: MaskSpec;
45
50
  /**
46
51
  * True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +
47
- * absolute url for the `@WpAuthWebhook` hook to verify a vendor signature over. Rides the route
52
+ * absolute url for the `webhook(...)` hook to verify a vendor signature over. Rides the route
48
53
  * metadata for the same reason {@link formPost} does — the transport adapter decides how to read
49
54
  * the body from the ROUTE, without knowing the apiClass/methodName.
50
55
  */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RouteMetadata = void 0;
4
+ const auth_mode_1 = require("./auth-mode");
4
5
  /**
5
6
  * Route metadata stored per-method at runtime.
6
7
  * Used internally by http-routing and http-client as the runtime representation
@@ -16,6 +17,18 @@ class RouteMetadata {
16
17
  bodyParameterIndex;
17
18
  responseType;
18
19
  streaming;
20
+ apiClass;
21
+ // webpieces-disable no-any-unknown -- erased application policy is validated by AuthorizationHook at server startup
22
+ authorization;
23
+ localOnly = false;
24
+ /** Clients clone metadata per call so a concrete winning mode governs both trust directions. */
25
+ withSelectedAuth(method) {
26
+ if (!this.authMeta?.methods.includes(method))
27
+ throw new Error('Selected credential is not declared by this endpoint.');
28
+ const route = Object.assign(new RouteMetadata(this.httpMethod, this.path, this.methodName, this.operation), this);
29
+ route.authMeta = new auth_mode_1.AuthMeta([method], method.kind === 'public' ? this.authMeta.publicReason : undefined);
30
+ return route;
31
+ }
19
32
  httpMethod;
20
33
  path;
21
34
  methodName;
@@ -37,7 +50,7 @@ class RouteMetadata {
37
50
  mask;
38
51
  /**
39
52
  * True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +
40
- * absolute url for the `@WpAuthWebhook` hook to verify a vendor signature over. Rides the route
53
+ * absolute url for the `webhook(...)` hook to verify a vendor signature over. Rides the route
41
54
  * metadata for the same reason {@link formPost} does — the transport adapter decides how to read
42
55
  * the body from the ROUTE, without knowing the apiClass/methodName.
43
56
  */
@@ -1 +1 @@
1
- {"version":3,"file":"RouteMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadata.ts"],"names":[],"mappings":";;;AAMA;;;;;;;;GAQG;AACH,MAAa,aAAa;IA6CT;IAQA;IAEA;IAEA;IAEA;IA1Db,UAAU,CAAS;IACnB,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,mBAAmB,CAAU;IAC7B,QAAQ,CAAY;IACpB,wFAAwF;IACxF,OAAO,CAAU;IACjB;;;;OAIG;IACM,QAAQ,CAAU;IAC3B;;;;OAIG;IACM,IAAI,CAAY;IACzB;;;;;OAKG;IACM,OAAO,CAAU;IAC1B;;;;;;;;;;OAUG;IACM,UAAU,CAAU;IAE7B,YACI,UAAkB,EAClB,IAAY,EACZ,UAAkB;IAClB,iFAAiF;IACxE,SAA4B,EACrC,mBAA4B,EAC5B,QAAmB,EACnB,OAAgB,EAChB,WAAoB,KAAK,EACzB,IAAe,EACf,UAAmB,KAAK;IACxB,8DAA8D;IACrD,oBAAqD,EAAE;IAChE,8EAA8E;IACrE,kBAA2B;IACpC,yFAAyF;IAChF,eAAqC,MAAM;IACpD,+EAA+E;IACtE,SAAqC,EAC9C,aAAsB,KAAK;QAflB,cAAS,GAAT,SAAS,CAAmB;QAQ5B,sBAAiB,GAAjB,iBAAiB,CAAsC;QAEvD,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,iBAAY,GAAZ,YAAY,CAA+B;QAE3C,cAAS,GAAT,SAAS,CAA4B;QAG9C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,CAAC;CACJ;AA7ED,sCA6EC","sourcesContent":["import { MaskSpec } from './LogFieldMask';\nimport { AuthMeta } from './auth-mode';\nimport { EndpointResponseType, HttpParameterBinding } from './HttpContract';\nimport { StreamingEndpointMetadata } from './StreamingContract';\nimport { EndpointOperation } from './HttpEndpointOptions';\n\n/**\n * Route metadata stored per-method at runtime.\n * Used internally by http-routing and http-client as the runtime representation\n * of a route. Constructed from @ApiPath + @Endpoint metadata by ProxyClient\n * and ApiRoutingFactory.\n *\n * Lives in its own file (one class per file) purely for file size, exactly as `api-kind.ts` and\n * `external-caller.ts` were split off `decorators.ts` before it. Nothing about its role changed.\n */\nexport class RouteMetadata {\n httpMethod: string;\n path: string;\n methodName: string;\n controllerClassName?: string;\n authMeta?: AuthMeta;\n /** The API contract class name (e.g. 'SaveApi') — distinct from the controller name. */\n apiName?: string;\n /**\n * True when @Endpoint(..., { formPost: true }): the body is application/x-www-form-urlencoded\n * (flat key→value), not JSON. Rides the route metadata so the per-route body parse can branch\n * without knowing the apiClass/methodName. Default false = JSON.\n */\n readonly formPost: boolean;\n /**\n * The @MaskLog field-mask spec for this route, or undefined when the method declared none. Read\n * ONCE here at route-build time and handed to {@link LogApiCallImpl} via ApiMethodInfo, so the per-call\n * log path pays for masking only on routes that opted in (the rest stay on plain JSON.stringify).\n */\n readonly mask?: MaskSpec;\n /**\n * True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +\n * absolute url for the `@WpAuthWebhook` hook to verify a vendor signature over. Rides the route\n * metadata for the same reason {@link formPost} does — the transport adapter decides how to read\n * the body from the ROUTE, without knowing the apiClass/methodName.\n */\n readonly rawBody: boolean;\n /**\n * True when @Endpoint(..., { background: true }): this route is plumbing the user did not ask\n * for (a log shipper, a heartbeat, a telemetry flush). Rides the route metadata for the same\n * reason {@link formPost} and {@link rawBody} do — each consumer branches on the ROUTE, without\n * knowing the apiClass/methodName.\n *\n * Read by {@link LogApiCallImpl} (via ApiMethodInfo, exactly as {@link mask} is) so no\n * `[API-*-req]`/`[API-*-resp-*]` line is emitted for the call, and readable from an app's\n * `RequestLifecycleListener.onRequestStart/onRequestEnd` so a progress bar and any app-level RPC\n * instrumentation can skip it too. Default false = an ordinary, fully-logged route.\n */\n readonly background: boolean;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n /** Explicit side-effect semantics; deliberately independent of the HTTP verb. */\n readonly operation: EndpointOperation,\n controllerClassName?: string,\n authMeta?: AuthMeta,\n apiName?: string,\n formPost: boolean = false,\n mask?: MaskSpec,\n rawBody: boolean = false,\n /** Explicit path/query mappings, in API declaration order. */\n readonly parameterBindings: readonly HttpParameterBinding[] = [],\n /** The sole unannotated POST parameter, or undefined for a bodyless route. */\n readonly bodyParameterIndex?: number,\n /** Whether clients receive only the body or the complete status/header/body response. */\n readonly responseType: EndpointResponseType = 'body',\n /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */\n readonly streaming?: StreamingEndpointMetadata,\n background: boolean = false,\n ) {\n this.httpMethod = httpMethod;\n this.path = path;\n this.methodName = methodName;\n this.controllerClassName = controllerClassName;\n this.authMeta = authMeta;\n this.apiName = apiName;\n this.formPost = formPost;\n this.mask = mask;\n this.rawBody = rawBody;\n this.parameterBindings = parameterBindings;\n this.bodyParameterIndex = bodyParameterIndex;\n this.responseType = responseType;\n this.streaming = streaming;\n this.background = background;\n }\n}\n"]}
1
+ {"version":3,"file":"RouteMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadata.ts"],"names":[],"mappings":";;;AACA,2CAAuC;AAKvC;;;;;;;;GAQG;AACH,MAAa,aAAa;IAyDT;IAQA;IAEA;IAEA;IAEA;IAtEb,QAAQ,CAAY;IACpB,oHAAoH;IACpH,aAAa,CAA+D;IAC5E,SAAS,GAAG,KAAK,CAAC;IAElB,gGAAgG;IAChG,gBAAgB,CAAC,MAAsC;QACnD,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QACvH,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC;QAClH,KAAK,CAAC,QAAQ,GAAG,IAAI,oBAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC3G,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,UAAU,CAAS;IACnB,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,mBAAmB,CAAU;IAC7B,QAAQ,CAAY;IACpB,wFAAwF;IACxF,OAAO,CAAU;IACjB;;;;OAIG;IACM,QAAQ,CAAU;IAC3B;;;;OAIG;IACM,IAAI,CAAY;IACzB;;;;;OAKG;IACM,OAAO,CAAU;IAC1B;;;;;;;;;;OAUG;IACM,UAAU,CAAU;IAE7B,YACI,UAAkB,EAClB,IAAY,EACZ,UAAkB;IAClB,iFAAiF;IACxE,SAA4B,EACrC,mBAA4B,EAC5B,QAAmB,EACnB,OAAgB,EAChB,WAAoB,KAAK,EACzB,IAAe,EACf,UAAmB,KAAK;IACxB,8DAA8D;IACrD,oBAAqD,EAAE;IAChE,8EAA8E;IACrE,kBAA2B;IACpC,yFAAyF;IAChF,eAAqC,MAAM;IACpD,+EAA+E;IACtE,SAAqC,EAC9C,aAAsB,KAAK;QAflB,cAAS,GAAT,SAAS,CAAmB;QAQ5B,sBAAiB,GAAjB,iBAAiB,CAAsC;QAEvD,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,iBAAY,GAAZ,YAAY,CAA+B;QAE3C,cAAS,GAAT,SAAS,CAA4B;QAG9C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,CAAC;CACJ;AAzFD,sCAyFC","sourcesContent":["import { MaskSpec } from './LogFieldMask';\nimport { AuthMeta } from './auth-mode';\nimport { EndpointResponseType, HttpParameterBinding } from './HttpContract';\nimport { StreamingEndpointMetadata } from './StreamingContract';\nimport { EndpointOperation } from './HttpEndpointOptions';\n\n/**\n * Route metadata stored per-method at runtime.\n * Used internally by http-routing and http-client as the runtime representation\n * of a route. Constructed from @ApiPath + @Endpoint metadata by ProxyClient\n * and ApiRoutingFactory.\n *\n * Lives in its own file (one class per file) purely for file size, exactly as `api-kind.ts` and\n * `external-caller.ts` were split off `decorators.ts` before it. Nothing about its role changed.\n */\nexport class RouteMetadata {\n apiClass?: Function;\n // webpieces-disable no-any-unknown -- erased application policy is validated by AuthorizationHook at server startup\n authorization?: import('./authorization').AuthorizationRequirement<unknown>;\n localOnly = false;\n\n /** Clients clone metadata per call so a concrete winning mode governs both trust directions. */\n withSelectedAuth(method: import('./auth-mode').AuthMode): RouteMetadata {\n if (!this.authMeta?.methods.includes(method)) throw new Error('Selected credential is not declared by this endpoint.');\n const route = Object.assign(new RouteMetadata(this.httpMethod, this.path, this.methodName, this.operation), this);\n route.authMeta = new AuthMeta([method], method.kind === 'public' ? this.authMeta.publicReason : undefined);\n return route;\n }\n httpMethod: string;\n path: string;\n methodName: string;\n controllerClassName?: string;\n authMeta?: AuthMeta;\n /** The API contract class name (e.g. 'SaveApi') — distinct from the controller name. */\n apiName?: string;\n /**\n * True when @Endpoint(..., { formPost: true }): the body is application/x-www-form-urlencoded\n * (flat key→value), not JSON. Rides the route metadata so the per-route body parse can branch\n * without knowing the apiClass/methodName. Default false = JSON.\n */\n readonly formPost: boolean;\n /**\n * The @MaskLog field-mask spec for this route, or undefined when the method declared none. Read\n * ONCE here at route-build time and handed to {@link LogApiCallImpl} via ApiMethodInfo, so the per-call\n * log path pays for masking only on routes that opted in (the rest stay on plain JSON.stringify).\n */\n readonly mask?: MaskSpec;\n /**\n * True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +\n * absolute url for the `webhook(...)` hook to verify a vendor signature over. Rides the route\n * metadata for the same reason {@link formPost} does — the transport adapter decides how to read\n * the body from the ROUTE, without knowing the apiClass/methodName.\n */\n readonly rawBody: boolean;\n /**\n * True when @Endpoint(..., { background: true }): this route is plumbing the user did not ask\n * for (a log shipper, a heartbeat, a telemetry flush). Rides the route metadata for the same\n * reason {@link formPost} and {@link rawBody} do — each consumer branches on the ROUTE, without\n * knowing the apiClass/methodName.\n *\n * Read by {@link LogApiCallImpl} (via ApiMethodInfo, exactly as {@link mask} is) so no\n * `[API-*-req]`/`[API-*-resp-*]` line is emitted for the call, and readable from an app's\n * `RequestLifecycleListener.onRequestStart/onRequestEnd` so a progress bar and any app-level RPC\n * instrumentation can skip it too. Default false = an ordinary, fully-logged route.\n */\n readonly background: boolean;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n /** Explicit side-effect semantics; deliberately independent of the HTTP verb. */\n readonly operation: EndpointOperation,\n controllerClassName?: string,\n authMeta?: AuthMeta,\n apiName?: string,\n formPost: boolean = false,\n mask?: MaskSpec,\n rawBody: boolean = false,\n /** Explicit path/query mappings, in API declaration order. */\n readonly parameterBindings: readonly HttpParameterBinding[] = [],\n /** The sole unannotated POST parameter, or undefined for a bodyless route. */\n readonly bodyParameterIndex?: number,\n /** Whether clients receive only the body or the complete status/header/body response. */\n readonly responseType: EndpointResponseType = 'body',\n /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */\n readonly streaming?: StreamingEndpointMetadata,\n background: boolean = false,\n ) {\n this.httpMethod = httpMethod;\n this.path = path;\n this.methodName = methodName;\n this.controllerClassName = controllerClassName;\n this.authMeta = authMeta;\n this.apiName = apiName;\n this.formPost = formPost;\n this.mask = mask;\n this.rawBody = rawBody;\n this.parameterBindings = parameterBindings;\n this.bodyParameterIndex = bodyParameterIndex;\n this.responseType = responseType;\n this.streaming = streaming;\n this.background = background;\n }\n}\n"]}