@webpieces/http-routing 0.4.991 → 0.4.993
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/package.json +7 -7
- package/src/AuthHooks.d.ts +12 -0
- package/src/AuthHooks.js.map +1 -1
- package/src/AuthHooksCompileAssertions.js +9 -0
- package/src/AuthHooksCompileAssertions.js.map +1 -1
- package/src/AuthenticatedCallerContext.d.ts +14 -1
- package/src/AuthenticatedCallerContext.js +30 -4
- package/src/AuthenticatedCallerContext.js.map +1 -1
- package/src/AuthorizationHook.d.ts +6 -1
- package/src/AuthorizationHook.js +11 -4
- package/src/AuthorizationHook.js.map +1 -1
- package/src/CredentialVerifiers.d.ts +6 -0
- package/src/CredentialVerifiers.js +8 -0
- package/src/CredentialVerifiers.js.map +1 -1
- package/src/filters/AuthFilter.js +3 -3
- package/src/filters/AuthFilter.js.map +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,11 @@ invocation before it reaches `AuthFilter`; the framework then checks the caller
|
|
|
18
18
|
`oidcSameProject()` admits any service account of this service's GCP project (open off GCP, where
|
|
19
19
|
there is no project), and `oidcSvcs([...])` only the listed accounts, which may belong to other
|
|
20
20
|
projects; an endpoint may pair the two, and the token is then verified once against both. A bound
|
|
21
|
-
`OidcHook` receives all of the endpoint's OIDC methods (`OidcAuthMethods`)
|
|
21
|
+
`OidcHook` receives all of the endpoint's OIDC methods (`OidcAuthMethods`), and its
|
|
22
|
+
`originatingServiceRoles(identity)` grants a service account roles when it ORIGINATES a call: a
|
|
23
|
+
service is a caller with roles and no user id, so a `ROLES` endpoint admits it beside users (#1420).
|
|
24
|
+
It is asked only when no caller was forwarded — a hop relaying a user never adds its own roles.
|
|
25
|
+
Local tokens exercise
|
|
22
26
|
the filter chain without proving deployed IAM or Google signature validation.
|
|
23
27
|
See [the Cloud Run OIDC security boundary](../../../docs/architecture/cloud-run-oidc.md) for the
|
|
24
28
|
current verifier behavior and the assumptions required for that deployment model.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-routing",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.993",
|
|
4
4
|
"description": "Decorator-based routing with auto-wiring for WebPieces",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
25
|
-
"@webpieces/core-context": "0.4.
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/gcp-identity": "0.4.
|
|
25
|
+
"@webpieces/core-context": "0.4.993",
|
|
26
|
+
"@webpieces/core-util": "0.4.993",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.993",
|
|
28
28
|
"inversify": "7.10.4",
|
|
29
29
|
"jsonwebtoken": "9.0.2",
|
|
30
30
|
"minimatch": "10.0.1",
|
|
31
|
-
"@webpieces/http-client-core": "0.4.
|
|
32
|
-
"@webpieces/http-client-node": "0.4.
|
|
33
|
-
"@webpieces/cloudtasks-client": "0.4.
|
|
31
|
+
"@webpieces/http-client-core": "0.4.993",
|
|
32
|
+
"@webpieces/http-client-node": "0.4.993",
|
|
33
|
+
"@webpieces/cloudtasks-client": "0.4.993"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/src/AuthHooks.d.ts
CHANGED
|
@@ -32,6 +32,18 @@ export declare const JWT_HOOK: unique symbol;
|
|
|
32
32
|
*/
|
|
33
33
|
export declare abstract class OidcHook {
|
|
34
34
|
abstract verifyOidc(token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity>;
|
|
35
|
+
/**
|
|
36
|
+
* The roles a verified service account holds when it ORIGINATES a call — a cron job, a worker, an
|
|
37
|
+
* AI agent's own service account — so one `@WpAuthorization ROLES` endpoint can admit both users
|
|
38
|
+
* and that service (#1420). A service is a caller with roles and no user id: the roles are
|
|
39
|
+
* published under the SAME `USER_ROLES` key a user's are, and propagate down the stack with it.
|
|
40
|
+
*
|
|
41
|
+
* Asked ONLY when the hop carries no forwarded USER_ID / USER_ROLES. A service RELAYING a user's
|
|
42
|
+
* (or an upstream caller's) identity contributes nothing of its own, so a user with no roles behind
|
|
43
|
+
* a role-bearing service is still denied. Return `[]` for an account the app grants no role — and
|
|
44
|
+
* with no OidcHook bound, no service has any role.
|
|
45
|
+
*/
|
|
46
|
+
abstract originatingServiceRoles(identity: AuthenticatedMachineIdentity): Promise<readonly string[]>;
|
|
35
47
|
}
|
|
36
48
|
/**
|
|
37
49
|
* DI identifier for the optional {@link OidcHook} binding. It is a Symbol (not the class) so the app
|
package/src/AuthHooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAIA,gFAAgF;AAChF,MAAa,SAAS;IAEE;IACA;IAFpB,YACoB,KAAa,EACb,qBAA6B;QAD7B,UAAK,GAAL,KAAK,CAAQ;QACb,0BAAqB,GAArB,qBAAqB,CAAQ;QAE7C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC9E,CAAC;IACL,CAAC;CACJ;AAVD,8BAUC;AAED,+FAA+F;AAC/F,MAAsB,OAAO;CAG5B;AAHD,0BAGC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAsB,QAAQ;
|
|
1
|
+
{"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAIA,gFAAgF;AAChF,MAAa,SAAS;IAEE;IACA;IAFpB,YACoB,KAAa,EACb,qBAA6B;QAD7B,UAAK,GAAL,KAAK,CAAQ;QACb,0BAAqB,GAArB,qBAAqB,CAAQ;QAE7C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC9E,CAAC;IACL,CAAC;CACJ;AAVD,8BAUC;AAED,+FAA+F;AAC/F,MAAsB,OAAO;CAG5B;AAHD,0BAGC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAsB,QAAQ;CAe7B;AAfD,4BAeC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAsB,UAAU;CAiB/B;AAjBD,gCAiBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { HttpRequest, RawHttpRequest } from '@webpieces/core-context';\nimport { OidcAuthMethods } from '@webpieces/core-util';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';\n\n/** Framework-normalized result from an application-owned JWT mint operation. */\nexport class MintedJwt {\n constructor(\n public readonly token: string,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '') throw new Error('Minted JWT token must be non-empty.');\n if (!Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error('Minted JWT expiry must be a finite epoch-second value.');\n }\n }\n}\n\n/** Optional application JWT credential authority. Authorization lives in AuthorizationHook. */\nexport abstract class JwtHook<TMintRequest> {\n abstract mint(request: TMintRequest): Promise<MintedJwt>;\n abstract parseJwt(token: string): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link JwtHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(JWT_HOOK)`\n * correct — undefined when unbound. The JwtHook class stays the TYPE and the impl base.\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 JWT_HOOK = Symbol.for('JwtHook');\n\n/**\n * OidcHook - the OPTIONAL override for Google OIDC service-to-service verification. Its DI token is the\n * {@link OIDC_HOOK} Symbol injected via `@inject(OIDC_HOOK)` (a Symbol, because the app container uses\n * autobind; rebindable in tests). Bind one ONLY to customize the caller policy — e.g. an app that reads an\n * `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO\n * OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}\n * directly, so a server that wires nothing still verifies Google OIDC against the endpoint's caller\n * scopes. `verifyOidc` receives EVERY OIDC method the endpoint declares ({@link OidcAuthMethods}: one,\n * or one `oidcSameProject()` (`callerScope: 'same-project'`) plus one `oidcSvcs([...])`\n * (`callerScope: 'services'` with its `callers`)), because they are one bearer credential: verify\n * the token once, admit the call when ANY of them admits the caller, and throw otherwise.\n */\nexport abstract class OidcHook {\n abstract verifyOidc(token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity>;\n\n /**\n * The roles a verified service account holds when it ORIGINATES a call — a cron job, a worker, an\n * AI agent's own service account — so one `@WpAuthorization ROLES` endpoint can admit both users\n * and that service (#1420). A service is a caller with roles and no user id: the roles are\n * published under the SAME `USER_ROLES` key a user's are, and propagate down the stack with it.\n *\n * Asked ONLY when the hop carries no forwarded USER_ID / USER_ROLES. A service RELAYING a user's\n * (or an upstream caller's) identity contributes nothing of its own, so a user with no roles behind\n * a role-bearing service is still denied. Return `[]` for an account the app grants no role — and\n * with no OidcHook bound, no service has any role.\n */\n abstract originatingServiceRoles(identity: AuthenticatedMachineIdentity): Promise<readonly string[]>;\n}\n\n/**\n * DI identifier for the optional {@link OidcHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(OIDC_HOOK)`\n * correct — undefined when unbound. The OidcHook class stays the TYPE and the impl base.\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 OIDC_HOOK = Symbol.for('OidcHook');\n\n/**\n * ApiKeyHook - the OPTIONAL mechanism behind `apiKey(regime, credentials)`: authenticate a\n * CUSTOMER-held api key\n * against the app's own datastore and return the context to seed. Its DI token is the\n * {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container\n * uses autobind; rebindable in tests). The fourth hook, symmetric with {@link JwtHook} /\n * {@link OidcHook} / `VendorAuthVerifier` (@webpieces/core-context):\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);\n * ```\n *\n * When NO ApiKeyHook is bound, a server SERVING an `apiKey(...)` endpoint refuses to boot (#1362,\n * `AuthFilter.assertRouteVerifiable`), exactly as it does for an unbound JwtHook. There is no framework default and there never will be\n * one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's\n * choice of header names.\n *\n * THE ONE THING THIS HAS THAT `JwtHook.parseJwt` DOES NOT: it receives the whole {@link HttpRequest},\n * not one pre-extracted token. A real key regime validates the key TOGETHER WITH a second header — the\n * organization the caller is acting for — and a hook handed one header's value physically cannot do\n * that cross-check. The framework therefore EXTRACTS no api-key header: which headers carry the\n * credential is the app's business, and `getHeader` / `getHeaderValues` read as many as the regime\n * needs. The contract's `credentials` list DECLARES those header names for readers and spec\n * generators — it is documentation of the regime, never an instruction to this hook, so the hook\n * stays the single place the pair is actually validated. (Being ASYNC is no longer a difference — every hook here is, and for the same reason: an\n * app's strategy reaches the network.)\n *\n * ONE hook serves EVERY regime: `regime` selects which, so a server with a partner-api regime and an\n * internal-tooling regime switches on it rather than binding a token per regime.\n */\nexport abstract class ApiKeyHook {\n /**\n * AUTHENTICATE one inbound request. Return who the caller is plus the {@link AuthenticatedCaller.entries}\n * the framework seeds into `RequestContext` via `putTrusted`, or throw\n * {@link ApiUnauthorizedError} to deny.\n *\n * NOTE the seeded entries are TRUSTED context keys, so return only what THIS hook proved from the\n * credential it just verified. Anything the caller merely asserted on the wire is not admitted by\n * `apiKey(...)` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),\n * because a customer is not an internal service.\n *\n * @param regime the first argument of the contract's `apiKey(regime, credentials)` — which key\n * regime this route belongs to.\n * @param request the inbound request; read as many headers as the regime needs with\n * `getHeader` / `getHeaderValues`, either by raw name or by {@link ContextKey}.\n */\n abstract verifyApiKey(regime: string, request: HttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link ApiKeyHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(API_KEY_HOOK)`\n * correct — undefined when unbound. The ApiKeyHook class stays the TYPE and the impl base.\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 API_KEY_HOOK = Symbol.for('ApiKeyHook');\n"]}
|
|
@@ -112,18 +112,27 @@ class AuthHooksCompileAssertions {
|
|
|
112
112
|
const named = first.callerScope === 'services' ? first.callers[0] : 'same-project';
|
|
113
113
|
return new AuthConfig_1.AuthenticatedMachineIdentity('oidc', named);
|
|
114
114
|
}
|
|
115
|
+
async originatingServiceRoles(_identity) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
115
118
|
};
|
|
116
119
|
void class extends AuthHooks_2.OidcHook {
|
|
117
120
|
// @ts-expect-error verifyOidc takes the endpoint's OidcAuthMethods; the deleted caller-list override must not compile
|
|
118
121
|
async verifyOidc(_token, _callers) {
|
|
119
122
|
return new AuthConfig_1.AuthenticatedMachineIdentity('oidc', 'caller@example.test');
|
|
120
123
|
}
|
|
124
|
+
async originatingServiceRoles(_identity) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
121
127
|
};
|
|
122
128
|
void class extends AuthHooks_2.OidcHook {
|
|
123
129
|
// @ts-expect-error verifyOidc takes every OIDC method of the endpoint, not one, so a pair is verified once
|
|
124
130
|
async verifyOidc(_token, _method) {
|
|
125
131
|
return new AuthConfig_1.AuthenticatedMachineIdentity('oidc', 'caller@example.test');
|
|
126
132
|
}
|
|
133
|
+
async originatingServiceRoles(_identity) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
127
136
|
};
|
|
128
137
|
}
|
|
129
138
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthHooksCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooksCompileAssertions.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n// @ts-expect-error HeaderReader is DELETED — an ApiKeyHook now receives the whole HttpRequest, so it\n// reaches getHeaderValues and the ContextKey overload of getHeader that the narrow reader hid.\nimport type { HeaderReader } from './AuthHooks';\n// @ts-expect-error AuthValues is RENAMED to AuthenticatedCaller — there is no alias, by policy.\nimport type { AuthValues } from './AuthConfig';\nimport { OidcAuthMethod, OidcAuthMethods } from '@webpieces/core-util';\nimport { VendorAuthMinter, VendorAuthVerifier } from '@webpieces/core-context';\n// @ts-expect-error WebhookAuthCallback is REPLACED by VendorAuthVerifier (the inbound role) — no alias, by policy.\nimport type { WebhookAuthCallback } from './AuthHooks';\n// @ts-expect-error WEBHOOK_AUTH_CALLBACK is REPLACED by VENDOR_AUTH_VERIFIER.\nimport { WEBHOOK_AUTH_CALLBACK } from './AuthHooks';\nimport * as Routing from './index';\nimport { AuthFilter } from './filters/AuthFilter';\nimport { DefaultOidcVerifier } from './DefaultOidcVerifier';\nimport { ApiKeyHook, OidcHook } from './AuthHooks';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';\n\n/**\n * COMPILE-TIME assertions that the four auth hooks share ONE shape, and that every spelling this\n * change deleted has actually stopped compiling. Each `@ts-expect-error` FAILS THE BUILD (TS2578,\n * \"unused '@ts-expect-error' directive\") the moment the thing it guards starts compiling again.\n *\n * WHY THIS IS NOT A `.spec.ts` FILE — the same reason as {@link JwtHookCompileAssertions}:\n * tsconfig.lib.json EXCLUDES specs and vitest strips types with esbuild, so a `@ts-expect-error` in a\n * spec is inert and the suite passes either way. A type-level guarantee has to be asserted in a file\n * the type-checker actually compiles.\n *\n * WHY IT MATTERS HERE. This repo ships no backwards-compatibility shims: the compile error IS the\n * migration. An implementor that kept `verify(name, request, raw)` or a `HeaderReader` parameter\n * would otherwise sit there compiling, and an accepted shape is never migrated.\n */\nexport class AuthHooksCompileAssertions {\n /**\n * The DELETED type names, kept referenced so the two `@ts-expect-error`s above are load-bearing\n * rather than decorative. Both resolve to `any` under the suppressed error; the assertion is the\n * IMPORT failing, not what these aliases denote.\n */\n // webpieces-disable no-any-unknown -- a suppressed import of a deleted symbol resolves to any; the assertion is the import error itself\n deletedNames(): void {\n const headerReaderIsGone: HeaderReader | undefined = undefined;\n const authValuesIsGone: AuthValues | undefined = undefined;\n const webhookAuthCallbackIsGone: WebhookAuthCallback | undefined = undefined;\n void headerReaderIsGone;\n void authValuesIsGone;\n void webhookAuthCallbackIsGone;\n void WEBHOOK_AUTH_CALLBACK;\n // @ts-expect-error VendorAuthHook is DELETED (#1358): this server binds the INBOUND role VendorAuthVerifier\n void Routing.VendorAuthHook;\n // @ts-expect-error VENDOR_AUTH_HOOK is DELETED (#1358): bind VENDOR_AUTH_VERIFIER\n void Routing.VENDOR_AUTH_HOOK;\n // @ts-expect-error the OUTBOUND role is the client's (http-client-node); the router never exports it\n void Routing.VENDOR_AUTH_MINTER;\n }\n\n /**\n * The server's AuthFilter takes the INBOUND role only. Handing it an outbound minter must not\n * compile: a minter cannot verify, and an AuthFilter depending on one would construct a client's\n * credential source just to receive a call (#1358).\n */\n authFilterRefusesTheMinter(oidc: DefaultOidcVerifier, minter: VendorAuthMinter): void {\n // @ts-expect-error a VendorAuthMinter cannot verify: AuthFilter takes a VendorAuthVerifier\n void new AuthFilter(oidc, undefined, undefined, undefined, minter);\n }\n\n /** The ALIGNED spellings must keep compiling; asserted by the ABSENCE of an error. */\n legitimate(): void {\n void class extends VendorAuthVerifier {\n // The vendors it handles, read at route registration so an unhandled name fails boot (#1362).\n override supportedVendorNames(): readonly string[] {\n return ['sentry'];\n }\n\n // No meaningless mint()/refresh(): a server-only verifier implements verify and nothing else.\n override async verify(_name: string, request: RawHttpRequest): Promise<AuthenticatedCaller> {\n // No `raw!`, no `if (!raw) throw`: AuthFilter checked once and the TYPE carries it.\n const bytes: Buffer = request.raw.rawBody;\n return new AuthenticatedCaller(`sentry:${bytes.length}`);\n }\n };\n void class extends ApiKeyHook {\n override async verifyApiKey(_name: string, request: HttpRequest): Promise<AuthenticatedCaller> {\n // Both of these were UNREACHABLE through the deleted one-method HeaderReader.\n const all: string[] | undefined = request.getHeaderValues('x-api-key');\n return new AuthenticatedCaller(all?.[0] ?? 'anonymous');\n }\n };\n }\n\n /** Every spelling this change deleted must now be UNWRITABLE. */\n rejected(): void {\n void class extends VendorAuthVerifier {\n override supportedVendorNames(): readonly string[] {\n return ['sentry'];\n }\n // @ts-expect-error verify takes (name, RawHttpRequest): the three-parameter spelling is gone\n override async verify(_name: string, _request: HttpRequest, _raw: RawRequest): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n };\n void class extends VendorAuthVerifier {\n override supportedVendorNames(): readonly string[] {\n return ['sentry'];\n }\n // @ts-expect-error verify returns the caller it proved: a void override must not compile\n override async verify(_name: string, _request: RawHttpRequest): Promise<void> {\n // a hook that verifies and then has no way to say what it proved\n }\n };\n }\n\n /**\n * {@link OidcHook.verifyOidc} receives EVERY OIDC method the endpoint declares ({@link OidcAuthMethods}),\n * so a hook verifies the token once and can tell `oidcSameProject()` from `oidcSvcs([...])`. The\n * caller-list override this change deletes (`callers: string[]`, where an EMPTY list meant \"any\n * Google-signed caller\") and a one-method override, which could not see the other scope of a pair,\n * must not compile.\n */\n oidcHookTakesTheMethods(): void {\n void class extends OidcHook {\n override async verifyOidc(_token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity> {\n const first = methods[0];\n const named = first.callerScope === 'services' ? first.callers[0] : 'same-project';\n return new AuthenticatedMachineIdentity('oidc', named);\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes the endpoint's OidcAuthMethods; the deleted caller-list override must not compile\n override async verifyOidc(_token: string, _callers: string[]): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes every OIDC method of the endpoint, not one, so a pair is verified once\n override async verifyOidc(_token: string, _method: OidcAuthMethod): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n };\n }\n\n /**\n * An {@link HttpRequest} whose `raw` is merely OPTIONAL is NOT a {@link RawHttpRequest}. This is\n * the assignment that makes the narrowing real: without it, `RawHttpRequest` would be a comment.\n */\n rawIsNotOptional(): void {\n const maybeRaw = new HttpRequest('POST', '/hook/sentry/issue', new Map<string, string[]>());\n // @ts-expect-error HttpRequest.raw is optional, so it is not assignable to RawHttpRequest\n const narrowed: RawHttpRequest = maybeRaw;\n void narrowed;\n\n // The other direction is free: a RawHttpRequest IS an HttpRequest everywhere one is wanted.\n const proven = new HttpRequest(\n 'POST',\n '/hook/sentry/issue',\n new Map<string, string[]>(),\n new RawRequest('https://example.com/hook', Buffer.from('{}', 'utf8'), '1.2.3.4'),\n ) as RawHttpRequest;\n const widened: HttpRequest = proven;\n void widened;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AuthHooksCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooksCompileAssertions.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n// @ts-expect-error HeaderReader is DELETED — an ApiKeyHook now receives the whole HttpRequest, so it\n// reaches getHeaderValues and the ContextKey overload of getHeader that the narrow reader hid.\nimport type { HeaderReader } from './AuthHooks';\n// @ts-expect-error AuthValues is RENAMED to AuthenticatedCaller — there is no alias, by policy.\nimport type { AuthValues } from './AuthConfig';\nimport { OidcAuthMethod, OidcAuthMethods } from '@webpieces/core-util';\nimport { VendorAuthMinter, VendorAuthVerifier } from '@webpieces/core-context';\n// @ts-expect-error WebhookAuthCallback is REPLACED by VendorAuthVerifier (the inbound role) — no alias, by policy.\nimport type { WebhookAuthCallback } from './AuthHooks';\n// @ts-expect-error WEBHOOK_AUTH_CALLBACK is REPLACED by VENDOR_AUTH_VERIFIER.\nimport { WEBHOOK_AUTH_CALLBACK } from './AuthHooks';\nimport * as Routing from './index';\nimport { AuthFilter } from './filters/AuthFilter';\nimport { DefaultOidcVerifier } from './DefaultOidcVerifier';\nimport { ApiKeyHook, OidcHook } from './AuthHooks';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';\n\n/**\n * COMPILE-TIME assertions that the four auth hooks share ONE shape, and that every spelling this\n * change deleted has actually stopped compiling. Each `@ts-expect-error` FAILS THE BUILD (TS2578,\n * \"unused '@ts-expect-error' directive\") the moment the thing it guards starts compiling again.\n *\n * WHY THIS IS NOT A `.spec.ts` FILE — the same reason as {@link JwtHookCompileAssertions}:\n * tsconfig.lib.json EXCLUDES specs and vitest strips types with esbuild, so a `@ts-expect-error` in a\n * spec is inert and the suite passes either way. A type-level guarantee has to be asserted in a file\n * the type-checker actually compiles.\n *\n * WHY IT MATTERS HERE. This repo ships no backwards-compatibility shims: the compile error IS the\n * migration. An implementor that kept `verify(name, request, raw)` or a `HeaderReader` parameter\n * would otherwise sit there compiling, and an accepted shape is never migrated.\n */\nexport class AuthHooksCompileAssertions {\n /**\n * The DELETED type names, kept referenced so the two `@ts-expect-error`s above are load-bearing\n * rather than decorative. Both resolve to `any` under the suppressed error; the assertion is the\n * IMPORT failing, not what these aliases denote.\n */\n // webpieces-disable no-any-unknown -- a suppressed import of a deleted symbol resolves to any; the assertion is the import error itself\n deletedNames(): void {\n const headerReaderIsGone: HeaderReader | undefined = undefined;\n const authValuesIsGone: AuthValues | undefined = undefined;\n const webhookAuthCallbackIsGone: WebhookAuthCallback | undefined = undefined;\n void headerReaderIsGone;\n void authValuesIsGone;\n void webhookAuthCallbackIsGone;\n void WEBHOOK_AUTH_CALLBACK;\n // @ts-expect-error VendorAuthHook is DELETED (#1358): this server binds the INBOUND role VendorAuthVerifier\n void Routing.VendorAuthHook;\n // @ts-expect-error VENDOR_AUTH_HOOK is DELETED (#1358): bind VENDOR_AUTH_VERIFIER\n void Routing.VENDOR_AUTH_HOOK;\n // @ts-expect-error the OUTBOUND role is the client's (http-client-node); the router never exports it\n void Routing.VENDOR_AUTH_MINTER;\n }\n\n /**\n * The server's AuthFilter takes the INBOUND role only. Handing it an outbound minter must not\n * compile: a minter cannot verify, and an AuthFilter depending on one would construct a client's\n * credential source just to receive a call (#1358).\n */\n authFilterRefusesTheMinter(oidc: DefaultOidcVerifier, minter: VendorAuthMinter): void {\n // @ts-expect-error a VendorAuthMinter cannot verify: AuthFilter takes a VendorAuthVerifier\n void new AuthFilter(oidc, undefined, undefined, undefined, minter);\n }\n\n /** The ALIGNED spellings must keep compiling; asserted by the ABSENCE of an error. */\n legitimate(): void {\n void class extends VendorAuthVerifier {\n // The vendors it handles, read at route registration so an unhandled name fails boot (#1362).\n override supportedVendorNames(): readonly string[] {\n return ['sentry'];\n }\n\n // No meaningless mint()/refresh(): a server-only verifier implements verify and nothing else.\n override async verify(_name: string, request: RawHttpRequest): Promise<AuthenticatedCaller> {\n // No `raw!`, no `if (!raw) throw`: AuthFilter checked once and the TYPE carries it.\n const bytes: Buffer = request.raw.rawBody;\n return new AuthenticatedCaller(`sentry:${bytes.length}`);\n }\n };\n void class extends ApiKeyHook {\n override async verifyApiKey(_name: string, request: HttpRequest): Promise<AuthenticatedCaller> {\n // Both of these were UNREACHABLE through the deleted one-method HeaderReader.\n const all: string[] | undefined = request.getHeaderValues('x-api-key');\n return new AuthenticatedCaller(all?.[0] ?? 'anonymous');\n }\n };\n }\n\n /** Every spelling this change deleted must now be UNWRITABLE. */\n rejected(): void {\n void class extends VendorAuthVerifier {\n override supportedVendorNames(): readonly string[] {\n return ['sentry'];\n }\n // @ts-expect-error verify takes (name, RawHttpRequest): the three-parameter spelling is gone\n override async verify(_name: string, _request: HttpRequest, _raw: RawRequest): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n };\n void class extends VendorAuthVerifier {\n override supportedVendorNames(): readonly string[] {\n return ['sentry'];\n }\n // @ts-expect-error verify returns the caller it proved: a void override must not compile\n override async verify(_name: string, _request: RawHttpRequest): Promise<void> {\n // a hook that verifies and then has no way to say what it proved\n }\n };\n }\n\n /**\n * {@link OidcHook.verifyOidc} receives EVERY OIDC method the endpoint declares ({@link OidcAuthMethods}),\n * so a hook verifies the token once and can tell `oidcSameProject()` from `oidcSvcs([...])`. The\n * caller-list override this change deletes (`callers: string[]`, where an EMPTY list meant \"any\n * Google-signed caller\") and a one-method override, which could not see the other scope of a pair,\n * must not compile.\n */\n oidcHookTakesTheMethods(): void {\n void class extends OidcHook {\n override async verifyOidc(_token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity> {\n const first = methods[0];\n const named = first.callerScope === 'services' ? first.callers[0] : 'same-project';\n return new AuthenticatedMachineIdentity('oidc', named);\n }\n override async originatingServiceRoles(_identity: AuthenticatedMachineIdentity): Promise<readonly string[]> {\n return [];\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes the endpoint's OidcAuthMethods; the deleted caller-list override must not compile\n override async verifyOidc(_token: string, _callers: string[]): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n override async originatingServiceRoles(_identity: AuthenticatedMachineIdentity): Promise<readonly string[]> {\n return [];\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes every OIDC method of the endpoint, not one, so a pair is verified once\n override async verifyOidc(_token: string, _method: OidcAuthMethod): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n override async originatingServiceRoles(_identity: AuthenticatedMachineIdentity): Promise<readonly string[]> {\n return [];\n }\n };\n }\n\n /**\n * An {@link HttpRequest} whose `raw` is merely OPTIONAL is NOT a {@link RawHttpRequest}. This is\n * the assignment that makes the narrowing real: without it, `RawHttpRequest` would be a comment.\n */\n rawIsNotOptional(): void {\n const maybeRaw = new HttpRequest('POST', '/hook/sentry/issue', new Map<string, string[]>());\n // @ts-expect-error HttpRequest.raw is optional, so it is not assignable to RawHttpRequest\n const narrowed: RawHttpRequest = maybeRaw;\n void narrowed;\n\n // The other direction is free: a RawHttpRequest IS an HttpRequest everywhere one is wanted.\n const proven = new HttpRequest(\n 'POST',\n '/hook/sentry/issue',\n new Map<string, string[]>(),\n new RawRequest('https://example.com/hook', Buffer.from('{}', 'utf8'), '1.2.3.4'),\n ) as RawHttpRequest;\n const widened: HttpRequest = proven;\n void widened;\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthenticatedCaller } from './AuthConfig';
|
|
1
|
+
import { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';
|
|
2
2
|
/** Shared trusted principal publication and inbound header reconciliation for HTTP and MCP. */
|
|
3
3
|
export declare class AuthenticatedCallerContext {
|
|
4
4
|
private static readonly establishedScopes;
|
|
@@ -8,6 +8,19 @@ export declare class AuthenticatedCallerContext {
|
|
|
8
8
|
static hasCompletedAuthentication(): boolean;
|
|
9
9
|
publish(caller: AuthenticatedCaller): void;
|
|
10
10
|
private validateCanonicalIdentity;
|
|
11
|
+
/**
|
|
12
|
+
* The internal-hop twin of {@link publish}, for a caller-VERIFIED service credential (OIDC, shared
|
|
13
|
+
* secret): stamp the current-hop machine proof and, when this service ORIGINATED the call, publish
|
|
14
|
+
* it as a caller with the roles its account was granted (`OidcHook.originatingServiceRoles`) and no
|
|
15
|
+
* user id — `[]` when it was granted none.
|
|
16
|
+
*
|
|
17
|
+
* A service RELAYING an established caller — a forwarded USER_ID or USER_ROLES is pending on this
|
|
18
|
+
* hop, which every published caller upstream carries — contributes nothing of its own: its hop is
|
|
19
|
+
* admitted by its machine identity, the forwarded caller is admitted by {@link reconcileWireTrust},
|
|
20
|
+
* and a user (or a role-less service) behind it is still denied a ROLES endpoint. `rolesOf` is
|
|
21
|
+
* therefore asked only for an originating service.
|
|
22
|
+
*/
|
|
23
|
+
publishVerifiedService(identity: AuthenticatedMachineIdentity, rolesOf: (identity: AuthenticatedMachineIdentity) => Promise<readonly string[]>): Promise<void>;
|
|
11
24
|
reconcileWireTrust(callerVerified: boolean): void;
|
|
12
25
|
/**
|
|
13
26
|
* On a browser-reachable route, an inbound trusted header must match what the authenticator
|
|
@@ -31,8 +31,12 @@ class AuthenticatedCallerContext {
|
|
|
31
31
|
core_context_1.RequestContext.putTrusted(AuthConfig_1.AUTHENTICATED_CALLER_KEY, caller);
|
|
32
32
|
if (caller.userId !== undefined) {
|
|
33
33
|
core_context_1.RequestContext.putTrusted(core_util_1.WebpiecesCoreHeaders.USER_ID, caller.userId);
|
|
34
|
-
core_context_1.RequestContext.putTrusted(core_util_1.WebpiecesCoreHeaders.USER_ROLES, JSON.stringify(caller.roles));
|
|
35
34
|
}
|
|
35
|
+
// A service / AI agent is a caller with roles and NO user id (#1420): the SAME USER_ROLES key
|
|
36
|
+
// carries its roles, so ROLES authorizes it and the roles propagate on OIDC / shared-secret hops.
|
|
37
|
+
// EVERY published caller gets it, `[]` included: a forwarded USER_ROLES is how a downstream hop
|
|
38
|
+
// knows the service calling it is RELAYING an established caller, and so contributes no role.
|
|
39
|
+
core_context_1.RequestContext.putTrusted(core_util_1.WebpiecesCoreHeaders.USER_ROLES, JSON.stringify(caller.roles));
|
|
36
40
|
if (caller.machine) {
|
|
37
41
|
core_context_1.RequestContext.putTrusted(AuthorizationHook_1.VERIFIED_MACHINE_CALLER, new AuthorizationHook_1.VerifiedMachineCaller(caller.machine.mechanism, caller.machine.identity));
|
|
38
42
|
}
|
|
@@ -43,9 +47,10 @@ class AuthenticatedCallerContext {
|
|
|
43
47
|
(caller.userId === undefined && !caller.machine)) {
|
|
44
48
|
throw new core_util_1.ApiImplementationError('Verified caller must contain a nonempty user ID or verified machine identity.');
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
// Roles belong to a verified CALLER: a user, or a machine (a service / AI agent with no user id).
|
|
51
|
+
// The first check above already guarantees one of the two is present.
|
|
52
|
+
if (!Array.isArray(caller.roles) || !caller.roles.every((role) => typeof role === 'string' && role.trim().length > 0)) {
|
|
53
|
+
throw new core_util_1.ApiImplementationError('Verified caller roles must be nonempty strings.');
|
|
49
54
|
}
|
|
50
55
|
for (const entry of caller.entries) {
|
|
51
56
|
if ((entry.key.name === core_util_1.WebpiecesCoreHeaders.USER_ID.name && entry.value !== caller.userId) ||
|
|
@@ -54,6 +59,27 @@ class AuthenticatedCallerContext {
|
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* The internal-hop twin of {@link publish}, for a caller-VERIFIED service credential (OIDC, shared
|
|
64
|
+
* secret): stamp the current-hop machine proof and, when this service ORIGINATED the call, publish
|
|
65
|
+
* it as a caller with the roles its account was granted (`OidcHook.originatingServiceRoles`) and no
|
|
66
|
+
* user id — `[]` when it was granted none.
|
|
67
|
+
*
|
|
68
|
+
* A service RELAYING an established caller — a forwarded USER_ID or USER_ROLES is pending on this
|
|
69
|
+
* hop, which every published caller upstream carries — contributes nothing of its own: its hop is
|
|
70
|
+
* admitted by its machine identity, the forwarded caller is admitted by {@link reconcileWireTrust},
|
|
71
|
+
* and a user (or a role-less service) behind it is still denied a ROLES endpoint. `rolesOf` is
|
|
72
|
+
* therefore asked only for an originating service.
|
|
73
|
+
*/
|
|
74
|
+
async publishVerifiedService(identity, rolesOf) {
|
|
75
|
+
const relaying = core_context_1.PendingWireTrust.holds(core_util_1.WebpiecesCoreHeaders.USER_ID) ||
|
|
76
|
+
core_context_1.PendingWireTrust.holds(core_util_1.WebpiecesCoreHeaders.USER_ROLES);
|
|
77
|
+
if (relaying) {
|
|
78
|
+
core_context_1.RequestContext.putTrusted(AuthorizationHook_1.VERIFIED_MACHINE_CALLER, new AuthorizationHook_1.VerifiedMachineCaller(identity.mechanism, identity.identity));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.publish(new AuthConfig_1.AuthenticatedCaller(undefined, [...await rolesOf(identity)], [], {}, identity));
|
|
82
|
+
}
|
|
57
83
|
reconcileWireTrust(callerVerified) {
|
|
58
84
|
const pending = core_context_1.PendingWireTrust.takeAll();
|
|
59
85
|
for (const item of pending) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthenticatedCallerContext.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthenticatedCallerContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { PendingWireTrust, PendingTrustedValue, RequestContext } from '@webpieces/core-context';\nimport { ApiUnauthorizedError, ApiImplementationError, LogManager, WebpiecesCoreHeaders } from '@webpieces/core-util';\nimport { VERIFIED_MACHINE_CALLER, VerifiedMachineCaller } from './AuthorizationHook';\nimport { AuthenticatedCaller, AUTHENTICATED_CALLER_KEY } from './AuthConfig';\n\nconst log = LogManager.getLogger('AuthenticatedCallerContext');\n\n/** Shared trusted principal publication and inbound header reconciliation for HTTP and MCP. */\nexport class AuthenticatedCallerContext {\n private static readonly establishedScopes = new WeakSet<object>();\n private static readonly publishedScopes = new WeakSet<object>();\n private static readonly completedScopes = new WeakSet<object>();\n\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n static hasEstablishedIngress(): boolean {\n return this.establishedScopes.has(RequestContext.activeScopeIdentity());\n }\n\n // webpieces-disable no-function-outside-class -- authentication completion is scoped metadata, not a transferable context value\n static hasCompletedAuthentication(): boolean {\n return this.completedScopes.has(RequestContext.activeScopeIdentity());\n }\n\n publish(caller: AuthenticatedCaller): void {\n this.validateCanonicalIdentity(caller);\n for (const entry of caller.entries) {\n // ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a\n // proven identity: the app's hook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n // A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,\n // so it is written with the same typed verb every other proven value goes through.\n RequestContext.putTrusted(AUTHENTICATED_CALLER_KEY, caller);\n if (caller.userId !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.USER_ID, caller.userId);\n RequestContext.putTrusted(WebpiecesCoreHeaders.USER_ROLES, JSON.stringify(caller.roles));\n }\n if (caller.machine) {\n RequestContext.putTrusted(VERIFIED_MACHINE_CALLER, new VerifiedMachineCaller(caller.machine.mechanism, caller.machine.identity));\n }\n AuthenticatedCallerContext.publishedScopes.add(RequestContext.activeScopeIdentity());\n }\n\n private validateCanonicalIdentity(caller: AuthenticatedCaller): void {\n if ((caller.userId !== undefined && (typeof caller.userId !== 'string' || !caller.userId.trim())) ||\n (caller.userId === undefined && !caller.machine)) {\n throw new ApiImplementationError('Verified caller must contain a nonempty user ID or verified machine identity.');\n }\n if (!Array.isArray(caller.roles) || !caller.roles.every((role: string) => typeof role === 'string' && role.trim().length > 0) ||\n (caller.userId === undefined && caller.roles.length > 0)) {\n throw new ApiImplementationError('Verified caller roles must be nonempty strings belonging to a verified user.');\n }\n for (const entry of caller.entries) {\n if ((entry.key.name === WebpiecesCoreHeaders.USER_ID.name && entry.value !== caller.userId) ||\n (entry.key.name === WebpiecesCoreHeaders.USER_ROLES.name && entry.value !== JSON.stringify(caller.roles))) {\n throw new ApiImplementationError('Canonical user ID/roles entries disagree with the verified caller.');\n }\n }\n }\n\n reconcileWireTrust(callerVerified: boolean): void {\n const pending = PendingWireTrust.takeAll();\n for (const item of pending) {\n if (callerVerified) {\n RequestContext.putTrusted(item.key, item.value);\n } else {\n this.requireVouched(item);\n }\n }\n const machine = RequestContext.getTrusted(VERIFIED_MACHINE_CALLER);\n const scope = RequestContext.activeScopeIdentity();\n AuthenticatedCallerContext.completedScopes.add(scope);\n if (AuthenticatedCallerContext.publishedScopes.has(scope) ||\n (machine instanceof VerifiedMachineCaller && machine.isCurrentHop())) {\n AuthenticatedCallerContext.establishedScopes.add(scope);\n }\n }\n\n /**\n * On a browser-reachable route, an inbound trusted header must match what the authenticator\n * itself derived, or the request dies. Both failure shapes are rejections, not repairs:\n *\n * - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the\n * credential win is not safe, because upstream rate limiters commonly bucket on the header\n * rather than the token: the request was already counted against the wrong principal, so\n * every forged header would be a free rate-limit bypass. No honest caller contradicts its own\n * credential.\n * - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a\n * value a stranger typed. This is the common case, not the exotic one: the framework's\n * {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,\n * so any other trusted key a caller sends lands here.\n *\n * The pending value is discarded either way — the throw is what leaves the request.\n */\n private requireVouched(item: PendingTrustedValue): void {\n const vouched = RequestContext.getTrusted(item.key);\n if (vouched === item.value) {\n return;\n }\n log.error(\n `Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +\n `not authenticate its caller, and the credential ` +\n (vouched === undefined\n ? 'vouched for no such value'\n : 'derived a different value') +\n '.',\n );\n throw new ApiUnauthorizedError(\n `Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`,\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AuthenticatedCallerContext.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthenticatedCallerContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { PendingWireTrust, PendingTrustedValue, RequestContext } from '@webpieces/core-context';\nimport { ApiUnauthorizedError, ApiImplementationError, LogManager, WebpiecesCoreHeaders } from '@webpieces/core-util';\nimport { VERIFIED_MACHINE_CALLER, VerifiedMachineCaller } from './AuthorizationHook';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity, AUTHENTICATED_CALLER_KEY } from './AuthConfig';\n\nconst log = LogManager.getLogger('AuthenticatedCallerContext');\n\n/** Shared trusted principal publication and inbound header reconciliation for HTTP and MCP. */\nexport class AuthenticatedCallerContext {\n private static readonly establishedScopes = new WeakSet<object>();\n private static readonly publishedScopes = new WeakSet<object>();\n private static readonly completedScopes = new WeakSet<object>();\n\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n static hasEstablishedIngress(): boolean {\n return this.establishedScopes.has(RequestContext.activeScopeIdentity());\n }\n\n // webpieces-disable no-function-outside-class -- authentication completion is scoped metadata, not a transferable context value\n static hasCompletedAuthentication(): boolean {\n return this.completedScopes.has(RequestContext.activeScopeIdentity());\n }\n\n publish(caller: AuthenticatedCaller): void {\n this.validateCanonicalIdentity(caller);\n for (const entry of caller.entries) {\n // ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a\n // proven identity: the app's hook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n // A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,\n // so it is written with the same typed verb every other proven value goes through.\n RequestContext.putTrusted(AUTHENTICATED_CALLER_KEY, caller);\n if (caller.userId !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.USER_ID, caller.userId);\n }\n // A service / AI agent is a caller with roles and NO user id (#1420): the SAME USER_ROLES key\n // carries its roles, so ROLES authorizes it and the roles propagate on OIDC / shared-secret hops.\n // EVERY published caller gets it, `[]` included: a forwarded USER_ROLES is how a downstream hop\n // knows the service calling it is RELAYING an established caller, and so contributes no role.\n RequestContext.putTrusted(WebpiecesCoreHeaders.USER_ROLES, JSON.stringify(caller.roles));\n if (caller.machine) {\n RequestContext.putTrusted(VERIFIED_MACHINE_CALLER, new VerifiedMachineCaller(caller.machine.mechanism, caller.machine.identity));\n }\n AuthenticatedCallerContext.publishedScopes.add(RequestContext.activeScopeIdentity());\n }\n\n private validateCanonicalIdentity(caller: AuthenticatedCaller): void {\n if ((caller.userId !== undefined && (typeof caller.userId !== 'string' || !caller.userId.trim())) ||\n (caller.userId === undefined && !caller.machine)) {\n throw new ApiImplementationError('Verified caller must contain a nonempty user ID or verified machine identity.');\n }\n // Roles belong to a verified CALLER: a user, or a machine (a service / AI agent with no user id).\n // The first check above already guarantees one of the two is present.\n if (!Array.isArray(caller.roles) || !caller.roles.every((role: string) => typeof role === 'string' && role.trim().length > 0)) {\n throw new ApiImplementationError('Verified caller roles must be nonempty strings.');\n }\n for (const entry of caller.entries) {\n if ((entry.key.name === WebpiecesCoreHeaders.USER_ID.name && entry.value !== caller.userId) ||\n (entry.key.name === WebpiecesCoreHeaders.USER_ROLES.name && entry.value !== JSON.stringify(caller.roles))) {\n throw new ApiImplementationError('Canonical user ID/roles entries disagree with the verified caller.');\n }\n }\n }\n\n /**\n * The internal-hop twin of {@link publish}, for a caller-VERIFIED service credential (OIDC, shared\n * secret): stamp the current-hop machine proof and, when this service ORIGINATED the call, publish\n * it as a caller with the roles its account was granted (`OidcHook.originatingServiceRoles`) and no\n * user id — `[]` when it was granted none.\n *\n * A service RELAYING an established caller — a forwarded USER_ID or USER_ROLES is pending on this\n * hop, which every published caller upstream carries — contributes nothing of its own: its hop is\n * admitted by its machine identity, the forwarded caller is admitted by {@link reconcileWireTrust},\n * and a user (or a role-less service) behind it is still denied a ROLES endpoint. `rolesOf` is\n * therefore asked only for an originating service.\n */\n async publishVerifiedService(\n identity: AuthenticatedMachineIdentity,\n rolesOf: (identity: AuthenticatedMachineIdentity) => Promise<readonly string[]>,\n ): Promise<void> {\n const relaying = PendingWireTrust.holds(WebpiecesCoreHeaders.USER_ID) ||\n PendingWireTrust.holds(WebpiecesCoreHeaders.USER_ROLES);\n if (relaying) {\n RequestContext.putTrusted(VERIFIED_MACHINE_CALLER, new VerifiedMachineCaller(identity.mechanism, identity.identity));\n return;\n }\n this.publish(new AuthenticatedCaller(undefined, [...await rolesOf(identity)], [], {}, identity));\n }\n\n reconcileWireTrust(callerVerified: boolean): void {\n const pending = PendingWireTrust.takeAll();\n for (const item of pending) {\n if (callerVerified) {\n RequestContext.putTrusted(item.key, item.value);\n } else {\n this.requireVouched(item);\n }\n }\n const machine = RequestContext.getTrusted(VERIFIED_MACHINE_CALLER);\n const scope = RequestContext.activeScopeIdentity();\n AuthenticatedCallerContext.completedScopes.add(scope);\n if (AuthenticatedCallerContext.publishedScopes.has(scope) ||\n (machine instanceof VerifiedMachineCaller && machine.isCurrentHop())) {\n AuthenticatedCallerContext.establishedScopes.add(scope);\n }\n }\n\n /**\n * On a browser-reachable route, an inbound trusted header must match what the authenticator\n * itself derived, or the request dies. Both failure shapes are rejections, not repairs:\n *\n * - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the\n * credential win is not safe, because upstream rate limiters commonly bucket on the header\n * rather than the token: the request was already counted against the wrong principal, so\n * every forged header would be a free rate-limit bypass. No honest caller contradicts its own\n * credential.\n * - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a\n * value a stranger typed. This is the common case, not the exotic one: the framework's\n * {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,\n * so any other trusted key a caller sends lands here.\n *\n * The pending value is discarded either way — the throw is what leaves the request.\n */\n private requireVouched(item: PendingTrustedValue): void {\n const vouched = RequestContext.getTrusted(item.key);\n if (vouched === item.value) {\n return;\n }\n log.error(\n `Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +\n `not authenticate its caller, and the credential ` +\n (vouched === undefined\n ? 'vouched for no such value'\n : 'derived a different value') +\n '.',\n );\n throw new ApiUnauthorizedError(\n `Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`,\n );\n }\n}\n"]}
|
|
@@ -28,7 +28,12 @@ export declare class AuthorizationService {
|
|
|
28
28
|
validate(requirement: AuthorizationRequirement<unknown>): void;
|
|
29
29
|
authorize(requirement: AuthorizationRequirement<unknown>): Promise<void>;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The caller's canonical roles: a JSON string array under USER_ROLES — `[]` for a verified user with no
|
|
33
|
+
* roles, and the granted roles of a role-bearing machine caller (a service / AI agent with no user id).
|
|
34
|
+
* A machine granted no role carries `[]` (or no USER_ROLES at all, on a hop whose upstream forwarded
|
|
35
|
+
* none), which every ROLES requirement denies.
|
|
36
|
+
*/
|
|
32
37
|
export declare class CanonicalUserRoles {
|
|
33
38
|
static read(): readonly string[];
|
|
34
39
|
}
|
package/src/AuthorizationHook.js
CHANGED
|
@@ -81,7 +81,9 @@ let AuthorizationService = class AuthorizationService {
|
|
|
81
81
|
return;
|
|
82
82
|
break;
|
|
83
83
|
case core_util_1.AuthorizationType.ROLES:
|
|
84
|
-
|
|
84
|
+
// USER_ROLES alone (#1420): a service / AI agent is a caller with roles and no user id.
|
|
85
|
+
// An intermediate OIDC hop never contributes roles, so a user with none is still denied.
|
|
86
|
+
if (requirement.roles.some((role) => CanonicalUserRoles.read().includes(role)))
|
|
85
87
|
return;
|
|
86
88
|
break;
|
|
87
89
|
case core_util_1.AuthorizationType.CUSTOM: {
|
|
@@ -102,17 +104,22 @@ exports.AuthorizationService = AuthorizationService = tslib_1.__decorate([
|
|
|
102
104
|
tslib_1.__param(0, (0, inversify_1.inject)(exports.AUTHORIZATION_HOOK)),
|
|
103
105
|
tslib_1.__metadata("design:paramtypes", [AuthorizationHook])
|
|
104
106
|
], AuthorizationService);
|
|
105
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* The caller's canonical roles: a JSON string array under USER_ROLES — `[]` for a verified user with no
|
|
109
|
+
* roles, and the granted roles of a role-bearing machine caller (a service / AI agent with no user id).
|
|
110
|
+
* A machine granted no role carries `[]` (or no USER_ROLES at all, on a hop whose upstream forwarded
|
|
111
|
+
* none), which every ROLES requirement denies.
|
|
112
|
+
*/
|
|
106
113
|
class CanonicalUserRoles {
|
|
107
114
|
// webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration
|
|
108
115
|
static read() {
|
|
109
116
|
const encoded = core_context_1.RequestContext.getTrusted(core_util_1.WebpiecesCoreHeaders.USER_ROLES);
|
|
110
117
|
if (encoded === undefined)
|
|
111
|
-
throw new core_util_1.ApiForbiddenError('Verified
|
|
118
|
+
throw new core_util_1.ApiForbiddenError('Verified caller roles are missing.');
|
|
112
119
|
// webpieces-disable no-any-unknown -- JSON is validated before it becomes canonical roles
|
|
113
120
|
const value = JSON.parse(encoded);
|
|
114
121
|
if (!Array.isArray(value) || !value.every((role) => typeof role === 'string' && role.trim().length > 0)) {
|
|
115
|
-
throw new core_util_1.ApiForbiddenError('Canonical
|
|
122
|
+
throw new core_util_1.ApiForbiddenError('Canonical caller roles must be a JSON string array.');
|
|
116
123
|
}
|
|
117
124
|
return value;
|
|
118
125
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthorizationHook.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthorizationHook.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"AuthorizationHook.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthorizationHook.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { ApiForbiddenError, AuthorizationDeclaration, AuthorizationRequirement, AuthorizationType, ContextKey, WebpiecesCoreHeaders } from '@webpieces/core-util';\n\n/** An application validates its own policy; the framework supplies no organization semantics. */\nexport abstract class AuthorizationHook<TPolicy> {\n // webpieces-disable no-any-unknown -- runtime application policy must be validated rather than cast to its generic type\n abstract validatePolicy(value: unknown): TPolicy;\n abstract authorize(policy: TPolicy): Promise<void>;\n\n // webpieces-disable no-any-unknown -- closes the validated generic value before the framework erases its type\n compile(value: unknown): CompiledAuthorizationPolicy {\n const policy = this.validatePolicy(value);\n return new CompiledAuthorizationPolicy(() => this.authorize(policy));\n }\n}\n\nexport class CompiledAuthorizationPolicy {\n constructor(private readonly evaluation: () => Promise<void>) {}\n\n authorize(): Promise<void> {\n return this.evaluation();\n }\n}\n\n// webpieces-disable no-symbol-di-tokens -- optional application hook must not be auto-bound by the framework container\nexport const AUTHORIZATION_HOOK = Symbol.for('AuthorizationHook');\n\n/** Current-hop machine proof is context-only; delegated headers never create it. */\nexport class VerifiedMachineCaller {\n private readonly scopeIdentity = RequestContext.activeScopeIdentity();\n\n isCurrentHop(): boolean {\n return this.scopeIdentity === RequestContext.activeScopeIdentity();\n }\n constructor(public readonly mechanism: string, public readonly identity: string) {\n if (!mechanism.trim() || !identity.trim()) throw new Error('Verified machine evidence must name its mechanism and identity.');\n }\n}\n\nexport const VERIFIED_MACHINE_CALLER = ContextKey.trusted<VerifiedMachineCaller>(\n 'verifiedMachineCaller', 'established by the successful current-hop credential verifier', undefined, false, false,\n);\n\n/** One compiled policy governs receiving endpoints, MCP visibility, and served documents. */\n@provideFrameworkSingleton()\nexport class AuthorizationService {\n private readonly compiled = new WeakMap<object, CompiledAuthorizationPolicy>();\n\n constructor(\n @optional() @inject(AUTHORIZATION_HOOK) private readonly hook?: AuthorizationHook<never>,\n ) {}\n\n // webpieces-disable no-any-unknown -- the custom hook validates erased application policy at startup\n validate(requirement: AuthorizationRequirement<unknown>): void {\n AuthorizationDeclaration.validate(requirement);\n if (requirement.authType !== AuthorizationType.CUSTOM || this.compiled.has(requirement)) return;\n if (!this.hook) throw new Error('@WpAuthorization CUSTOM requires a bound AuthorizationHook (AUTHORIZATION_HOOK).');\n this.compiled.set(requirement, this.hook.compile(requirement.appPolicy));\n }\n\n // webpieces-disable no-any-unknown -- standard branches are narrowed; custom policies execute their compiled typed closure\n async authorize(requirement: AuthorizationRequirement<unknown>): Promise<void> {\n this.validate(requirement);\n const userId = RequestContext.getTrusted(WebpiecesCoreHeaders.USER_ID);\n const isUser = typeof userId === 'string' && userId.trim().length > 0;\n const machine = RequestContext.getTrusted(VERIFIED_MACHINE_CALLER);\n const isMachine = machine instanceof VerifiedMachineCaller && machine.isCurrentHop();\n switch (requirement.authType) {\n case AuthorizationType.ANONYMOUS: return;\n case AuthorizationType.ALL_USERS:\n if (isUser) return;\n break;\n case AuthorizationType.SERVICE_ONLY:\n if (isMachine) return;\n break;\n case AuthorizationType.USERS_OR_SERVICES:\n if (isUser || isMachine) return;\n break;\n case AuthorizationType.ROLES:\n // USER_ROLES alone (#1420): a service / AI agent is a caller with roles and no user id.\n // An intermediate OIDC hop never contributes roles, so a user with none is still denied.\n if (requirement.roles.some((role: string) => CanonicalUserRoles.read().includes(role))) return;\n break;\n case AuthorizationType.CUSTOM: {\n const policy = this.compiled.get(requirement);\n if (!policy) throw new Error('CUSTOM authorization was not compiled at startup.');\n await policy.authorize();\n return;\n }\n }\n throw new ApiForbiddenError(`Caller does not satisfy ${requirement.authType} authorization.`);\n }\n}\n\n/**\n * The caller's canonical roles: a JSON string array under USER_ROLES — `[]` for a verified user with no\n * roles, and the granted roles of a role-bearing machine caller (a service / AI agent with no user id).\n * A machine granted no role carries `[]` (or no USER_ROLES at all, on a hop whose upstream forwarded\n * none), which every ROLES requirement denies.\n */\nexport class CanonicalUserRoles {\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n static read(): readonly string[] {\n const encoded = RequestContext.getTrusted(WebpiecesCoreHeaders.USER_ROLES);\n if (encoded === undefined) throw new ApiForbiddenError('Verified caller roles are missing.');\n // webpieces-disable no-any-unknown -- JSON is validated before it becomes canonical roles\n const value: unknown = JSON.parse(encoded);\n if (!Array.isArray(value) || !value.every((role: unknown) => typeof role === 'string' && role.trim().length > 0)) {\n throw new ApiForbiddenError('Canonical caller roles must be a JSON string array.');\n }\n return value as string[];\n }\n}\n"]}
|
|
@@ -48,6 +48,12 @@ export declare class CredentialVerifiers {
|
|
|
48
48
|
verifyJwt(token: string): Promise<AuthenticatedCaller>;
|
|
49
49
|
/** `oidcSameProject()` / `oidcSvcs([...])`: one token, verified once against every declared scope. */
|
|
50
50
|
verifyOidc(token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity>;
|
|
51
|
+
/**
|
|
52
|
+
* The roles the bound {@link OidcHook} grants an ORIGINATING service account, or none when no hook
|
|
53
|
+
* is bound (the framework {@link DefaultOidcVerifier} grants no service a role). Pass it to
|
|
54
|
+
* `AuthenticatedCallerContext.publishVerifiedService`, which asks only when nothing was forwarded.
|
|
55
|
+
*/
|
|
56
|
+
oidcOriginatingServiceRoles(identity: AuthenticatedMachineIdentity): Promise<readonly string[]>;
|
|
51
57
|
/** `sharedSecret(key)`: the `Webpieces <secret>` value against the bound secrets for `key`. */
|
|
52
58
|
verifySharedSecret(provided: string, secretKey: string): void;
|
|
53
59
|
/** `apiKey(regime, credentials)`: the app's {@link ApiKeyHook}, over the whole inbound request. */
|
|
@@ -105,6 +105,14 @@ let CredentialVerifiers = class CredentialVerifiers {
|
|
|
105
105
|
throw new core_util_1.ApiImplementationError('OIDC verifier must return its verified machine identity.');
|
|
106
106
|
return identity;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The roles the bound {@link OidcHook} grants an ORIGINATING service account, or none when no hook
|
|
110
|
+
* is bound (the framework {@link DefaultOidcVerifier} grants no service a role). Pass it to
|
|
111
|
+
* `AuthenticatedCallerContext.publishVerifiedService`, which asks only when nothing was forwarded.
|
|
112
|
+
*/
|
|
113
|
+
async oidcOriginatingServiceRoles(identity) {
|
|
114
|
+
return this.oidcHook ? this.oidcHook.originatingServiceRoles(identity) : [];
|
|
115
|
+
}
|
|
108
116
|
/** `sharedSecret(key)`: the `Webpieces <secret>` value against the bound secrets for `key`. */
|
|
109
117
|
verifySharedSecret(provided, secretKey) {
|
|
110
118
|
const accepted = this.authConfig?.sharedSecrets[secretKey];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CredentialVerifiers.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/CredentialVerifiers.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport { HttpRequest, provideFrameworkSingleton } from '@webpieces/core-context';\nimport {\n ApiImplementationError,\n ApiUnauthorizedError,\n AuthMethod,\n OidcAuthMethods,\n} from '@webpieces/core-util';\nimport {\n AuthConfig,\n AUTH_CONFIG,\n AuthenticatedCaller,\n AuthenticatedMachineIdentity,\n SharedSecrets,\n} from './AuthConfig';\nimport { ApiKeyHook, API_KEY_HOOK, JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK } from './AuthHooks';\nimport { DefaultOidcVerifier } from './DefaultOidcVerifier';\n\n/**\n * The ONE constant-time comparison behind `sharedSecret(...)`, shared by {@link AuthFilter} and\n * {@link CredentialVerifiers} so the two doors cannot disagree about what a matching secret is.\n */\nexport class SharedSecretComparison {\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n // webpieces-disable no-function-outside-class -- stateless comparison shared by two verifiers\n static matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' &&\n SharedSecretComparison.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' &&\n SharedSecretComparison.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n // webpieces-disable no-function-outside-class -- stateless comparison shared by two verifiers\n private static constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) {\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n }\n}\n\n/**\n * The credential VERIFIERS an app bound, for a door that is not a route — today the MCP HTTP endpoint\n * (`@webpieces/mcp-server`), whose `setAuth([...])` takes the same `@WpAuth` builders an endpoint\n * does. It holds exactly the pieces {@link AuthFilter} holds, resolved from the same container with the\n * same tokens, so a `jwt()` on an MCP door is verified by the SAME `JwtHook` that verifies a `jwt()`\n * endpoint, and `oidcSameProject()` by the same `OidcHook` / {@link DefaultOidcVerifier}:\n *\n * - `oidc` → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}.\n * - `jwt` → the bound {@link JwtHook}.\n * - `shared-secret` → the bound {@link AuthConfig}'s values, compared in constant time.\n * - `apikey` → the bound {@link ApiKeyHook}.\n *\n * It verifies; it does not publish. Publishing the caller into `RequestContext` and reconciling inbound\n * trusted headers stay the door's job, exactly as they are {@link AuthFilter}'s.\n *\n * Get it with `router.credentialVerifiers()`. {@link missingVerifier} is the startup check: a door that\n * accepts a mode nobody can verify refuses to boot, naming the binding line, the same rule #1362 gives\n * served routes.\n */\n@provideFrameworkSingleton()\nexport class CredentialVerifiers {\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {}\n\n /**\n * The binding line a door must add before it can accept `method`, or `undefined` when the method\n * is verifiable with what is bound. OIDC always is (the framework verifier). `vendorAuth` and\n * `public` are not credential verifiers a door can use, so they are reported too.\n */\n missingVerifier(method: AuthMethod): string | undefined {\n switch (method.kind) {\n case 'oidc':\n return undefined;\n case 'jwt':\n return this.jwtHook\n ? undefined\n : 'binder.bind(JWT_HOOK).to(YourJwtHook).inSingletonScope();';\n case 'apikey':\n return this.apiKeyHook\n ? undefined\n : 'binder.bind(API_KEY_HOOK).to(YourApiKeyHook).inSingletonScope();';\n case 'shared-secret':\n return this.authConfig?.sharedSecrets[method.secretKey]\n ? undefined\n : `binder.bind(AUTH_CONFIG).to(YourAuthConfig).inSingletonScope(); // holding { '${method.secretKey}': new SharedSecrets(secret1, secret2) }`;\n case 'vendorAuth':\n return `vendorAuth('${method.name}') identifies a vendor, not a caller to authorize; it cannot be verified on this door`;\n }\n }\n\n /** `jwt()`: the app's {@link JwtHook}. Throws `ApiUnauthorizedError` to deny. */\n async verifyJwt(token: string): Promise<AuthenticatedCaller> {\n if (!this.jwtHook)\n throw new ApiImplementationError('jwt() was accepted with no JwtHook bound.');\n return this.jwtHook.parseJwt(token);\n }\n\n /** `oidcSameProject()` / `oidcSvcs([...])`: one token, verified once against every declared scope. */\n async verifyOidc(\n token: string,\n methods: OidcAuthMethods,\n ): Promise<AuthenticatedMachineIdentity> {\n const identity = this.oidcHook\n ? await this.oidcHook.verifyOidc(token, methods)\n : await this.oidcVerifier.verify(token, methods);\n if (!identity)\n throw new ApiImplementationError(\n 'OIDC verifier must return its verified machine identity.',\n );\n return identity;\n }\n\n /** `sharedSecret(key)`: the `Webpieces <secret>` value against the bound secrets for `key`. */\n verifySharedSecret(provided: string, secretKey: string): void {\n const accepted = this.authConfig?.sharedSecrets[secretKey];\n if (!accepted)\n throw new ApiImplementationError(\n `sharedSecret('${secretKey}') was accepted with no AuthConfig value bound.`,\n );\n if (!SharedSecretComparison.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError(\n 'Invalid shared secret for sharedSecret(...) credential',\n );\n }\n }\n\n /** `apiKey(regime, credentials)`: the app's {@link ApiKeyHook}, over the whole inbound request. */\n async verifyApiKey(regime: string, request: HttpRequest): Promise<AuthenticatedCaller> {\n if (!this.apiKeyHook)\n throw new ApiImplementationError('apiKey(...) was accepted with no ApiKeyHook bound.');\n return this.apiKeyHook.verifyApiKey(regime, request);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"CredentialVerifiers.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/CredentialVerifiers.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport { HttpRequest, provideFrameworkSingleton } from '@webpieces/core-context';\nimport {\n ApiImplementationError,\n ApiUnauthorizedError,\n AuthMethod,\n OidcAuthMethods,\n} from '@webpieces/core-util';\nimport {\n AuthConfig,\n AUTH_CONFIG,\n AuthenticatedCaller,\n AuthenticatedMachineIdentity,\n SharedSecrets,\n} from './AuthConfig';\nimport { ApiKeyHook, API_KEY_HOOK, JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK } from './AuthHooks';\nimport { DefaultOidcVerifier } from './DefaultOidcVerifier';\n\n/**\n * The ONE constant-time comparison behind `sharedSecret(...)`, shared by {@link AuthFilter} and\n * {@link CredentialVerifiers} so the two doors cannot disagree about what a matching secret is.\n */\nexport class SharedSecretComparison {\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n // webpieces-disable no-function-outside-class -- stateless comparison shared by two verifiers\n static matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' &&\n SharedSecretComparison.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' &&\n SharedSecretComparison.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n // webpieces-disable no-function-outside-class -- stateless comparison shared by two verifiers\n private static constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) {\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n }\n}\n\n/**\n * The credential VERIFIERS an app bound, for a door that is not a route — today the MCP HTTP endpoint\n * (`@webpieces/mcp-server`), whose `setAuth([...])` takes the same `@WpAuth` builders an endpoint\n * does. It holds exactly the pieces {@link AuthFilter} holds, resolved from the same container with the\n * same tokens, so a `jwt()` on an MCP door is verified by the SAME `JwtHook` that verifies a `jwt()`\n * endpoint, and `oidcSameProject()` by the same `OidcHook` / {@link DefaultOidcVerifier}:\n *\n * - `oidc` → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}.\n * - `jwt` → the bound {@link JwtHook}.\n * - `shared-secret` → the bound {@link AuthConfig}'s values, compared in constant time.\n * - `apikey` → the bound {@link ApiKeyHook}.\n *\n * It verifies; it does not publish. Publishing the caller into `RequestContext` and reconciling inbound\n * trusted headers stay the door's job, exactly as they are {@link AuthFilter}'s.\n *\n * Get it with `router.credentialVerifiers()`. {@link missingVerifier} is the startup check: a door that\n * accepts a mode nobody can verify refuses to boot, naming the binding line, the same rule #1362 gives\n * served routes.\n */\n@provideFrameworkSingleton()\nexport class CredentialVerifiers {\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {}\n\n /**\n * The binding line a door must add before it can accept `method`, or `undefined` when the method\n * is verifiable with what is bound. OIDC always is (the framework verifier). `vendorAuth` and\n * `public` are not credential verifiers a door can use, so they are reported too.\n */\n missingVerifier(method: AuthMethod): string | undefined {\n switch (method.kind) {\n case 'oidc':\n return undefined;\n case 'jwt':\n return this.jwtHook\n ? undefined\n : 'binder.bind(JWT_HOOK).to(YourJwtHook).inSingletonScope();';\n case 'apikey':\n return this.apiKeyHook\n ? undefined\n : 'binder.bind(API_KEY_HOOK).to(YourApiKeyHook).inSingletonScope();';\n case 'shared-secret':\n return this.authConfig?.sharedSecrets[method.secretKey]\n ? undefined\n : `binder.bind(AUTH_CONFIG).to(YourAuthConfig).inSingletonScope(); // holding { '${method.secretKey}': new SharedSecrets(secret1, secret2) }`;\n case 'vendorAuth':\n return `vendorAuth('${method.name}') identifies a vendor, not a caller to authorize; it cannot be verified on this door`;\n }\n }\n\n /** `jwt()`: the app's {@link JwtHook}. Throws `ApiUnauthorizedError` to deny. */\n async verifyJwt(token: string): Promise<AuthenticatedCaller> {\n if (!this.jwtHook)\n throw new ApiImplementationError('jwt() was accepted with no JwtHook bound.');\n return this.jwtHook.parseJwt(token);\n }\n\n /** `oidcSameProject()` / `oidcSvcs([...])`: one token, verified once against every declared scope. */\n async verifyOidc(\n token: string,\n methods: OidcAuthMethods,\n ): Promise<AuthenticatedMachineIdentity> {\n const identity = this.oidcHook\n ? await this.oidcHook.verifyOidc(token, methods)\n : await this.oidcVerifier.verify(token, methods);\n if (!identity)\n throw new ApiImplementationError(\n 'OIDC verifier must return its verified machine identity.',\n );\n return identity;\n }\n\n /**\n * The roles the bound {@link OidcHook} grants an ORIGINATING service account, or none when no hook\n * is bound (the framework {@link DefaultOidcVerifier} grants no service a role). Pass it to\n * `AuthenticatedCallerContext.publishVerifiedService`, which asks only when nothing was forwarded.\n */\n async oidcOriginatingServiceRoles(identity: AuthenticatedMachineIdentity): Promise<readonly string[]> {\n return this.oidcHook ? this.oidcHook.originatingServiceRoles(identity) : [];\n }\n\n /** `sharedSecret(key)`: the `Webpieces <secret>` value against the bound secrets for `key`. */\n verifySharedSecret(provided: string, secretKey: string): void {\n const accepted = this.authConfig?.sharedSecrets[secretKey];\n if (!accepted)\n throw new ApiImplementationError(\n `sharedSecret('${secretKey}') was accepted with no AuthConfig value bound.`,\n );\n if (!SharedSecretComparison.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError(\n 'Invalid shared secret for sharedSecret(...) credential',\n );\n }\n }\n\n /** `apiKey(regime, credentials)`: the app's {@link ApiKeyHook}, over the whole inbound request. */\n async verifyApiKey(regime: string, request: HttpRequest): Promise<AuthenticatedCaller> {\n if (!this.apiKeyHook)\n throw new ApiImplementationError('apiKey(...) was accepted with no ApiKeyHook bound.');\n return this.apiKeyHook.verifyApiKey(regime, request);\n }\n}\n"]}
|
|
@@ -10,7 +10,6 @@ const core_util_2 = require("@webpieces/core-util");
|
|
|
10
10
|
const AuthConfig_1 = require("../AuthConfig");
|
|
11
11
|
const AuthHooks_1 = require("../AuthHooks");
|
|
12
12
|
const AuthenticatedCallerContext_1 = require("../AuthenticatedCallerContext");
|
|
13
|
-
const AuthorizationHook_1 = require("../AuthorizationHook");
|
|
14
13
|
const DefaultOidcVerifier_1 = require("../DefaultOidcVerifier");
|
|
15
14
|
const CredentialVerifiers_1 = require("../CredentialVerifiers");
|
|
16
15
|
const UnverifiableAuthModeError_1 = require("../UnverifiableAuthModeError");
|
|
@@ -231,7 +230,8 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
231
230
|
return;
|
|
232
231
|
case 'shared-secret':
|
|
233
232
|
this.enforceSharedSecret(this.credential(header, SHARED_SECRET_SCHEME), mode.secretKey, meta.routeMeta);
|
|
234
|
-
|
|
233
|
+
// A shared secret grants no role: an originating holder is a role-less service caller.
|
|
234
|
+
await this.callerContext.publishVerifiedService(new AuthConfig_1.AuthenticatedMachineIdentity('shared-secret', mode.secretKey), () => Promise.resolve([]));
|
|
235
235
|
return;
|
|
236
236
|
case 'vendorAuth':
|
|
237
237
|
await this.enforceVendorAuth(mode.name, meta);
|
|
@@ -456,7 +456,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
456
456
|
: await this.oidcVerifier.verify(token, methods);
|
|
457
457
|
if (!identity)
|
|
458
458
|
throw new core_util_1.ApiImplementationError('OIDC verifier must return its verified machine identity.');
|
|
459
|
-
|
|
459
|
+
await this.callerContext.publishVerifiedService(identity, (verified) => this.oidcHook ? this.oidcHook.originatingServiceRoles(verified) : Promise.resolve([]));
|
|
460
460
|
}
|
|
461
461
|
/** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */
|
|
462
462
|
enforceSharedSecret(provided, secretKey, routeMeta) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/AuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport {\n provideFrameworkSingleton,\n HttpRequest,\n RawHttpRequest,\n RequestContext,\n VendorAuthVerifier,\n VENDOR_AUTH_VERIFIER,\n} from '@webpieces/core-context';\nimport {\n AuthMode,\n OidcAuthMethod,\n OidcAuthMethods,\n ApiEndpointNotFoundError,\n ApiUnauthorizedError,\n ApiImplementationError,\n LogManager,\n RouteMetadata,\n RuntimeLocality,\n Surface,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, SharedSecrets } from '../AuthConfig';\nimport { ApiKeyHook, API_KEY_HOOK, JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK } from '../AuthHooks';\nimport { AuthenticatedCallerContext } from '../AuthenticatedCallerContext';\nimport { VERIFIED_MACHINE_CALLER, VerifiedMachineCaller } from '../AuthorizationHook';\nimport { DefaultOidcVerifier } from '../DefaultOidcVerifier';\nimport { SharedSecretComparison } from '../CredentialVerifiers';\nimport { UnverifiableAuthModeError } from '../UnverifiableAuthModeError';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/**\n * The ONE credential header, read straight off the inbound HttpRequest.\n *\n * Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would\n * put the caller's credential into RequestContext and hence onto every outbound call this service\n * makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.\n */\nconst AUTHORIZATION_HEADER = 'authorization';\n\n/**\n * The scheme (first word of the Authorization value) names WHICH credential follows, so a secret\n * can never be mistaken for a token, nor accepted where the other was expected:\n *\n * Authorization: Bearer <user JWT | service OIDC token>\n * Authorization: Webpieces <sharedSecret(...) value>\n *\n * The scheme is REQUIRED. A bare value with no scheme is rejected.\n */\nconst BEARER_SCHEME = 'Bearer';\nconst SHARED_SECRET_SCHEME = 'Webpieces';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC\n * default. An app binds only the pieces its SERVED routes declare — and a served route that declares\n * a mode whose piece is absent FAILS STARTUP ({@link assertRouteVerifiable}, #1362), so none of the\n * rows below is ever \"not enabled\" at request time:\n * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). Startup\n * requires an AuthConfig holding the route's key.\n * - jwt → the bound {@link JwtHook} (`parseJwt`, awaited — an app's\n * strategy may reach a JWKS or a datastore). Startup requires a JwtHook: JWT needs\n * an app secret + payload shape.\n * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}\n * run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC against\n * the endpoint's caller scope (`oidcSameProject()` or `oidcSvcs([...])`).\n * - vendorAuth → the bound {@link VendorAuthVerifier} `verify`s the VENDOR's credential over the\n * retained raw request. Startup requires a verifier whose `supportedVendorNames()` lists the\n * route's name: an unverified vendor call is never waved through because wiring was\n * forgotten. The INBOUND role only — this filter never depends on the outbound\n * VendorAuthMinter (#1358).\n * - apikey → the bound {@link ApiKeyHook} looks the CUSTOMER's key up (async, over the whole\n * header set) and returns the context to seed. Startup requires an ApiKeyHook.\n * - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so\n * a logged-out page still knows who is logged in; never fails.\n * - local-only → serve only when {@link RuntimeLocality} says this process is a developer's\n * machine; otherwise 404, indistinguishable from the route not existing (which,\n * off-local, it does not — `ApiRoutingFactory` never registered it).\n *\n * Zero wiring = OIDC just works; an app only binds the hooks it actually uses.\n */\n@provideFrameworkSingleton()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private readonly callerContext = new AuthenticatedCallerContext();\n\n constructor(\n // Framework default, always available — verifies Google OIDC with zero app wiring.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- AuthFilter is DI-resolved via the esbuild/vitest path, which elides type-only imports (no design:paramtypes), so every param needs its explicit token\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // @optional: only bind an AuthConfig to enable sharedSecret(...) endpoints (startup refuses a\n // served one without it — see assertRouteVerifiable).\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // @optional: only bind a JwtHook to enable jwt() endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // @optional: only bind an OidcHook to OVERRIDE the DefaultOidcVerifier caller policy.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // @optional: only bind a VendorAuthVerifier to enable vendorAuth(...) endpoints. Unbound = a\n // server serving one refuses to boot (assertRouteVerifiable), never an allow-all.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional()\n @inject(VENDOR_AUTH_VERIFIER)\n private readonly vendorAuthVerifier?: VendorAuthVerifier,\n // @optional: only bind an ApiKeyHook to enable apiKey(...) endpoints. Unbound = a server serving\n // one refuses to boot, for the same reason as the vendor verifier above.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {\n super();\n }\n\n /**\n * STARTUP check (#1362), called by route registration for EVERY served route: each auth mode the\n * route declares must be verifiable by what is bound, or boot fails naming the endpoint, the mode\n * and the exact binding line. STRICT — a multi-method route (`[oidcSameProject(), vendorAuth('x')]`)\n * with no verifier fails too: a declared alternative nobody can verify is dead, and is almost\n * always a wiring mistake. OIDC needs no app binding (the framework verifier) and a public route\n * none.\n *\n * Reads bindings this filter already holds and nothing else: it never calls `verify`, `parseJwt`\n * or `verifyApiKey`, touches no network, and constructs nothing (#1358's cycle stays closed).\n * The request path resolves each piece through the SAME `require*` methods, so there is one\n * decision, made at boot. The one boot-only check is a verifier's `supportedVendorNames()`: at request time\n * the verifier's OWN dispatch stays the backstop for a name it does not know, and refuses (401).\n */\n assertRouteVerifiable(routeMeta: RouteMetadata): void {\n const auth = routeMeta.authMeta;\n if (!auth)\n throw new ApiImplementationError('Every endpoint requires @WpAuth or @WpAuthPublic.');\n for (const method of auth.methods) {\n switch (method.kind) {\n case 'jwt':\n this.requireJwtHook(routeMeta);\n break;\n case 'shared-secret':\n this.requireSharedSecrets(method.secretKey, routeMeta);\n break;\n case 'vendorAuth':\n this.assertVendorDeclared(\n this.requireVendorVerifier(method.name, routeMeta),\n method.name,\n routeMeta,\n );\n break;\n case 'apikey':\n this.requireApiKeyHook(method.regime, routeMeta);\n break;\n case 'oidc':\n case 'public':\n break;\n }\n }\n }\n\n // webpieces-disable no-function-outside-class -- static pure formatter of the route identity, kept beside its only callers\n private static endpointOf(routeMeta: RouteMetadata): string {\n return `${routeMeta.apiName ?? routeMeta.controllerClassName ?? 'UnknownApi'}.${routeMeta.methodName} (${routeMeta.httpMethod} ${routeMeta.path})`;\n }\n\n private unverifiable(\n routeMeta: RouteMetadata,\n mode: string,\n problem: string,\n bindLine: string,\n ): UnverifiableAuthModeError {\n const endpoint = AuthFilter.endpointOf(routeMeta);\n return new UnverifiableAuthModeError(\n `Endpoint ${endpoint} declares ${mode}, but ${problem}, so this server cannot verify it and ` +\n `refuses to start. Add:\\n ${bindLine}\\n` +\n `(Or remove ${mode} from the endpoint's @WpAuth if this server should not accept it.)`,\n endpoint,\n mode,\n );\n }\n\n private requireJwtHook(routeMeta: RouteMetadata): JwtHook<never> {\n if (this.jwtHook) return this.jwtHook;\n throw this.unverifiable(\n routeMeta,\n 'jwt()',\n 'no JwtHook is bound',\n 'binder.bind(JWT_HOOK).to(YourJwtHook).inSingletonScope();',\n );\n }\n\n private requireSharedSecrets(secretKey: string, routeMeta: RouteMetadata): SharedSecrets {\n const config = this.authConfig;\n const mode = `sharedSecret('${secretKey}')`;\n if (!config) {\n throw this.unverifiable(\n routeMeta,\n mode,\n 'no AuthConfig is bound',\n `binder.bind(AUTH_CONFIG).to(YourAuthConfig).inSingletonScope(); // YourAuthConfig extends AuthConfig and passes { '${secretKey}': new SharedSecrets(secret1, secret2) }`,\n );\n }\n const accepted = config.sharedSecrets[secretKey];\n if (accepted) return accepted;\n throw this.unverifiable(\n routeMeta,\n mode,\n `the bound AuthConfig (${config.constructor.name}) holds no SharedSecrets for '${secretKey}'`,\n `'${secretKey}': new SharedSecrets(secret1, secret2), // add to the sharedSecrets ${config.constructor.name} passes to AuthConfig, which AUTH_CONFIG is bound to`,\n );\n }\n\n private requireVendorVerifier(name: string, routeMeta: RouteMetadata): VendorAuthVerifier {\n const verifier = this.vendorAuthVerifier;\n const mode = `vendorAuth('${name}')`;\n if (!verifier) {\n throw this.unverifiable(\n routeMeta,\n mode,\n 'no VendorAuthVerifier is bound (a VENDOR_AUTH_MINTER binding only signs OUTBOUND calls)',\n 'binder.bind(VENDOR_AUTH_VERIFIER).to(YourVendorAuthVerifier).inSingletonScope();',\n );\n }\n return verifier;\n }\n\n /** Boot only: the bound verifier must DECLARE the route's vendor (#1362). Never calls verify. */\n private assertVendorDeclared(\n verifier: VendorAuthVerifier,\n name: string,\n routeMeta: RouteMetadata,\n ): void {\n const supported = verifier.supportedVendorNames();\n if (supported.includes(name)) return;\n const mode = `vendorAuth('${name}')`;\n const declared = supported.map((n: string) => `'${n}'`).join(', ');\n throw this.unverifiable(\n routeMeta,\n mode,\n `the bound VendorAuthVerifier (${verifier.constructor.name}) returns [${declared}] from supportedVendorNames(), which does not include '${name}'`,\n `'${name}' to ${verifier.constructor.name}.supportedVendorNames() (return [${declared}, '${name}']) AND handle '${name}' in its verify()`,\n );\n }\n\n private requireApiKeyHook(regime: string, routeMeta: RouteMetadata): ApiKeyHook {\n if (this.apiKeyHook) return this.apiKeyHook;\n throw this.unverifiable(\n routeMeta,\n `apiKey('${regime}', credentials)`,\n 'no ApiKeyHook is bound',\n 'binder.bind(API_KEY_HOOK).to(YourApiKeyHook).inSingletonScope();',\n );\n }\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\n override async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n const auth = meta.routeMeta.authMeta;\n if (!auth)\n throw new ApiImplementationError('Every endpoint requires @WpAuth or @WpAuthPublic.');\n if (meta.routeMeta.localOnly) this.enforceLocalOnly(meta);\n if (meta.invocationAuthentication) {\n meta.invocationAuthentication.assertTarget(meta.routeMeta);\n return nextFilter.invoke(meta);\n }\n const header = RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n if (auth.methods[0].kind === 'public') {\n await this.bestEffortJwt(header);\n this.callerContext.reconcileWireTrust(false);\n return nextFilter.invoke(meta);\n }\n const selected = await this.authenticateAlternatives(auth.methods, header, meta);\n this.applySurface(selected);\n this.callerContext.reconcileWireTrust(AuthFilter.verifiesCaller(selected));\n return nextFilter.invoke(meta);\n }\n\n /**\n * Try each declared credential in order. The endpoint's OIDC methods (one `oidcSameProject()` and\n * one `oidcSvcs([...])` may pair) are ONE bearer credential, so they are ONE alternative, tried at\n * the position of the first: the token is verified once against all of them.\n */\n private async authenticateAlternatives(\n methods: readonly AuthMode[],\n header: string | undefined,\n meta: MethodMeta,\n ): Promise<AuthMode> {\n const oidc = AuthFilter.oidcMethodsOf(methods);\n const alternatives = methods.filter(\n (method: AuthMode) => method.kind !== 'oidc' || method === oidc?.[0],\n );\n for (const method of alternatives) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- failed credentials may try another declared authenticator; invariants propagate\n try {\n await this.authenticateMethod(method, header, meta, oidc);\n return method;\n } catch (err: unknown) {\n // webpieces-disable no-any-unknown -- structured credential rejection at the boundary\n const error = toError(err);\n if (!(error instanceof ApiUnauthorizedError) || alternatives.length === 1)\n throw error;\n }\n }\n throw new ApiUnauthorizedError(\n 'No declared credential mechanism authenticated this request.',\n );\n }\n\n /** The endpoint's OIDC methods in declaration order, or undefined when it declares none. */\n // webpieces-disable no-function-outside-class -- static pure reader of the AuthMode list, kept beside its only caller\n private static oidcMethodsOf(methods: readonly AuthMode[]): OidcAuthMethods | undefined {\n const oidc = methods.filter(\n (method: AuthMode): method is OidcAuthMethod => method.kind === 'oidc',\n );\n const first = oidc[0];\n return first === undefined ? undefined : [first, ...oidc.slice(1)];\n }\n\n private async authenticateMethod(\n mode: AuthMode,\n header: string | undefined,\n meta: MethodMeta,\n oidc: OidcAuthMethods | undefined,\n ): Promise<void> {\n switch (mode.kind) {\n case 'jwt':\n await this.enforceJwt(header, meta.routeMeta);\n return;\n case 'oidc':\n await this.enforceOidc(header, oidc ?? [mode]);\n return;\n case 'shared-secret':\n this.enforceSharedSecret(\n this.credential(header, SHARED_SECRET_SCHEME),\n mode.secretKey,\n meta.routeMeta,\n );\n RequestContext.putTrusted(\n VERIFIED_MACHINE_CALLER,\n new VerifiedMachineCaller('shared-secret', mode.secretKey),\n );\n return;\n case 'vendorAuth':\n await this.enforceVendorAuth(mode.name, meta);\n return;\n case 'apikey':\n await this.enforceApiKey(mode.regime, meta);\n return;\n case 'public':\n throw new ApiImplementationError('Public is not a protected auth alternative.');\n }\n }\n\n /**\n * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `jwt()` is a browser\n * GUI, `apiKey(...)` is an external partner. See {@link Surface}.\n *\n * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one\n * INHERITS it unchanged and this returns immediately. Two different callers put a value there\n * before this runs, and neither may be overwritten:\n *\n * - the MCP bridge, which stamps `llm` before invoking a `@WpAuthorization` tool through `ApiFactory`\n * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the\n * model on the other end;\n * - an OIDC (`oidcSameProject()` / `oidcSvcs(...)`) service-to-service hop, whose caller's surface arrived on the wire. OIDC is an\n * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the\n * propagated value survives.\n *\n * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this\n * key: on a `jwt()` edge a caller who also SENDS `x-wp-surface` must match the value the\n * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.\n */\n private applySurface(mode: AuthMode): void {\n if (RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE) !== undefined) {\n return;\n }\n const surface = AuthFilter.surfaceFor(mode);\n if (surface !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, surface);\n }\n }\n\n /**\n * The auth mode -> {@link Surface} table, and the ONE place it is written down.\n *\n * `undefined` means \"this mode is not a surface\", which is a different statement from \"unknown\":\n * `oidc` and `shared-secret` are internal hops that RELAY somebody else's surface, `vendorAuth`'s\n * sender is a vendor with no webpieces surface at all, and `local-only` is a developer's own box.\n *\n * An exhaustive switch with NO `default`, like {@link verifiesCaller} and\n * `DestinationTrust.forAuthMode`: a new AuthMode kind must fail to compile here rather than\n * silently landing on `gui` (which would publish 266 to a partner) or on `public-api` (which would\n * publish a real 4xx to a GUI).\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors verifiesCaller)\n private static surfaceFor(mode: AuthMode): Surface | undefined {\n switch (mode.kind) {\n case 'jwt':\n return 'gui';\n case 'apikey':\n return 'public-api';\n case 'oidc':\n case 'shared-secret':\n case 'vendorAuth':\n case 'public':\n return undefined;\n }\n }\n\n /**\n * `vendorAuth(name)`: hand the app's {@link VendorAuthVerifier} the verbatim request and let it call the\n * VENDOR's own validator. A missing verifier, or one not declaring `name`, is not a case here: it\n * failed STARTUP ({@link assertRouteVerifiable}). Two ways to fail, both 401, both before the\n * controller is entered:\n *\n * 1. NO raw request — the transport kept no bytes. `assertEveryVendorAuthEndpointRetainsRawBody`\n * normally makes this a startup error, so reaching it means either a hand-registered route or\n * an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The\n * message says which fix applies rather than leaving a bare 401.\n * 2. The verifier threw — the credential did not verify (including a name its own dispatch does\n * not know: that stays the request-time backstop).\n *\n * Case 1 is checked HERE and nowhere else. {@link hasRawBytes} narrows the request to\n * {@link RawHttpRequest} at this one gate, so the verifier's signature promises `raw` is present and\n * no vendor implementation ever writes `raw!` or a guard of its own.\n */\n private async enforceVendorAuth(name: string, meta: MethodMeta): Promise<void> {\n const verifier = this.requireVendorVerifier(name, meta.routeMeta);\n const request = RequestContext.getRequest();\n if (!this.hasRawBytes(request)) {\n log.warn(\n `Refusing vendorAuth('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +\n `no raw bytes. Declare @Endpoint(POST, path, WRITE, EXTERNAL_INBOUND, { calledBy: '${name}', rawBody: true }); a ` +\n `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`,\n );\n throw new ApiUnauthorizedError(\n 'Vendor credential cannot be verified: no raw request was retained',\n );\n }\n // Throws ApiUnauthorizedError to deny. On success the vendor account the signature proved is\n // stamped through the SAME path a jwt or api-key caller takes.\n const caller = await verifier.verify(name, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * The ONE place the framework decides a request carries the verbatim bytes. A TYPE PREDICATE, so\n * the `true` branch hands {@link enforceVendorAuth} a {@link RawHttpRequest} with no cast and no\n * non-null assertion — the bad state stops being representable past this line rather than being\n * re-thrown about by every hook.\n */\n private hasRawBytes(request: HttpRequest | undefined): request is RawHttpRequest {\n return request?.raw !== undefined;\n }\n\n /**\n * `apiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound\n * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they\n * describe the contract for generators; the hook owns extraction. A missing hook is not a case here:\n * it failed STARTUP ({@link assertRouteVerifiable}). Two ways to fail, both 401, both before the\n * controller:\n *\n * 1. NO inbound request in scope — there are no headers to read, so there is nothing to verify. That\n * means a caller drove this route without publishing an HttpRequest; the message says so rather\n * than leaving a bare 401.\n * 2. The hook threw — the key, or the key/organization pair, did not check out.\n *\n * On success the hook's {@link AuthenticatedCaller} is stamped exactly as a jwt parse's is, which is\n * what puts the resolved organization into `RequestContext` for every downstream repository call.\n */\n private async enforceApiKey(regime: string, meta: MethodMeta): Promise<void> {\n const hook = this.requireApiKeyHook(regime, meta.routeMeta);\n const request = RequestContext.getRequest();\n if (!request) {\n log.warn(\n `Refusing apiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +\n `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +\n `publish an HttpRequest carrying the api-key headers.`,\n );\n throw new ApiUnauthorizedError(\n 'API key cannot be verified: no inbound request was published',\n );\n }\n // Throws ApiUnauthorizedError to deny. The hook gets the WHOLE request so it can cross-check\n // the key against a second header (the organization the customer is acting for).\n const caller = await hook.verifyApiKey(regime, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND\n * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client\n * omits trusted keys for a destination that cannot verify it, and the server rejects trusted keys\n * on a route that cannot verify the sender. One rule, two ends — if they disagreed, every call\n * would fail with a 401 that looks like a framework bug.\n *\n * - `oidc` / `shared-secret` → TRUE. An internal service is on the other end and the trusted\n * context it forwarded may be believed. This is what makes cross-service identity propagation\n * work.\n * - `jwt` / `public` → FALSE. A user JWT proves who the USER is; the SENDER is still whoever\n * holds the token, i.e. a browser.\n * - `local-only` → FALSE. It verifies WHERE WE ARE RUNNING, not who is calling — anything on\n * localhost reaches it, and it has no authenticator, so nothing can ever vouch for an inbound\n * trusted header. Any such header therefore rejects the request, which is exactly right.\n *\n * - `apikey` → FALSE. See the comment on that branch: the sender is a CUSTOMER.\n *\n * An exhaustive switch with NO `default`, returning on every branch: a new AuthMode kind is a\n * COMPILE error here (TS7030, no ending return) rather than silently landing on one posture. The\n * boolean expression this replaced defaulted every future mode to \"not verified\" — the safe\n * answer, but arrived at by accident rather than by decision.\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors DestinationTrust.forAuthMode)\n private static verifiesCaller(mode: AuthMode): boolean {\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return true;\n case 'jwt':\n case 'public':\n // `vendorAuth` DOES authenticate its sender — but the sender is an outside VENDOR, not a\n // peer in this repo, and a vendor neither speaks nor forwards webpieces context headers.\n // So there is no forwarded identity to believe, and admitting one would mean trusting a\n // key a vendor's payload could carry. Same answer as the OUTBOUND half\n // (DestinationTrust.forAuthMode), which is the invariant that keeps the two ends agreeing.\n case 'vendorAuth':\n // `apikey` authenticates the SENDER — but the sender is a CUSTOMER's codebase, not a peer\n // service in this repo, so its forwarded trusted context is exactly what must NOT be\n // believed: admitting it would let a partner assert another customer's org id on the wire.\n // The hook's OWN derived entries still land (applyAuthenticatedCaller), and reconcileWireTrust then\n // admits an inbound trusted header only when the hook independently derived the same value.\n case 'apikey':\n return false;\n }\n }\n\n /**\n * `@WpLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the\n * endpoint did not exist.\n *\n * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all\n * (`ApiRoutingFactory` skips it), so the ordinary way to reach this path already answers 404. A\n * 403 from here would be a DIFFERENT answer from the same framework for the same endpoint, and\n * the difference is itself the leak: 403 confirms \"this path exists in production, you merely\n * lack permission\", which is a map of the dev-only surface for anyone probing. A local-only\n * endpoint should not admit it exists. Both gates therefore return the same 404, and this one is\n * the backstop for routes registered by hand through `RouteBuilder` rather than by\n * `ApiRoutingFactory`.\n *\n * The log line names WHICH reason applies, because \"you are deployed\" and \"nobody declared a\n * locality\" have completely different fixes and both look like a bare 404 from outside.\n */\n private enforceLocalOnly(meta: MethodMeta): void {\n if (RuntimeLocality.isLocalDevelopment()) {\n return;\n }\n log.warn(\n `Refusing @WpLocalOnly endpoint ${meta.routeMeta.path}: ` +\n (RuntimeLocality.isDeclared()\n ? 'this process declared itself DEPLOYED.'\n : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +\n 'Pass the locality into RuntimeSetupOptions if this really is a developer machine.'),\n );\n // Same shape as an unregistered route — see the method doc for why this is not a 403.\n throw new ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(header: string | undefined, routeMeta: RouteMetadata): Promise<void> {\n const hook = this.requireJwtHook(routeMeta);\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) throw new ApiUnauthorizedError('Authentication required');\n this.callerContext.publish(await hook.parseJwt(token));\n }\n\n private async enforceOidc(header: string | undefined, methods: OidcAuthMethods): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) throw new ApiUnauthorizedError('Missing OIDC bearer token');\n const identity = this.oidcHook\n ? await this.oidcHook.verifyOidc(token, methods)\n : await this.oidcVerifier.verify(token, methods);\n if (!identity)\n throw new ApiImplementationError(\n 'OIDC verifier must return its verified machine identity.',\n );\n RequestContext.putTrusted(\n VERIFIED_MACHINE_CALLER,\n new VerifiedMachineCaller(identity.mechanism, identity.identity),\n );\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(\n provided: string | undefined,\n secretKey: string,\n routeMeta: RouteMetadata,\n ): void {\n const accepted = this.requireSharedSecrets(secretKey, routeMeta);\n if (!provided || !SharedSecretComparison.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError('Invalid shared secret for sharedSecret(...) endpoint');\n }\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private async bestEffortJwt(header: string | undefined): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.jwtHook || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.callerContext.publish(await this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n if (!(error instanceof ApiUnauthorizedError)) throw error;\n log.debug(\n 'Best-effort JWT parse on a public endpoint failed (treating as anonymous): ',\n error,\n );\n }\n }\n\n /**\n * The credential value IF the header carries the expected scheme, else undefined.\n *\n * Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent\n * where a JWT is expected), yields undefined and the caller 401s.\n */\n private credential(header: string | undefined, scheme: string): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = `${scheme} `;\n return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AuthFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/AuthFilter.ts"],"names":[],"mappings":";;;;;AAAA,yCAA6C;AAC7C,0DAOiC;AACjC,oDAa8B;AAC9B,oDAAuD;AAGvD,8CAAqG;AACrG,4CAAgG;AAChG,8EAA2E;AAC3E,gEAA6D;AAC7D,gEAAgE;AAChE,4EAAyE;AAEzE,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,oBAAoB,GAAG,eAAe,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,aAAa,GAAG,QAAQ,CAAC;AAC/B,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGI,IAAM,UAAU,kBAAhB,MAAM,UAAW,SAAQ,kBAAuC;IAMjB;IAII;IAGH;IAGC;IAM/B;IAIkC;IAzBtC,aAAa,GAAG,IAAI,uDAA0B,EAAE,CAAC;IAElE,YAGkD,YAAiC,EAI7B,UAAuB,EAG1B,OAAwB,EAGvB,QAAmB,EAMlD,kBAAuC,EAIL,UAAuB;QAE1E,KAAK,EAAE,CAAC;QAtBsC,iBAAY,GAAZ,YAAY,CAAqB;QAI7B,eAAU,GAAV,UAAU,CAAa;QAG1B,YAAO,GAAP,OAAO,CAAiB;QAGvB,aAAQ,GAAR,QAAQ,CAAW;QAMlD,uBAAkB,GAAlB,kBAAkB,CAAqB;QAIL,eAAU,GAAV,UAAU,CAAa;IAG9E,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,qBAAqB,CAAC,SAAwB;QAC1C,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC;QAChC,IAAI,CAAC,IAAI;YACL,MAAM,IAAI,kCAAsB,CAAC,mDAAmD,CAAC,CAAC;QAC1F,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAChC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;gBAClB,KAAK,KAAK;oBACN,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;oBAC/B,MAAM;gBACV,KAAK,eAAe;oBAChB,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;oBACvD,MAAM;gBACV,KAAK,YAAY;oBACb,IAAI,CAAC,oBAAoB,CACrB,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,EAClD,MAAM,CAAC,IAAI,EACX,SAAS,CACZ,CAAC;oBACF,MAAM;gBACV,KAAK,QAAQ;oBACT,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;oBACjD,MAAM;gBACV,KAAK,MAAM,CAAC;gBACZ,KAAK,QAAQ;oBACT,MAAM;YACd,CAAC;QACL,CAAC;IACL,CAAC;IAED,2HAA2H;IACnH,MAAM,CAAC,UAAU,CAAC,SAAwB;QAC9C,OAAO,GAAG,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,mBAAmB,IAAI,YAAY,IAAI,SAAS,CAAC,UAAU,KAAK,SAAS,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,GAAG,CAAC;IACvJ,CAAC;IAEO,YAAY,CAChB,SAAwB,EACxB,IAAY,EACZ,OAAe,EACf,QAAgB;QAEhB,MAAM,QAAQ,GAAG,YAAU,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAClD,OAAO,IAAI,qDAAyB,CAChC,YAAY,QAAQ,aAAa,IAAI,SAAS,OAAO,wCAAwC;YACzF,+BAA+B,QAAQ,IAAI;YAC3C,cAAc,IAAI,oEAAoE,EAC1F,QAAQ,EACR,IAAI,CACP,CAAC;IACN,CAAC;IAEO,cAAc,CAAC,SAAwB;QAC3C,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACtC,MAAM,IAAI,CAAC,YAAY,CACnB,SAAS,EACT,OAAO,EACP,qBAAqB,EACrB,2DAA2D,CAC9D,CAAC;IACN,CAAC;IAEO,oBAAoB,CAAC,SAAiB,EAAE,SAAwB;QACpE,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;QAC/B,MAAM,IAAI,GAAG,iBAAiB,SAAS,IAAI,CAAC;QAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,CAAC,YAAY,CACnB,SAAS,EACT,IAAI,EACJ,wBAAwB,EACxB,wHAAwH,SAAS,0CAA0C,CAC9K,CAAC;QACN,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,IAAI,CAAC,YAAY,CACnB,SAAS,EACT,IAAI,EACJ,yBAAyB,MAAM,CAAC,WAAW,CAAC,IAAI,iCAAiC,SAAS,GAAG,EAC7F,IAAI,SAAS,yEAAyE,MAAM,CAAC,WAAW,CAAC,IAAI,sDAAsD,CACtK,CAAC;IACN,CAAC;IAEO,qBAAqB,CAAC,IAAY,EAAE,SAAwB;QAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,kBAAkB,CAAC;QACzC,MAAM,IAAI,GAAG,eAAe,IAAI,IAAI,CAAC;QACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,MAAM,IAAI,CAAC,YAAY,CACnB,SAAS,EACT,IAAI,EACJ,yFAAyF,EACzF,kFAAkF,CACrF,CAAC;QACN,CAAC;QACD,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED,iGAAiG;IACzF,oBAAoB,CACxB,QAA4B,EAC5B,IAAY,EACZ,SAAwB;QAExB,MAAM,SAAS,GAAG,QAAQ,CAAC,oBAAoB,EAAE,CAAC;QAClD,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO;QACrC,MAAM,IAAI,GAAG,eAAe,IAAI,IAAI,CAAC;QACrC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnE,MAAM,IAAI,CAAC,YAAY,CACnB,SAAS,EACT,IAAI,EACJ,iCAAiC,QAAQ,CAAC,WAAW,CAAC,IAAI,cAAc,QAAQ,0DAA0D,IAAI,GAAG,EACjJ,IAAI,IAAI,QAAQ,QAAQ,CAAC,WAAW,CAAC,IAAI,oCAAoC,QAAQ,MAAM,IAAI,mBAAmB,IAAI,mBAAmB,CAC5I,CAAC;IACN,CAAC;IAEO,iBAAiB,CAAC,MAAc,EAAE,SAAwB;QAC9D,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC,UAAU,CAAC;QAC5C,MAAM,IAAI,CAAC,YAAY,CACnB,SAAS,EACT,WAAW,MAAM,iBAAiB,EAClC,wBAAwB,EACxB,kEAAkE,CACrE,CAAC;IACN,CAAC;IAED,iGAAiG;IACxF,KAAK,CAAC,MAAM,CACjB,IAAgB,EAChB,UAAoD;QAEpD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;QACrC,IAAI,CAAC,IAAI;YACL,MAAM,IAAI,kCAAsB,CAAC,mDAAmD,CAAC,CAAC;QAC1F,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS;YAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAChC,IAAI,CAAC,wBAAwB,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3D,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,MAAM,MAAM,GAAG,6BAAc,CAAC,UAAU,EAAE,EAAE,SAAS,CAAC,oBAAoB,CAAC,CAAC;QAC5E,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACpC,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YACjC,IAAI,CAAC,aAAa,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;YAC7C,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;QACjF,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC5B,IAAI,CAAC,aAAa,CAAC,kBAAkB,CAAC,YAAU,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC3E,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,wBAAwB,CAClC,OAA4B,EAC5B,MAA0B,EAC1B,IAAgB;QAEhB,MAAM,IAAI,GAAG,YAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAC/B,CAAC,MAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,CACvE,CAAC;QACF,KAAK,MAAM,MAAM,IAAI,YAAY,EAAE,CAAC;YAChC,iJAAiJ;YACjJ,IAAI,CAAC;gBACD,MAAM,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC1D,OAAO,MAAM,CAAC;YAClB,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,sFAAsF;gBACtF,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;gBAC3B,IAAI,CAAC,CAAC,KAAK,YAAY,gCAAoB,CAAC,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;oBACrE,MAAM,KAAK,CAAC;YACpB,CAAC;QACL,CAAC;QACD,MAAM,IAAI,gCAAoB,CAC1B,8DAA8D,CACjE,CAAC;IACN,CAAC;IAED,4FAA4F;IAC5F,sHAAsH;IAC9G,MAAM,CAAC,aAAa,CAAC,OAA4B;QACrD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CACvB,CAAC,MAAgB,EAA4B,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CACzE,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAC5B,IAAc,EACd,MAA0B,EAC1B,IAAgB,EAChB,IAAiC;QAEjC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,KAAK;gBACN,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;gBAC9C,OAAO;YACX,KAAK,MAAM;gBACP,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC/C,OAAO;YACX,KAAK,eAAe;gBAChB,IAAI,CAAC,mBAAmB,CACpB,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,oBAAoB,CAAC,EAC7C,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,SAAS,CACjB,CAAC;gBACF,uFAAuF;gBACvF,MAAM,IAAI,CAAC,aAAa,CAAC,sBAAsB,CAC3C,IAAI,yCAA4B,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,EACjE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAC5B,CAAC;gBACF,OAAO;YACX,KAAK,YAAY;gBACb,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC9C,OAAO;YACX,KAAK,QAAQ;gBACT,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBAC5C,OAAO;YACX,KAAK,QAAQ;gBACT,MAAM,IAAI,kCAAsB,CAAC,6CAA6C,CAAC,CAAC;QACxF,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACK,YAAY,CAAC,IAAc;QAC/B,IAAI,6BAAc,CAAC,UAAU,CAAC,gCAAoB,CAAC,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;YACxE,OAAO;QACX,CAAC;QACD,MAAM,OAAO,GAAG,YAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,6BAAc,CAAC,UAAU,CAAC,gCAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACrE,CAAC;IACL,CAAC;IAED;;;;;;;;;;;OAWG;IACH,mJAAmJ;IAC3I,MAAM,CAAC,UAAU,CAAC,IAAc;QACpC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,KAAK;gBACN,OAAO,KAAK,CAAC;YACjB,KAAK,QAAQ;gBACT,OAAO,YAAY,CAAC;YACxB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe,CAAC;YACrB,KAAK,YAAY,CAAC;YAClB,KAAK,QAAQ;gBACT,OAAO,SAAS,CAAC;QACzB,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACK,KAAK,CAAC,iBAAiB,CAAC,IAAY,EAAE,IAAgB;QAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAClE,MAAM,OAAO,GAAG,6BAAc,CAAC,UAAU,EAAE,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CACJ,wBAAwB,IAAI,eAAe,IAAI,CAAC,SAAS,CAAC,IAAI,gCAAgC;gBAC1F,qFAAqF,IAAI,yBAAyB;gBAClH,yFAAyF,CAChG,CAAC;YACF,MAAM,IAAI,gCAAoB,CAC1B,mEAAmE,CACtE,CAAC;QACN,CAAC;QACD,6FAA6F;QAC7F,+DAA+D;QAC/D,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACK,WAAW,CAAC,OAAgC;QAChD,OAAO,OAAO,EAAE,GAAG,KAAK,SAAS,CAAC;IACtC,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACK,KAAK,CAAC,aAAa,CAAC,MAAc,EAAE,IAAgB;QACxD,MAAM,IAAI,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5D,MAAM,OAAO,GAAG,6BAAc,CAAC,UAAU,EAAE,CAAC;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,GAAG,CAAC,IAAI,CACJ,oBAAoB,MAAM,eAAe,IAAI,CAAC,SAAS,CAAC,IAAI,8BAA8B;gBACtF,0FAA0F;gBAC1F,sDAAsD,CAC7D,CAAC;YACF,MAAM,IAAI,gCAAoB,CAC1B,8DAA8D,CACjE,CAAC;QACN,CAAC;QACD,6FAA6F;QAC7F,iFAAiF;QACjF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,iKAAiK;IACzJ,MAAM,CAAC,cAAc,CAAC,IAAc;QACxC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,IAAI,CAAC;YAChB,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,yFAAyF;YACzF,yFAAyF;YACzF,wFAAwF;YACxF,uEAAuE;YACvE,2FAA2F;YAC3F,KAAK,YAAY,CAAC;YAClB,0FAA0F;YAC1F,qFAAqF;YACrF,2FAA2F;YAC3F,oGAAoG;YACpG,4FAA4F;YAC5F,KAAK,QAAQ;gBACT,OAAO,KAAK,CAAC;QACrB,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,gBAAgB,CAAC,IAAgB;QACrC,IAAI,2BAAe,CAAC,kBAAkB,EAAE,EAAE,CAAC;YACvC,OAAO;QACX,CAAC;QACD,GAAG,CAAC,IAAI,CACJ,kCAAkC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI;YACrD,CAAC,2BAAe,CAAC,UAAU,EAAE;gBACzB,CAAC,CAAC,wCAAwC;gBAC1C,CAAC,CAAC,iFAAiF;oBACjF,mFAAmF,CAAC,CACjG,CAAC;QACF,sFAAsF;QACtF,MAAM,IAAI,oCAAwB,CAAC,kBAAkB,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAChF,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,MAA0B,EAAE,SAAwB;QACzE,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACrD,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,gCAAoB,CAAC,yBAAyB,CAAC,CAAC;QACtE,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,MAA0B,EAAE,OAAwB;QAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACrD,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,gCAAoB,CAAC,2BAA2B,CAAC,CAAC;QACxE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;YAC1B,CAAC,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC;YAChD,CAAC,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC,QAAQ;YACT,MAAM,IAAI,kCAAsB,CAC5B,0DAA0D,CAC7D,CAAC;QACN,MAAM,IAAI,CAAC,aAAa,CAAC,sBAAsB,CAC3C,QAAQ,EACR,CAAC,QAAsC,EAAE,EAAE,CACvC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAC5F,CAAC;IACN,CAAC;IAED,8FAA8F;IACtF,mBAAmB,CACvB,QAA4B,EAC5B,SAAiB,EACjB,SAAwB;QAExB,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACjE,IAAI,CAAC,QAAQ,IAAI,CAAC,4CAAsB,CAAC,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YACzE,MAAM,IAAI,gCAAoB,CAAC,sDAAsD,CAAC,CAAC;QAC3F,CAAC;IACL,CAAC;IAED,4FAA4F;IACpF,KAAK,CAAC,aAAa,CAAC,MAA0B;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACrD,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1B,OAAO;QACX,CAAC;QACD,yKAAyK;QACzK,IAAI,CAAC;YACD,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QACnE,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,CAAC,KAAK,YAAY,gCAAoB,CAAC;gBAAE,MAAM,KAAK,CAAC;YAC1D,GAAG,CAAC,KAAK,CACL,6EAA6E,EAC7E,KAAK,CACR,CAAC;QACN,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACK,UAAU,CAAC,MAA0B,EAAE,MAAc;QACzD,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC;QAC5B,OAAO,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnF,CAAC;CACJ,CAAA;AAxiBY,gCAAU;qBAAV,UAAU;IAFtB,IAAA,wCAAyB,GAAE;IAC5B,iGAAiG;;IAOxF,mBAAA,IAAA,kBAAM,EAAC,yCAAmB,CAAC,CAAA;IAI3B,mBAAA,IAAA,oBAAQ,GAAE,CAAA;IAAE,mBAAA,IAAA,kBAAM,EAAC,wBAAW,CAAC,CAAA;IAG/B,mBAAA,IAAA,oBAAQ,GAAE,CAAA;IAAE,mBAAA,IAAA,kBAAM,EAAC,oBAAQ,CAAC,CAAA;IAG5B,mBAAA,IAAA,oBAAQ,GAAE,CAAA;IAAE,mBAAA,IAAA,kBAAM,EAAC,qBAAS,CAAC,CAAA;IAI7B,mBAAA,IAAA,oBAAQ,GAAE,CAAA;IACV,mBAAA,IAAA,kBAAM,EAAC,mCAAoB,CAAC,CAAA;IAK5B,mBAAA,IAAA,oBAAQ,GAAE,CAAA;IAAE,mBAAA,IAAA,kBAAM,EAAC,wBAAY,CAAC,CAAA;6CApB2B,yCAAmB;QAIhB,uBAAU;QAGhB,mBAAO;QAGL,oBAAQ;QAM7B,iCAAkB;QAIQ,sBAAU;GA1BrE,UAAU,CAwiBtB","sourcesContent":["import { inject, optional } from 'inversify';\nimport {\n provideFrameworkSingleton,\n HttpRequest,\n RawHttpRequest,\n RequestContext,\n VendorAuthVerifier,\n VENDOR_AUTH_VERIFIER,\n} from '@webpieces/core-context';\nimport {\n AuthMode,\n OidcAuthMethod,\n OidcAuthMethods,\n ApiEndpointNotFoundError,\n ApiUnauthorizedError,\n ApiImplementationError,\n LogManager,\n RouteMetadata,\n RuntimeLocality,\n Surface,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, AuthenticatedMachineIdentity, SharedSecrets } from '../AuthConfig';\nimport { ApiKeyHook, API_KEY_HOOK, JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK } from '../AuthHooks';\nimport { AuthenticatedCallerContext } from '../AuthenticatedCallerContext';\nimport { DefaultOidcVerifier } from '../DefaultOidcVerifier';\nimport { SharedSecretComparison } from '../CredentialVerifiers';\nimport { UnverifiableAuthModeError } from '../UnverifiableAuthModeError';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/**\n * The ONE credential header, read straight off the inbound HttpRequest.\n *\n * Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would\n * put the caller's credential into RequestContext and hence onto every outbound call this service\n * makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.\n */\nconst AUTHORIZATION_HEADER = 'authorization';\n\n/**\n * The scheme (first word of the Authorization value) names WHICH credential follows, so a secret\n * can never be mistaken for a token, nor accepted where the other was expected:\n *\n * Authorization: Bearer <user JWT | service OIDC token>\n * Authorization: Webpieces <sharedSecret(...) value>\n *\n * The scheme is REQUIRED. A bare value with no scheme is rejected.\n */\nconst BEARER_SCHEME = 'Bearer';\nconst SHARED_SECRET_SCHEME = 'Webpieces';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC\n * default. An app binds only the pieces its SERVED routes declare — and a served route that declares\n * a mode whose piece is absent FAILS STARTUP ({@link assertRouteVerifiable}, #1362), so none of the\n * rows below is ever \"not enabled\" at request time:\n * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). Startup\n * requires an AuthConfig holding the route's key.\n * - jwt → the bound {@link JwtHook} (`parseJwt`, awaited — an app's\n * strategy may reach a JWKS or a datastore). Startup requires a JwtHook: JWT needs\n * an app secret + payload shape.\n * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}\n * run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC against\n * the endpoint's caller scope (`oidcSameProject()` or `oidcSvcs([...])`).\n * - vendorAuth → the bound {@link VendorAuthVerifier} `verify`s the VENDOR's credential over the\n * retained raw request. Startup requires a verifier whose `supportedVendorNames()` lists the\n * route's name: an unverified vendor call is never waved through because wiring was\n * forgotten. The INBOUND role only — this filter never depends on the outbound\n * VendorAuthMinter (#1358).\n * - apikey → the bound {@link ApiKeyHook} looks the CUSTOMER's key up (async, over the whole\n * header set) and returns the context to seed. Startup requires an ApiKeyHook.\n * - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so\n * a logged-out page still knows who is logged in; never fails.\n * - local-only → serve only when {@link RuntimeLocality} says this process is a developer's\n * machine; otherwise 404, indistinguishable from the route not existing (which,\n * off-local, it does not — `ApiRoutingFactory` never registered it).\n *\n * Zero wiring = OIDC just works; an app only binds the hooks it actually uses.\n */\n@provideFrameworkSingleton()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private readonly callerContext = new AuthenticatedCallerContext();\n\n constructor(\n // Framework default, always available — verifies Google OIDC with zero app wiring.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- AuthFilter is DI-resolved via the esbuild/vitest path, which elides type-only imports (no design:paramtypes), so every param needs its explicit token\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // @optional: only bind an AuthConfig to enable sharedSecret(...) endpoints (startup refuses a\n // served one without it — see assertRouteVerifiable).\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // @optional: only bind a JwtHook to enable jwt() endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // @optional: only bind an OidcHook to OVERRIDE the DefaultOidcVerifier caller policy.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // @optional: only bind a VendorAuthVerifier to enable vendorAuth(...) endpoints. Unbound = a\n // server serving one refuses to boot (assertRouteVerifiable), never an allow-all.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional()\n @inject(VENDOR_AUTH_VERIFIER)\n private readonly vendorAuthVerifier?: VendorAuthVerifier,\n // @optional: only bind an ApiKeyHook to enable apiKey(...) endpoints. Unbound = a server serving\n // one refuses to boot, for the same reason as the vendor verifier above.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {\n super();\n }\n\n /**\n * STARTUP check (#1362), called by route registration for EVERY served route: each auth mode the\n * route declares must be verifiable by what is bound, or boot fails naming the endpoint, the mode\n * and the exact binding line. STRICT — a multi-method route (`[oidcSameProject(), vendorAuth('x')]`)\n * with no verifier fails too: a declared alternative nobody can verify is dead, and is almost\n * always a wiring mistake. OIDC needs no app binding (the framework verifier) and a public route\n * none.\n *\n * Reads bindings this filter already holds and nothing else: it never calls `verify`, `parseJwt`\n * or `verifyApiKey`, touches no network, and constructs nothing (#1358's cycle stays closed).\n * The request path resolves each piece through the SAME `require*` methods, so there is one\n * decision, made at boot. The one boot-only check is a verifier's `supportedVendorNames()`: at request time\n * the verifier's OWN dispatch stays the backstop for a name it does not know, and refuses (401).\n */\n assertRouteVerifiable(routeMeta: RouteMetadata): void {\n const auth = routeMeta.authMeta;\n if (!auth)\n throw new ApiImplementationError('Every endpoint requires @WpAuth or @WpAuthPublic.');\n for (const method of auth.methods) {\n switch (method.kind) {\n case 'jwt':\n this.requireJwtHook(routeMeta);\n break;\n case 'shared-secret':\n this.requireSharedSecrets(method.secretKey, routeMeta);\n break;\n case 'vendorAuth':\n this.assertVendorDeclared(\n this.requireVendorVerifier(method.name, routeMeta),\n method.name,\n routeMeta,\n );\n break;\n case 'apikey':\n this.requireApiKeyHook(method.regime, routeMeta);\n break;\n case 'oidc':\n case 'public':\n break;\n }\n }\n }\n\n // webpieces-disable no-function-outside-class -- static pure formatter of the route identity, kept beside its only callers\n private static endpointOf(routeMeta: RouteMetadata): string {\n return `${routeMeta.apiName ?? routeMeta.controllerClassName ?? 'UnknownApi'}.${routeMeta.methodName} (${routeMeta.httpMethod} ${routeMeta.path})`;\n }\n\n private unverifiable(\n routeMeta: RouteMetadata,\n mode: string,\n problem: string,\n bindLine: string,\n ): UnverifiableAuthModeError {\n const endpoint = AuthFilter.endpointOf(routeMeta);\n return new UnverifiableAuthModeError(\n `Endpoint ${endpoint} declares ${mode}, but ${problem}, so this server cannot verify it and ` +\n `refuses to start. Add:\\n ${bindLine}\\n` +\n `(Or remove ${mode} from the endpoint's @WpAuth if this server should not accept it.)`,\n endpoint,\n mode,\n );\n }\n\n private requireJwtHook(routeMeta: RouteMetadata): JwtHook<never> {\n if (this.jwtHook) return this.jwtHook;\n throw this.unverifiable(\n routeMeta,\n 'jwt()',\n 'no JwtHook is bound',\n 'binder.bind(JWT_HOOK).to(YourJwtHook).inSingletonScope();',\n );\n }\n\n private requireSharedSecrets(secretKey: string, routeMeta: RouteMetadata): SharedSecrets {\n const config = this.authConfig;\n const mode = `sharedSecret('${secretKey}')`;\n if (!config) {\n throw this.unverifiable(\n routeMeta,\n mode,\n 'no AuthConfig is bound',\n `binder.bind(AUTH_CONFIG).to(YourAuthConfig).inSingletonScope(); // YourAuthConfig extends AuthConfig and passes { '${secretKey}': new SharedSecrets(secret1, secret2) }`,\n );\n }\n const accepted = config.sharedSecrets[secretKey];\n if (accepted) return accepted;\n throw this.unverifiable(\n routeMeta,\n mode,\n `the bound AuthConfig (${config.constructor.name}) holds no SharedSecrets for '${secretKey}'`,\n `'${secretKey}': new SharedSecrets(secret1, secret2), // add to the sharedSecrets ${config.constructor.name} passes to AuthConfig, which AUTH_CONFIG is bound to`,\n );\n }\n\n private requireVendorVerifier(name: string, routeMeta: RouteMetadata): VendorAuthVerifier {\n const verifier = this.vendorAuthVerifier;\n const mode = `vendorAuth('${name}')`;\n if (!verifier) {\n throw this.unverifiable(\n routeMeta,\n mode,\n 'no VendorAuthVerifier is bound (a VENDOR_AUTH_MINTER binding only signs OUTBOUND calls)',\n 'binder.bind(VENDOR_AUTH_VERIFIER).to(YourVendorAuthVerifier).inSingletonScope();',\n );\n }\n return verifier;\n }\n\n /** Boot only: the bound verifier must DECLARE the route's vendor (#1362). Never calls verify. */\n private assertVendorDeclared(\n verifier: VendorAuthVerifier,\n name: string,\n routeMeta: RouteMetadata,\n ): void {\n const supported = verifier.supportedVendorNames();\n if (supported.includes(name)) return;\n const mode = `vendorAuth('${name}')`;\n const declared = supported.map((n: string) => `'${n}'`).join(', ');\n throw this.unverifiable(\n routeMeta,\n mode,\n `the bound VendorAuthVerifier (${verifier.constructor.name}) returns [${declared}] from supportedVendorNames(), which does not include '${name}'`,\n `'${name}' to ${verifier.constructor.name}.supportedVendorNames() (return [${declared}, '${name}']) AND handle '${name}' in its verify()`,\n );\n }\n\n private requireApiKeyHook(regime: string, routeMeta: RouteMetadata): ApiKeyHook {\n if (this.apiKeyHook) return this.apiKeyHook;\n throw this.unverifiable(\n routeMeta,\n `apiKey('${regime}', credentials)`,\n 'no ApiKeyHook is bound',\n 'binder.bind(API_KEY_HOOK).to(YourApiKeyHook).inSingletonScope();',\n );\n }\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\n override async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n const auth = meta.routeMeta.authMeta;\n if (!auth)\n throw new ApiImplementationError('Every endpoint requires @WpAuth or @WpAuthPublic.');\n if (meta.routeMeta.localOnly) this.enforceLocalOnly(meta);\n if (meta.invocationAuthentication) {\n meta.invocationAuthentication.assertTarget(meta.routeMeta);\n return nextFilter.invoke(meta);\n }\n const header = RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n if (auth.methods[0].kind === 'public') {\n await this.bestEffortJwt(header);\n this.callerContext.reconcileWireTrust(false);\n return nextFilter.invoke(meta);\n }\n const selected = await this.authenticateAlternatives(auth.methods, header, meta);\n this.applySurface(selected);\n this.callerContext.reconcileWireTrust(AuthFilter.verifiesCaller(selected));\n return nextFilter.invoke(meta);\n }\n\n /**\n * Try each declared credential in order. The endpoint's OIDC methods (one `oidcSameProject()` and\n * one `oidcSvcs([...])` may pair) are ONE bearer credential, so they are ONE alternative, tried at\n * the position of the first: the token is verified once against all of them.\n */\n private async authenticateAlternatives(\n methods: readonly AuthMode[],\n header: string | undefined,\n meta: MethodMeta,\n ): Promise<AuthMode> {\n const oidc = AuthFilter.oidcMethodsOf(methods);\n const alternatives = methods.filter(\n (method: AuthMode) => method.kind !== 'oidc' || method === oidc?.[0],\n );\n for (const method of alternatives) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- failed credentials may try another declared authenticator; invariants propagate\n try {\n await this.authenticateMethod(method, header, meta, oidc);\n return method;\n } catch (err: unknown) {\n // webpieces-disable no-any-unknown -- structured credential rejection at the boundary\n const error = toError(err);\n if (!(error instanceof ApiUnauthorizedError) || alternatives.length === 1)\n throw error;\n }\n }\n throw new ApiUnauthorizedError(\n 'No declared credential mechanism authenticated this request.',\n );\n }\n\n /** The endpoint's OIDC methods in declaration order, or undefined when it declares none. */\n // webpieces-disable no-function-outside-class -- static pure reader of the AuthMode list, kept beside its only caller\n private static oidcMethodsOf(methods: readonly AuthMode[]): OidcAuthMethods | undefined {\n const oidc = methods.filter(\n (method: AuthMode): method is OidcAuthMethod => method.kind === 'oidc',\n );\n const first = oidc[0];\n return first === undefined ? undefined : [first, ...oidc.slice(1)];\n }\n\n private async authenticateMethod(\n mode: AuthMode,\n header: string | undefined,\n meta: MethodMeta,\n oidc: OidcAuthMethods | undefined,\n ): Promise<void> {\n switch (mode.kind) {\n case 'jwt':\n await this.enforceJwt(header, meta.routeMeta);\n return;\n case 'oidc':\n await this.enforceOidc(header, oidc ?? [mode]);\n return;\n case 'shared-secret':\n this.enforceSharedSecret(\n this.credential(header, SHARED_SECRET_SCHEME),\n mode.secretKey,\n meta.routeMeta,\n );\n // A shared secret grants no role: an originating holder is a role-less service caller.\n await this.callerContext.publishVerifiedService(\n new AuthenticatedMachineIdentity('shared-secret', mode.secretKey),\n () => Promise.resolve([]),\n );\n return;\n case 'vendorAuth':\n await this.enforceVendorAuth(mode.name, meta);\n return;\n case 'apikey':\n await this.enforceApiKey(mode.regime, meta);\n return;\n case 'public':\n throw new ApiImplementationError('Public is not a protected auth alternative.');\n }\n }\n\n /**\n * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `jwt()` is a browser\n * GUI, `apiKey(...)` is an external partner. See {@link Surface}.\n *\n * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one\n * INHERITS it unchanged and this returns immediately. Two different callers put a value there\n * before this runs, and neither may be overwritten:\n *\n * - the MCP bridge, which stamps `llm` before invoking a `@WpAuthorization` tool through `ApiFactory`\n * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the\n * model on the other end;\n * - an OIDC (`oidcSameProject()` / `oidcSvcs(...)`) service-to-service hop, whose caller's surface arrived on the wire. OIDC is an\n * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the\n * propagated value survives.\n *\n * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this\n * key: on a `jwt()` edge a caller who also SENDS `x-wp-surface` must match the value the\n * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.\n */\n private applySurface(mode: AuthMode): void {\n if (RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE) !== undefined) {\n return;\n }\n const surface = AuthFilter.surfaceFor(mode);\n if (surface !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, surface);\n }\n }\n\n /**\n * The auth mode -> {@link Surface} table, and the ONE place it is written down.\n *\n * `undefined` means \"this mode is not a surface\", which is a different statement from \"unknown\":\n * `oidc` and `shared-secret` are internal hops that RELAY somebody else's surface, `vendorAuth`'s\n * sender is a vendor with no webpieces surface at all, and `local-only` is a developer's own box.\n *\n * An exhaustive switch with NO `default`, like {@link verifiesCaller} and\n * `DestinationTrust.forAuthMode`: a new AuthMode kind must fail to compile here rather than\n * silently landing on `gui` (which would publish 266 to a partner) or on `public-api` (which would\n * publish a real 4xx to a GUI).\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors verifiesCaller)\n private static surfaceFor(mode: AuthMode): Surface | undefined {\n switch (mode.kind) {\n case 'jwt':\n return 'gui';\n case 'apikey':\n return 'public-api';\n case 'oidc':\n case 'shared-secret':\n case 'vendorAuth':\n case 'public':\n return undefined;\n }\n }\n\n /**\n * `vendorAuth(name)`: hand the app's {@link VendorAuthVerifier} the verbatim request and let it call the\n * VENDOR's own validator. A missing verifier, or one not declaring `name`, is not a case here: it\n * failed STARTUP ({@link assertRouteVerifiable}). Two ways to fail, both 401, both before the\n * controller is entered:\n *\n * 1. NO raw request — the transport kept no bytes. `assertEveryVendorAuthEndpointRetainsRawBody`\n * normally makes this a startup error, so reaching it means either a hand-registered route or\n * an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The\n * message says which fix applies rather than leaving a bare 401.\n * 2. The verifier threw — the credential did not verify (including a name its own dispatch does\n * not know: that stays the request-time backstop).\n *\n * Case 1 is checked HERE and nowhere else. {@link hasRawBytes} narrows the request to\n * {@link RawHttpRequest} at this one gate, so the verifier's signature promises `raw` is present and\n * no vendor implementation ever writes `raw!` or a guard of its own.\n */\n private async enforceVendorAuth(name: string, meta: MethodMeta): Promise<void> {\n const verifier = this.requireVendorVerifier(name, meta.routeMeta);\n const request = RequestContext.getRequest();\n if (!this.hasRawBytes(request)) {\n log.warn(\n `Refusing vendorAuth('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +\n `no raw bytes. Declare @Endpoint(POST, path, WRITE, EXTERNAL_INBOUND, { calledBy: '${name}', rawBody: true }); a ` +\n `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`,\n );\n throw new ApiUnauthorizedError(\n 'Vendor credential cannot be verified: no raw request was retained',\n );\n }\n // Throws ApiUnauthorizedError to deny. On success the vendor account the signature proved is\n // stamped through the SAME path a jwt or api-key caller takes.\n const caller = await verifier.verify(name, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * The ONE place the framework decides a request carries the verbatim bytes. A TYPE PREDICATE, so\n * the `true` branch hands {@link enforceVendorAuth} a {@link RawHttpRequest} with no cast and no\n * non-null assertion — the bad state stops being representable past this line rather than being\n * re-thrown about by every hook.\n */\n private hasRawBytes(request: HttpRequest | undefined): request is RawHttpRequest {\n return request?.raw !== undefined;\n }\n\n /**\n * `apiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound\n * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they\n * describe the contract for generators; the hook owns extraction. A missing hook is not a case here:\n * it failed STARTUP ({@link assertRouteVerifiable}). Two ways to fail, both 401, both before the\n * controller:\n *\n * 1. NO inbound request in scope — there are no headers to read, so there is nothing to verify. That\n * means a caller drove this route without publishing an HttpRequest; the message says so rather\n * than leaving a bare 401.\n * 2. The hook threw — the key, or the key/organization pair, did not check out.\n *\n * On success the hook's {@link AuthenticatedCaller} is stamped exactly as a jwt parse's is, which is\n * what puts the resolved organization into `RequestContext` for every downstream repository call.\n */\n private async enforceApiKey(regime: string, meta: MethodMeta): Promise<void> {\n const hook = this.requireApiKeyHook(regime, meta.routeMeta);\n const request = RequestContext.getRequest();\n if (!request) {\n log.warn(\n `Refusing apiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +\n `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +\n `publish an HttpRequest carrying the api-key headers.`,\n );\n throw new ApiUnauthorizedError(\n 'API key cannot be verified: no inbound request was published',\n );\n }\n // Throws ApiUnauthorizedError to deny. The hook gets the WHOLE request so it can cross-check\n // the key against a second header (the organization the customer is acting for).\n const caller = await hook.verifyApiKey(regime, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND\n * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client\n * omits trusted keys for a destination that cannot verify it, and the server rejects trusted keys\n * on a route that cannot verify the sender. One rule, two ends — if they disagreed, every call\n * would fail with a 401 that looks like a framework bug.\n *\n * - `oidc` / `shared-secret` → TRUE. An internal service is on the other end and the trusted\n * context it forwarded may be believed. This is what makes cross-service identity propagation\n * work.\n * - `jwt` / `public` → FALSE. A user JWT proves who the USER is; the SENDER is still whoever\n * holds the token, i.e. a browser.\n * - `local-only` → FALSE. It verifies WHERE WE ARE RUNNING, not who is calling — anything on\n * localhost reaches it, and it has no authenticator, so nothing can ever vouch for an inbound\n * trusted header. Any such header therefore rejects the request, which is exactly right.\n *\n * - `apikey` → FALSE. See the comment on that branch: the sender is a CUSTOMER.\n *\n * An exhaustive switch with NO `default`, returning on every branch: a new AuthMode kind is a\n * COMPILE error here (TS7030, no ending return) rather than silently landing on one posture. The\n * boolean expression this replaced defaulted every future mode to \"not verified\" — the safe\n * answer, but arrived at by accident rather than by decision.\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors DestinationTrust.forAuthMode)\n private static verifiesCaller(mode: AuthMode): boolean {\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return true;\n case 'jwt':\n case 'public':\n // `vendorAuth` DOES authenticate its sender — but the sender is an outside VENDOR, not a\n // peer in this repo, and a vendor neither speaks nor forwards webpieces context headers.\n // So there is no forwarded identity to believe, and admitting one would mean trusting a\n // key a vendor's payload could carry. Same answer as the OUTBOUND half\n // (DestinationTrust.forAuthMode), which is the invariant that keeps the two ends agreeing.\n case 'vendorAuth':\n // `apikey` authenticates the SENDER — but the sender is a CUSTOMER's codebase, not a peer\n // service in this repo, so its forwarded trusted context is exactly what must NOT be\n // believed: admitting it would let a partner assert another customer's org id on the wire.\n // The hook's OWN derived entries still land (applyAuthenticatedCaller), and reconcileWireTrust then\n // admits an inbound trusted header only when the hook independently derived the same value.\n case 'apikey':\n return false;\n }\n }\n\n /**\n * `@WpLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the\n * endpoint did not exist.\n *\n * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all\n * (`ApiRoutingFactory` skips it), so the ordinary way to reach this path already answers 404. A\n * 403 from here would be a DIFFERENT answer from the same framework for the same endpoint, and\n * the difference is itself the leak: 403 confirms \"this path exists in production, you merely\n * lack permission\", which is a map of the dev-only surface for anyone probing. A local-only\n * endpoint should not admit it exists. Both gates therefore return the same 404, and this one is\n * the backstop for routes registered by hand through `RouteBuilder` rather than by\n * `ApiRoutingFactory`.\n *\n * The log line names WHICH reason applies, because \"you are deployed\" and \"nobody declared a\n * locality\" have completely different fixes and both look like a bare 404 from outside.\n */\n private enforceLocalOnly(meta: MethodMeta): void {\n if (RuntimeLocality.isLocalDevelopment()) {\n return;\n }\n log.warn(\n `Refusing @WpLocalOnly endpoint ${meta.routeMeta.path}: ` +\n (RuntimeLocality.isDeclared()\n ? 'this process declared itself DEPLOYED.'\n : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +\n 'Pass the locality into RuntimeSetupOptions if this really is a developer machine.'),\n );\n // Same shape as an unregistered route — see the method doc for why this is not a 403.\n throw new ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(header: string | undefined, routeMeta: RouteMetadata): Promise<void> {\n const hook = this.requireJwtHook(routeMeta);\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) throw new ApiUnauthorizedError('Authentication required');\n this.callerContext.publish(await hook.parseJwt(token));\n }\n\n private async enforceOidc(header: string | undefined, methods: OidcAuthMethods): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) throw new ApiUnauthorizedError('Missing OIDC bearer token');\n const identity = this.oidcHook\n ? await this.oidcHook.verifyOidc(token, methods)\n : await this.oidcVerifier.verify(token, methods);\n if (!identity)\n throw new ApiImplementationError(\n 'OIDC verifier must return its verified machine identity.',\n );\n await this.callerContext.publishVerifiedService(\n identity,\n (verified: AuthenticatedMachineIdentity) =>\n this.oidcHook ? this.oidcHook.originatingServiceRoles(verified) : Promise.resolve([]),\n );\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(\n provided: string | undefined,\n secretKey: string,\n routeMeta: RouteMetadata,\n ): void {\n const accepted = this.requireSharedSecrets(secretKey, routeMeta);\n if (!provided || !SharedSecretComparison.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError('Invalid shared secret for sharedSecret(...) endpoint');\n }\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private async bestEffortJwt(header: string | undefined): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.jwtHook || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.callerContext.publish(await this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n if (!(error instanceof ApiUnauthorizedError)) throw error;\n log.debug(\n 'Best-effort JWT parse on a public endpoint failed (treating as anonymous): ',\n error,\n );\n }\n }\n\n /**\n * The credential value IF the header carries the expected scheme, else undefined.\n *\n * Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent\n * where a JWT is expected), yields undefined and the caller 401s.\n */\n private credential(header: string | undefined, scheme: string): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = `${scheme} `;\n return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;\n }\n}\n"]}
|