@webpieces/core-util 0.4.725 → 0.4.727
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ContextKey.d.ts +5 -3
- package/src/ContextKey.js +4 -2
- package/src/ContextKey.js.map +1 -1
- package/src/filters/Filter.d.ts +1 -1
- package/src/filters/Filter.js +1 -1
- package/src/filters/Filter.js.map +1 -1
- package/src/http/DestinationTrust.d.ts +2 -1
- package/src/http/DestinationTrust.js +2 -1
- package/src/http/DestinationTrust.js.map +1 -1
- package/src/http/RuntimeLocality.d.ts +2 -2
- package/src/http/RuntimeLocality.js.map +1 -1
package/package.json
CHANGED
package/src/ContextKey.d.ts
CHANGED
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
*
|
|
46
46
|
* The constructor is PRIVATE — you cannot make a key without picking a branch, and there
|
|
47
47
|
* is no default. A default would make the permissive branch the shortest thing to type,
|
|
48
|
-
* which is exactly the "widening that is an ABSENCE rather than a token" that
|
|
48
|
+
* which is exactly the "widening that is an ABSENCE rather than a token" that
|
|
49
|
+
* `.claude/rules/no-backwards-compat.md`
|
|
49
50
|
* rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so
|
|
50
51
|
* "trusted with no stated reason" cannot be written down.
|
|
51
52
|
*
|
|
@@ -82,7 +83,8 @@
|
|
|
82
83
|
* statement, never a default.
|
|
83
84
|
*
|
|
84
85
|
* The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can
|
|
85
|
-
* reject the wrong kind of key at COMPILE time rather than throwing at runtime (
|
|
86
|
+
* reject the wrong kind of key at COMPILE time rather than throwing at runtime (`.claude/rules/no-backwards-compat.md`
|
|
87
|
+
* treats a
|
|
86
88
|
* runtime throw standing in for an expressible type as a defect).
|
|
87
89
|
*/
|
|
88
90
|
/** The two kinds of context value. See the {@link ContextKey} class doc. */
|
|
@@ -111,7 +113,7 @@ export type AnyUntrustedContextKey = ContextKey<unknown, 'untrusted'>;
|
|
|
111
113
|
* goes straight to `putUntrusted` with no cast. Written as one type with a mixed `Trust` parameter
|
|
112
114
|
* there would be nothing to drop, the `else` would stay mixed, and the class would need a second
|
|
113
115
|
* `isUntrusted()` predicate to type the branch its own negative already decided — one runtime question
|
|
114
|
-
* with two spellings, which is the shim shape
|
|
116
|
+
* with two spellings, which is the shim shape `.claude/rules/no-backwards-compat.md` rejects.
|
|
115
117
|
*
|
|
116
118
|
* Mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)` takes one, but
|
|
117
119
|
* no WRITE verb does. A write must name the trust level, which is what keeps the `trusted` label
|
package/src/ContextKey.js
CHANGED
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
*
|
|
47
47
|
* The constructor is PRIVATE — you cannot make a key without picking a branch, and there
|
|
48
48
|
* is no default. A default would make the permissive branch the shortest thing to type,
|
|
49
|
-
* which is exactly the "widening that is an ABSENCE rather than a token" that
|
|
49
|
+
* which is exactly the "widening that is an ABSENCE rather than a token" that
|
|
50
|
+
* `.claude/rules/no-backwards-compat.md`
|
|
50
51
|
* rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so
|
|
51
52
|
* "trusted with no stated reason" cannot be written down.
|
|
52
53
|
*
|
|
@@ -83,7 +84,8 @@
|
|
|
83
84
|
* statement, never a default.
|
|
84
85
|
*
|
|
85
86
|
* The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can
|
|
86
|
-
* reject the wrong kind of key at COMPILE time rather than throwing at runtime (
|
|
87
|
+
* reject the wrong kind of key at COMPILE time rather than throwing at runtime (`.claude/rules/no-backwards-compat.md`
|
|
88
|
+
* treats a
|
|
87
89
|
* runtime throw standing in for an expressible type as a defect).
|
|
88
90
|
*/
|
|
89
91
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/src/ContextKey.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;;;AAyCH,MAAa,UAAU;IAgBnB,qEAAqE;IAC5D,IAAI,CAAS;IAEtB;;;OAGG;IACM,UAAU,CAAU;IAE7B,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;QAEjB,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;IAC7B,CAAC;IAED;;;;;;;;OAQG;IACH,6KAA6K;IAC7K,MAAM,CAAC,OAAO,CACV,IAAY,EACZ,UAAkB,EAClB,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAe,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvG,CAAC;IAED;;;;;;;OAOG;IACH,uHAAuH;IACvH,MAAM,CAAC,SAAS,CACZ,IAAY,EACZ,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAiB,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC1G,CAAC;IAED,uEAAuE;IACvE,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC;IACzC,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;AAvJD,gCAuJC","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 * - `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 CLAUDE.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 (`@AuthOidc`,\n * `@AuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a\n * browser (`@AuthJwt`, 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.md 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.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\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 /** 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 ) {\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 }\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 ): ContextKey<V, 'trusted'> {\n return new ContextKey<V, 'trusted'>(name, 'trusted', provenance, httpHeader, maskInLogs, isLogged);\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 ): ContextKey<V, 'untrusted'> {\n return new ContextKey<V, 'untrusted'>(name, 'untrusted', undefined, httpHeader, maskInLogs, isLogged);\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 /**\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;;;AAyCH,MAAa,UAAU;IAgBnB,qEAAqE;IAC5D,IAAI,CAAS;IAEtB;;;OAGG;IACM,UAAU,CAAU;IAE7B,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;QAEjB,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;IAC7B,CAAC;IAED;;;;;;;;OAQG;IACH,6KAA6K;IAC7K,MAAM,CAAC,OAAO,CACV,IAAY,EACZ,UAAkB,EAClB,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAe,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvG,CAAC;IAED;;;;;;;OAOG;IACH,uHAAuH;IACvH,MAAM,CAAC,SAAS,CACZ,IAAY,EACZ,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAiB,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC1G,CAAC;IAED,uEAAuE;IACvE,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC;IACzC,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;AAvJD,gCAuJC","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 * - `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 (`@AuthOidc`,\n * `@AuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a\n * browser (`@AuthJwt`, 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\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 /** 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 ) {\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 }\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 ): ContextKey<V, 'trusted'> {\n return new ContextKey<V, 'trusted'>(name, 'trusted', provenance, httpHeader, maskInLogs, isLogged);\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 ): ContextKey<V, 'untrusted'> {\n return new ContextKey<V, 'untrusted'>(name, 'untrusted', undefined, httpHeader, maskInLogs, isLogged);\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 /**\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"]}
|
package/src/filters/Filter.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface Service<REQ, RESP> {
|
|
|
36
36
|
*
|
|
37
37
|
* Declaring the abstraction once, in the package both depend on, is what keeps them ONE concept.
|
|
38
38
|
* A second `Filter`/`Service` pair defined beside the client chain would be two spellings of one
|
|
39
|
-
* thing — the shim shape
|
|
39
|
+
* thing — the shim shape `.claude/rules/no-backwards-compat.md` rejects — and the two would drift.
|
|
40
40
|
*
|
|
41
41
|
* core-util is browser-safe and dependency-free, and so is this file: it imports nothing.
|
|
42
42
|
*/
|
package/src/filters/Filter.js
CHANGED
|
@@ -22,7 +22,7 @@ exports.Filter = void 0;
|
|
|
22
22
|
*
|
|
23
23
|
* Declaring the abstraction once, in the package both depend on, is what keeps them ONE concept.
|
|
24
24
|
* A second `Filter`/`Service` pair defined beside the client chain would be two spellings of one
|
|
25
|
-
* thing — the shim shape
|
|
25
|
+
* thing — the shim shape `.claude/rules/no-backwards-compat.md` rejects — and the two would drift.
|
|
26
26
|
*
|
|
27
27
|
* core-util is browser-safe and dependency-free, and so is this file: it imports nothing.
|
|
28
28
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Filter.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/filters/Filter.ts"],"names":[],"mappings":";;;AAkBA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAsB,MAAM;IAaxB;;;;;;;;OAQG;IACH,KAAK,CAAC,UAA6B;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC;QAElB,OAAO,IAAI,CAAC,KAAM,SAAQ,MAAiB;YACvC,KAAK,CAAC,MAAM,CAAC,IAAS,EAAE,WAA+B;gBACnD,8DAA8D;gBAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;oBACrB,MAAM,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC;iBACxD,CAAC,CAAC;YACP,CAAC;SACJ,CAAC,EAAE,CAAC;IACT,CAAC;IAED;;;;;;;;OAQG;IACH,YAAY,CAAC,GAAuB;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC;QAElB,OAAO;YACH,MAAM,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC;SAChD,CAAC;IACN,CAAC;CACJ;AAnDD,wBAmDC","sourcesContent":["/**\n * Service interface - Similar to Java WebPieces Service<REQ, RESP>.\n * Represents any component that can process a request and return a response.\n *\n * Used for:\n * - The final invocation at the end of a chain (a controller server-side, `fetch` client-side)\n * - Wrapping filters as services in the chain\n * - Functional composition of filters\n */\nexport interface Service<REQ, RESP> {\n /**\n * Invoke the service with the given metadata.\n * @param meta - Request metadata\n * @returns Promise of the response\n */\n invoke(meta: REQ): Promise<RESP>;\n}\n\n/**\n * Filter abstract class - Similar to Java WebPieces Filter<REQ, RESP>.\n *\n * Filters are STATELESS and can handle N concurrent requests.\n * They wrap the execution of subsequent filters and the final service.\n *\n * Key principles:\n * - STATELESS: No instance variables for request data\n * - COMPOSABLE: Use chain() methods for functional composition\n *\n * ## Why this lives in core-util rather than beside either chain that uses it\n *\n * There are TWO chains in webpieces and they are the same concept pointed in opposite directions:\n *\n * - INBOUND, server side: `Filter<MethodMeta, WpResponse<unknown>>` (@webpieces/http-routing) wraps\n * the controller invocation.\n * - OUTBOUND, client side: `Filter<ClientRequest, Response>` (@webpieces/http-client-core) wraps the\n * `fetch`, so a filter can re-point the URL, add headers, or sign the exact serialized bytes.\n *\n * Declaring the abstraction once, in the package both depend on, is what keeps them ONE concept.\n * A second `Filter`/`Service` pair defined beside the client chain would be two spellings of one\n * thing — the shim shape
|
|
1
|
+
{"version":3,"file":"Filter.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/filters/Filter.ts"],"names":[],"mappings":";;;AAkBA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAsB,MAAM;IAaxB;;;;;;;;OAQG;IACH,KAAK,CAAC,UAA6B;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC;QAElB,OAAO,IAAI,CAAC,KAAM,SAAQ,MAAiB;YACvC,KAAK,CAAC,MAAM,CAAC,IAAS,EAAE,WAA+B;gBACnD,8DAA8D;gBAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;oBACrB,MAAM,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC;iBACxD,CAAC,CAAC;YACP,CAAC;SACJ,CAAC,EAAE,CAAC;IACT,CAAC;IAED;;;;;;;;OAQG;IACH,YAAY,CAAC,GAAuB;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC;QAElB,OAAO;YACH,MAAM,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC;SAChD,CAAC;IACN,CAAC;CACJ;AAnDD,wBAmDC","sourcesContent":["/**\n * Service interface - Similar to Java WebPieces Service<REQ, RESP>.\n * Represents any component that can process a request and return a response.\n *\n * Used for:\n * - The final invocation at the end of a chain (a controller server-side, `fetch` client-side)\n * - Wrapping filters as services in the chain\n * - Functional composition of filters\n */\nexport interface Service<REQ, RESP> {\n /**\n * Invoke the service with the given metadata.\n * @param meta - Request metadata\n * @returns Promise of the response\n */\n invoke(meta: REQ): Promise<RESP>;\n}\n\n/**\n * Filter abstract class - Similar to Java WebPieces Filter<REQ, RESP>.\n *\n * Filters are STATELESS and can handle N concurrent requests.\n * They wrap the execution of subsequent filters and the final service.\n *\n * Key principles:\n * - STATELESS: No instance variables for request data\n * - COMPOSABLE: Use chain() methods for functional composition\n *\n * ## Why this lives in core-util rather than beside either chain that uses it\n *\n * There are TWO chains in webpieces and they are the same concept pointed in opposite directions:\n *\n * - INBOUND, server side: `Filter<MethodMeta, WpResponse<unknown>>` (@webpieces/http-routing) wraps\n * the controller invocation.\n * - OUTBOUND, client side: `Filter<ClientRequest, Response>` (@webpieces/http-client-core) wraps the\n * `fetch`, so a filter can re-point the URL, add headers, or sign the exact serialized bytes.\n *\n * Declaring the abstraction once, in the package both depend on, is what keeps them ONE concept.\n * A second `Filter`/`Service` pair defined beside the client chain would be two spellings of one\n * thing — the shim shape `.claude/rules/no-backwards-compat.md` rejects — and the two would drift.\n *\n * core-util is browser-safe and dependency-free, and so is this file: it imports nothing.\n */\nexport abstract class Filter<REQ, RESP> {\n //priority is determined by how it is chained only here\n //DO NOT add priority here\n\n /**\n * Filter method that wraps the next filter/service.\n *\n * @param meta - Metadata about the method being invoked\n * @param nextFilter - Next filter/service as a Service\n * @returns Promise of the response\n */\n abstract filter(meta: REQ, nextFilter: Service<REQ, RESP>): Promise<RESP>;\n\n /**\n * Chain this filter with another filter.\n * Returns a new Filter that composes both filters.\n *\n * Similar to Java: filter1.chain(filter2)\n *\n * @param nextFilter - The filter to execute after this one\n * @returns Composed filter\n */\n chain(nextFilter: Filter<REQ, RESP>): Filter<REQ, RESP> {\n const self = this;\n\n return new (class extends Filter<REQ, RESP> {\n async filter(meta: REQ, nextService: Service<REQ, RESP>): Promise<RESP> {\n // Call outer filter, passing next filter wrapped as a Service\n return self.filter(meta, {\n invoke: (m: REQ) => nextFilter.filter(m, nextService),\n });\n }\n })();\n }\n\n /**\n * Chain this filter with a final service (controller).\n * Returns a Service that can be invoked.\n *\n * Similar to Java: filter.chain(service)\n *\n * @param svc - The final service (controller) to execute\n * @returns Service wrapping the entire filter chain\n */\n chainService(svc: Service<REQ, RESP>): Service<REQ, RESP> {\n const self = this;\n\n return {\n invoke: (meta: REQ) => self.filter(meta, svc),\n };\n }\n}\n"]}
|
|
@@ -20,7 +20,8 @@ import { AuthMode } from './auth-mode';
|
|
|
20
20
|
* ## Why it is a class with a private constructor and no boolean parameter
|
|
21
21
|
*
|
|
22
22
|
* `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the
|
|
23
|
-
* "widening that is an ABSENCE rather than a token"
|
|
23
|
+
* "widening that is an ABSENCE rather than a token" `.claude/rules/no-backwards-compat.md` rejects — the
|
|
24
|
+
* permissive answer would be
|
|
24
25
|
* what you get by not typing anything. There is no way to build a DestinationTrust except from the
|
|
25
26
|
* destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does
|
|
26
27
|
* not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is
|
|
@@ -21,7 +21,8 @@ exports.DestinationTrust = void 0;
|
|
|
21
21
|
* ## Why it is a class with a private constructor and no boolean parameter
|
|
22
22
|
*
|
|
23
23
|
* `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the
|
|
24
|
-
* "widening that is an ABSENCE rather than a token"
|
|
24
|
+
* "widening that is an ABSENCE rather than a token" `.claude/rules/no-backwards-compat.md` rejects — the
|
|
25
|
+
* permissive answer would be
|
|
25
26
|
* what you get by not typing anything. There is no way to build a DestinationTrust except from the
|
|
26
27
|
* destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does
|
|
27
28
|
* not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAGA
|
|
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,CAAC;YACd,8EAA8E;YAC9E,uFAAuF;YACvF,wFAAwF;YACxF,iFAAiF;YACjF,KAAK,YAAY;gBACb,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAkB;QACrB,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;;AA7DL,4CA8DC","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 — `@AuthOidc` / `@AuthSharedSecret`. On a `@AuthJwt` or `@Public`\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 `@Public` 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 (@AuthOidc / @AuthSharedSecret), 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 (@AuthJwt / @Public / @AuthWebhook /\n * @AuthApiKey / @AuthLocalOnly / 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 // @AuthWebhook 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 // @AuthApiKey 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 // @AuthLocalOnly 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 go on the wire to this destination? Untrusted keys always may — nobody was ever\n * going to make a security decision on them. A trusted key may only when the destination will\n * authenticate US, because that is the only case its `AuthFilter` will admit it.\n */\n allows(key: AnyContextKey): boolean {\n return this.verifiesCaller || !key.isTrusted();\n }\n}\n"]}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* - `'deployed'` — anywhere else (staging, prod, CI, a container). `@AuthLocalOnly` endpoints are
|
|
6
6
|
* not registered and, if reached anyway, 404.
|
|
7
7
|
*
|
|
8
|
-
* A `boolean` would have made the DANGEROUS half (`true`) unnameable and ungreppable — see
|
|
9
|
-
* shim shape #5. `grep -rn "'local'" ` over a repo's startup now lists every place that claims to be a
|
|
8
|
+
* A `boolean` would have made the DANGEROUS half (`true`) unnameable and ungreppable — see
|
|
9
|
+
* `.claude/rules/no-backwards-compat.md` shim shape #5. `grep -rn "'local'" ` over a repo's startup now lists every place that claims to be a
|
|
10
10
|
* developer's machine.
|
|
11
11
|
*/
|
|
12
12
|
export type Locality = 'local' | 'deployed';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuntimeLocality.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RuntimeLocality.ts"],"names":[],"mappings":";;;AAaA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAa,eAAe;IACxB,+FAA+F;IACvF,MAAM,CAAC,QAAQ,CAAuB;IAE9C;;;;;;OAMG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,OAAO,CAAC,QAAkB;QAC7B,eAAe,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,kBAAkB;QACrB,OAAO,eAAe,CAAC,QAAQ,KAAK,OAAO,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,UAAU;QACb,OAAO,eAAe,CAAC,QAAQ,KAAK,SAAS,CAAC;IAClD,CAAC;IAED,8DAA8D;IAC9D,yJAAyJ;IACzJ,MAAM,CAAC,KAAK;QACR,eAAe,CAAC,QAAQ,GAAG,SAAS,CAAC;IACzC,CAAC;CACJ;AAzCD,0CAyCC","sourcesContent":["/**\n * Where this process is running, as a NAMED token rather than a boolean:\n *\n * - `'local'` — a developer's machine. `@AuthLocalOnly` endpoints exist and serve.\n * - `'deployed'` — anywhere else (staging, prod, CI, a container). `@AuthLocalOnly` endpoints are\n * not registered and, if reached anyway, 404.\n *\n * A `boolean` would have made the DANGEROUS half (`true`) unnameable and ungreppable — see
|
|
1
|
+
{"version":3,"file":"RuntimeLocality.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RuntimeLocality.ts"],"names":[],"mappings":";;;AAaA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAa,eAAe;IACxB,+FAA+F;IACvF,MAAM,CAAC,QAAQ,CAAuB;IAE9C;;;;;;OAMG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,OAAO,CAAC,QAAkB;QAC7B,eAAe,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,kBAAkB;QACrB,OAAO,eAAe,CAAC,QAAQ,KAAK,OAAO,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,UAAU;QACb,OAAO,eAAe,CAAC,QAAQ,KAAK,SAAS,CAAC;IAClD,CAAC;IAED,8DAA8D;IAC9D,yJAAyJ;IACzJ,MAAM,CAAC,KAAK;QACR,eAAe,CAAC,QAAQ,GAAG,SAAS,CAAC;IACzC,CAAC;CACJ;AAzCD,0CAyCC","sourcesContent":["/**\n * Where this process is running, as a NAMED token rather than a boolean:\n *\n * - `'local'` — a developer's machine. `@AuthLocalOnly` endpoints exist and serve.\n * - `'deployed'` — anywhere else (staging, prod, CI, a container). `@AuthLocalOnly` endpoints are\n * not registered and, if reached anyway, 404.\n *\n * A `boolean` would have made the DANGEROUS half (`true`) unnameable and ungreppable — see\n * `.claude/rules/no-backwards-compat.md` shim shape #5. `grep -rn \"'local'\" ` over a repo's startup now lists every place that claims to be a\n * developer's machine.\n */\nexport type Locality = 'local' | 'deployed';\n\n/**\n * RuntimeLocality - the ONE answer to \"am I running on a developer's machine?\", for the one part of\n * webpieces that needs it: {@link AuthLocalOnly}.\n *\n * ## Why this is a seam and not a `process.env` read\n *\n * The framework cannot compute this itself and must not try. \"Local\" is a fact about the DEPLOYMENT\n * PLATFORM: Cloud Run derives it from `K_SERVICE`, ECS from `ECS_CONTAINER_METADATA_URI`, a laptop\n * from the absence of both. Baking any one of those into core-util would hardcode a cloud vendor into\n * the framework core, and core-util is browser-safe (it may not read `process.env` at all). So the\n * ENVIRONMENT tells the framework, exactly as it tells it the logging backend\n * ({@link LogManager.setFactory}), the header set ({@link HeaderRegistry.configure}) and its own\n * identity ({@link ServiceInfo.setInfo}). (The {@link ApiCallContext} seam is NOT in that list any\n * more: it is a constructor argument to {@link LogApiCallImpl}, not a startup install.)\n *\n * It is a VALUE holder rather than an interface-plus-impl (the `ApiCallContext` shape) because there\n * is no behavior to plug in — the answer is one token fixed at startup. Per CLAUDE.md, data is a\n * class; only behavior is an interface.\n *\n * ## Where it is declared\n *\n * `RuntimeSetupOptions` takes it as a REQUIRED, positional constructor argument, so `setupRuntime`\n * declares it on every server and no server can boot without having stated it. That is the same\n * forcing function `@Endpoint(path, kind)` uses: a required positional argument turns \"we forgot\" into\n * a compile error instead of a runtime guess.\n *\n * ## FAIL SAFE when nothing declared it\n *\n * {@link isLocalDevelopment} returns `false` until {@link declare} is called. An undeclared process is\n * treated as DEPLOYED, so the failure mode of a forgotten wiring call is \"my local-only endpoint 404s\n * on my laptop\" — annoying and instantly visible — never \"my local-only endpoint is live in\n * production\". The permissive answer is never the one you get by not typing anything.\n */\nexport class RuntimeLocality {\n /** Process-global; set once at startup. `undefined` = never declared = treated as deployed. */\n private static locality: Locality | undefined;\n\n /**\n * State where this process is running. Call it at startup — `setupRuntime` does it for you from\n * `RuntimeSetupOptions.locality`.\n *\n * LAST CALL WINS, mirroring {@link ServiceInfo.setInfo}: an in-process test can legitimately boot\n * two servers back-to-back.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static declare(locality: Locality): void {\n RuntimeLocality.locality = locality;\n }\n\n /**\n * True ONLY when a startup explicitly declared `'local'`. Undeclared reads as deployed — see the\n * fail-safe note on the class. Does not throw: a wrong answer here must refuse an endpoint, never\n * 500 unrelated traffic.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static isLocalDevelopment(): boolean {\n return RuntimeLocality.locality === 'local';\n }\n\n /**\n * Whether anything declared a locality at all. Used ONLY to make the refusal log say which of the\n * two reasons applies — \"you are deployed\" vs \"nobody ever told me\" — because those have very\n * different fixes and a developer staring at a 404 on their own laptop needs to know which.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static isDeclared(): boolean {\n return RuntimeLocality.locality !== undefined;\n }\n\n /** Reset — for tests, mirroring {@link ServiceInfo.clear}. */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static clear(): void {\n RuntimeLocality.locality = undefined;\n }\n}\n"]}
|