@webpieces/core-context 0.4.964 → 0.4.966
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 +1 -1
- package/src/AuthenticatedCaller.js +1 -1
- package/src/AuthenticatedCaller.js.map +1 -1
- package/src/HttpRequest.d.ts +2 -2
- package/src/HttpRequest.js.map +1 -1
- package/src/VendorAuthMinter.d.ts +124 -0
- package/src/VendorAuthMinter.js +113 -0
- package/src/VendorAuthMinter.js.map +1 -0
- package/src/VendorAuthRolesCompileAssertions.d.ts +26 -0
- package/src/VendorAuthRolesCompileAssertions.js +97 -0
- package/src/VendorAuthRolesCompileAssertions.js.map +1 -0
- package/src/VendorAuthVerifier.d.ts +44 -0
- package/src/VendorAuthVerifier.js +35 -0
- package/src/VendorAuthVerifier.js.map +1 -0
- package/src/index.d.ts +2 -1
- package/src/index.js +11 -6
- package/src/index.js.map +1 -1
- package/src/VendorAuthHook.d.ts +0 -127
- package/src/VendorAuthHook.js +0 -101
- package/src/VendorAuthHook.js.map +0 -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.966",
|
|
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.966",
|
|
26
26
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"reflect-metadata": "0.2.2"
|
|
@@ -2,7 +2,7 @@ import { ContextTuple } from '@webpieces/core-util';
|
|
|
2
2
|
/**
|
|
3
3
|
* AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook
|
|
4
4
|
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
5
|
-
* {@link
|
|
5
|
+
* {@link VendorAuthVerifier.verify}. Four fields, three jobs:
|
|
6
6
|
*
|
|
7
7
|
* - `userId` — WHO the caller is, as the credential proved it.
|
|
8
8
|
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
@@ -4,7 +4,7 @@ exports.AuthenticatedMachineIdentity = exports.AuthenticatedCaller = void 0;
|
|
|
4
4
|
/**
|
|
5
5
|
* AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook
|
|
6
6
|
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
7
|
-
* {@link
|
|
7
|
+
* {@link VendorAuthVerifier.verify}. Four fields, three jobs:
|
|
8
8
|
*
|
|
9
9
|
* - `userId` — WHO the caller is, as the credential proved it.
|
|
10
10
|
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
@@ -1 +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
|
|
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 VendorAuthVerifier.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
|
@@ -46,8 +46,8 @@ 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
|
|
50
|
-
* absence of the bytes is checked in exactly ONE place — `AuthFilter` 401s before it calls the
|
|
49
|
+
* It exists so a vendor's VendorAuthVerifier never writes `request.raw!` or an `if (!raw) throw`. The
|
|
50
|
+
* absence of the bytes is checked in exactly ONE place — `AuthFilter` 401s before it calls the verifier —
|
|
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.
|
|
53
53
|
*
|
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 * `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
|
|
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 VendorAuthVerifier 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 verifier —\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"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE outbound request, as the thing being authenticated — what {@link VendorAuthMinter.mint}
|
|
3
|
+
* receives. Data only.
|
|
4
|
+
*
|
|
5
|
+
* Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}
|
|
6
|
+
* and {@link body}:
|
|
7
|
+
*
|
|
8
|
+
* - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature
|
|
9
|
+
* computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook
|
|
10
|
+
* client the pre-filter URL is the client's own service name, which is not a destination at all.
|
|
11
|
+
* - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization
|
|
12
|
+
* happens before the filter chain runs and the transport sends this same string verbatim, so the
|
|
13
|
+
* bytes signed and the bytes sent cannot differ. A library that re-serializes internally signs one
|
|
14
|
+
* byte sequence and sends another, and the failure is silent.
|
|
15
|
+
*/
|
|
16
|
+
export declare class SignableRequest {
|
|
17
|
+
/** The FINAL absolute URL this request is about to be sent to. */
|
|
18
|
+
readonly url: string;
|
|
19
|
+
/** The HTTP method, e.g. 'POST'. */
|
|
20
|
+
readonly httpMethod: string;
|
|
21
|
+
/** The EXACT serialized body, or undefined for a call with no body. */
|
|
22
|
+
readonly body: string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
|
|
25
|
+
* timestamp another filter set, a content-type) reads them, and the headers the minter
|
|
26
|
+
* returns are merged by the caller — so there is exactly one place headers are added and it
|
|
27
|
+
* is the return value.
|
|
28
|
+
*/
|
|
29
|
+
readonly headers: ReadonlyMap<string, string>;
|
|
30
|
+
/** The API contract's class name, e.g. 'PartnerWebhookApi'. */
|
|
31
|
+
readonly contractName: string;
|
|
32
|
+
/** The contract method being called, e.g. 'deliver'. */
|
|
33
|
+
readonly methodName: string;
|
|
34
|
+
constructor(
|
|
35
|
+
/** The FINAL absolute URL this request is about to be sent to. */
|
|
36
|
+
url: string,
|
|
37
|
+
/** The HTTP method, e.g. 'POST'. */
|
|
38
|
+
httpMethod: string,
|
|
39
|
+
/** The EXACT serialized body, or undefined for a call with no body. */
|
|
40
|
+
body: string | undefined,
|
|
41
|
+
/**
|
|
42
|
+
* The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
|
|
43
|
+
* timestamp another filter set, a content-type) reads them, and the headers the minter
|
|
44
|
+
* returns are merged by the caller — so there is exactly one place headers are added and it
|
|
45
|
+
* is the return value.
|
|
46
|
+
*/
|
|
47
|
+
headers: ReadonlyMap<string, string>,
|
|
48
|
+
/** The API contract's class name, e.g. 'PartnerWebhookApi'. */
|
|
49
|
+
contractName: string,
|
|
50
|
+
/** The contract method being called, e.g. 'deliver'. */
|
|
51
|
+
methodName: string);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* VendorAuthMinter - the OUTBOUND half of `vendorAuth(name)`: the app-bound mechanism that presents a
|
|
55
|
+
* credential in a scheme the FRAMEWORK does not know when THIS process calls a vendor / partner.
|
|
56
|
+
* `name` selects the vendor, so one minter serves every vendor this process calls.
|
|
57
|
+
*
|
|
58
|
+
* It is one of TWO independent, optional DI roles behind the one contract declaration
|
|
59
|
+
* `vendorAuth(name)` — the direction is decided by whether this runtime is SENDING or RECEIVING:
|
|
60
|
+
*
|
|
61
|
+
* | | who presents the credential | who checks it | the role this process binds |
|
|
62
|
+
* |---|---|---|---|
|
|
63
|
+
* | WE call a vendor / partner | us | them | `VendorAuthMinter` ({@link mint}, {@link refresh}) |
|
|
64
|
+
* | a vendor calls US | them | us | `VendorAuthVerifier` (`verify`) |
|
|
65
|
+
*
|
|
66
|
+
* ## Why two roles and not one hook (#1358)
|
|
67
|
+
*
|
|
68
|
+
* The outbound RPC client (`NodeProxyClient`) is constructed for EVERY typed client, including the
|
|
69
|
+
* ordinary `oidcSameProject()` client an inbound verifier uses to look a credential up. When one
|
|
70
|
+
* binding served both directions, every outbound proxy constructed the app's inbound verifier — and
|
|
71
|
+
* a verifier that depends on an RPC proxy closed a construction cycle (`Circular dependency found`)
|
|
72
|
+
* before the process could listen. The outbound client therefore depends on THIS role only, and the
|
|
73
|
+
* server's `AuthFilter` on the verifier only, so the graph is acyclic whatever order the bindings
|
|
74
|
+
* were registered in. A client-only process implements no meaningless `verify`, and a server-only
|
|
75
|
+
* one no meaningless `mint` / `refresh`.
|
|
76
|
+
*
|
|
77
|
+
* The converse holds for THIS role: every RPC client depends on the minter, so a minter reads a
|
|
78
|
+
* credential this process already holds (a paired worker token, an OAuth token store) and must not
|
|
79
|
+
* itself inject a typed RPC client. Validating a credential server-side is the verifier's job, on
|
|
80
|
+
* the server.
|
|
81
|
+
*
|
|
82
|
+
* ## It FAILS CLOSED
|
|
83
|
+
*
|
|
84
|
+
* With no minter bound, every outbound `vendorAuth(...)` call THROWS `MissingVendorAuthMinterError`
|
|
85
|
+
* before anything is sent, rather than going out unauthenticated. There is no framework default and
|
|
86
|
+
* there never will be one. OIDC, shared-secret and public calls never need it.
|
|
87
|
+
*
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // the CLI / client process's BindModule
|
|
90
|
+
* binder.bind(VENDOR_AUTH_MINTER).to(MyVendorAuthMinter).inSingletonScope();
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* ## Refresh-once, and who owns serializing it
|
|
94
|
+
*
|
|
95
|
+
* The node client's `OutboundAuthFilter` calls {@link refresh} ONCE when the vendor answers a raw
|
|
96
|
+
* `401`, mints again and re-sends ONCE (never for a streaming upload, whose body cannot be replayed).
|
|
97
|
+
* A second 401 is a failure of THIS service. Serializing a refresh across instances — Clover's
|
|
98
|
+
* refresh tokens are single-use across Cloud Run instances — is the minter's job: webpieces calls it
|
|
99
|
+
* once per logical call and cannot see the other instances. A refresh renews from its OWN valid
|
|
100
|
+
* credential source; it never authenticates through the call it is refreshing for.
|
|
101
|
+
*/
|
|
102
|
+
export declare abstract class VendorAuthMinter {
|
|
103
|
+
/**
|
|
104
|
+
* Produce the headers that authenticate ONE outbound request, or throw to refuse to send it. An
|
|
105
|
+
* empty map is legal and means "this vendor needs no header".
|
|
106
|
+
*
|
|
107
|
+
* @param name the string on the contract's `vendorAuth(name)` — which vendor this is.
|
|
108
|
+
* @param request the FINAL request: the absolute url and the exact bytes about to go on the wire.
|
|
109
|
+
*/
|
|
110
|
+
abstract mint(name: string, request: SignableRequest): Promise<Map<string, string>>;
|
|
111
|
+
/**
|
|
112
|
+
* The vendor answered `401` to a credential {@link mint} produced. Renew whatever the credential
|
|
113
|
+
* is minted from (refresh an OAuth token, log in again). Called at most ONCE per logical call;
|
|
114
|
+
* {@link mint} runs again afterwards and the request is re-sent once.
|
|
115
|
+
*/
|
|
116
|
+
abstract refresh(name: string): Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* DI identifier for the optional {@link VendorAuthMinter} binding — the OUTBOUND role only. It is a
|
|
120
|
+
* Symbol (not the class) so the app container's inversify autobind never auto-constructs this token,
|
|
121
|
+
* keeping `@optional() @inject(VENDOR_AUTH_MINTER)` correct — undefined when unbound, which is what
|
|
122
|
+
* makes the fail-closed refusal reachable.
|
|
123
|
+
*/
|
|
124
|
+
export declare const VENDOR_AUTH_MINTER: unique symbol;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VENDOR_AUTH_MINTER = exports.VendorAuthMinter = exports.SignableRequest = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* ONE outbound request, as the thing being authenticated — what {@link VendorAuthMinter.mint}
|
|
6
|
+
* receives. 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 minter
|
|
36
|
+
* returns are merged by the caller — so there is exactly one place headers are added and it
|
|
37
|
+
* is the 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
|
+
* VendorAuthMinter - the OUTBOUND half of `vendorAuth(name)`: the app-bound mechanism that presents a
|
|
55
|
+
* credential in a scheme the FRAMEWORK does not know when THIS process calls a vendor / partner.
|
|
56
|
+
* `name` selects the vendor, so one minter serves every vendor this process calls.
|
|
57
|
+
*
|
|
58
|
+
* It is one of TWO independent, optional DI roles behind the one contract declaration
|
|
59
|
+
* `vendorAuth(name)` — the direction is decided by whether this runtime is SENDING or RECEIVING:
|
|
60
|
+
*
|
|
61
|
+
* | | who presents the credential | who checks it | the role this process binds |
|
|
62
|
+
* |---|---|---|---|
|
|
63
|
+
* | WE call a vendor / partner | us | them | `VendorAuthMinter` ({@link mint}, {@link refresh}) |
|
|
64
|
+
* | a vendor calls US | them | us | `VendorAuthVerifier` (`verify`) |
|
|
65
|
+
*
|
|
66
|
+
* ## Why two roles and not one hook (#1358)
|
|
67
|
+
*
|
|
68
|
+
* The outbound RPC client (`NodeProxyClient`) is constructed for EVERY typed client, including the
|
|
69
|
+
* ordinary `oidcSameProject()` client an inbound verifier uses to look a credential up. When one
|
|
70
|
+
* binding served both directions, every outbound proxy constructed the app's inbound verifier — and
|
|
71
|
+
* a verifier that depends on an RPC proxy closed a construction cycle (`Circular dependency found`)
|
|
72
|
+
* before the process could listen. The outbound client therefore depends on THIS role only, and the
|
|
73
|
+
* server's `AuthFilter` on the verifier only, so the graph is acyclic whatever order the bindings
|
|
74
|
+
* were registered in. A client-only process implements no meaningless `verify`, and a server-only
|
|
75
|
+
* one no meaningless `mint` / `refresh`.
|
|
76
|
+
*
|
|
77
|
+
* The converse holds for THIS role: every RPC client depends on the minter, so a minter reads a
|
|
78
|
+
* credential this process already holds (a paired worker token, an OAuth token store) and must not
|
|
79
|
+
* itself inject a typed RPC client. Validating a credential server-side is the verifier's job, on
|
|
80
|
+
* the server.
|
|
81
|
+
*
|
|
82
|
+
* ## It FAILS CLOSED
|
|
83
|
+
*
|
|
84
|
+
* With no minter bound, every outbound `vendorAuth(...)` call THROWS `MissingVendorAuthMinterError`
|
|
85
|
+
* before anything is sent, rather than going out unauthenticated. There is no framework default and
|
|
86
|
+
* there never will be one. OIDC, shared-secret and public calls never need it.
|
|
87
|
+
*
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // the CLI / client process's BindModule
|
|
90
|
+
* binder.bind(VENDOR_AUTH_MINTER).to(MyVendorAuthMinter).inSingletonScope();
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* ## Refresh-once, and who owns serializing it
|
|
94
|
+
*
|
|
95
|
+
* The node client's `OutboundAuthFilter` calls {@link refresh} ONCE when the vendor answers a raw
|
|
96
|
+
* `401`, mints again and re-sends ONCE (never for a streaming upload, whose body cannot be replayed).
|
|
97
|
+
* A second 401 is a failure of THIS service. Serializing a refresh across instances — Clover's
|
|
98
|
+
* refresh tokens are single-use across Cloud Run instances — is the minter's job: webpieces calls it
|
|
99
|
+
* once per logical call and cannot see the other instances. A refresh renews from its OWN valid
|
|
100
|
+
* credential source; it never authenticates through the call it is refreshing for.
|
|
101
|
+
*/
|
|
102
|
+
class VendorAuthMinter {
|
|
103
|
+
}
|
|
104
|
+
exports.VendorAuthMinter = VendorAuthMinter;
|
|
105
|
+
/**
|
|
106
|
+
* DI identifier for the optional {@link VendorAuthMinter} binding — the OUTBOUND role only. It is a
|
|
107
|
+
* Symbol (not the class) so the app container's inversify autobind never auto-constructs this token,
|
|
108
|
+
* keeping `@optional() @inject(VENDOR_AUTH_MINTER)` correct — undefined when unbound, which is what
|
|
109
|
+
* makes the fail-closed refusal reachable.
|
|
110
|
+
*/
|
|
111
|
+
// 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)
|
|
112
|
+
exports.VENDOR_AUTH_MINTER = Symbol.for('VendorAuthMinter');
|
|
113
|
+
//# sourceMappingURL=VendorAuthMinter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VendorAuthMinter.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/VendorAuthMinter.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAsB,gBAAgB;CAgBrC;AAhBD,4CAgBC;AAED;;;;;GAKG;AACH,mNAAmN;AACtM,QAAA,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC","sourcesContent":["/**\n * ONE outbound request, as the thing being authenticated — what {@link VendorAuthMinter.mint}\n * receives. 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 minter\n * returns are merged by the caller — so there is exactly one place headers are added and it\n * is the 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 * VendorAuthMinter - the OUTBOUND half of `vendorAuth(name)`: the app-bound mechanism that presents a\n * credential in a scheme the FRAMEWORK does not know when THIS process calls a vendor / partner.\n * `name` selects the vendor, so one minter serves every vendor this process calls.\n *\n * It is one of TWO independent, optional DI roles behind the one contract declaration\n * `vendorAuth(name)` — the direction is decided by whether this runtime is SENDING or RECEIVING:\n *\n * | | who presents the credential | who checks it | the role this process binds |\n * |---|---|---|---|\n * | WE call a vendor / partner | us | them | `VendorAuthMinter` ({@link mint}, {@link refresh}) |\n * | a vendor calls US | them | us | `VendorAuthVerifier` (`verify`) |\n *\n * ## Why two roles and not one hook (#1358)\n *\n * The outbound RPC client (`NodeProxyClient`) is constructed for EVERY typed client, including the\n * ordinary `oidcSameProject()` client an inbound verifier uses to look a credential up. When one\n * binding served both directions, every outbound proxy constructed the app's inbound verifier — and\n * a verifier that depends on an RPC proxy closed a construction cycle (`Circular dependency found`)\n * before the process could listen. The outbound client therefore depends on THIS role only, and the\n * server's `AuthFilter` on the verifier only, so the graph is acyclic whatever order the bindings\n * were registered in. A client-only process implements no meaningless `verify`, and a server-only\n * one no meaningless `mint` / `refresh`.\n *\n * The converse holds for THIS role: every RPC client depends on the minter, so a minter reads a\n * credential this process already holds (a paired worker token, an OAuth token store) and must not\n * itself inject a typed RPC client. Validating a credential server-side is the verifier's job, on\n * the server.\n *\n * ## It FAILS CLOSED\n *\n * With no minter bound, every outbound `vendorAuth(...)` call THROWS `MissingVendorAuthMinterError`\n * before anything is sent, rather than going out unauthenticated. There is no framework default and\n * there never will be one. OIDC, shared-secret and public calls never need it.\n *\n * ```typescript\n * // the CLI / client process's BindModule\n * binder.bind(VENDOR_AUTH_MINTER).to(MyVendorAuthMinter).inSingletonScope();\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 minter's job: webpieces calls it\n * once per logical call and cannot see the other instances. A refresh renews from its OWN valid\n * credential source; it never authenticates through the call it is refreshing for.\n */\nexport abstract class VendorAuthMinter {\n /**\n * Produce the headers that authenticate ONE outbound request, or throw to refuse to send it. An\n * 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 * The vendor answered `401` to a credential {@link mint} produced. Renew whatever the credential\n * is minted from (refresh an OAuth token, log in again). Called at most ONCE per logical call;\n * {@link mint} runs again afterwards and the request is re-sent once.\n */\n abstract refresh(name: string): Promise<void>;\n}\n\n/**\n * DI identifier for the optional {@link VendorAuthMinter} binding — the OUTBOUND role only. It is a\n * Symbol (not the class) so the app container's inversify autobind never auto-constructs this token,\n * keeping `@optional() @inject(VENDOR_AUTH_MINTER)` correct — undefined when unbound, which is what\n * makes the fail-closed refusal reachable.\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_MINTER = Symbol.for('VendorAuthMinter');\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPILE-TIME proof of the #1358 role split. `vendorAuth(name)` is served by TWO independent DI
|
|
3
|
+
* roles — the outbound {@link VendorAuthMinter} and the inbound {@link VendorAuthVerifier} — and the
|
|
4
|
+
* unified `VendorAuthHook` / `VENDOR_AUTH_HOOK` they replace is DELETED, not aliased. Each
|
|
5
|
+
* `@ts-expect-error` FAILS THE BUILD (TS2578) the moment the thing it guards starts compiling again.
|
|
6
|
+
*
|
|
7
|
+
* A compiled file, never a `.spec.ts`: tsconfig.lib.json excludes specs and vitest strips types with
|
|
8
|
+
* esbuild, so a `@ts-expect-error` in a spec is inert.
|
|
9
|
+
*
|
|
10
|
+
* Why the split must be a TYPE fact and not a convention: the unified hook forced a client-only
|
|
11
|
+
* process to implement a meaningless `verify` and a server-only one a meaningless `mint`/`refresh`,
|
|
12
|
+
* and — far worse — made every outbound RPC proxy construct the server's inbound verifier, which
|
|
13
|
+
* closed a DI cycle the moment the verifier depended on an ordinary OIDC RPC client.
|
|
14
|
+
*/
|
|
15
|
+
export declare class VendorAuthRolesCompileAssertions {
|
|
16
|
+
/** The unified hook is gone: no alias, no `@deprecated`, by policy. */
|
|
17
|
+
unifiedHookIsDeleted(): void;
|
|
18
|
+
/** Each role is implementable ALONE — asserted by the absence of an error on the classes above. */
|
|
19
|
+
eachRoleStandsAlone(): void;
|
|
20
|
+
/** A minter-only class does not implement verification, so it cannot fill the inbound role. */
|
|
21
|
+
minterIsNotAVerifier(): void;
|
|
22
|
+
/** A verifier-only class does not implement minting or refresh, so it cannot fill the outbound role. */
|
|
23
|
+
verifierIsNotAMinter(): void;
|
|
24
|
+
/** The two binding identities are distinct unique symbols: one cannot be passed as the other. */
|
|
25
|
+
tokensAreNotInterchangeable(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VendorAuthRolesCompileAssertions = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const AuthenticatedCaller_1 = require("./AuthenticatedCaller");
|
|
6
|
+
const CoreContext = tslib_1.__importStar(require("./index"));
|
|
7
|
+
const VendorAuthMinter_1 = require("./VendorAuthMinter");
|
|
8
|
+
const VendorAuthVerifier_1 = require("./VendorAuthVerifier");
|
|
9
|
+
/** A CLIENT-only process's minter: mint + refresh, and NOTHING meaningless beside them. */
|
|
10
|
+
class MinterOnly extends VendorAuthMinter_1.VendorAuthMinter {
|
|
11
|
+
async mint(_name, _request) {
|
|
12
|
+
return new Map([['Authorization', 'Bearer held-worker-token']]);
|
|
13
|
+
}
|
|
14
|
+
async refresh(_name) {
|
|
15
|
+
// renew from this process's own credential source
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** A SERVER-only process's verifier: verify, and NOTHING meaningless beside it. */
|
|
19
|
+
class VerifierOnly extends VendorAuthVerifier_1.VendorAuthVerifier {
|
|
20
|
+
async verify(_name, request) {
|
|
21
|
+
return new AuthenticatedCaller_1.AuthenticatedCaller(`vendor:${request.raw.rawBody.length}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* COMPILE-TIME proof of the #1358 role split. `vendorAuth(name)` is served by TWO independent DI
|
|
26
|
+
* roles — the outbound {@link VendorAuthMinter} and the inbound {@link VendorAuthVerifier} — and the
|
|
27
|
+
* unified `VendorAuthHook` / `VENDOR_AUTH_HOOK` they replace is DELETED, not aliased. Each
|
|
28
|
+
* `@ts-expect-error` FAILS THE BUILD (TS2578) the moment the thing it guards starts compiling again.
|
|
29
|
+
*
|
|
30
|
+
* A compiled file, never a `.spec.ts`: tsconfig.lib.json excludes specs and vitest strips types with
|
|
31
|
+
* esbuild, so a `@ts-expect-error` in a spec is inert.
|
|
32
|
+
*
|
|
33
|
+
* Why the split must be a TYPE fact and not a convention: the unified hook forced a client-only
|
|
34
|
+
* process to implement a meaningless `verify` and a server-only one a meaningless `mint`/`refresh`,
|
|
35
|
+
* and — far worse — made every outbound RPC proxy construct the server's inbound verifier, which
|
|
36
|
+
* closed a DI cycle the moment the verifier depended on an ordinary OIDC RPC client.
|
|
37
|
+
*/
|
|
38
|
+
class VendorAuthRolesCompileAssertions {
|
|
39
|
+
/** The unified hook is gone: no alias, no `@deprecated`, by policy. */
|
|
40
|
+
unifiedHookIsDeleted() {
|
|
41
|
+
// @ts-expect-error VendorAuthHook is DELETED — split into VendorAuthMinter (outbound) + VendorAuthVerifier (inbound)
|
|
42
|
+
void CoreContext.VendorAuthHook;
|
|
43
|
+
// @ts-expect-error VENDOR_AUTH_HOOK is DELETED — bind VENDOR_AUTH_MINTER (client) and/or VENDOR_AUTH_VERIFIER (server)
|
|
44
|
+
void CoreContext.VENDOR_AUTH_HOOK;
|
|
45
|
+
}
|
|
46
|
+
/** Each role is implementable ALONE — asserted by the absence of an error on the classes above. */
|
|
47
|
+
eachRoleStandsAlone() {
|
|
48
|
+
const minter = new MinterOnly();
|
|
49
|
+
const verifier = new VerifierOnly();
|
|
50
|
+
void minter;
|
|
51
|
+
void verifier;
|
|
52
|
+
}
|
|
53
|
+
/** A minter-only class does not implement verification, so it cannot fill the inbound role. */
|
|
54
|
+
minterIsNotAVerifier() {
|
|
55
|
+
// @ts-expect-error a minter has no verify(): it is not a VendorAuthVerifier
|
|
56
|
+
const verifier = new MinterOnly();
|
|
57
|
+
void verifier;
|
|
58
|
+
void class extends VendorAuthMinter_1.VendorAuthMinter {
|
|
59
|
+
async mint() {
|
|
60
|
+
return new Map();
|
|
61
|
+
}
|
|
62
|
+
async refresh() {
|
|
63
|
+
// renew
|
|
64
|
+
}
|
|
65
|
+
// @ts-expect-error verify is not a member of the outbound role, so it cannot be overridden there
|
|
66
|
+
async verify(_name, _request) {
|
|
67
|
+
return new AuthenticatedCaller_1.AuthenticatedCaller('never');
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A verifier-only class does not implement minting or refresh, so it cannot fill the outbound role. */
|
|
72
|
+
verifierIsNotAMinter() {
|
|
73
|
+
// @ts-expect-error a verifier has no mint()/refresh(): it is not a VendorAuthMinter
|
|
74
|
+
const minter = new VerifierOnly();
|
|
75
|
+
void minter;
|
|
76
|
+
void class extends VendorAuthVerifier_1.VendorAuthVerifier {
|
|
77
|
+
async verify() {
|
|
78
|
+
return new AuthenticatedCaller_1.AuthenticatedCaller('vendor');
|
|
79
|
+
}
|
|
80
|
+
// @ts-expect-error mint is not a member of the inbound role, so it cannot be overridden there
|
|
81
|
+
async mint() {
|
|
82
|
+
return new Map();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** The two binding identities are distinct unique symbols: one cannot be passed as the other. */
|
|
87
|
+
tokensAreNotInterchangeable() {
|
|
88
|
+
// @ts-expect-error the inbound token is not the outbound token
|
|
89
|
+
const outbound = VendorAuthVerifier_1.VENDOR_AUTH_VERIFIER;
|
|
90
|
+
// @ts-expect-error the outbound token is not the inbound token
|
|
91
|
+
const inbound = VendorAuthMinter_1.VENDOR_AUTH_MINTER;
|
|
92
|
+
void outbound;
|
|
93
|
+
void inbound;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.VendorAuthRolesCompileAssertions = VendorAuthRolesCompileAssertions;
|
|
97
|
+
//# sourceMappingURL=VendorAuthRolesCompileAssertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VendorAuthRolesCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/VendorAuthRolesCompileAssertions.ts"],"names":[],"mappings":";;;;AAAA,+DAA4D;AAE5D,6DAAuC;AACvC,yDAA2F;AAC3F,6DAAgF;AAEhF,2FAA2F;AAC3F,MAAM,UAAW,SAAQ,mCAAgB;IAC5B,KAAK,CAAC,IAAI,CAAC,KAAa,EAAE,QAAyB;QACxD,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAEQ,KAAK,CAAC,OAAO,CAAC,KAAa;QAChC,kDAAkD;IACtD,CAAC;CACJ;AAED,mFAAmF;AACnF,MAAM,YAAa,SAAQ,uCAAkB;IAChC,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,OAAuB;QACxD,OAAO,IAAI,yCAAmB,CAAC,UAAU,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3E,CAAC;CACJ;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,gCAAgC;IACzC,uEAAuE;IACvE,oBAAoB;QAChB,qHAAqH;QACrH,KAAK,WAAW,CAAC,cAAc,CAAC;QAChC,uHAAuH;QACvH,KAAK,WAAW,CAAC,gBAAgB,CAAC;IACtC,CAAC;IAED,mGAAmG;IACnG,mBAAmB;QACf,MAAM,MAAM,GAAqB,IAAI,UAAU,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAuB,IAAI,YAAY,EAAE,CAAC;QACxD,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ,CAAC;IAClB,CAAC;IAED,+FAA+F;IAC/F,oBAAoB;QAChB,4EAA4E;QAC5E,MAAM,QAAQ,GAAuB,IAAI,UAAU,EAAE,CAAC;QACtD,KAAK,QAAQ,CAAC;QACd,KAAK,KAAM,SAAQ,mCAAgB;YACtB,KAAK,CAAC,IAAI;gBACf,OAAO,IAAI,GAAG,EAAE,CAAC;YACrB,CAAC;YAEQ,KAAK,CAAC,OAAO;gBAClB,QAAQ;YACZ,CAAC;YAED,iGAAiG;YACxF,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,QAAwB;gBACzD,OAAO,IAAI,yCAAmB,CAAC,OAAO,CAAC,CAAC;YAC5C,CAAC;SACJ,CAAC;IACN,CAAC;IAED,wGAAwG;IACxG,oBAAoB;QAChB,oFAAoF;QACpF,MAAM,MAAM,GAAqB,IAAI,YAAY,EAAE,CAAC;QACpD,KAAK,MAAM,CAAC;QACZ,KAAK,KAAM,SAAQ,uCAAkB;YACxB,KAAK,CAAC,MAAM;gBACjB,OAAO,IAAI,yCAAmB,CAAC,QAAQ,CAAC,CAAC;YAC7C,CAAC;YAED,8FAA8F;YACrF,KAAK,CAAC,IAAI;gBACf,OAAO,IAAI,GAAG,EAAE,CAAC;YACrB,CAAC;SACJ,CAAC;IACN,CAAC;IAED,iGAAiG;IACjG,2BAA2B;QACvB,+DAA+D;QAC/D,MAAM,QAAQ,GAA8B,yCAAoB,CAAC;QACjE,+DAA+D;QAC/D,MAAM,OAAO,GAAgC,qCAAkB,CAAC;QAChE,KAAK,QAAQ,CAAC;QACd,KAAK,OAAO,CAAC;IACjB,CAAC;CACJ;AAhED,4EAgEC","sourcesContent":["import { AuthenticatedCaller } from './AuthenticatedCaller';\nimport { RawHttpRequest } from './HttpRequest';\nimport * as CoreContext from './index';\nimport { SignableRequest, VendorAuthMinter, VENDOR_AUTH_MINTER } from './VendorAuthMinter';\nimport { VendorAuthVerifier, VENDOR_AUTH_VERIFIER } from './VendorAuthVerifier';\n\n/** A CLIENT-only process's minter: mint + refresh, and NOTHING meaningless beside them. */\nclass MinterOnly extends VendorAuthMinter {\n override async mint(_name: string, _request: SignableRequest): Promise<Map<string, string>> {\n return new Map([['Authorization', 'Bearer held-worker-token']]);\n }\n\n override async refresh(_name: string): Promise<void> {\n // renew from this process's own credential source\n }\n}\n\n/** A SERVER-only process's verifier: verify, and NOTHING meaningless beside it. */\nclass VerifierOnly extends VendorAuthVerifier {\n override async verify(_name: string, request: RawHttpRequest): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller(`vendor:${request.raw.rawBody.length}`);\n }\n}\n\n/**\n * COMPILE-TIME proof of the #1358 role split. `vendorAuth(name)` is served by TWO independent DI\n * roles — the outbound {@link VendorAuthMinter} and the inbound {@link VendorAuthVerifier} — and the\n * unified `VendorAuthHook` / `VENDOR_AUTH_HOOK` they replace is DELETED, not aliased. Each\n * `@ts-expect-error` FAILS THE BUILD (TS2578) the moment the thing it guards starts compiling again.\n *\n * A compiled file, never a `.spec.ts`: tsconfig.lib.json excludes specs and vitest strips types with\n * esbuild, so a `@ts-expect-error` in a spec is inert.\n *\n * Why the split must be a TYPE fact and not a convention: the unified hook forced a client-only\n * process to implement a meaningless `verify` and a server-only one a meaningless `mint`/`refresh`,\n * and — far worse — made every outbound RPC proxy construct the server's inbound verifier, which\n * closed a DI cycle the moment the verifier depended on an ordinary OIDC RPC client.\n */\nexport class VendorAuthRolesCompileAssertions {\n /** The unified hook is gone: no alias, no `@deprecated`, by policy. */\n unifiedHookIsDeleted(): void {\n // @ts-expect-error VendorAuthHook is DELETED — split into VendorAuthMinter (outbound) + VendorAuthVerifier (inbound)\n void CoreContext.VendorAuthHook;\n // @ts-expect-error VENDOR_AUTH_HOOK is DELETED — bind VENDOR_AUTH_MINTER (client) and/or VENDOR_AUTH_VERIFIER (server)\n void CoreContext.VENDOR_AUTH_HOOK;\n }\n\n /** Each role is implementable ALONE — asserted by the absence of an error on the classes above. */\n eachRoleStandsAlone(): void {\n const minter: VendorAuthMinter = new MinterOnly();\n const verifier: VendorAuthVerifier = new VerifierOnly();\n void minter;\n void verifier;\n }\n\n /** A minter-only class does not implement verification, so it cannot fill the inbound role. */\n minterIsNotAVerifier(): void {\n // @ts-expect-error a minter has no verify(): it is not a VendorAuthVerifier\n const verifier: VendorAuthVerifier = new MinterOnly();\n void verifier;\n void class extends VendorAuthMinter {\n override async mint(): Promise<Map<string, string>> {\n return new Map();\n }\n\n override async refresh(): Promise<void> {\n // renew\n }\n\n // @ts-expect-error verify is not a member of the outbound role, so it cannot be overridden there\n override async verify(_name: string, _request: RawHttpRequest): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('never');\n }\n };\n }\n\n /** A verifier-only class does not implement minting or refresh, so it cannot fill the outbound role. */\n verifierIsNotAMinter(): void {\n // @ts-expect-error a verifier has no mint()/refresh(): it is not a VendorAuthMinter\n const minter: VendorAuthMinter = new VerifierOnly();\n void minter;\n void class extends VendorAuthVerifier {\n override async verify(): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('vendor');\n }\n\n // @ts-expect-error mint is not a member of the inbound role, so it cannot be overridden there\n override async mint(): Promise<Map<string, string>> {\n return new Map();\n }\n };\n }\n\n /** The two binding identities are distinct unique symbols: one cannot be passed as the other. */\n tokensAreNotInterchangeable(): void {\n // @ts-expect-error the inbound token is not the outbound token\n const outbound: typeof VENDOR_AUTH_MINTER = VENDOR_AUTH_VERIFIER;\n // @ts-expect-error the outbound token is not the inbound token\n const inbound: typeof VENDOR_AUTH_VERIFIER = VENDOR_AUTH_MINTER;\n void outbound;\n void inbound;\n }\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AuthenticatedCaller } from './AuthenticatedCaller';
|
|
2
|
+
import { RawHttpRequest } from './HttpRequest';
|
|
3
|
+
/**
|
|
4
|
+
* VendorAuthVerifier - the INBOUND half of `vendorAuth(name)`: the app-bound mechanism that checks a
|
|
5
|
+
* credential in a scheme the FRAMEWORK does not know when a vendor / partner calls THIS process.
|
|
6
|
+
* `name` selects the vendor, so one verifier serves every vendor this server receives.
|
|
7
|
+
*
|
|
8
|
+
* It is one of TWO independent, optional DI roles behind the one contract declaration
|
|
9
|
+
* `vendorAuth(name)`; its outbound twin is `VendorAuthMinter`. Only the server's `AuthFilter`
|
|
10
|
+
* depends on this role. No outbound RPC client does — so a verifier may itself depend on an ordinary
|
|
11
|
+
* `oidcSameProject()` RPC client (to look a credential or a signing secret up) without closing a
|
|
12
|
+
* construction cycle (#1358).
|
|
13
|
+
*
|
|
14
|
+
* ## It FAILS CLOSED
|
|
15
|
+
*
|
|
16
|
+
* With no verifier bound, every inbound `vendorAuth(...)` endpoint answers 401 before the controller
|
|
17
|
+
* runs. There is no framework default and there never will be one — never an allow-all.
|
|
18
|
+
*
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // the API server's BindModule
|
|
21
|
+
* binder.bind(VENDOR_AUTH_VERIFIER).to(MyVendorAuthVerifier).inSingletonScope();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare abstract class VendorAuthVerifier {
|
|
25
|
+
/**
|
|
26
|
+
* Verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's credential
|
|
27
|
+
* proved; throw `ApiUnauthorizedError` to deny. The framework seeds `entries` with
|
|
28
|
+
* `RequestContext.putTrusted`, as it does for a jwt or api-key caller. `vendorAuth` stays
|
|
29
|
+
* caller-NOT-verified for trusted-header purposes: a vendor is not a peer service, so nothing it
|
|
30
|
+
* merely ASSERTS on the wire is admitted.
|
|
31
|
+
*
|
|
32
|
+
* @param name the string on the contract's `vendorAuth(name)` — which vendor this route is.
|
|
33
|
+
* @param request narrowed to {@link RawHttpRequest}: `request.raw` (the verbatim bytes + absolute
|
|
34
|
+
* url) is PRESENT, because a served `vendorAuth(...)` endpoint requires
|
|
35
|
+
* `@Endpoint(..., { rawBody: true })` at wiring time.
|
|
36
|
+
*/
|
|
37
|
+
abstract verify(name: string, request: RawHttpRequest): Promise<AuthenticatedCaller>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* DI identifier for the optional {@link VendorAuthVerifier} binding — the INBOUND role only. A Symbol
|
|
41
|
+
* for the same reason as `VENDOR_AUTH_MINTER`: autobind never constructs it, so an unbound verifier
|
|
42
|
+
* is `undefined` and the fail-closed 401 is reachable.
|
|
43
|
+
*/
|
|
44
|
+
export declare const VENDOR_AUTH_VERIFIER: unique symbol;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VENDOR_AUTH_VERIFIER = exports.VendorAuthVerifier = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* VendorAuthVerifier - the INBOUND half of `vendorAuth(name)`: the app-bound mechanism that checks a
|
|
6
|
+
* credential in a scheme the FRAMEWORK does not know when a vendor / partner calls THIS process.
|
|
7
|
+
* `name` selects the vendor, so one verifier serves every vendor this server receives.
|
|
8
|
+
*
|
|
9
|
+
* It is one of TWO independent, optional DI roles behind the one contract declaration
|
|
10
|
+
* `vendorAuth(name)`; its outbound twin is `VendorAuthMinter`. Only the server's `AuthFilter`
|
|
11
|
+
* depends on this role. No outbound RPC client does — so a verifier may itself depend on an ordinary
|
|
12
|
+
* `oidcSameProject()` RPC client (to look a credential or a signing secret up) without closing a
|
|
13
|
+
* construction cycle (#1358).
|
|
14
|
+
*
|
|
15
|
+
* ## It FAILS CLOSED
|
|
16
|
+
*
|
|
17
|
+
* With no verifier bound, every inbound `vendorAuth(...)` endpoint answers 401 before the controller
|
|
18
|
+
* runs. There is no framework default and there never will be one — never an allow-all.
|
|
19
|
+
*
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // the API server's BindModule
|
|
22
|
+
* binder.bind(VENDOR_AUTH_VERIFIER).to(MyVendorAuthVerifier).inSingletonScope();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class VendorAuthVerifier {
|
|
26
|
+
}
|
|
27
|
+
exports.VendorAuthVerifier = VendorAuthVerifier;
|
|
28
|
+
/**
|
|
29
|
+
* DI identifier for the optional {@link VendorAuthVerifier} binding — the INBOUND role only. A Symbol
|
|
30
|
+
* for the same reason as `VENDOR_AUTH_MINTER`: autobind never constructs it, so an unbound verifier
|
|
31
|
+
* is `undefined` and the fail-closed 401 is reachable.
|
|
32
|
+
*/
|
|
33
|
+
// 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)
|
|
34
|
+
exports.VENDOR_AUTH_VERIFIER = Symbol.for('VendorAuthVerifier');
|
|
35
|
+
//# sourceMappingURL=VendorAuthVerifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VendorAuthVerifier.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/VendorAuthVerifier.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAsB,kBAAkB;CAcvC;AAdD,gDAcC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC","sourcesContent":["import { AuthenticatedCaller } from './AuthenticatedCaller';\nimport { RawHttpRequest } from './HttpRequest';\n\n/**\n * VendorAuthVerifier - the INBOUND half of `vendorAuth(name)`: the app-bound mechanism that checks a\n * credential in a scheme the FRAMEWORK does not know when a vendor / partner calls THIS process.\n * `name` selects the vendor, so one verifier serves every vendor this server receives.\n *\n * It is one of TWO independent, optional DI roles behind the one contract declaration\n * `vendorAuth(name)`; its outbound twin is `VendorAuthMinter`. Only the server's `AuthFilter`\n * depends on this role. No outbound RPC client does — so a verifier may itself depend on an ordinary\n * `oidcSameProject()` RPC client (to look a credential or a signing secret up) without closing a\n * construction cycle (#1358).\n *\n * ## It FAILS CLOSED\n *\n * With no verifier bound, every inbound `vendorAuth(...)` endpoint answers 401 before the controller\n * runs. There is no framework default and there never will be one — never an allow-all.\n *\n * ```typescript\n * // the API server's BindModule\n * binder.bind(VENDOR_AUTH_VERIFIER).to(MyVendorAuthVerifier).inSingletonScope();\n * ```\n */\nexport abstract class VendorAuthVerifier {\n /**\n * Verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's credential\n * 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 VendorAuthVerifier} binding — the INBOUND role only. A Symbol\n * for the same reason as `VENDOR_AUTH_MINTER`: autobind never constructs it, so an unbound verifier\n * is `undefined` and the fail-closed 401 is reachable.\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_VERIFIER = Symbol.for('VendorAuthVerifier');\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export { RequestContextApiCallContext } from './RequestContextApiCallContext';
|
|
|
5
5
|
export { HttpRequest, RawHttpRequest } from './HttpRequest';
|
|
6
6
|
export { RawRequest } from './RawRequest';
|
|
7
7
|
export { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthenticatedCaller';
|
|
8
|
-
export {
|
|
8
|
+
export { VendorAuthMinter, VENDOR_AUTH_MINTER, SignableRequest } from './VendorAuthMinter';
|
|
9
|
+
export { VendorAuthVerifier, VENDOR_AUTH_VERIFIER } from './VendorAuthVerifier';
|
|
9
10
|
export { provideSingletonDefaultForApi } from './provide';
|
|
10
11
|
export { Provider } from './provide';
|
|
11
12
|
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.SignableRequest = exports.
|
|
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.VENDOR_AUTH_VERIFIER = exports.VendorAuthVerifier = exports.SignableRequest = exports.VENDOR_AUTH_MINTER = exports.VendorAuthMinter = 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; } });
|
|
@@ -19,11 +19,16 @@ Object.defineProperty(exports, "RawRequest", { enumerable: true, get: function (
|
|
|
19
19
|
var AuthenticatedCaller_1 = require("./AuthenticatedCaller");
|
|
20
20
|
Object.defineProperty(exports, "AuthenticatedCaller", { enumerable: true, get: function () { return AuthenticatedCaller_1.AuthenticatedCaller; } });
|
|
21
21
|
Object.defineProperty(exports, "AuthenticatedMachineIdentity", { enumerable: true, get: function () { return AuthenticatedCaller_1.AuthenticatedMachineIdentity; } });
|
|
22
|
-
// vendorAuth(name):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Object.defineProperty(exports, "
|
|
22
|
+
// vendorAuth(name): TWO independent optional DI roles — the outbound minter (a client sends) and the
|
|
23
|
+
// inbound verifier (a server receives). Never one binding for both: that put the inbound verifier
|
|
24
|
+
// into every outbound RPC proxy's construction and closed a startup cycle (#1358).
|
|
25
|
+
var VendorAuthMinter_1 = require("./VendorAuthMinter");
|
|
26
|
+
Object.defineProperty(exports, "VendorAuthMinter", { enumerable: true, get: function () { return VendorAuthMinter_1.VendorAuthMinter; } });
|
|
27
|
+
Object.defineProperty(exports, "VENDOR_AUTH_MINTER", { enumerable: true, get: function () { return VendorAuthMinter_1.VENDOR_AUTH_MINTER; } });
|
|
28
|
+
Object.defineProperty(exports, "SignableRequest", { enumerable: true, get: function () { return VendorAuthMinter_1.SignableRequest; } });
|
|
29
|
+
var VendorAuthVerifier_1 = require("./VendorAuthVerifier");
|
|
30
|
+
Object.defineProperty(exports, "VendorAuthVerifier", { enumerable: true, get: function () { return VendorAuthVerifier_1.VendorAuthVerifier; } });
|
|
31
|
+
Object.defineProperty(exports, "VENDOR_AUTH_VERIFIER", { enumerable: true, get: function () { return VendorAuthVerifier_1.VENDOR_AUTH_VERIFIER; } });
|
|
27
32
|
// DI provider decorators (shared DI seam; http-routing re-exports for back-compat)
|
|
28
33
|
var provide_1 = require("./provide");
|
|
29
34
|
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;AACnB,6FAA6F;AAC7F,6DAA0F;AAAjF,0HAAA,mBAAmB,OAAA;AAAE,mIAAA,4BAA4B,OAAA;AAC1D,
|
|
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,qGAAqG;AACrG,kGAAkG;AAClG,mFAAmF;AACnF,uDAA2F;AAAlF,oHAAA,gBAAgB,OAAA;AAAE,sHAAA,kBAAkB,OAAA;AAAE,mHAAA,eAAe,OAAA;AAC9D,2DAAgF;AAAvE,wHAAA,kBAAkB,OAAA;AAAE,0HAAA,oBAAoB,OAAA;AAEjD,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): TWO independent optional DI roles — the outbound minter (a client sends) and the\n// inbound verifier (a server receives). Never one binding for both: that put the inbound verifier\n// into every outbound RPC proxy's construction and closed a startup cycle (#1358).\nexport { VendorAuthMinter, VENDOR_AUTH_MINTER, SignableRequest } from './VendorAuthMinter';\nexport { VendorAuthVerifier, VENDOR_AUTH_VERIFIER } from './VendorAuthVerifier';\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"]}
|
package/src/VendorAuthHook.d.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
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;
|
package/src/VendorAuthHook.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|