@webpieces/core-util 0.4.959 → 0.4.961
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
CHANGED
|
@@ -18,6 +18,12 @@ class DeletedAuthCompileAssertions {
|
|
|
18
18
|
void CoreUtil.WpAuthWebhook;
|
|
19
19
|
// @ts-expect-error webhook(name) is renamed vendorAuth(name): one VendorAuthHook mints, refreshes and verifies
|
|
20
20
|
void CoreUtil.webhook;
|
|
21
|
+
// @ts-expect-error EXTERNAL is split: EXTERNAL_INBOUND (an outside system calls us) or EXTERNAL_OUTBOUND (we call it)
|
|
22
|
+
void CoreUtil.EXTERNAL;
|
|
23
|
+
// @ts-expect-error renamed assertExternalEndpointKinds, which also checks EXTERNAL_OUTBOUND contracts
|
|
24
|
+
void CoreUtil.assertEveryExternalEndpointDeclaresCaller;
|
|
25
|
+
// @ts-expect-error renamed assertEveryVendorAuthEndpointRetainsRawBody
|
|
26
|
+
void CoreUtil.assertEveryWebhookEndpointRetainsRawBody;
|
|
21
27
|
// @ts-expect-error use WpAuth([apiKey(...)]) and WpAuthorization
|
|
22
28
|
void CoreUtil.WpAuthApiKey;
|
|
23
29
|
// @ts-expect-error use separate WpLocalOnly, authentication and authorization
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAAA,2DAAqC;AAErC,4FAA4F;AAC5F,MAAa,4BAA4B;IACrC,cAAc;QACV,2DAA2D;QAC3D,KAAK,QAAQ,CAAC,SAAS,CAAC;QACxB,oGAAoG;QACpG,KAAK,QAAQ,CAAC,UAAU,CAAC;QACzB,4HAA4H;QAC5H,KAAK,QAAQ,CAAC,IAAI,CAAC;QACnB,uEAAuE;QACvE,KAAK,QAAQ,CAAC,kBAAkB,CAAC;QACjC,qEAAqE;QACrE,KAAK,QAAQ,CAAC,aAAa,CAAC;QAC5B,+GAA+G;QAC/G,KAAK,QAAQ,CAAC,OAAO,CAAC;QACtB,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,8EAA8E;QAC9E,KAAK,QAAQ,CAAC,eAAe,CAAC;QAC9B,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,oEAAoE;QACpE,KAAK,QAAQ,CAAC,WAAW,CAAC;QAC1B,kEAAkE;QAClE,KAAK,QAAQ,CAAC,aAAa,CAAC;IAChC,CAAC;CACJ;
|
|
1
|
+
{"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAAA,2DAAqC;AAErC,4FAA4F;AAC5F,MAAa,4BAA4B;IACrC,cAAc;QACV,2DAA2D;QAC3D,KAAK,QAAQ,CAAC,SAAS,CAAC;QACxB,oGAAoG;QACpG,KAAK,QAAQ,CAAC,UAAU,CAAC;QACzB,4HAA4H;QAC5H,KAAK,QAAQ,CAAC,IAAI,CAAC;QACnB,uEAAuE;QACvE,KAAK,QAAQ,CAAC,kBAAkB,CAAC;QACjC,qEAAqE;QACrE,KAAK,QAAQ,CAAC,aAAa,CAAC;QAC5B,+GAA+G;QAC/G,KAAK,QAAQ,CAAC,OAAO,CAAC;QACtB,sHAAsH;QACtH,KAAK,QAAQ,CAAC,QAAQ,CAAC;QACvB,sGAAsG;QACtG,KAAK,QAAQ,CAAC,yCAAyC,CAAC;QACxD,uEAAuE;QACvE,KAAK,QAAQ,CAAC,wCAAwC,CAAC;QACvD,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;AA/BD,oEA+BC","sourcesContent":["import * as CoreUtil from '../index';\n\n/** Each removed spelling has its own assertion so one missing export cannot mask a shim. */\nexport class DeletedAuthCompileAssertions {\n removedExports(): void {\n // @ts-expect-error use WpAuth([jwt()]) and WpAuthorization\n void CoreUtil.WpAuthJwt;\n // @ts-expect-error use WpAuth([oidcSameProject()]) or WpAuth([oidcSvcs([...])]) and WpAuthorization\n void CoreUtil.WpAuthOidc;\n // @ts-expect-error oidc(...) is removed: oidcSameProject() for this GCP project, oidcSvcs([...]) for named service accounts\n void CoreUtil.oidc;\n // @ts-expect-error use WpAuth([sharedSecret(...)]) and WpAuthorization\n void CoreUtil.WpAuthSharedSecret;\n // @ts-expect-error use WpAuth([vendorAuth(...)]) and WpAuthorization\n void CoreUtil.WpAuthWebhook;\n // @ts-expect-error webhook(name) is renamed vendorAuth(name): one VendorAuthHook mints, refreshes and verifies\n void CoreUtil.webhook;\n // @ts-expect-error EXTERNAL is split: EXTERNAL_INBOUND (an outside system calls us) or EXTERNAL_OUTBOUND (we call it)\n void CoreUtil.EXTERNAL;\n // @ts-expect-error renamed assertExternalEndpointKinds, which also checks EXTERNAL_OUTBOUND contracts\n void CoreUtil.assertEveryExternalEndpointDeclaresCaller;\n // @ts-expect-error renamed assertEveryVendorAuthEndpointRetainsRawBody\n void CoreUtil.assertEveryWebhookEndpointRetainsRawBody;\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"]}
|
|
@@ -67,9 +67,11 @@ export declare class DestinationTrust {
|
|
|
67
67
|
*/
|
|
68
68
|
listedKeys(): readonly AnyContextKey[] | undefined;
|
|
69
69
|
/**
|
|
70
|
-
* The
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* The destination of a route of OURS, from its auth mode alone. An HTTP client always uses
|
|
71
|
+
* {@link forRoute}, which also covers an `EXTERNAL_OUTBOUND` route; this remains for a caller
|
|
72
|
+
* that holds only the mode — the cloud-task client, whose @PubSub contracts can never be
|
|
73
|
+
* EXTERNAL_OUTBOUND. `undefined` (an endpoint that declared no mode) is treated as
|
|
74
|
+
* un-verifying, i.e. the SAFE answer — an absent declaration must never be the widest one.
|
|
73
75
|
*/
|
|
74
76
|
static forAuthMode(mode: AuthMode | undefined): DestinationTrust;
|
|
75
77
|
/**
|
|
@@ -79,9 +79,11 @@ class DestinationTrust {
|
|
|
79
79
|
return this.externalKeys;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* The
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* The destination of a route of OURS, from its auth mode alone. An HTTP client always uses
|
|
83
|
+
* {@link forRoute}, which also covers an `EXTERNAL_OUTBOUND` route; this remains for a caller
|
|
84
|
+
* that holds only the mode — the cloud-task client, whose @PubSub contracts can never be
|
|
85
|
+
* EXTERNAL_OUTBOUND. `undefined` (an endpoint that declared no mode) is treated as
|
|
86
|
+
* un-verifying, i.e. the SAFE answer — an absent declaration must never be the widest one.
|
|
85
87
|
*/
|
|
86
88
|
// 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
|
|
87
89
|
static forAuthMode(mode) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,gBAAgB;IAgBJ;IAMA;IArBrB;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAEhF;;;;OAIG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAEtF,YACqB,cAAuB;IACxC;;;;OAIG;IACc,YAAkD;QANlD,mBAAc,GAAd,cAAc,CAAS;QAMvB,iBAAY,GAAZ,YAAY,CAAsC;IACpE,CAAC;IAEJ;;;;;;OAMG;IACH,wGAAwG;IACxG,MAAM,CAAC,QAAQ,CAAC,KAAoB;QAChC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;YACzB,OAAO,IAAI,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAED;;;;OAIG;IACH,UAAU;QACN,OAAO,IAAI,CAAC,YAAY,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,kFAAkF;YAClF,oFAAoF;YACpF,mFAAmF;YACnF,qCAAqC;YACrC,KAAK,YAAY,CAAC;YAClB,qFAAqF;YACrF,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,iCAAiC;YACjC,KAAK,QAAQ;gBACb,4EAA4E;gBAC5E,uFAAuF;gBACvF,wFAAwF;gBACxF,iFAAiF;gBAC7E,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAkB;QACrB,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5E,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;IAED;;;;;;OAMG;IACH,gBAAgB,CAAC,GAAkB;QAC/B,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAClD,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;;AArHL,4CAsHC","sourcesContent":["import { AnyContextKey } from '../ContextKey';\nimport { AuthMode } from './auth-mode';\nimport { RouteMetadata } from './RouteMetadata';\n\n/**\n * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key\n * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?\n *\n * ## Why the client has to answer this at all\n *\n * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a\n * route that verified WHO called it — `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`\n * route the same header must match what the authenticator independently derived, or the request is\n * REJECTED with a 401.\n *\n * That rule is correct, and it means a client that ships `x-user-id` to a `@WpAuthPublic` endpoint is\n * building a request the callee is obliged to reject. Before this class the outbound builders\n * forwarded EVERY transferred key with no idea what the destination was, so an internal service\n * calling another service's public or JWT endpoint 401'd itself. The fix belongs on the producing\n * side: don't send what cannot possibly be believed.\n *\n * ## Why it is a class with a private constructor and no boolean parameter\n *\n * `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the\n * \"widening that is an ABSENCE rather than a token\" `.claude/rules/no-backwards-compat.md` rejects — the\n * permissive answer would be\n * what you get by not typing anything. There is no way to build a DestinationTrust except from the\n * destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does\n * not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is\n * asked. The two instances are PRIVATE for the same reason: exposing them would be a second spelling\n * that skips the derivation.\n *\n * Per CLAUDE.md: data-only structure, so a class rather than an interface or a bare boolean.\n */\nexport class DestinationTrust {\n /**\n * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to\n * believe context WE vouch for — this is the service-to-service identity propagation that trusted\n * keys keep an `httpHeader` for.\n */\n private static readonly VERIFIES_CALLER = new DestinationTrust(true, undefined);\n\n /**\n * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / vendorAuth(...) /\n * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.\n * Untrusted keys still travel.\n */\n private static readonly CANNOT_VERIFY_CALLER = new DestinationTrust(false, undefined);\n\n private constructor(\n private readonly verifiesCaller: boolean,\n /**\n * Set ONLY for an `EXTERNAL_OUTBOUND` destination: the `@ExternalContextKeys` list, which is\n * then the WHOLE of what may be sent. Undefined for every destination in this repo, whose wire\n * is governed by {@link forAuthMode} exactly as before.\n */\n private readonly externalKeys: readonly AnyContextKey[] | undefined,\n ) {}\n\n /**\n * The destination of ONE route. An `EXTERNAL_OUTBOUND` route (an outside system's API) gets a\n * destination that sends ONLY its `@ExternalContextKeys` and believes NO response header: a vendor\n * is somebody else's process, so our `x-request-id`, `x-webpieces-*` and app keys stay home unless\n * the contract lists them, and nothing it answers may write into our context. Every other route is\n * {@link forAuthMode}, unchanged — so the wire to our own servers is identical.\n */\n // webpieces-disable no-function-outside-class -- static factory beside forAuthMode, for the same reason\n static forRoute(route: RouteMetadata): DestinationTrust {\n if (route.externalOutbound) {\n return new DestinationTrust(false, route.externalContextKeys);\n }\n return DestinationTrust.forAuthMode(route.authMeta?.methods[0]);\n }\n\n /**\n * The keys an `EXTERNAL_OUTBOUND` destination sends — every one is REQUIRED, so a builder throws\n * before any I/O when one has no value. Undefined for every other destination, whose builder\n * iterates the registry's transferred keys and filters them with {@link allows}.\n */\n listedKeys(): readonly AnyContextKey[] | undefined {\n return this.externalKeys;\n }\n\n /**\n * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an\n * endpoint that declared no mode) is treated as un-verifying, i.e. the SAFE answer — an absent\n * declaration must never be the widest one.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor, exactly as ContextKey.trusted/untrusted do: the destination's auth mode must be part of the CALL, and a DI-injected instance method would let a caller hold one without ever naming a route\n static forAuthMode(mode: AuthMode | undefined): DestinationTrust {\n if (mode === undefined) {\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return DestinationTrust.VERIFIES_CALLER;\n case 'jwt':\n case 'public':\n // vendorAuth(...) authenticates an OUTSIDE VENDOR, which is not the same thing as\n // authenticating a peer in this repo. The vendor knows nothing of webpieces context\n // headers and would never send one, so there is no identity to propagate in either\n // direction. Trusted keys stay home.\n case 'vendorAuth':\n // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is\n // another company's codebase, so nothing it forwards may be believed, and no webpieces\n // client can call it anyway (the framework extracts no api-key header — the app's hook\n // owns which headers carry the credential; the contract's `credentials` list only DESCRIBES\n // them). Trusted keys stay home.\n case 'apikey':\n // @WpLocalOnly authenticates NOBODY — it gates on the environment, not on a\n // credential — so a browser with curl on the same laptop is indistinguishable from us.\n // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to\n // AuthMode is a compile error here rather than a silent permissive fallthrough.)\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n }\n\n /**\n * May this key cross the boundary between us and this destination? Untrusted keys always may —\n * nobody was ever going to make a security decision on them. A trusted key may only when we and\n * the destination authenticate each other.\n *\n * ONE method answers BOTH directions, deliberately, because it is one question:\n *\n * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination\n * will authenticate US, because that is the only case its `AuthFilter` will admit it.\n * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a\n * destination this client authenticated to — an `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` peer is a\n * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is\n * somebody else's process whose response headers are an assertion, not a proof.\n *\n * For every destination in this repo the two directions are one question, so the response side\n * ({@link believesResponse}) delegates here; only an `EXTERNAL_OUTBOUND` destination answers them\n * differently (it sends listed keys and believes none).\n */\n allows(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return this.externalKeys.includes(key);\n return this.verifiesCaller || !key.isTrusted();\n }\n\n /**\n * May a key that came BACK on this destination's response be merged into our context? Exactly\n * {@link allows} for a destination in this repo — and NOTHING from an `EXTERNAL_OUTBOUND` one,\n * whose response headers are an outside system's, not a peer's. A separate method because for\n * that destination the two directions genuinely differ: a key we LIST to send (`x-request-id`)\n * is not thereby a key we let the vendor write back.\n */\n believesResponse(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return false;\n return this.allows(key);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,gBAAgB;IAgBJ;IAMA;IArBrB;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAEhF;;;;OAIG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAEtF,YACqB,cAAuB;IACxC;;;;OAIG;IACc,YAAkD;QANlD,mBAAc,GAAd,cAAc,CAAS;QAMvB,iBAAY,GAAZ,YAAY,CAAsC;IACpE,CAAC;IAEJ;;;;;;OAMG;IACH,wGAAwG;IACxG,MAAM,CAAC,QAAQ,CAAC,KAAoB;QAChC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;YACzB,OAAO,IAAI,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAED;;;;OAIG;IACH,UAAU;QACN,OAAO,IAAI,CAAC,YAAY,CAAC;IAC7B,CAAC;IAED;;;;;;OAMG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,kFAAkF;YAClF,oFAAoF;YACpF,mFAAmF;YACnF,qCAAqC;YACrC,KAAK,YAAY,CAAC;YAClB,qFAAqF;YACrF,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,iCAAiC;YACjC,KAAK,QAAQ;gBACb,4EAA4E;gBAC5E,uFAAuF;gBACvF,wFAAwF;gBACxF,iFAAiF;gBAC7E,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAkB;QACrB,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5E,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;IAED;;;;;;OAMG;IACH,gBAAgB,CAAC,GAAkB;QAC/B,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAClD,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;;AAvHL,4CAwHC","sourcesContent":["import { AnyContextKey } from '../ContextKey';\nimport { AuthMode } from './auth-mode';\nimport { RouteMetadata } from './RouteMetadata';\n\n/**\n * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key\n * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?\n *\n * ## Why the client has to answer this at all\n *\n * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a\n * route that verified WHO called it — `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`\n * route the same header must match what the authenticator independently derived, or the request is\n * REJECTED with a 401.\n *\n * That rule is correct, and it means a client that ships `x-user-id` to a `@WpAuthPublic` endpoint is\n * building a request the callee is obliged to reject. Before this class the outbound builders\n * forwarded EVERY transferred key with no idea what the destination was, so an internal service\n * calling another service's public or JWT endpoint 401'd itself. The fix belongs on the producing\n * side: don't send what cannot possibly be believed.\n *\n * ## Why it is a class with a private constructor and no boolean parameter\n *\n * `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the\n * \"widening that is an ABSENCE rather than a token\" `.claude/rules/no-backwards-compat.md` rejects — the\n * permissive answer would be\n * what you get by not typing anything. There is no way to build a DestinationTrust except from the\n * destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does\n * not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is\n * asked. The two instances are PRIVATE for the same reason: exposing them would be a second spelling\n * that skips the derivation.\n *\n * Per CLAUDE.md: data-only structure, so a class rather than an interface or a bare boolean.\n */\nexport class DestinationTrust {\n /**\n * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to\n * believe context WE vouch for — this is the service-to-service identity propagation that trusted\n * keys keep an `httpHeader` for.\n */\n private static readonly VERIFIES_CALLER = new DestinationTrust(true, undefined);\n\n /**\n * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / vendorAuth(...) /\n * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.\n * Untrusted keys still travel.\n */\n private static readonly CANNOT_VERIFY_CALLER = new DestinationTrust(false, undefined);\n\n private constructor(\n private readonly verifiesCaller: boolean,\n /**\n * Set ONLY for an `EXTERNAL_OUTBOUND` destination: the `@ExternalContextKeys` list, which is\n * then the WHOLE of what may be sent. Undefined for every destination in this repo, whose wire\n * is governed by {@link forAuthMode} exactly as before.\n */\n private readonly externalKeys: readonly AnyContextKey[] | undefined,\n ) {}\n\n /**\n * The destination of ONE route. An `EXTERNAL_OUTBOUND` route (an outside system's API) gets a\n * destination that sends ONLY its `@ExternalContextKeys` and believes NO response header: a vendor\n * is somebody else's process, so our `x-request-id`, `x-webpieces-*` and app keys stay home unless\n * the contract lists them, and nothing it answers may write into our context. Every other route is\n * {@link forAuthMode}, unchanged — so the wire to our own servers is identical.\n */\n // webpieces-disable no-function-outside-class -- static factory beside forAuthMode, for the same reason\n static forRoute(route: RouteMetadata): DestinationTrust {\n if (route.externalOutbound) {\n return new DestinationTrust(false, route.externalContextKeys);\n }\n return DestinationTrust.forAuthMode(route.authMeta?.methods[0]);\n }\n\n /**\n * The keys an `EXTERNAL_OUTBOUND` destination sends — every one is REQUIRED, so a builder throws\n * before any I/O when one has no value. Undefined for every other destination, whose builder\n * iterates the registry's transferred keys and filters them with {@link allows}.\n */\n listedKeys(): readonly AnyContextKey[] | undefined {\n return this.externalKeys;\n }\n\n /**\n * The destination of a route of OURS, from its auth mode alone. An HTTP client always uses\n * {@link forRoute}, which also covers an `EXTERNAL_OUTBOUND` route; this remains for a caller\n * that holds only the mode — the cloud-task client, whose @PubSub contracts can never be\n * EXTERNAL_OUTBOUND. `undefined` (an endpoint that declared no mode) is treated as\n * un-verifying, i.e. the SAFE answer — an absent declaration must never be the widest one.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor, exactly as ContextKey.trusted/untrusted do: the destination's auth mode must be part of the CALL, and a DI-injected instance method would let a caller hold one without ever naming a route\n static forAuthMode(mode: AuthMode | undefined): DestinationTrust {\n if (mode === undefined) {\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return DestinationTrust.VERIFIES_CALLER;\n case 'jwt':\n case 'public':\n // vendorAuth(...) authenticates an OUTSIDE VENDOR, which is not the same thing as\n // authenticating a peer in this repo. The vendor knows nothing of webpieces context\n // headers and would never send one, so there is no identity to propagate in either\n // direction. Trusted keys stay home.\n case 'vendorAuth':\n // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is\n // another company's codebase, so nothing it forwards may be believed, and no webpieces\n // client can call it anyway (the framework extracts no api-key header — the app's hook\n // owns which headers carry the credential; the contract's `credentials` list only DESCRIBES\n // them). Trusted keys stay home.\n case 'apikey':\n // @WpLocalOnly authenticates NOBODY — it gates on the environment, not on a\n // credential — so a browser with curl on the same laptop is indistinguishable from us.\n // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to\n // AuthMode is a compile error here rather than a silent permissive fallthrough.)\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n }\n\n /**\n * May this key cross the boundary between us and this destination? Untrusted keys always may —\n * nobody was ever going to make a security decision on them. A trusted key may only when we and\n * the destination authenticate each other.\n *\n * ONE method answers BOTH directions, deliberately, because it is one question:\n *\n * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination\n * will authenticate US, because that is the only case its `AuthFilter` will admit it.\n * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a\n * destination this client authenticated to — an `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` peer is a\n * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is\n * somebody else's process whose response headers are an assertion, not a proof.\n *\n * For every destination in this repo the two directions are one question, so the response side\n * ({@link believesResponse}) delegates here; only an `EXTERNAL_OUTBOUND` destination answers them\n * differently (it sends listed keys and believes none).\n */\n allows(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return this.externalKeys.includes(key);\n return this.verifiesCaller || !key.isTrusted();\n }\n\n /**\n * May a key that came BACK on this destination's response be merged into our context? Exactly\n * {@link allows} for a destination in this repo — and NOTHING from an `EXTERNAL_OUTBOUND` one,\n * whose response headers are an outside system's, not a peer's. A separate method because for\n * that destination the two directions genuinely differ: a key we LIST to send (`x-request-id`)\n * is not thereby a key we let the vendor write back.\n */\n believesResponse(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return false;\n return this.allows(key);\n }\n}\n"]}
|