@webpieces/core-context 0.4.992 → 0.4.993
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/core-context",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.993",
|
|
4
4
|
"description": "AsyncLocalStorage-based context management for request-scoped data",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-util": "0.4.
|
|
25
|
+
"@webpieces/core-util": "0.4.993",
|
|
26
26
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"reflect-metadata": "0.2.2"
|
|
@@ -4,9 +4,11 @@ import { ContextTuple } from '@webpieces/core-util';
|
|
|
4
4
|
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
5
5
|
* {@link VendorAuthVerifier.verify}. Four fields, three jobs:
|
|
6
6
|
*
|
|
7
|
-
* - `userId` — WHO the caller is, as the credential proved it.
|
|
7
|
+
* - `userId` — WHO the caller is, as the credential proved it. `undefined` for a service /
|
|
8
|
+
* AI agent, which then carries `machine` instead (#1420).
|
|
8
9
|
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
9
|
-
*
|
|
10
|
+
* (`ROLES`) reads — a user's OR a machine's: a service is a caller with roles and
|
|
11
|
+
* no user id, published under the same USER_ROLES key, `claims` carries credential facts for the authenticator to validate and publish
|
|
10
12
|
* as trusted application context before AuthorizationHook evaluates policy.
|
|
11
13
|
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
12
14
|
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
@@ -6,9 +6,11 @@ exports.AuthenticatedMachineIdentity = exports.AuthenticatedCaller = void 0;
|
|
|
6
6
|
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
7
7
|
* {@link VendorAuthVerifier.verify}. Four fields, three jobs:
|
|
8
8
|
*
|
|
9
|
-
* - `userId` — WHO the caller is, as the credential proved it.
|
|
9
|
+
* - `userId` — WHO the caller is, as the credential proved it. `undefined` for a service /
|
|
10
|
+
* AI agent, which then carries `machine` instead (#1420).
|
|
10
11
|
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
11
|
-
*
|
|
12
|
+
* (`ROLES`) reads — a user's OR a machine's: a service is a caller with roles and
|
|
13
|
+
* no user id, published under the same USER_ROLES key, `claims` carries credential facts for the authenticator to validate and publish
|
|
12
14
|
* as trusted application context before AuthorizationHook evaluates policy.
|
|
13
15
|
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
14
16
|
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthenticatedCaller.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/AuthenticatedCaller.ts"],"names":[],"mappings":";;;AAEA
|
|
1
|
+
{"version":3,"file":"AuthenticatedCaller.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/AuthenticatedCaller.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAa,mBAAmB;IAER;IACA;IACA;IAEA;IACA;IANpB,YACoB,MAA0B,EAC1B,QAAkB,EAAE,EACpB,UAA0B,EAAE;IAC5C,wGAAwG;IACxF,SAAkC,EAAE,EACpC,OAAsC;QALtC,WAAM,GAAN,MAAM,CAAoB;QAC1B,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAAqB;QAE5B,WAAM,GAAN,MAAM,CAA8B;QACpC,YAAO,GAAP,OAAO,CAA+B;IACvD,CAAC;CACP;AATD,kDASC;AAED,mFAAmF;AACnF,MAAa,4BAA4B;IACT;IAAmC;IAA/D,YAA4B,SAAiB,EAAkB,QAAgB;QAAnD,cAAS,GAAT,SAAS,CAAQ;QAAkB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;CACtF;AAFD,oEAEC","sourcesContent":["import { ContextTuple } from '@webpieces/core-util';\n\n/**\n * AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook\n * that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and\n * {@link VendorAuthVerifier.verify}. Four fields, three jobs:\n *\n * - `userId` — WHO the caller is, as the credential proved it. `undefined` for a service /\n * AI agent, which then carries `machine` instead (#1420).\n * - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check\n * (`ROLES`) reads — a user's OR a machine's: a service is a caller with roles and\n * no user id, published under the same USER_ROLES key, `claims` carries credential facts for the authenticator to validate and publish\n * as trusted application context before AuthorizationHook evaluates policy.\n * - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with\n * {@link RequestContext.putTrusted}, so return only what THIS authenticator\n * derived from the credential it just verified.\n *\n * NAMING, said out loud so it is not \"fixed\" back: it is deliberately NOT a `TrustedContextMap`.\n * Three of the four fields are not context, and it is not a Map — it is the authenticated caller,\n * and the context is one thing it carries.\n *\n * Data-only structure (a class, per the guidelines).\n */\nexport class AuthenticatedCaller {\n constructor(\n public readonly userId: string | undefined,\n public readonly roles: string[] = [],\n public readonly entries: ContextTuple[] = [],\n // webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)\n public readonly claims: Record<string, unknown> = {},\n public readonly machine?: AuthenticatedMachineIdentity,\n ) {}\n}\n\n/** Verified external machine identity does not grant trusted-header delegation. */\nexport class AuthenticatedMachineIdentity {\n constructor(public readonly mechanism: string, public readonly identity: string) {}\n}\n"]}
|
|
@@ -62,6 +62,12 @@ declare class PendingWireTrustImpl {
|
|
|
62
62
|
* into the context.
|
|
63
63
|
*/
|
|
64
64
|
stash(key: AnyTrustedContextKey, value: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Does this request carry a not-yet-reconciled wire value for `key`? A read-only peek: it admits
|
|
67
|
+
* nothing. An OIDC hop asks it to tell an ORIGINATING service (nothing forwarded) from one relaying
|
|
68
|
+
* somebody else's identity, whose forwarded roles must win over any role of the relaying service.
|
|
69
|
+
*/
|
|
70
|
+
holds(key: AnyTrustedContextKey): boolean;
|
|
65
71
|
/**
|
|
66
72
|
* Everything stashed for this request, and CLEAR the pen in the same step — so reconciliation
|
|
67
73
|
* cannot run twice and a value cannot be re-admitted later by anything else.
|
package/src/PendingWireTrust.js
CHANGED
|
@@ -79,6 +79,14 @@ class PendingWireTrustImpl {
|
|
|
79
79
|
pending.set(key.name, new PendingTrustedValue(key, value));
|
|
80
80
|
RequestContext_1.RequestContext.put(PENDING_WIRE_TRUST_KEY, pending);
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Does this request carry a not-yet-reconciled wire value for `key`? A read-only peek: it admits
|
|
84
|
+
* nothing. An OIDC hop asks it to tell an ORIGINATING service (nothing forwarded) from one relaying
|
|
85
|
+
* somebody else's identity, whose forwarded roles must win over any role of the relaying service.
|
|
86
|
+
*/
|
|
87
|
+
holds(key) {
|
|
88
|
+
return this.current()?.has(key.name) ?? false;
|
|
89
|
+
}
|
|
82
90
|
/**
|
|
83
91
|
* Everything stashed for this request, and CLEAR the pen in the same step — so reconciliation
|
|
84
92
|
* cannot run twice and a value cannot be re-admitted later by anything else.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PendingWireTrust.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/PendingWireTrust.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAElD;;;;GAIG;AACH,MAAM,sBAAsB,GAAG,kCAAkC,CAAC;AAElE;;;;GAIG;AACH,MAAa,mBAAmB;IAER;IACA;IAFpB,YACoB,GAAyB,EACzB,KAAa;QADb,QAAG,GAAH,GAAG,CAAsB;QACzB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,kDAKC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,oBAAoB;IACtB;;;OAGG;IACH,KAAK,CAAC,GAAyB,EAAE,KAAa;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,GAAG,EAA+B,CAAC;QACzE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QAC3D,+BAAc,CAAC,GAAG,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC;IAED;;;OAGG;IACH,OAAO;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,+BAAc,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,CAAC;IAEO,OAAO;QACX,OAAO,+BAAc,CAAC,GAAG,CAAmC,sBAAsB,CAAC,CAAC;IACxF,CAAC;CACJ;AAED,sEAAsE;AACzD,QAAA,gBAAgB,GAAG,IAAI,oBAAoB,EAAE,CAAC","sourcesContent":["import { AnyTrustedContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * Reserved (deliberately UNREGISTERED) slot holding the pending map. Not a ContextKey: it is\n * framework plumbing that exists only between the inbound fill and the AuthFilter, and giving it a\n * ContextKey would make it transferrable/loggable, neither of which it should ever be.\n */\nconst PENDING_WIRE_TRUST_KEY = '__webpieces_pending_wire_trust__';\n\n/**\n * One trusted key that ARRIVED ON THE WIRE and has not yet been vouched for. Data-only (a class,\n * per the guidelines). Carries the key as well as the value so a rejection message can name the\n * HTTP header the caller actually sent, not just the context name.\n */\nexport class PendingTrustedValue {\n constructor(\n public readonly key: AnyTrustedContextKey,\n public readonly value: string,\n ) {}\n}\n\n/**\n * PendingWireTrust - the holding pen between \"a trusted key arrived on the wire\" and \"we know\n * whether we may believe it\".\n *\n * ## The hole this closes\n *\n * `RequestContextHeaders.fillFromRequest` runs at the TRANSPORT level (`ExpressWrapper`), and\n * `AuthFilter` runs later, as a filter. So the wire always gets to write first and the authenticator\n * second. If the inbound loop wrote trusted keys straight into the context, then for the entire\n * window before AuthFilter ran — and forever after, on any route where the authenticator does not\n * happen to stamp that particular key — a value typed by whoever sent the request would be sitting\n * in the slot that `getTrusted` reads. `curl -H 'x-user-id: victim'` would be an authenticated\n * identity.\n *\n * That is not hypothetical on this codebase: the framework's own {@link DefaultJwtHook} returns an\n * EMPTY `AuthenticatedCaller.entries`, so a fully verified `jwt()` request stamps no context entries at\n * all and would leave a forged `x-user-id` completely unopposed.\n *\n * ## The fix\n *\n * Inbound trusted values never enter the context. They are stashed HERE, and `AuthFilter` decides\n * what to do with them once it knows who the caller is:\n *\n * - `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` — the CALLER's own identity was verified, so this is an\n * internal service passing along context it already holds. Admit the pending values as trusted.\n * This is what makes service-to-service propagation of a verified userId work, and it is the whole\n * reason trusted keys are allowed to have an `httpHeader` at all.\n * - `jwt()` / public — the caller may be a browser or anyone with curl. A pending value is\n * admitted ONLY if the authenticator independently derived the SAME value. Anything else — a\n * different value, or a value nothing vouched for — rejects the request.\n *\n * Note the deliberate asymmetry with a \"strip it and carry on\" design: a mismatch is not merely\n * neutralized, it FAILS. Rate limiters commonly bucket on the inbound header rather than on the JWT,\n * so a request whose header says `alice` and whose JWT says `bob` was rate-limited as the wrong\n * principal. Letting the JWT quietly win would turn every forged header into a free rate-limit\n * bypass. There is no legitimate caller that sends a header contradicting its own credential.\n *\n * ## If nothing reconciles\n *\n * `AuthFilter` is auto-installed on every route, so reconciliation always happens on a webpieces\n * server. Should a non-webpieces transport ever call `fillFromRequest` without one, the pending\n * values simply never arrive — the trusted key reads as absent. That is the fail-SAFE direction, and\n * it is intentional: silence beats an unvouched value.\n *\n * Module-global instance (like {@link RequestContext} itself) rather than a DI singleton — it is\n * stateless plumbing over the ambient context, used by one class on each side of the boundary.\n */\nclass PendingWireTrustImpl {\n /**\n * Hold an inbound wire value for a trusted key. Called by the inbound fill INSTEAD of writing it\n * into the context.\n */\n stash(key: AnyTrustedContextKey, value: string): void {\n const pending = this.current() ?? new Map<string, PendingTrustedValue>();\n pending.set(key.name, new PendingTrustedValue(key, value));\n RequestContext.put(PENDING_WIRE_TRUST_KEY, pending);\n }\n\n /**\n * Everything stashed for this request, and CLEAR the pen in the same step — so reconciliation\n * cannot run twice and a value cannot be re-admitted later by anything else.\n */\n takeAll(): PendingTrustedValue[] {\n const pending = this.current();\n if (!pending) {\n return [];\n }\n RequestContext.remove(PENDING_WIRE_TRUST_KEY);\n return Array.from(pending.values());\n }\n\n private current(): Map<string, PendingTrustedValue> | undefined {\n return RequestContext.get<Map<string, PendingTrustedValue>>(PENDING_WIRE_TRUST_KEY);\n }\n}\n\n/** The process-wide holding pen. See {@link PendingWireTrustImpl}. */\nexport const PendingWireTrust = new PendingWireTrustImpl();\n"]}
|
|
1
|
+
{"version":3,"file":"PendingWireTrust.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/PendingWireTrust.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAElD;;;;GAIG;AACH,MAAM,sBAAsB,GAAG,kCAAkC,CAAC;AAElE;;;;GAIG;AACH,MAAa,mBAAmB;IAER;IACA;IAFpB,YACoB,GAAyB,EACzB,KAAa;QADb,QAAG,GAAH,GAAG,CAAsB;QACzB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,kDAKC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,oBAAoB;IACtB;;;OAGG;IACH,KAAK,CAAC,GAAyB,EAAE,KAAa;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,GAAG,EAA+B,CAAC;QACzE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QAC3D,+BAAc,CAAC,GAAG,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAyB;QAC3B,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC;IAClD,CAAC;IAED;;;OAGG;IACH,OAAO;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,+BAAc,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,CAAC;IAEO,OAAO;QACX,OAAO,+BAAc,CAAC,GAAG,CAAmC,sBAAsB,CAAC,CAAC;IACxF,CAAC;CACJ;AAED,sEAAsE;AACzD,QAAA,gBAAgB,GAAG,IAAI,oBAAoB,EAAE,CAAC","sourcesContent":["import { AnyTrustedContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * Reserved (deliberately UNREGISTERED) slot holding the pending map. Not a ContextKey: it is\n * framework plumbing that exists only between the inbound fill and the AuthFilter, and giving it a\n * ContextKey would make it transferrable/loggable, neither of which it should ever be.\n */\nconst PENDING_WIRE_TRUST_KEY = '__webpieces_pending_wire_trust__';\n\n/**\n * One trusted key that ARRIVED ON THE WIRE and has not yet been vouched for. Data-only (a class,\n * per the guidelines). Carries the key as well as the value so a rejection message can name the\n * HTTP header the caller actually sent, not just the context name.\n */\nexport class PendingTrustedValue {\n constructor(\n public readonly key: AnyTrustedContextKey,\n public readonly value: string,\n ) {}\n}\n\n/**\n * PendingWireTrust - the holding pen between \"a trusted key arrived on the wire\" and \"we know\n * whether we may believe it\".\n *\n * ## The hole this closes\n *\n * `RequestContextHeaders.fillFromRequest` runs at the TRANSPORT level (`ExpressWrapper`), and\n * `AuthFilter` runs later, as a filter. So the wire always gets to write first and the authenticator\n * second. If the inbound loop wrote trusted keys straight into the context, then for the entire\n * window before AuthFilter ran — and forever after, on any route where the authenticator does not\n * happen to stamp that particular key — a value typed by whoever sent the request would be sitting\n * in the slot that `getTrusted` reads. `curl -H 'x-user-id: victim'` would be an authenticated\n * identity.\n *\n * That is not hypothetical on this codebase: the framework's own {@link DefaultJwtHook} returns an\n * EMPTY `AuthenticatedCaller.entries`, so a fully verified `jwt()` request stamps no context entries at\n * all and would leave a forged `x-user-id` completely unopposed.\n *\n * ## The fix\n *\n * Inbound trusted values never enter the context. They are stashed HERE, and `AuthFilter` decides\n * what to do with them once it knows who the caller is:\n *\n * - `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` — the CALLER's own identity was verified, so this is an\n * internal service passing along context it already holds. Admit the pending values as trusted.\n * This is what makes service-to-service propagation of a verified userId work, and it is the whole\n * reason trusted keys are allowed to have an `httpHeader` at all.\n * - `jwt()` / public — the caller may be a browser or anyone with curl. A pending value is\n * admitted ONLY if the authenticator independently derived the SAME value. Anything else — a\n * different value, or a value nothing vouched for — rejects the request.\n *\n * Note the deliberate asymmetry with a \"strip it and carry on\" design: a mismatch is not merely\n * neutralized, it FAILS. Rate limiters commonly bucket on the inbound header rather than on the JWT,\n * so a request whose header says `alice` and whose JWT says `bob` was rate-limited as the wrong\n * principal. Letting the JWT quietly win would turn every forged header into a free rate-limit\n * bypass. There is no legitimate caller that sends a header contradicting its own credential.\n *\n * ## If nothing reconciles\n *\n * `AuthFilter` is auto-installed on every route, so reconciliation always happens on a webpieces\n * server. Should a non-webpieces transport ever call `fillFromRequest` without one, the pending\n * values simply never arrive — the trusted key reads as absent. That is the fail-SAFE direction, and\n * it is intentional: silence beats an unvouched value.\n *\n * Module-global instance (like {@link RequestContext} itself) rather than a DI singleton — it is\n * stateless plumbing over the ambient context, used by one class on each side of the boundary.\n */\nclass PendingWireTrustImpl {\n /**\n * Hold an inbound wire value for a trusted key. Called by the inbound fill INSTEAD of writing it\n * into the context.\n */\n stash(key: AnyTrustedContextKey, value: string): void {\n const pending = this.current() ?? new Map<string, PendingTrustedValue>();\n pending.set(key.name, new PendingTrustedValue(key, value));\n RequestContext.put(PENDING_WIRE_TRUST_KEY, pending);\n }\n\n /**\n * Does this request carry a not-yet-reconciled wire value for `key`? A read-only peek: it admits\n * nothing. An OIDC hop asks it to tell an ORIGINATING service (nothing forwarded) from one relaying\n * somebody else's identity, whose forwarded roles must win over any role of the relaying service.\n */\n holds(key: AnyTrustedContextKey): boolean {\n return this.current()?.has(key.name) ?? false;\n }\n\n /**\n * Everything stashed for this request, and CLEAR the pen in the same step — so reconciliation\n * cannot run twice and a value cannot be re-admitted later by anything else.\n */\n takeAll(): PendingTrustedValue[] {\n const pending = this.current();\n if (!pending) {\n return [];\n }\n RequestContext.remove(PENDING_WIRE_TRUST_KEY);\n return Array.from(pending.values());\n }\n\n private current(): Map<string, PendingTrustedValue> | undefined {\n return RequestContext.get<Map<string, PendingTrustedValue>>(PENDING_WIRE_TRUST_KEY);\n }\n}\n\n/** The process-wide holding pen. See {@link PendingWireTrustImpl}. */\nexport const PendingWireTrust = new PendingWireTrustImpl();\n"]}
|