@webpieces/core-context 0.4.956 → 0.4.958
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/AuthenticatedCaller.d.ts +34 -0
- package/src/AuthenticatedCaller.js +50 -0
- package/src/AuthenticatedCaller.js.map +1 -0
- package/src/HttpRequest.d.ts +3 -3
- package/src/HttpRequest.js +1 -1
- package/src/HttpRequest.js.map +1 -1
- package/src/RequestContextHeaders.js +20 -7
- package/src/RequestContextHeaders.js.map +1 -1
- package/src/VendorAuthHook.d.ts +127 -0
- package/src/VendorAuthHook.js +101 -0
- package/src/VendorAuthHook.js.map +1 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +10 -1
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/core-context",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.958",
|
|
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.958",
|
|
26
26
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"reflect-metadata": "0.2.2"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ContextTuple } from '@webpieces/core-util';
|
|
2
|
+
/**
|
|
3
|
+
* AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook
|
|
4
|
+
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
5
|
+
* {@link VendorAuthHook.verify}. Four fields, three jobs:
|
|
6
|
+
*
|
|
7
|
+
* - `userId` — WHO the caller is, as the credential proved it.
|
|
8
|
+
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
9
|
+
* reads, `claims` carries credential facts for the authenticator to validate and publish
|
|
10
|
+
* as trusted application context before AuthorizationHook evaluates policy.
|
|
11
|
+
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
12
|
+
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
13
|
+
* derived from the credential it just verified.
|
|
14
|
+
*
|
|
15
|
+
* NAMING, said out loud so it is not "fixed" back: it is deliberately NOT a `TrustedContextMap`.
|
|
16
|
+
* Three of the four fields are not context, and it is not a Map — it is the authenticated caller,
|
|
17
|
+
* and the context is one thing it carries.
|
|
18
|
+
*
|
|
19
|
+
* Data-only structure (a class, per the guidelines).
|
|
20
|
+
*/
|
|
21
|
+
export declare class AuthenticatedCaller {
|
|
22
|
+
readonly userId: string | undefined;
|
|
23
|
+
readonly roles: string[];
|
|
24
|
+
readonly entries: ContextTuple[];
|
|
25
|
+
readonly claims: Record<string, unknown>;
|
|
26
|
+
readonly machine?: AuthenticatedMachineIdentity | undefined;
|
|
27
|
+
constructor(userId: string | undefined, roles?: string[], entries?: ContextTuple[], claims?: Record<string, unknown>, machine?: AuthenticatedMachineIdentity | undefined);
|
|
28
|
+
}
|
|
29
|
+
/** Verified external machine identity does not grant trusted-header delegation. */
|
|
30
|
+
export declare class AuthenticatedMachineIdentity {
|
|
31
|
+
readonly mechanism: string;
|
|
32
|
+
readonly identity: string;
|
|
33
|
+
constructor(mechanism: string, identity: string);
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthenticatedMachineIdentity = exports.AuthenticatedCaller = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook
|
|
6
|
+
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
7
|
+
* {@link VendorAuthHook.verify}. Four fields, three jobs:
|
|
8
|
+
*
|
|
9
|
+
* - `userId` — WHO the caller is, as the credential proved it.
|
|
10
|
+
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
11
|
+
* reads, `claims` carries credential facts for the authenticator to validate and publish
|
|
12
|
+
* as trusted application context before AuthorizationHook evaluates policy.
|
|
13
|
+
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
14
|
+
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
15
|
+
* derived from the credential it just verified.
|
|
16
|
+
*
|
|
17
|
+
* NAMING, said out loud so it is not "fixed" back: it is deliberately NOT a `TrustedContextMap`.
|
|
18
|
+
* Three of the four fields are not context, and it is not a Map — it is the authenticated caller,
|
|
19
|
+
* and the context is one thing it carries.
|
|
20
|
+
*
|
|
21
|
+
* Data-only structure (a class, per the guidelines).
|
|
22
|
+
*/
|
|
23
|
+
class AuthenticatedCaller {
|
|
24
|
+
userId;
|
|
25
|
+
roles;
|
|
26
|
+
entries;
|
|
27
|
+
claims;
|
|
28
|
+
machine;
|
|
29
|
+
constructor(userId, roles = [], entries = [],
|
|
30
|
+
// webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)
|
|
31
|
+
claims = {}, machine) {
|
|
32
|
+
this.userId = userId;
|
|
33
|
+
this.roles = roles;
|
|
34
|
+
this.entries = entries;
|
|
35
|
+
this.claims = claims;
|
|
36
|
+
this.machine = machine;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.AuthenticatedCaller = AuthenticatedCaller;
|
|
40
|
+
/** Verified external machine identity does not grant trusted-header delegation. */
|
|
41
|
+
class AuthenticatedMachineIdentity {
|
|
42
|
+
mechanism;
|
|
43
|
+
identity;
|
|
44
|
+
constructor(mechanism, identity) {
|
|
45
|
+
this.mechanism = mechanism;
|
|
46
|
+
this.identity = identity;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.AuthenticatedMachineIdentity = AuthenticatedMachineIdentity;
|
|
50
|
+
//# sourceMappingURL=AuthenticatedCaller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthenticatedCaller.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/AuthenticatedCaller.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;GAkBG;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 VendorAuthHook.verify}. Four fields, three jobs:\n *\n * - `userId` — WHO the caller is, as the credential proved it.\n * - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check\n * reads, `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"]}
|
package/src/HttpRequest.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare class HttpRequest {
|
|
|
21
21
|
/**
|
|
22
22
|
* The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`
|
|
23
23
|
* route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:
|
|
24
|
-
* `
|
|
24
|
+
* `vendorAuth(...)` has nothing to verify without it and 401s rather than waving the call
|
|
25
25
|
* through. A spec driving a webhook route in-process supplies one here, the same way a spec
|
|
26
26
|
* today supplies an `authorization` header.
|
|
27
27
|
*/
|
|
@@ -32,7 +32,7 @@ export declare class HttpRequest {
|
|
|
32
32
|
/**
|
|
33
33
|
* The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`
|
|
34
34
|
* route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:
|
|
35
|
-
* `
|
|
35
|
+
* `vendorAuth(...)` has nothing to verify without it and 401s rather than waving the call
|
|
36
36
|
* through. A spec driving a webhook route in-process supplies one here, the same way a spec
|
|
37
37
|
* today supplies an `authorization` header.
|
|
38
38
|
*/
|
|
@@ -46,7 +46,7 @@ export declare class HttpRequest {
|
|
|
46
46
|
* RawHttpRequest - an {@link HttpRequest} on a `@Endpoint(..., { rawBody: true })` route, where
|
|
47
47
|
* {@link HttpRequest.raw} is PRESENT rather than optional.
|
|
48
48
|
*
|
|
49
|
-
* It exists so a vendor's
|
|
49
|
+
* It exists so a vendor's VendorAuthHook never writes `request.raw!` or an `if (!raw) throw`. The
|
|
50
50
|
* absence of the bytes is checked in exactly ONE place — `AuthFilter` 401s before it calls the hook —
|
|
51
51
|
* and this type is what carries the result of that check into the signature, so the bad state is
|
|
52
52
|
* unrepresentable downstream instead of re-guarded at every implementor.
|
package/src/HttpRequest.js
CHANGED
|
@@ -25,7 +25,7 @@ class HttpRequest {
|
|
|
25
25
|
/**
|
|
26
26
|
* The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`
|
|
27
27
|
* route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:
|
|
28
|
-
* `
|
|
28
|
+
* `vendorAuth(...)` has nothing to verify without it and 401s rather than waving the call
|
|
29
29
|
* through. A spec driving a webhook route in-process supplies one here, the same way a spec
|
|
30
30
|
* today supplies an `authorization` header.
|
|
31
31
|
*/
|
package/src/HttpRequest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpRequest.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/HttpRequest.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;GAYG;AACH,MAAa,WAAW;IAEA;IACA;IAEA;IAQA;IAZpB,YACoB,MAAc,EACd,IAAY;IAC5B,iFAAiF;IACjE,OAA8B;IAC9C;;;;;;OAMG;IACa,GAAgB;QAXhB,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAEZ,YAAO,GAAP,OAAO,CAAuB;QAQ9B,QAAG,GAAH,GAAG,CAAa;IACjC,CAAC;IAEJ,8FAA8F;IAC9F,SAAS,CAAC,GAA2B;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/D,CAAC;IAED,8BAA8B;IAC9B,eAAe,CAAC,GAA2B;QACvC,MAAM,IAAI,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1F,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;CACJ;AA3BD,kCA2BC","sourcesContent":["import { ContextKey, AnyContextKey } from '@webpieces/core-util';\nimport { RawRequest } from './RawRequest';\n\n/**\n * HttpRequest - webpieces' TRANSPORT-NEUTRAL inbound request.\n *\n * This is @webpieces/http-routing's own request type (http-routing re-exports it), NOT\n * express's `req`. Each transport adapter (the express adapter in @webpieces/http-server, or\n * any other TypeScript web framework) builds an HttpRequest from its native request and hands\n * it to the router. Filters and the auth layer read it via `RequestContext.getRequest()`\n * instead of touching express — which is what lets the SAME filter chain run over HTTP and\n * in-process (tests build an HttpRequest carrying their credential).\n *\n * It lives in core-context (alongside RequestContext, which stores it in AsyncLocalStorage)\n * to avoid a core-context → http-routing cycle; it is a pure data holder (no express, no DI).\n */\nexport class HttpRequest {\n constructor(\n public readonly method: string,\n public readonly path: string,\n /** Header name (lowercased) -> values (HTTP allows multiple values per name). */\n public readonly headers: Map<string, string[]>,\n /**\n * The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`\n * route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:\n * `
|
|
1
|
+
{"version":3,"file":"HttpRequest.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/HttpRequest.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;GAYG;AACH,MAAa,WAAW;IAEA;IACA;IAEA;IAQA;IAZpB,YACoB,MAAc,EACd,IAAY;IAC5B,iFAAiF;IACjE,OAA8B;IAC9C;;;;;;OAMG;IACa,GAAgB;QAXhB,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAEZ,YAAO,GAAP,OAAO,CAAuB;QAQ9B,QAAG,GAAH,GAAG,CAAa;IACjC,CAAC;IAEJ,8FAA8F;IAC9F,SAAS,CAAC,GAA2B;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/D,CAAC;IAED,8BAA8B;IAC9B,eAAe,CAAC,GAA2B;QACvC,MAAM,IAAI,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1F,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;CACJ;AA3BD,kCA2BC","sourcesContent":["import { ContextKey, AnyContextKey } from '@webpieces/core-util';\nimport { RawRequest } from './RawRequest';\n\n/**\n * HttpRequest - webpieces' TRANSPORT-NEUTRAL inbound request.\n *\n * This is @webpieces/http-routing's own request type (http-routing re-exports it), NOT\n * express's `req`. Each transport adapter (the express adapter in @webpieces/http-server, or\n * any other TypeScript web framework) builds an HttpRequest from its native request and hands\n * it to the router. Filters and the auth layer read it via `RequestContext.getRequest()`\n * instead of touching express — which is what lets the SAME filter chain run over HTTP and\n * in-process (tests build an HttpRequest carrying their credential).\n *\n * It lives in core-context (alongside RequestContext, which stores it in AsyncLocalStorage)\n * to avoid a core-context → http-routing cycle; it is a pure data holder (no express, no DI).\n */\nexport class HttpRequest {\n constructor(\n public readonly method: string,\n public readonly path: string,\n /** Header name (lowercased) -> values (HTTP allows multiple values per name). */\n public readonly headers: Map<string, string[]>,\n /**\n * The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`\n * route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:\n * `vendorAuth(...)` has nothing to verify without it and 401s rather than waving the call\n * through. A spec driving a webhook route in-process supplies one here, the same way a spec\n * today supplies an `authorization` header.\n */\n public readonly raw?: RawRequest,\n ) {}\n\n /** First value of a header, looked up by ContextKey.httpHeader (or a raw lowercased name). */\n getHeader(key: AnyContextKey | string): string | undefined {\n const values = this.getHeaderValues(key);\n return values && values.length > 0 ? values[0] : undefined;\n }\n\n /** All values of a header. */\n getHeaderValues(key: AnyContextKey | string): string[] | undefined {\n const name = (typeof key === 'string' ? key : (key.httpHeader ?? key.name)).toLowerCase();\n return this.headers.get(name);\n }\n}\n\n/**\n * RawHttpRequest - an {@link HttpRequest} on a `@Endpoint(..., { rawBody: true })` route, where\n * {@link HttpRequest.raw} is PRESENT rather than optional.\n *\n * It exists so a vendor's VendorAuthHook never writes `request.raw!` or an `if (!raw) throw`. The\n * absence of the bytes is checked in exactly ONE place — `AuthFilter` 401s before it calls the hook —\n * and this type is what carries the result of that check into the signature, so the bad state is\n * unrepresentable downstream instead of re-guarded at every implementor.\n *\n * A TYPE, not a class: it narrows a field of an existing class rather than describing new data, so\n * the transports keep building the one `HttpRequest` they always built.\n */\nexport type RawHttpRequest = HttpRequest & { readonly raw: RawRequest };\n"]}
|
|
@@ -47,23 +47,35 @@ let RequestContextHeaders = class RequestContextHeaders {
|
|
|
47
47
|
buildOutboundHeaders(destination) {
|
|
48
48
|
this.requireActiveContext();
|
|
49
49
|
const headers = new Map();
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// An EXTERNAL_OUTBOUND destination lists EXACTLY what it sends, and each listed key is
|
|
51
|
+
// required; every other destination filters the registry's transferred keys
|
|
52
|
+
// (getTransferredKeys() is precomputed at configure() time).
|
|
53
|
+
const listed = destination.listedKeys();
|
|
54
|
+
for (const key of listed ?? core_util_1.HeaderRegistry.get().getTransferredKeys()) {
|
|
52
55
|
if (!destination.allows(key)) {
|
|
53
56
|
continue;
|
|
54
57
|
}
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
+
// The key list is AnyContextKey[] — mixed in both value type and trust — so this reads
|
|
59
|
+
// through getAny (serialization to the wire, not a trust decision) and narrows with the
|
|
60
|
+
// typeof-string guard; every transferred value is a wire string.
|
|
58
61
|
const value = RequestContext_1.RequestContext.getAny(key);
|
|
59
62
|
if (typeof value === 'string' && value !== '') {
|
|
60
63
|
headers.set(key.httpHeader, value);
|
|
61
64
|
}
|
|
65
|
+
else if (listed !== undefined) {
|
|
66
|
+
throw new core_util_1.ApiImplementationError(`Context key '${key.name}' (header ${key.httpHeader}) is listed in @ExternalContextKeys for this ` +
|
|
67
|
+
`outbound call but has no value in the RequestContext. Put it in the context before calling — ` +
|
|
68
|
+
`nothing was sent.`);
|
|
69
|
+
}
|
|
62
70
|
}
|
|
63
71
|
// CLIENT_VERSION is transferred, but each hop sends ITS OWN build version (not the inherited
|
|
64
72
|
// one) so a downstream server logs which build actually called it. Overwrite whatever the loop
|
|
65
73
|
// copied from an inbound clientVersion with ours; if THIS service has no version, drop it
|
|
66
74
|
// rather than forward the caller's as if it were ours. Non-throwing read — absent before setInfo.
|
|
75
|
+
// An EXTERNAL_OUTBOUND destination gets no version stamp unless it LISTS the key.
|
|
76
|
+
if (!destination.allows(core_util_1.WebpiecesCoreHeaders.CLIENT_VERSION)) {
|
|
77
|
+
return headers;
|
|
78
|
+
}
|
|
67
79
|
const myVersion = core_util_1.ServiceInfo.getVersion();
|
|
68
80
|
const clientVersionHeader = core_util_1.WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader;
|
|
69
81
|
if (myVersion) {
|
|
@@ -143,8 +155,9 @@ let RequestContextHeaders = class RequestContextHeaders {
|
|
|
143
155
|
if (value === null || value === '') {
|
|
144
156
|
continue;
|
|
145
157
|
}
|
|
146
|
-
if (!destination.
|
|
147
|
-
// DROPPED: a trusted key asserted by a destination we did not authenticate to
|
|
158
|
+
if (!destination.believesResponse(key)) {
|
|
159
|
+
// DROPPED: a trusted key asserted by a destination we did not authenticate to, or ANY
|
|
160
|
+
// key from an EXTERNAL_OUTBOUND (vendor) destination.
|
|
148
161
|
continue;
|
|
149
162
|
}
|
|
150
163
|
const merged = key.mergeResponseValue(RequestContext_1.RequestContext.getAny(key), value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestContextHeaders.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextHeaders.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AnyContextKey,\n DestinationTrust,\n HeaderRegistry,\n RecorderKeys,\n ServiceInfo,\n TestCaseRecorder,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton } from './frameworkProvide';\nimport { PendingWireTrust } from './PendingWireTrust';\nimport { HttpRequest } from './HttpRequest';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextHeaders - the magic context ↔ the wire, for a SERVER. Both directions live here:\n *\n * inbound {@link fillFromRequest} the published HttpRequest's headers -> the context\n * outbound {@link buildOutboundHeaders} the context -> the next hop's request headers\n * response-out {@link buildResponseHeaders} the context -> THIS hop's response headers\n * response-in {@link acceptResponseHeaders} a callee's response headers -> the context\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * Every transferred key with a non-empty value THAT THIS DESTINATION MAY RECEIVE, under its wire\n * name. Nothing is rewritten.\n *\n * That includes `x-request-id`, which propagates unchanged: one id correlates the whole call\n * tree, so the callee keeps ours rather than minting its own. ({@link fillFromRequest} only\n * generates an id when the inbound request carries none.)\n *\n * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.\n * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its\n * CALLER, so shipping one to a `@WpAuthPublic` / `jwt()` endpoint builds a request the callee is\n * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own\n * AuthMode — there is no \"send everything\" default to fall into. Untrusted keys always travel.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n *\n * @throws Error when called outside `RequestContext.run(...)` — see the class doc.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n this.requireActiveContext();\n\n const headers = new Map<string, string>();\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n // getTransferredKeys() is AnyContextKey[] — mixed in both value type and trust — so this\n // reads through getAny (serialization to the wire, not a trust decision) and narrows with\n // the typeof-string guard; every transferred value is a wire string.\n const value = RequestContext.getAny(key);\n if (typeof value === 'string' && value !== '') {\n headers.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION is transferred, but each hop sends ITS OWN build version (not the inherited\n // one) so a downstream server logs which build actually called it. Overwrite whatever the loop\n // copied from an inbound clientVersion with ours; if THIS service has no version, drop it\n // rather than forward the caller's as if it were ours. Non-throwing read — absent before setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n headers.set(clientVersionHeader, myVersion);\n } else {\n headers.delete(clientVersionHeader);\n }\n\n return headers;\n }\n\n /**\n * OUTBOUND, RESPONSE direction — the mirror of {@link buildOutboundHeaders}: every key that\n * declares a `responseHeader` and has a value in this context, under its response wire name.\n *\n * This is what the server's ONE response choke point writes onto EVERY response (success and\n * error, webpieces' default body and an app's own). Before it there was a single hard-coded\n * `res.setHeader('x-request-id', ...)` in `ExpressWrapper.stampTransactionId` that no other key\n * could join without editing that method; `x-request-id` is now an ordinary registry entry and\n * the loop is the whole feature.\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing — deliberately, and unlike\n * {@link buildOutboundHeaders}. A response is written on paths where the context was never\n * established: a malformed or oversize body is refused BEFORE `fillFromRequest` mints an id. That\n * is the accepted known issue the deleted `stampTransactionId` recorded, and throwing here would\n * turn a 400 into a 500.\n *\n * A `collect` key holds a LIST, so it is emitted as the comma-separated list HTTP already has a\n * grammar for. A value that is neither a string nor a list of strings is skipped: a response\n * header is text, and an object-valued key (the api tag, the recorder) has no wire form.\n */\n buildResponseHeaders(): Map<string, string> {\n const headers = new Map<string, string>();\n if (!RequestContext.isActive()) {\n return headers;\n }\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const wire = this.toResponseWireValue(RequestContext.getAny(key));\n if (wire !== undefined && wire !== '') {\n headers.set(key.responseHeader!, wire);\n }\n }\n return headers;\n }\n\n /** One context value -> its response-header text, or undefined when it has no wire form. */\n // webpieces-disable no-any-unknown -- the context store is honestly type-erased; the key's own V types the public accessors\n private toResponseWireValue(value: unknown): string | undefined {\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n // webpieces-disable no-any-unknown -- a collect key's list is type-erased here, exactly as every other serialization read is\n const parts = (value as unknown[]).filter((v: unknown) => typeof v === 'string');\n return parts.length > 0 ? parts.join(', ') : undefined;\n }\n return undefined;\n }\n\n /**\n * INBOUND, RESPONSE direction — the exact inverse of {@link buildResponseHeaders}, and the reason\n * a value can travel UP the call tree hop by hop: a webpieces client calls this with the response\n * it just received, and every declared response key lands in the CALLER's context.\n *\n * TRUST IS THE MIRROR OF THE INBOUND PROBLEM, and is not hand-waved. A response is another\n * process's ASSERTION, caller-asserted from our seat. An UNTRUSTED response key is admitted —\n * nobody was ever going to make a security decision on it. A TRUSTED one is admitted only from a\n * destination this client AUTHENTICATED TO (the same {@link DestinationTrust} table that decides\n * what may ride OUT) and is otherwise DROPPED. Without that, a partner's server sets `x-user-id`\n * on its response and this process believes it.\n *\n * MERGE is the key's own declaration, because fan-out is the normal case: one request calling\n * five services yields five values for one key, and last-write-wins silently keeps one. See\n * {@link ContextKey.mergeResponseValue}.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n this.requireActiveContext();\n\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.allows(key)) {\n // DROPPED: a trusted key asserted by a destination we did not authenticate to.\n continue;\n }\n const merged = key.mergeResponseValue(RequestContext.getAny(key), value);\n if (key.isTrusted()) {\n RequestContext.putTrusted(key, merged);\n } else {\n RequestContext.putUntrusted(key, merged);\n }\n }\n }\n\n /**\n * INBOUND — the exact inverse of {@link buildOutboundHeaders}. Publish the request, move every\n * transferrable header off it into the context (read by wire name, stored under the key's\n * `name`), and mint an `x-request-id` if the caller sent none.\n *\n * The request is a PARAMETER, not something we fish back out of the context. Publishing and\n * filling are therefore one atomic step that cannot be half-done or done out of order — the\n * older `setRequest()` + `fillContext()` pair could silently skip the transfer entirely when a\n * caller forgot the first half.\n *\n * This is a PRECONDITION of calling into http-routing, and it belongs ABOVE the api boundary.\n * `WebpiecesMiddleware` does it for every HTTP request; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. The api proxy only checks that a\n * request scope exists — it never builds one.\n *\n * @throws Error when called outside `RequestContext.run(...)`.\n */\n fillFromRequest(request: HttpRequest): void {\n this.requireActiveContext();\n\n RequestContext.setRequest(request);\n\n // Stamp the inbound method+path as top-level logged keys (jsonPayload.httpMethod / requestPath)\n // so EVERY log line of this request carries them. Sourced from the just-published HttpRequest;\n // NOT transferred over the wire, so a downstream hop stamps its own inbound values.\n RequestContext.putUntrusted(WebpiecesCoreHeaders.HTTP_METHOD, request.method);\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_PATH, request.path);\n\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n const values = request.getHeaderValues(key);\n if (values && values.length > 0) {\n this.acceptInbound(key, values[0]);\n }\n }\n\n if (!RequestContext.hasKey(WebpiecesCoreHeaders.REQUEST_ID)) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());\n this.stampRequestIdSource();\n }\n }\n\n /**\n * ONE inbound header -> the context, routed by the key's TRUST.\n *\n * An untrusted key goes straight in — nobody was ever going to make a security decision on it.\n *\n * A TRUSTED key does NOT. This transport-level fill runs BEFORE any filter, so at this instant\n * nothing has verified who the caller is; writing the value now would mean `getTrusted` could\n * return a header a stranger typed. It is stashed in {@link PendingWireTrust} instead and\n * admitted (or rejected) by `AuthFilter`, which knows the route's auth mode. See that class for\n * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`\n * and therefore the reason service-to-service identity propagation works at all.\n *\n * There is NO cast here, on EITHER branch. `isTrusted()` is a type predicate over the\n * {@link AnyContextKey} union, so the `if` holds a trusted key and the `else` holds an untrusted\n * one — the runtime check produces the type it proves. Trust is binary and the union has exactly\n * two constituents, so the `else` is the whole remaining case rather than a silent drop.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key, value);\n } else {\n RequestContext.putUntrusted(key, value);\n }\n }\n\n /**\n * Record that WE minted the id — only ever called from the generate branch above, so the key is\n * ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.\n *\n * Uses the non-throwing `getName()`: this runs PER REQUEST, and a missing log field must not 500\n * live traffic. A server that booted already ran `setupRuntime`, which calls `ServiceInfo.setInfo`\n * with its required name+version, so the name is always there in practice; only a test driving the\n * context directly sees undefined.\n */\n private stampRequestIdSource(): void {\n const svcName = ServiceInfo.getName();\n if (svcName) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);\n }\n }\n\n /** The id every log line of this request, and every downstream hop, will carry. */\n private generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * The recorder travelling in the context, when a test is recording this call. Absent in normal\n * operation, and ALWAYS absent in a browser — which is why recording lives on the server-side\n * client and never in the isomorphic core.\n */\n findRecorder(): TestCaseRecorder | undefined {\n if (!RequestContext.isActive()) {\n return undefined;\n }\n return RequestContext.getUntrusted<TestCaseRecorder>(RecorderKeys.RECORDER);\n }\n\n /** Guard both directions: no ambient request scope means there is no context to fill or read. */\n private requireActiveContext(): void {\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. A webpieces server-side client only works inside ' +\n 'RequestContext.run(...), which a top-level server filter normally establishes. ' +\n 'In a test, wrap the call: await RequestContext.run(async () => client.foo(req));',\n );\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RequestContextHeaders.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextHeaders.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AnyContextKey,\n ApiImplementationError,\n DestinationTrust,\n HeaderRegistry,\n RecorderKeys,\n ServiceInfo,\n TestCaseRecorder,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton } from './frameworkProvide';\nimport { PendingWireTrust } from './PendingWireTrust';\nimport { HttpRequest } from './HttpRequest';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextHeaders - the magic context ↔ the wire, for a SERVER. Both directions live here:\n *\n * inbound {@link fillFromRequest} the published HttpRequest's headers -> the context\n * outbound {@link buildOutboundHeaders} the context -> the next hop's request headers\n * response-out {@link buildResponseHeaders} the context -> THIS hop's response headers\n * response-in {@link acceptResponseHeaders} a callee's response headers -> the context\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * Every transferred key with a non-empty value THAT THIS DESTINATION MAY RECEIVE, under its wire\n * name. Nothing is rewritten.\n *\n * That includes `x-request-id`, which propagates unchanged: one id correlates the whole call\n * tree, so the callee keeps ours rather than minting its own. ({@link fillFromRequest} only\n * generates an id when the inbound request carries none.)\n *\n * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.\n * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its\n * CALLER, so shipping one to a `@WpAuthPublic` / `jwt()` endpoint builds a request the callee is\n * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own\n * AuthMode — there is no \"send everything\" default to fall into. Untrusted keys always travel.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n *\n * @throws Error when called outside `RequestContext.run(...)` — see the class doc.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n this.requireActiveContext();\n\n const headers = new Map<string, string>();\n // An EXTERNAL_OUTBOUND destination lists EXACTLY what it sends, and each listed key is\n // required; every other destination filters the registry's transferred keys\n // (getTransferredKeys() is precomputed at configure() time).\n const listed = destination.listedKeys();\n for (const key of listed ?? HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n // The key list is AnyContextKey[] — mixed in both value type and trust — so this reads\n // through getAny (serialization to the wire, not a trust decision) and narrows with the\n // typeof-string guard; every transferred value is a wire string.\n const value = RequestContext.getAny(key);\n if (typeof value === 'string' && value !== '') {\n headers.set(key.httpHeader!, value);\n } else if (listed !== undefined) {\n throw new ApiImplementationError(\n `Context key '${key.name}' (header ${key.httpHeader}) is listed in @ExternalContextKeys for this ` +\n `outbound call but has no value in the RequestContext. Put it in the context before calling — ` +\n `nothing was sent.`,\n );\n }\n }\n\n // CLIENT_VERSION is transferred, but each hop sends ITS OWN build version (not the inherited\n // one) so a downstream server logs which build actually called it. Overwrite whatever the loop\n // copied from an inbound clientVersion with ours; if THIS service has no version, drop it\n // rather than forward the caller's as if it were ours. Non-throwing read — absent before setInfo.\n // An EXTERNAL_OUTBOUND destination gets no version stamp unless it LISTS the key.\n if (!destination.allows(WebpiecesCoreHeaders.CLIENT_VERSION)) {\n return headers;\n }\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n headers.set(clientVersionHeader, myVersion);\n } else {\n headers.delete(clientVersionHeader);\n }\n\n return headers;\n }\n\n /**\n * OUTBOUND, RESPONSE direction — the mirror of {@link buildOutboundHeaders}: every key that\n * declares a `responseHeader` and has a value in this context, under its response wire name.\n *\n * This is what the server's ONE response choke point writes onto EVERY response (success and\n * error, webpieces' default body and an app's own). Before it there was a single hard-coded\n * `res.setHeader('x-request-id', ...)` in `ExpressWrapper.stampTransactionId` that no other key\n * could join without editing that method; `x-request-id` is now an ordinary registry entry and\n * the loop is the whole feature.\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing — deliberately, and unlike\n * {@link buildOutboundHeaders}. A response is written on paths where the context was never\n * established: a malformed or oversize body is refused BEFORE `fillFromRequest` mints an id. That\n * is the accepted known issue the deleted `stampTransactionId` recorded, and throwing here would\n * turn a 400 into a 500.\n *\n * A `collect` key holds a LIST, so it is emitted as the comma-separated list HTTP already has a\n * grammar for. A value that is neither a string nor a list of strings is skipped: a response\n * header is text, and an object-valued key (the api tag, the recorder) has no wire form.\n */\n buildResponseHeaders(): Map<string, string> {\n const headers = new Map<string, string>();\n if (!RequestContext.isActive()) {\n return headers;\n }\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const wire = this.toResponseWireValue(RequestContext.getAny(key));\n if (wire !== undefined && wire !== '') {\n headers.set(key.responseHeader!, wire);\n }\n }\n return headers;\n }\n\n /** One context value -> its response-header text, or undefined when it has no wire form. */\n // webpieces-disable no-any-unknown -- the context store is honestly type-erased; the key's own V types the public accessors\n private toResponseWireValue(value: unknown): string | undefined {\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n // webpieces-disable no-any-unknown -- a collect key's list is type-erased here, exactly as every other serialization read is\n const parts = (value as unknown[]).filter((v: unknown) => typeof v === 'string');\n return parts.length > 0 ? parts.join(', ') : undefined;\n }\n return undefined;\n }\n\n /**\n * INBOUND, RESPONSE direction — the exact inverse of {@link buildResponseHeaders}, and the reason\n * a value can travel UP the call tree hop by hop: a webpieces client calls this with the response\n * it just received, and every declared response key lands in the CALLER's context.\n *\n * TRUST IS THE MIRROR OF THE INBOUND PROBLEM, and is not hand-waved. A response is another\n * process's ASSERTION, caller-asserted from our seat. An UNTRUSTED response key is admitted —\n * nobody was ever going to make a security decision on it. A TRUSTED one is admitted only from a\n * destination this client AUTHENTICATED TO (the same {@link DestinationTrust} table that decides\n * what may ride OUT) and is otherwise DROPPED. Without that, a partner's server sets `x-user-id`\n * on its response and this process believes it.\n *\n * MERGE is the key's own declaration, because fan-out is the normal case: one request calling\n * five services yields five values for one key, and last-write-wins silently keeps one. See\n * {@link ContextKey.mergeResponseValue}.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n this.requireActiveContext();\n\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.believesResponse(key)) {\n // DROPPED: a trusted key asserted by a destination we did not authenticate to, or ANY\n // key from an EXTERNAL_OUTBOUND (vendor) destination.\n continue;\n }\n const merged = key.mergeResponseValue(RequestContext.getAny(key), value);\n if (key.isTrusted()) {\n RequestContext.putTrusted(key, merged);\n } else {\n RequestContext.putUntrusted(key, merged);\n }\n }\n }\n\n /**\n * INBOUND — the exact inverse of {@link buildOutboundHeaders}. Publish the request, move every\n * transferrable header off it into the context (read by wire name, stored under the key's\n * `name`), and mint an `x-request-id` if the caller sent none.\n *\n * The request is a PARAMETER, not something we fish back out of the context. Publishing and\n * filling are therefore one atomic step that cannot be half-done or done out of order — the\n * older `setRequest()` + `fillContext()` pair could silently skip the transfer entirely when a\n * caller forgot the first half.\n *\n * This is a PRECONDITION of calling into http-routing, and it belongs ABOVE the api boundary.\n * `WebpiecesMiddleware` does it for every HTTP request; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. The api proxy only checks that a\n * request scope exists — it never builds one.\n *\n * @throws Error when called outside `RequestContext.run(...)`.\n */\n fillFromRequest(request: HttpRequest): void {\n this.requireActiveContext();\n\n RequestContext.setRequest(request);\n\n // Stamp the inbound method+path as top-level logged keys (jsonPayload.httpMethod / requestPath)\n // so EVERY log line of this request carries them. Sourced from the just-published HttpRequest;\n // NOT transferred over the wire, so a downstream hop stamps its own inbound values.\n RequestContext.putUntrusted(WebpiecesCoreHeaders.HTTP_METHOD, request.method);\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_PATH, request.path);\n\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n const values = request.getHeaderValues(key);\n if (values && values.length > 0) {\n this.acceptInbound(key, values[0]);\n }\n }\n\n if (!RequestContext.hasKey(WebpiecesCoreHeaders.REQUEST_ID)) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());\n this.stampRequestIdSource();\n }\n }\n\n /**\n * ONE inbound header -> the context, routed by the key's TRUST.\n *\n * An untrusted key goes straight in — nobody was ever going to make a security decision on it.\n *\n * A TRUSTED key does NOT. This transport-level fill runs BEFORE any filter, so at this instant\n * nothing has verified who the caller is; writing the value now would mean `getTrusted` could\n * return a header a stranger typed. It is stashed in {@link PendingWireTrust} instead and\n * admitted (or rejected) by `AuthFilter`, which knows the route's auth mode. See that class for\n * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`\n * and therefore the reason service-to-service identity propagation works at all.\n *\n * There is NO cast here, on EITHER branch. `isTrusted()` is a type predicate over the\n * {@link AnyContextKey} union, so the `if` holds a trusted key and the `else` holds an untrusted\n * one — the runtime check produces the type it proves. Trust is binary and the union has exactly\n * two constituents, so the `else` is the whole remaining case rather than a silent drop.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key, value);\n } else {\n RequestContext.putUntrusted(key, value);\n }\n }\n\n /**\n * Record that WE minted the id — only ever called from the generate branch above, so the key is\n * ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.\n *\n * Uses the non-throwing `getName()`: this runs PER REQUEST, and a missing log field must not 500\n * live traffic. A server that booted already ran `setupRuntime`, which calls `ServiceInfo.setInfo`\n * with its required name+version, so the name is always there in practice; only a test driving the\n * context directly sees undefined.\n */\n private stampRequestIdSource(): void {\n const svcName = ServiceInfo.getName();\n if (svcName) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);\n }\n }\n\n /** The id every log line of this request, and every downstream hop, will carry. */\n private generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * The recorder travelling in the context, when a test is recording this call. Absent in normal\n * operation, and ALWAYS absent in a browser — which is why recording lives on the server-side\n * client and never in the isomorphic core.\n */\n findRecorder(): TestCaseRecorder | undefined {\n if (!RequestContext.isActive()) {\n return undefined;\n }\n return RequestContext.getUntrusted<TestCaseRecorder>(RecorderKeys.RECORDER);\n }\n\n /** Guard both directions: no ambient request scope means there is no context to fill or read. */\n private requireActiveContext(): void {\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. A webpieces server-side client only works inside ' +\n 'RequestContext.run(...), which a top-level server filter normally establishes. ' +\n 'In a test, wrap the call: await RequestContext.run(async () => client.foo(req));',\n );\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { AuthenticatedCaller } from './AuthenticatedCaller';
|
|
2
|
+
import { RawHttpRequest } from './HttpRequest';
|
|
3
|
+
/**
|
|
4
|
+
* ONE outbound request, as the thing being authenticated — what {@link VendorAuthHook.mint} receives.
|
|
5
|
+
* Data only.
|
|
6
|
+
*
|
|
7
|
+
* Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}
|
|
8
|
+
* and {@link body}:
|
|
9
|
+
*
|
|
10
|
+
* - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature
|
|
11
|
+
* computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook
|
|
12
|
+
* client the pre-filter URL is the client's own service name, which is not a destination at all.
|
|
13
|
+
* - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization
|
|
14
|
+
* happens before the filter chain runs and the transport sends this same string verbatim, so the
|
|
15
|
+
* bytes signed and the bytes sent cannot differ. A library that re-serializes internally signs one
|
|
16
|
+
* byte sequence and sends another, and the failure is silent.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SignableRequest {
|
|
19
|
+
/** The FINAL absolute URL this request is about to be sent to. */
|
|
20
|
+
readonly url: string;
|
|
21
|
+
/** The HTTP method, e.g. 'POST'. */
|
|
22
|
+
readonly httpMethod: string;
|
|
23
|
+
/** The EXACT serialized body, or undefined for a call with no body. */
|
|
24
|
+
readonly body: string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
|
|
27
|
+
* timestamp another filter set, a content-type) reads them, and the headers the hook returns
|
|
28
|
+
* are merged by the caller — so there is exactly one place headers are added and it is the
|
|
29
|
+
* return value.
|
|
30
|
+
*/
|
|
31
|
+
readonly headers: ReadonlyMap<string, string>;
|
|
32
|
+
/** The API contract's class name, e.g. 'PartnerWebhookApi'. */
|
|
33
|
+
readonly contractName: string;
|
|
34
|
+
/** The contract method being called, e.g. 'deliver'. */
|
|
35
|
+
readonly methodName: string;
|
|
36
|
+
constructor(
|
|
37
|
+
/** The FINAL absolute URL this request is about to be sent to. */
|
|
38
|
+
url: string,
|
|
39
|
+
/** The HTTP method, e.g. 'POST'. */
|
|
40
|
+
httpMethod: string,
|
|
41
|
+
/** The EXACT serialized body, or undefined for a call with no body. */
|
|
42
|
+
body: string | undefined,
|
|
43
|
+
/**
|
|
44
|
+
* The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
|
|
45
|
+
* timestamp another filter set, a content-type) reads them, and the headers the hook returns
|
|
46
|
+
* are merged by the caller — so there is exactly one place headers are added and it is the
|
|
47
|
+
* return value.
|
|
48
|
+
*/
|
|
49
|
+
headers: ReadonlyMap<string, string>,
|
|
50
|
+
/** The API contract's class name, e.g. 'PartnerWebhookApi'. */
|
|
51
|
+
contractName: string,
|
|
52
|
+
/** The contract method being called, e.g. 'deliver'. */
|
|
53
|
+
methodName: string);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* VendorAuthHook - the ONE app-bound mechanism behind `vendorAuth(name)`: a credential in a scheme
|
|
57
|
+
* the FRAMEWORK does not know, on BOTH ends of a hop. Modelled on `JwtHook` (which mints on one side
|
|
58
|
+
* and parses on the other); `name` selects the vendor, so one hook serves every vendor.
|
|
59
|
+
*
|
|
60
|
+
* | | who presents the credential | who checks it | the method |
|
|
61
|
+
* |---|---|---|---|
|
|
62
|
+
* | WE call a vendor / partner | us | them | {@link mint}, and {@link refresh} once on a 401 |
|
|
63
|
+
* | a vendor calls US | them | us | {@link verify} |
|
|
64
|
+
*
|
|
65
|
+
* ## Why a hook and not a framework scheme
|
|
66
|
+
*
|
|
67
|
+
* Because there is no such thing as "the" vendor scheme. Clover wants a per-merchant OAuth bearer
|
|
68
|
+
* token; Toast a machine-client login token; Twilio signs the full absolute URL with its sorted
|
|
69
|
+
* parameters; Slack signs `v0:{timestamp}:{body}`; Meta the raw body alone. A decorator that took a
|
|
70
|
+
* secret would pick one and be wrong for everyone else. So the scheme lives in the app's hook, the
|
|
71
|
+
* VENDOR lives on the contract, and the framework carries neither.
|
|
72
|
+
*
|
|
73
|
+
* ## It FAILS CLOSED, on both ends
|
|
74
|
+
*
|
|
75
|
+
* With no hook bound, every outbound `vendorAuth(...)` call THROWS rather than going out
|
|
76
|
+
* unauthenticated, and every inbound `vendorAuth(...)` endpoint answers 401. There is no framework
|
|
77
|
+
* default and there never will be one.
|
|
78
|
+
*
|
|
79
|
+
* ```typescript
|
|
80
|
+
* // AppModule.ts, beside the JwtHook binding
|
|
81
|
+
* options.bind(VENDOR_AUTH_HOOK).to(MyVendorAuthHook);
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* ## Refresh-once, and who owns serializing it
|
|
85
|
+
*
|
|
86
|
+
* The node client's `OutboundAuthFilter` calls {@link refresh} ONCE when the vendor answers a raw
|
|
87
|
+
* `401`, mints again and re-sends ONCE (never for a streaming upload, whose body cannot be replayed).
|
|
88
|
+
* A second 401 is a failure of THIS service. Serializing a refresh across instances — Clover's
|
|
89
|
+
* refresh tokens are single-use across Cloud Run instances — is the hook's job: webpieces calls it
|
|
90
|
+
* once per logical call and cannot see the other instances.
|
|
91
|
+
*/
|
|
92
|
+
export declare abstract class VendorAuthHook {
|
|
93
|
+
/**
|
|
94
|
+
* CLIENT: produce the headers that authenticate ONE outbound request, or throw to refuse to send
|
|
95
|
+
* it. An empty map is legal and means "this vendor needs no header".
|
|
96
|
+
*
|
|
97
|
+
* @param name the string on the contract's `vendorAuth(name)` — which vendor this is.
|
|
98
|
+
* @param request the FINAL request: the absolute url and the exact bytes about to go on the wire.
|
|
99
|
+
*/
|
|
100
|
+
abstract mint(name: string, request: SignableRequest): Promise<Map<string, string>>;
|
|
101
|
+
/**
|
|
102
|
+
* CLIENT: the vendor answered `401` to a credential {@link mint} produced. Renew whatever the
|
|
103
|
+
* credential is minted from (refresh an OAuth token, log in again). Called at most ONCE per
|
|
104
|
+
* logical call; {@link mint} runs again afterwards and the request is re-sent once.
|
|
105
|
+
*/
|
|
106
|
+
abstract refresh(name: string): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* SERVER: verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's
|
|
109
|
+
* credential proved; throw `ApiUnauthorizedError` to deny. The framework seeds `entries` with
|
|
110
|
+
* `RequestContext.putTrusted`, as it does for a jwt or api-key caller. `vendorAuth` stays
|
|
111
|
+
* caller-NOT-verified for trusted-header purposes: a vendor is not a peer service, so nothing it
|
|
112
|
+
* merely ASSERTS on the wire is admitted.
|
|
113
|
+
*
|
|
114
|
+
* @param name the string on the contract's `vendorAuth(name)` — which vendor this route is.
|
|
115
|
+
* @param request narrowed to {@link RawHttpRequest}: `request.raw` (the verbatim bytes + absolute
|
|
116
|
+
* url) is PRESENT, because a served `vendorAuth(...)` endpoint requires
|
|
117
|
+
* `@Endpoint(..., { rawBody: true })` at wiring time.
|
|
118
|
+
*/
|
|
119
|
+
abstract verify(name: string, request: RawHttpRequest): Promise<AuthenticatedCaller>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* DI identifier for the optional {@link VendorAuthHook} binding. It is a Symbol (not the class) so the
|
|
123
|
+
* app container's inversify autobind never auto-constructs this token, keeping
|
|
124
|
+
* `@optional() @inject(VENDOR_AUTH_HOOK)` correct — undefined when unbound, which is what makes the
|
|
125
|
+
* fail-closed refusal reachable on both ends.
|
|
126
|
+
*/
|
|
127
|
+
export declare const VENDOR_AUTH_HOOK: unique symbol;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VENDOR_AUTH_HOOK = exports.VendorAuthHook = exports.SignableRequest = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* ONE outbound request, as the thing being authenticated — what {@link VendorAuthHook.mint} receives.
|
|
6
|
+
* Data only.
|
|
7
|
+
*
|
|
8
|
+
* Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}
|
|
9
|
+
* and {@link body}:
|
|
10
|
+
*
|
|
11
|
+
* - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature
|
|
12
|
+
* computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook
|
|
13
|
+
* client the pre-filter URL is the client's own service name, which is not a destination at all.
|
|
14
|
+
* - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization
|
|
15
|
+
* happens before the filter chain runs and the transport sends this same string verbatim, so the
|
|
16
|
+
* bytes signed and the bytes sent cannot differ. A library that re-serializes internally signs one
|
|
17
|
+
* byte sequence and sends another, and the failure is silent.
|
|
18
|
+
*/
|
|
19
|
+
class SignableRequest {
|
|
20
|
+
url;
|
|
21
|
+
httpMethod;
|
|
22
|
+
body;
|
|
23
|
+
headers;
|
|
24
|
+
contractName;
|
|
25
|
+
methodName;
|
|
26
|
+
constructor(
|
|
27
|
+
/** The FINAL absolute URL this request is about to be sent to. */
|
|
28
|
+
url,
|
|
29
|
+
/** The HTTP method, e.g. 'POST'. */
|
|
30
|
+
httpMethod,
|
|
31
|
+
/** The EXACT serialized body, or undefined for a call with no body. */
|
|
32
|
+
body,
|
|
33
|
+
/**
|
|
34
|
+
* The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
|
|
35
|
+
* timestamp another filter set, a content-type) reads them, and the headers the hook returns
|
|
36
|
+
* are merged by the caller — so there is exactly one place headers are added and it is the
|
|
37
|
+
* return value.
|
|
38
|
+
*/
|
|
39
|
+
headers,
|
|
40
|
+
/** The API contract's class name, e.g. 'PartnerWebhookApi'. */
|
|
41
|
+
contractName,
|
|
42
|
+
/** The contract method being called, e.g. 'deliver'. */
|
|
43
|
+
methodName) {
|
|
44
|
+
this.url = url;
|
|
45
|
+
this.httpMethod = httpMethod;
|
|
46
|
+
this.body = body;
|
|
47
|
+
this.headers = headers;
|
|
48
|
+
this.contractName = contractName;
|
|
49
|
+
this.methodName = methodName;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.SignableRequest = SignableRequest;
|
|
53
|
+
/**
|
|
54
|
+
* VendorAuthHook - the ONE app-bound mechanism behind `vendorAuth(name)`: a credential in a scheme
|
|
55
|
+
* the FRAMEWORK does not know, on BOTH ends of a hop. Modelled on `JwtHook` (which mints on one side
|
|
56
|
+
* and parses on the other); `name` selects the vendor, so one hook serves every vendor.
|
|
57
|
+
*
|
|
58
|
+
* | | who presents the credential | who checks it | the method |
|
|
59
|
+
* |---|---|---|---|
|
|
60
|
+
* | WE call a vendor / partner | us | them | {@link mint}, and {@link refresh} once on a 401 |
|
|
61
|
+
* | a vendor calls US | them | us | {@link verify} |
|
|
62
|
+
*
|
|
63
|
+
* ## Why a hook and not a framework scheme
|
|
64
|
+
*
|
|
65
|
+
* Because there is no such thing as "the" vendor scheme. Clover wants a per-merchant OAuth bearer
|
|
66
|
+
* token; Toast a machine-client login token; Twilio signs the full absolute URL with its sorted
|
|
67
|
+
* parameters; Slack signs `v0:{timestamp}:{body}`; Meta the raw body alone. A decorator that took a
|
|
68
|
+
* secret would pick one and be wrong for everyone else. So the scheme lives in the app's hook, the
|
|
69
|
+
* VENDOR lives on the contract, and the framework carries neither.
|
|
70
|
+
*
|
|
71
|
+
* ## It FAILS CLOSED, on both ends
|
|
72
|
+
*
|
|
73
|
+
* With no hook bound, every outbound `vendorAuth(...)` call THROWS rather than going out
|
|
74
|
+
* unauthenticated, and every inbound `vendorAuth(...)` endpoint answers 401. There is no framework
|
|
75
|
+
* default and there never will be one.
|
|
76
|
+
*
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // AppModule.ts, beside the JwtHook binding
|
|
79
|
+
* options.bind(VENDOR_AUTH_HOOK).to(MyVendorAuthHook);
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* ## Refresh-once, and who owns serializing it
|
|
83
|
+
*
|
|
84
|
+
* The node client's `OutboundAuthFilter` calls {@link refresh} ONCE when the vendor answers a raw
|
|
85
|
+
* `401`, mints again and re-sends ONCE (never for a streaming upload, whose body cannot be replayed).
|
|
86
|
+
* A second 401 is a failure of THIS service. Serializing a refresh across instances — Clover's
|
|
87
|
+
* refresh tokens are single-use across Cloud Run instances — is the hook's job: webpieces calls it
|
|
88
|
+
* once per logical call and cannot see the other instances.
|
|
89
|
+
*/
|
|
90
|
+
class VendorAuthHook {
|
|
91
|
+
}
|
|
92
|
+
exports.VendorAuthHook = VendorAuthHook;
|
|
93
|
+
/**
|
|
94
|
+
* DI identifier for the optional {@link VendorAuthHook} binding. It is a Symbol (not the class) so the
|
|
95
|
+
* app container's inversify autobind never auto-constructs this token, keeping
|
|
96
|
+
* `@optional() @inject(VENDOR_AUTH_HOOK)` correct — undefined when unbound, which is what makes the
|
|
97
|
+
* fail-closed refusal reachable on both ends.
|
|
98
|
+
*/
|
|
99
|
+
// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)
|
|
100
|
+
exports.VENDOR_AUTH_HOOK = Symbol.for('VendorAuthHook');
|
|
101
|
+
//# sourceMappingURL=VendorAuthHook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VendorAuthHook.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/VendorAuthHook.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;;GAcG;AACH,MAAa,eAAe;IAGJ;IAEA;IAEA;IAOA;IAEA;IAEA;IAjBpB;IACI,kEAAkE;IAClD,GAAW;IAC3B,oCAAoC;IACpB,UAAkB;IAClC,uEAAuE;IACvD,IAAwB;IACxC;;;;;OAKG;IACa,OAAoC;IACpD,+DAA+D;IAC/C,YAAoB;IACpC,wDAAwD;IACxC,UAAkB;QAflB,QAAG,GAAH,GAAG,CAAQ;QAEX,eAAU,GAAV,UAAU,CAAQ;QAElB,SAAI,GAAJ,IAAI,CAAoB;QAOxB,YAAO,GAAP,OAAO,CAA6B;QAEpC,iBAAY,GAAZ,YAAY,CAAQ;QAEpB,eAAU,GAAV,UAAU,CAAQ;IACnC,CAAC;CACP;AApBD,0CAoBC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAsB,cAAc;CA8BnC;AA9BD,wCA8BC;AAED;;;;;GAKG;AACH,mNAAmN;AACtM,QAAA,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC","sourcesContent":["import { AuthenticatedCaller } from './AuthenticatedCaller';\nimport { RawHttpRequest } from './HttpRequest';\n\n/**\n * ONE outbound request, as the thing being authenticated — what {@link VendorAuthHook.mint} receives.\n * Data only.\n *\n * Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}\n * and {@link body}:\n *\n * - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature\n * computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook\n * client the pre-filter URL is the client's own service name, which is not a destination at all.\n * - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization\n * happens before the filter chain runs and the transport sends this same string verbatim, so the\n * bytes signed and the bytes sent cannot differ. A library that re-serializes internally signs one\n * byte sequence and sends another, and the failure is silent.\n */\nexport class SignableRequest {\n constructor(\n /** The FINAL absolute URL this request is about to be sent to. */\n public readonly url: string,\n /** The HTTP method, e.g. 'POST'. */\n public readonly httpMethod: string,\n /** The EXACT serialized body, or undefined for a call with no body. */\n public readonly body: string | undefined,\n /**\n * The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a\n * timestamp another filter set, a content-type) reads them, and the headers the hook returns\n * are merged by the caller — so there is exactly one place headers are added and it is the\n * return value.\n */\n public readonly headers: ReadonlyMap<string, string>,\n /** The API contract's class name, e.g. 'PartnerWebhookApi'. */\n public readonly contractName: string,\n /** The contract method being called, e.g. 'deliver'. */\n public readonly methodName: string,\n ) {}\n}\n\n/**\n * VendorAuthHook - the ONE app-bound mechanism behind `vendorAuth(name)`: a credential in a scheme\n * the FRAMEWORK does not know, on BOTH ends of a hop. Modelled on `JwtHook` (which mints on one side\n * and parses on the other); `name` selects the vendor, so one hook serves every vendor.\n *\n * | | who presents the credential | who checks it | the method |\n * |---|---|---|---|\n * | WE call a vendor / partner | us | them | {@link mint}, and {@link refresh} once on a 401 |\n * | a vendor calls US | them | us | {@link verify} |\n *\n * ## Why a hook and not a framework scheme\n *\n * Because there is no such thing as \"the\" vendor scheme. Clover wants a per-merchant OAuth bearer\n * token; Toast a machine-client login token; Twilio signs the full absolute URL with its sorted\n * parameters; Slack signs `v0:{timestamp}:{body}`; Meta the raw body alone. A decorator that took a\n * secret would pick one and be wrong for everyone else. So the scheme lives in the app's hook, the\n * VENDOR lives on the contract, and the framework carries neither.\n *\n * ## It FAILS CLOSED, on both ends\n *\n * With no hook bound, every outbound `vendorAuth(...)` call THROWS rather than going out\n * unauthenticated, and every inbound `vendorAuth(...)` endpoint answers 401. There is no framework\n * default and there never will be one.\n *\n * ```typescript\n * // AppModule.ts, beside the JwtHook binding\n * options.bind(VENDOR_AUTH_HOOK).to(MyVendorAuthHook);\n * ```\n *\n * ## Refresh-once, and who owns serializing it\n *\n * The node client's `OutboundAuthFilter` calls {@link refresh} ONCE when the vendor answers a raw\n * `401`, mints again and re-sends ONCE (never for a streaming upload, whose body cannot be replayed).\n * A second 401 is a failure of THIS service. Serializing a refresh across instances — Clover's\n * refresh tokens are single-use across Cloud Run instances — is the hook's job: webpieces calls it\n * once per logical call and cannot see the other instances.\n */\nexport abstract class VendorAuthHook {\n /**\n * CLIENT: produce the headers that authenticate ONE outbound request, or throw to refuse to send\n * it. An empty map is legal and means \"this vendor needs no header\".\n *\n * @param name the string on the contract's `vendorAuth(name)` — which vendor this is.\n * @param request the FINAL request: the absolute url and the exact bytes about to go on the wire.\n */\n abstract mint(name: string, request: SignableRequest): Promise<Map<string, string>>;\n\n /**\n * CLIENT: the vendor answered `401` to a credential {@link mint} produced. Renew whatever the\n * credential is minted from (refresh an OAuth token, log in again). Called at most ONCE per\n * logical call; {@link mint} runs again afterwards and the request is re-sent once.\n */\n abstract refresh(name: string): Promise<void>;\n\n /**\n * SERVER: verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's\n * credential proved; throw `ApiUnauthorizedError` to deny. The framework seeds `entries` with\n * `RequestContext.putTrusted`, as it does for a jwt or api-key caller. `vendorAuth` stays\n * caller-NOT-verified for trusted-header purposes: a vendor is not a peer service, so nothing it\n * merely ASSERTS on the wire is admitted.\n *\n * @param name the string on the contract's `vendorAuth(name)` — which vendor this route is.\n * @param request narrowed to {@link RawHttpRequest}: `request.raw` (the verbatim bytes + absolute\n * url) is PRESENT, because a served `vendorAuth(...)` endpoint requires\n * `@Endpoint(..., { rawBody: true })` at wiring time.\n */\n abstract verify(name: string, request: RawHttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link VendorAuthHook} binding. It is a Symbol (not the class) so the\n * app container's inversify autobind never auto-constructs this token, keeping\n * `@optional() @inject(VENDOR_AUTH_HOOK)` correct — undefined when unbound, which is what makes the\n * fail-closed refusal reachable on both ends.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const VENDOR_AUTH_HOOK = Symbol.for('VendorAuthHook');\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type { RestorableContext } from './CapturedContext';
|
|
|
4
4
|
export { RequestContextApiCallContext } from './RequestContextApiCallContext';
|
|
5
5
|
export { HttpRequest, RawHttpRequest } from './HttpRequest';
|
|
6
6
|
export { RawRequest } from './RawRequest';
|
|
7
|
+
export { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthenticatedCaller';
|
|
8
|
+
export { VendorAuthHook, VENDOR_AUTH_HOOK, SignableRequest } from './VendorAuthHook';
|
|
7
9
|
export { provideSingletonDefaultForApi } from './provide';
|
|
8
10
|
export { Provider } from './provide';
|
|
9
11
|
export { provideFrameworkSingleton, provideFrameworkSingletonDefaultForApi, provideFrameworkTransient, bindFrameworkProvider, buildFrameworkModule, } from './frameworkProvide';
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StreamEventContext = exports.PendingTrustedValue = exports.PendingWireTrust = exports.RequestContextReader = exports.RequestContextHeaders = exports.buildFrameworkModule = exports.bindFrameworkProvider = exports.provideFrameworkTransient = exports.provideFrameworkSingletonDefaultForApi = exports.provideFrameworkSingleton = exports.Provider = exports.provideSingletonDefaultForApi = exports.RawRequest = exports.HttpRequest = exports.RequestContextApiCallContext = exports.RequestContext = void 0;
|
|
3
|
+
exports.StreamEventContext = exports.PendingTrustedValue = exports.PendingWireTrust = exports.RequestContextReader = exports.RequestContextHeaders = exports.buildFrameworkModule = exports.bindFrameworkProvider = exports.provideFrameworkTransient = exports.provideFrameworkSingletonDefaultForApi = exports.provideFrameworkSingleton = exports.Provider = exports.provideSingletonDefaultForApi = exports.SignableRequest = exports.VENDOR_AUTH_HOOK = exports.VendorAuthHook = exports.AuthenticatedMachineIdentity = exports.AuthenticatedCaller = exports.RawRequest = exports.HttpRequest = exports.RequestContextApiCallContext = exports.RequestContext = void 0;
|
|
4
4
|
// Context management with AsyncLocalStorage
|
|
5
5
|
var RequestContext_1 = require("./RequestContext");
|
|
6
6
|
Object.defineProperty(exports, "RequestContext", { enumerable: true, get: function () { return RequestContext_1.RequestContext; } });
|
|
@@ -15,6 +15,15 @@ Object.defineProperty(exports, "HttpRequest", { enumerable: true, get: function
|
|
|
15
15
|
// The verbatim bytes + absolute url a webhook SIGNATURE is computed over ({ rawBody: true } routes).
|
|
16
16
|
var RawRequest_1 = require("./RawRequest");
|
|
17
17
|
Object.defineProperty(exports, "RawRequest", { enumerable: true, get: function () { return RawRequest_1.RawRequest; } });
|
|
18
|
+
// What an authenticator PROVED about a caller (http-routing re-exports it beside its hooks).
|
|
19
|
+
var AuthenticatedCaller_1 = require("./AuthenticatedCaller");
|
|
20
|
+
Object.defineProperty(exports, "AuthenticatedCaller", { enumerable: true, get: function () { return AuthenticatedCaller_1.AuthenticatedCaller; } });
|
|
21
|
+
Object.defineProperty(exports, "AuthenticatedMachineIdentity", { enumerable: true, get: function () { return AuthenticatedCaller_1.AuthenticatedMachineIdentity; } });
|
|
22
|
+
// vendorAuth(name): ONE app hook that mints/refreshes on the client and verifies on the server.
|
|
23
|
+
var VendorAuthHook_1 = require("./VendorAuthHook");
|
|
24
|
+
Object.defineProperty(exports, "VendorAuthHook", { enumerable: true, get: function () { return VendorAuthHook_1.VendorAuthHook; } });
|
|
25
|
+
Object.defineProperty(exports, "VENDOR_AUTH_HOOK", { enumerable: true, get: function () { return VendorAuthHook_1.VENDOR_AUTH_HOOK; } });
|
|
26
|
+
Object.defineProperty(exports, "SignableRequest", { enumerable: true, get: function () { return VendorAuthHook_1.SignableRequest; } });
|
|
18
27
|
// DI provider decorators (shared DI seam; http-routing re-exports for back-compat)
|
|
19
28
|
var provide_1 = require("./provide");
|
|
20
29
|
Object.defineProperty(exports, "provideSingletonDefaultForApi", { enumerable: true, get: function () { return provide_1.provideSingletonDefaultForApi; } });
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/index.ts"],"names":[],"mappings":";;;AAAA,4CAA4C;AAC5C,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAiBvB,qGAAqG;AACrG,wFAAwF;AACxF,0EAA0E;AAC1E,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4D;AAAnD,0GAAA,WAAW,OAAA;AACpB,qGAAqG;AACrG,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/index.ts"],"names":[],"mappings":";;;AAAA,4CAA4C;AAC5C,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAiBvB,qGAAqG;AACrG,wFAAwF;AACxF,0EAA0E;AAC1E,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4D;AAAnD,0GAAA,WAAW,OAAA;AACpB,qGAAqG;AACrG,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,6FAA6F;AAC7F,6DAA0F;AAAjF,0HAAA,mBAAmB,OAAA;AAAE,mIAAA,4BAA4B,OAAA;AAC1D,gGAAgG;AAChG,mDAAqF;AAA5E,gHAAA,cAAc,OAAA;AAAE,kHAAA,gBAAgB,OAAA;AAAE,iHAAA,eAAe,OAAA;AAE1D,mFAAmF;AACnF,qCAA0D;AAAjD,wHAAA,6BAA6B,OAAA;AACtC,2FAA2F;AAC3F,qCAAqC;AAA5B,mGAAA,QAAQ,OAAA;AACjB,sFAAsF;AACtF,wEAAwE;AACxE,uDAM4B;AALxB,6HAAA,yBAAyB,OAAA;AACzB,0IAAA,sCAAsC,OAAA;AACtC,6HAAA,yBAAyB,OAAA;AACzB,yHAAA,qBAAqB,OAAA;AACrB,wHAAA,oBAAoB,OAAA;AAIxB,mFAAmF;AACnF,yFAAyF;AACzF,yBAAyB;AACzB,EAAE;AACF,8FAA8F;AAC9F,2FAA2F;AAC3F,uDAAuD;AACvD,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,oGAAoG;AACpG,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,uDAA2E;AAAlE,oHAAA,gBAAgB,OAAA;AAAE,uHAAA,mBAAmB,OAAA;AAC9C,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA","sourcesContent":["// Context management with AsyncLocalStorage\nexport { RequestContext } from './RequestContext';\n// The OPAQUE snapshot type that copyContext() produces and restoreContext()/runWithContext() accept.\n//\n// `export type`, NOT `export` — deliberately. Consumers need to NAME it (a field, a queue entry, a\n// parameter) and nothing more. A VALUE export would hand them the class object, and with it the static\n// `capture(...)`, whose capability token a cast can supply even though this barrel never exports the\n// token's type: `CapturedContext.capture(null as never, new Map([['userId','victim']]))` would compile\n// and forge a proven identity — the exact hole this whole change closes. A type-only export removes the\n// class object from the package surface, so there is no factory to reach and copyContext() really is\n// the only producer. (ContextCaptureAuthority is not exported here in any form.)\nexport type { CapturedContext } from './CapturedContext';\n// The narrowed snapshot — what withTrusted() / withoutTrusted() produce and what\n// runWithContext()/restoreContext() accept. A bare CapturedContext is NOT accepted by either, so every\n// call site states whether the proven identity travels with the work (`grep -rn withTrusted`) or is\n// deliberately dropped (`grep -rn withoutTrusted`). Type-only for the same reason as above: with no\n// class object on the surface there is no `of(...)` factory to reach, cast or not.\nexport type { RestorableContext } from './CapturedContext';\n// SERVER impl of the core-util ApiCallContext seam, bound to RequestContext. There is no install and\n// no side effect: each node package that logs api calls (LogApiFilter, NodeProxyClient,\n// TaskProxyClient) CONSTRUCTS one and hands it to its own LogApiCallImpl.\nexport { RequestContextApiCallContext } from './RequestContextApiCallContext';\n// Transport-neutral request stored in the context (http-routing's request type; re-exported there)\nexport { HttpRequest, RawHttpRequest } from './HttpRequest';\n// The verbatim bytes + absolute url a webhook SIGNATURE is computed over ({ rawBody: true } routes).\nexport { RawRequest } from './RawRequest';\n// What an authenticator PROVED about a caller (http-routing re-exports it beside its hooks).\nexport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthenticatedCaller';\n// vendorAuth(name): ONE app hook that mints/refreshes on the client and verifies on the server.\nexport { VendorAuthHook, VENDOR_AUTH_HOOK, SignableRequest } from './VendorAuthHook';\n\n// DI provider decorators (shared DI seam; http-routing re-exports for back-compat)\nexport { provideSingletonDefaultForApi } from './provide';\n// Guice-style Provider<T> — lazy singleton OR fresh-per-get, decided by T's binding scope.\nexport { Provider } from './provide';\n// Framework-only DI registry (packages/** use these; keeps framework classes out of a\n// client's buildProviderModule() global scan). See frameworkProvide.ts.\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n provideFrameworkTransient,\n bindFrameworkProvider,\n buildFrameworkModule,\n} from './frameworkProvide';\nexport type { FrameworkScope } from './frameworkProvide';\n\n// Outbound headers for a SERVER: reads RequestContext directly, fails fast outside\n// RequestContext.run(...). Server-side clients (http-client-node, cloudtasks-client) and\n// http-routing use THIS.\n//\n// ContextMgr is deliberately NOT re-exported. It is the browser's answer (an app-held store),\n// and only @webpieces/http-client-browser may name it — importing it here would let a node\n// package reach for a ContextReader it has no use for.\nexport { RequestContextHeaders } from './RequestContextHeaders';\n// The browser store's server counterpart, still used by the logging packages + http-server filters.\nexport { RequestContextReader } from './RequestContextReader';\nexport { PendingWireTrust, PendingTrustedValue } from './PendingWireTrust';\nexport { StreamEventContext } from './StreamEventContext';\n"]}
|